Improve Arr integerations

This commit is contained in:
Mukhtar Akere
2025-06-19 14:40:12 +01:00
parent c15e9d8f70
commit 086aa3b1ff
7 changed files with 472 additions and 145 deletions

View File

@@ -17,6 +17,37 @@
[data-bs-theme="dark"] .nav-pills .nav-link.active {
color: white !important;
}
.config-item.bg-light {
background-color: var(--bs-gray-100) !important;
border-left: 4px solid var(--bs-info) !important;
}
.config-item input[readonly] {
background-color: var(--bs-gray-200);
opacity: 1;
}
.config-item select[readonly] {
background-color: var(--bs-gray-200);
pointer-events: none;
}
/* Dark mode specific overrides */
[data-bs-theme="dark"] .config-item.bg-light {
background-color: var(--bs-gray-800) !important;
border-left: 4px solid var(--bs-info) !important;
}
[data-bs-theme="dark"] .config-item input[readonly] {
background-color: var(--bs-gray-700);
color: var(--bs-gray-300);
}
[data-bs-theme="dark"] .config-item select[readonly] {
background-color: var(--bs-gray-700);
color: var(--bs-gray-300);
}
</style>
<div class="container mt-4">
<form id="configForm">
@@ -80,7 +111,8 @@
<label>
<!-- Empty label to keep the button aligned -->
</label>
<div class="btn btn-primary w-100" onclick="registerMagnetLinkHandler();" id="registerMagnetLink">
<div class="btn btn-primary w-100" onclick="registerMagnetLinkHandler();"
id="registerMagnetLink">
Open Magnet Links in Decypharr
</div>
</div>
@@ -103,7 +135,8 @@
id="bindAddress"
name="bind_address"
placeholder="">
<small class="form-text text-muted">Bind address for the application(default is all interface)</small>
<small class="form-text text-muted">Bind address for the application(default is all
interface)</small>
</div>
</div>
<div class="col-md-2 mt-3">
@@ -150,7 +183,8 @@
id="minFileSize"
name="min_file_size"
placeholder="e.g., 10MB, 1GB">
<small class="form-text text-muted">Minimum file size to download (Empty for no limit)</small>
<small class="form-text text-muted">Minimum file size to download (Empty for no
limit)</small>
</div>
</div>
<div class="col-md-4 mt-3">
@@ -161,7 +195,8 @@
id="maxFileSize"
name="max_file_size"
placeholder="e.g., 50GB, 100MB">
<small class="form-text text-muted">Maximum file size to download (Empty for no limit)</small>
<small class="form-text text-muted">Maximum file size to download (Empty for no
limit)</small>
</div>
</div>
<div class="col-md-4 mt-3">
@@ -172,13 +207,15 @@
id="removeStalledAfter"
name="remove_stalled_after"
placeholder="e.g., 1m, 30s, 1h">
<small class="form-text text-muted">Remove torrents that have been stalled for this duration</small>
<small class="form-text text-muted">Remove torrents that have been stalled for this
duration</small>
</div>
</div>
</div>
</div>
<div class="mt-4 d-flex justify-content-end">
<button type="button" class="btn btn-primary next-step" data-next="2">Next <i class="bi bi-arrow-right"></i></button>
<button type="button" class="btn btn-primary next-step" data-next="2">Next <i
class="bi bi-arrow-right"></i></button>
</div>
</div>
@@ -196,7 +233,8 @@
<button type="button" class="btn btn-outline-secondary prev-step" data-prev="1">
<i class="bi bi-arrow-left"></i> Previous
</button>
<button type="button" class="btn btn-primary next-step" data-next="3">Next <i class="bi bi-arrow-right"></i></button>
<button type="button" class="btn btn-primary next-step" data-next="3">Next <i
class="bi bi-arrow-right"></i></button>
</div>
</div>
@@ -206,23 +244,31 @@
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label" for="qbit.download_folder">Symlink/Download Folder</label>
<input type="text" class="form-control" name="qbit.download_folder" id="qbit.download_folder">
<small class="form-text text-muted">Folder where the downloaded files will be stored</small>
<input type="text" class="form-control" name="qbit.download_folder"
id="qbit.download_folder">
<small class="form-text text-muted">Folder where the downloaded files will be
stored</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="qbit.refresh_interval">Refresh Interval (seconds)</label>
<input type="number" class="form-control" name="qbit.refresh_interval" id="qbit.refresh_interval">
<input type="number" class="form-control" name="qbit.refresh_interval"
id="qbit.refresh_interval">
</div>
<div class="col-md-5 mb-3">
<label class="form-label" for="qbit.max_downloads">Maximum Downloads Limit</label>
<input type="number" class="form-control" name="qbit.max_downloads" id="qbit.max_downloads">
<small class="form-text text-muted">Maximum number of simultaneous local downloads across all torrents</small>
<input type="number" class="form-control" name="qbit.max_downloads"
id="qbit.max_downloads">
<small class="form-text text-muted">Maximum number of simultaneous local downloads
across all torrents</small>
</div>
<div class="col mb-3">
<div class="form-check me-3 d-inline-block">
<input type="checkbox" class="form-check-input" name="qbit.skip_pre_cache" id="qbit.skip_pre_cache">
<label class="form-check-label" for="qbit.skip_pre_cache">Disable Pre-Cache On Download</label>
<small class="form-text text-muted">Unchecking this caches a tiny part of your file to speed up import</small>
<input type="checkbox" class="form-check-input" name="qbit.skip_pre_cache"
id="qbit.skip_pre_cache">
<label class="form-check-label" for="qbit.skip_pre_cache">Disable Pre-Cache On
Download</label>
<small class="form-text text-muted">Unchecking this caches a tiny part of your file
to speed up import</small>
</div>
</div>
</div>
@@ -231,7 +277,8 @@
<button type="button" class="btn btn-outline-secondary prev-step" data-prev="2">
<i class="bi bi-arrow-left"></i> Previous
</button>
<button type="button" class="btn btn-primary next-step" data-next="4">Next <i class="bi bi-arrow-right"></i></button>
<button type="button" class="btn btn-primary next-step" data-next="4">Next <i
class="bi bi-arrow-right"></i></button>
</div>
</div>
@@ -249,7 +296,8 @@
<button type="button" class="btn btn-outline-secondary prev-step" data-prev="3">
<i class="bi bi-arrow-left"></i> Previous
</button>
<button type="button" class="btn btn-primary next-step" data-next="5">Next <i class="bi bi-arrow-right"></i></button>
<button type="button" class="btn btn-primary next-step" data-next="5">Next <i
class="bi bi-arrow-right"></i></button>
</div>
</div>
@@ -259,7 +307,8 @@
<div class="row mb-3">
<div class="col">
<div class="form-check me-3 d-inline-block">
<input type="checkbox" class="form-check-input" name="repair.enabled" id="repair.enabled">
<input type="checkbox" class="form-check-input" name="repair.enabled"
id="repair.enabled">
<label class="form-check-label" for="repair.enabled">Enable Scheduled Repair</label>
</div>
</div>
@@ -268,34 +317,43 @@
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label" for="repair.interval">Scheduled Interval</label>
<input type="text" class="form-control" name="repair.interval" id="repair.interval" placeholder="e.g., 24h">
<small class="form-text text-muted">Interval for the repair process(e.g., 24h, 1d, 03:00, or a crontab)</small>
<input type="text" class="form-control" name="repair.interval" id="repair.interval"
placeholder="e.g., 24h">
<small class="form-text text-muted">Interval for the repair process(e.g., 24h, 1d,
03:00, or a crontab)</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="repair.workers">Workers</label>
<input type="text" class="form-control" name="repair.workers" id="repair.workers">
<small class="form-text text-muted">Number of workers to use for the repair process</small>
<small class="form-text text-muted">Number of workers to use for the repair
process</small>
</div>
<div class="col-md-5 mb-3">
<label class="form-label" for="repair.zurg_url">Zurg URL</label>
<input type="text" class="form-control" name="repair.zurg_url" id="repair.zurg_url" placeholder="http://zurg:9999">
<small class="form-text text-muted">If you have Zurg running, you can use it to speed up the repair process</small>
<input type="text" class="form-control" name="repair.zurg_url" id="repair.zurg_url"
placeholder="http://zurg:9999">
<small class="form-text text-muted">If you have Zurg running, you can use it to
speed up the repair process</small>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.use_webdav" id="repair.use_webdav">
<input type="checkbox" class="form-check-input" name="repair.use_webdav"
id="repair.use_webdav">
<label class="form-check-label" for="repair.use_webdav">Use Webdav</label>
</div>
<small class="form-text text-muted">Use Internal Webdav for repair(make sure webdav is enabled in the debrid section</small>
<small class="form-text text-muted">Use Internal Webdav for repair(make sure webdav
is enabled in the debrid section</small>
</div>
<div class="col-md-4 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
<input type="checkbox" class="form-check-input" name="repair.auto_process"
id="repair.auto_process">
<label class="form-check-label" for="repair.auto_process">Auto Process</label>
</div>
<small class="form-text text-muted">Automatically process the repair job(delete broken symlinks and searches the arr again)</small>
<small class="form-text text-muted">Automatically process the repair job(delete
broken symlinks and searches the arr again)</small>
</div>
</div>
</div>
@@ -335,15 +393,44 @@
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="debrid[${index}].api_key" >API Key</label>
<input type="password" class="form-control" name="debrid[${index}].api_key" id="debrid[${index}].api_key" required>
<div class="password-toggle-container">
<input type="password"
class="form-control has-toggle"
name="debrid[${index}].api_key"
id="debrid[${index}].api_key"
required>
<button type="button"
class="password-toggle-btn"
onclick="togglePassword('debrid[${index}].api_key');">
<i class="bi bi-eye" id="debrid[${index}].api_key_icon"></i>
</button>
</div>
<small class="form-text text-muted">API Key for the debrid service</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="debrid[${index}].download_api_keys">Download API Keys</label>
<div class="password-toggle-container">
<textarea class="form-control has-toggle"
name="debrid[${index}].download_api_keys"
id="debrid[${index}].download_api_keys"
rows="3"
style="font-family: monospace; resize: vertical;"
placeholder="Enter one API key per line&#10;key1&#10;key2&#10;key3"></textarea>
<button type="button"
class="password-toggle-btn"
style="top: 20px;"
onclick="togglePasswordTextarea('debrid[${index}].download_api_keys');">
<i class="bi bi-eye" id="debrid[${index}].download_api_keys_icon"></i>
</button>
</div>
<small class="form-text text-muted">Multiple API keys for download (one per line). If empty, main API key will be used.</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].folder">Mount/Rclone Folder</label>
<input type="text" class="form-control" name="debrid[${index}].folder" id="debrid[${index}].folder" placeholder="e.g. /mnt/remote/realdebrid" required>
<small class="form-text text-muted">Path to where you've mounted the debrid files. Usually your rclone path</small>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].rate_limit" >Rate Limit</label>
<input type="text" class="form-control" name="debrid[${index}].rate_limit" id="debrid[${index}].rate_limit" placeholder="e.g., 200/minute" value="250/minute">
<small class="form-text text-muted">Rate limit for the debrid service. Confirm your debrid service rate limit</small>
@@ -432,7 +519,17 @@
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].rc_pass">Rclone RC Password</label>
<input type="password" class="form-control webdav-field" name="debrid[${index}].rc_pass" id="debrid[${index}].rc_pass">
<div class="password-toggle-container">
<input type="password"
class="form-control webdav-field has-toggle"
name="debrid[${index}].rc_pass"
id="debrid[${index}].rc_pass">
<button type="button"
class="password-toggle-btn"
onclick="togglePassword('debrid[${index}].rc_pass');">
<i class="bi bi-eye" id="debrid[${index}].rc_pass_icon"></i>
</button>
</div>
<small class="form-text text-muted">Rclone RC Password for the webdav server</small>
</div>
<div class="col-md-3 mb-3">
@@ -538,7 +635,7 @@
const filterTemplate = (debridIndex, dirIndex, filterIndex, filterType) => {
let placeholder, label;
switch(filterType) {
switch (filterType) {
case 'include':
placeholder = "Text that should be included in filename";
label = "Include";
@@ -632,58 +729,87 @@
`;
};
const arrTemplate = (index) => `
<div class="config-item position-relative mb-3 p-3 border rounded">
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 m-2"
onclick="if(confirm('Are you sure you want to delete this arr?')) this.closest('.config-item').remove();"
title="Delete this arr">
<i class="bi bi-trash"></i>
</button>
<div class="row">
<div class="col-md-4 mb-3">
<label for="arr[${index}].name" class="form-label">Name</label>
<input type="text" class="form-control" name="arr[${index}].name" id="arr[${index}].name" required>
const arrTemplate = (index, data = {}) => `
<div class="config-item position-relative mb-3 p-3 border rounded ${data.source === 'auto' ? 'bg-light' : ''}">
${data.source !== 'auto' ? `
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 m-2"
onclick="if(confirm('Are you sure you want to delete this arr?')) this.closest('.config-item').remove();"
title="Delete this arr">
<i class="bi bi-trash"></i>
</button>
` : `
<div class="position-absolute top-0 end-0 m-2">
<span class="badge bg-info">Auto-detected</span>
</div>
<div class="col-md-4 mb-3">
<label for="arr[${index}].host" class="form-label">Host</label>
<input type="text" class="form-control" name="arr[${index}].host" id="arr[${index}].host" required>
</div>
<div class="col-md-4 mb-3">
<label for"arr[${index}].token" class="form-label">API Token</label>
<input type="password" class="form-control" name="arr[${index}].token" id="arr[${index}].token" required>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<select class="form-select" name="arr[${index}].selected_debrid" id="arr[${index}].selected_debrid">
<option value="" selected disabled>Select Arr Debrid</option>
<option value="realdebrid">Real Debrid</option>
<option value="alldebrid">AllDebrid</option>
<option value="debrid_link">Debrid Link</option>
<option value="torbox">Torbox</option>
</select>
</div>
<div class="col-md-2 mb-3">
<div class="form-check">
<label for="arr[${index}].cleanup" class="form-check-label">Cleanup Queue</label>
<input type="checkbox" class="form-check-input" name="arr[${index}].cleanup" id="arr[${index}].cleanup">
`}
<div class="row">
<input type="hidden" name="arr[${index}].source" value="${data.source || ''}">
<div class="col-md-4 mb-3">
<label for="arr[${index}].name" class="form-label">Name</label>
<input type="text"
class="form-control"
name="arr[${index}].name"
id="arr[${index}].name"
${data.source === 'auto' ? 'readonly' : 'required'}>
${data.source === 'auto' ? '<input type="hidden" name="arr[' + index + '].source" value="auto">' : ''}
</div>
<div class="col-md-4 mb-3">
<label for="arr[${index}].host" class="form-label">Host</label>
<input type="text"
class="form-control"
name="arr[${index}].host"
id="arr[${index}].host"
${data.source === 'auto' ? 'readonly' : 'required'}>
</div>
<div class="col-md-4 mb-3">
<label for="arr[${index}].token" class="form-label">API Token</label>
<div class="password-toggle-container">
<input type="password"
class="form-control has-toggle"
name="arr[${index}].token"
id="arr[${index}].token"
${data.source === 'auto' ? 'readonly' : 'required'}>
<button type="button"
class="password-toggle-btn"
onclick="togglePassword('arr[${index}].token');"
${data.source === 'auto' ? 'disabled style="opacity: 0.5;"' : ''}>
<i class="bi bi-eye" id="arr[${index}].token_icon"></i>
</button>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="form-check">
<label for="arr[${index}].skip_repair" class="form-check-label">Skip Repair</label>
<input type="checkbox" class="form-check-input" name="arr[${index}].skip_repair" id="arr[${index}].skip_repair">
<div class="row">
<div class="col-md-4 mb-3">
<label for="arr[${index}].selected_debrid" class="form-label">Select Arr Debrid</label>
<select class="form-select" name="arr[${index}].selected_debrid" id="arr[${index}].selected_debrid">
<option value="" selected disabled>Select Arr Debrid</option>
<option value="realdebrid">Real Debrid</option>
<option value="alldebrid">AllDebrid</option>
<option value="debrid_link">Debrid Link</option>
<option value="torbox">Torbox</option>
</select>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="form-check">
<label for="arr[${index}].download_uncached" class="form-check-label">Download Uncached</label>
<input type="checkbox" class="form-check-input" name="arr[${index}].download_uncached" id="arr[${index}].download_uncached">
<div class="col-md-2 mb-3">
<div class="form-check">
<label for="arr[${index}].cleanup" class="form-check-label">Cleanup Queue</label>
<input type="checkbox" class="form-check-input" name="arr[${index}].cleanup" id="arr[${index}].cleanup">
</div>
</div>
<div class="col-md-2 mb-3">
<div class="form-check">
<label for="arr[${index}].skip_repair" class="form-check-label">Skip Repair</label>
<input type="checkbox" class="form-check-input" name="arr[${index}].skip_repair" id="arr[${index}].skip_repair">
</div>
</div>
<div class="col-md-2 mb-3">
<div class="form-check">
<label for="arr[${index}].download_uncached" class="form-check-label">Download Uncached</label>
<input type="checkbox" class="form-check-input" name="arr[${index}].download_uncached" id="arr[${index}].download_uncached">
</div>
</div>
</div>
</div>
</div>
`;
`;
const debridDirectoryCounts = {};
const directoryFilterCounts = {};
@@ -739,6 +865,7 @@
debridDirectoryCounts[debridIndex]++;
return dirIndex;
}
function addFilter(debridIndex, dirIndex, filterType, filterValue = "") {
const dirKey = `${debridIndex}-${dirIndex}`;
if (!directoryFilterCounts[dirKey]) {
@@ -771,7 +898,7 @@
}
// Main functionality
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
let debridCount = 0;
let arrCount = 0;
let currentStep = 1;
@@ -785,21 +912,21 @@
// Step navigation
document.querySelectorAll('.nav-link').forEach(navLink => {
navLink.addEventListener('click', function() {
navLink.addEventListener('click', function () {
const stepNumber = parseInt(this.getAttribute('data-step'));
goToStep(stepNumber);
});
});
document.querySelectorAll('.next-step').forEach(button => {
button.addEventListener('click', function() {
button.addEventListener('click', function () {
const nextStep = parseInt(this.getAttribute('data-next'));
goToStep(nextStep);
});
});
document.querySelectorAll('.prev-step').forEach(button => {
button.addEventListener('click', function() {
button.addEventListener('click', function () {
const prevStep = parseInt(this.getAttribute('data-prev'));
goToStep(prevStep);
});
@@ -910,7 +1037,7 @@
addArrConfig();
});
$(document).on('change', '.useWebdav', function() {
$(document).on('change', '.useWebdav', function () {
const webdavConfig = $(this).closest('.config-item').find(`.webdav`);
if (webdavConfig.length === 0) return;
@@ -953,7 +1080,7 @@
// Save config logic
const response = await fetcher('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
@@ -1005,7 +1132,7 @@
if (data.use_webdav && data.directories) {
Object.entries(data.directories).forEach(([dirName, dirData]) => {
const dirIndex = addDirectory(debridCount, { name: dirName });
const dirIndex = addDirectory(debridCount, {name: dirName});
// Add filters if available
if (dirData.filters) {
@@ -1015,6 +1142,20 @@
}
});
}
if (data.download_api_keys && Array.isArray(data.download_api_keys)) {
const downloadKeysTextarea = container.querySelector(`[name="debrid[${debridCount}].download_api_keys"]`);
if (downloadKeysTextarea) {
downloadKeysTextarea.value = data.download_api_keys.join('\n');
}
}
}
const downloadKeysTextarea = newDebrid.querySelector(`[name="debrid[${debridCount}].download_api_keys"]`);
if (downloadKeysTextarea) {
downloadKeysTextarea.style.webkitTextSecurity = 'disc';
downloadKeysTextarea.style.textSecurity = 'disc';
downloadKeysTextarea.setAttribute('data-password-visible', 'false');
}
debridCount++;
@@ -1022,11 +1163,10 @@
function addArrConfig(data = {}) {
const container = document.getElementById('arrConfigs');
container.insertAdjacentHTML('beforeend', arrTemplate(arrCount));
container.insertAdjacentHTML('beforeend', arrTemplate(arrCount, data));
// Add a delete button to the new arr
// Don't add delete button for auto-detected arrs since it's already handled in template
const newArr = container.lastElementChild;
addDeleteButton(newArr, `Delete this arr`);
if (data) {
Object.entries(data).forEach(([key, value]) => {
@@ -1051,7 +1191,7 @@
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
deleteBtn.title = tooltip;
deleteBtn.addEventListener('click', function() {
deleteBtn.addEventListener('click', function () {
if (confirm('Are you sure you want to delete this item?')) {
element.remove();
}
@@ -1126,7 +1266,7 @@
const nameInput = document.querySelector(`[name="debrid[${i}].directory[${j}].name"]`);
if (nameInput && nameInput.value) {
const dirName = nameInput.value;
debrid.directories[dirName] = { filters: {} };
debrid.directories[dirName] = {filters: {}};
// Get directory key for filter counting
const dirKey = `${i}-${j}`;
@@ -1146,6 +1286,14 @@
}
}
let downloadApiKeysTextarea = document.querySelector(`[name="debrid[${i}].download_api_keys"]`);
if (downloadApiKeysTextarea && downloadApiKeysTextarea.value.trim()) {
debrid.download_api_keys = downloadApiKeysTextarea.value
.split('\n')
.map(key => key.trim())
.filter(key => key.length > 0);
}
if (debrid.name && debrid.api_key) {
config.debrids.push(debrid);
}
@@ -1163,7 +1311,8 @@
cleanup: document.querySelector(`[name="arr[${i}].cleanup"]`).checked,
skip_repair: document.querySelector(`[name="arr[${i}].skip_repair"]`).checked,
download_uncached: document.querySelector(`[name="arr[${i}].download_uncached"]`).checked,
selectedDebrid: document.querySelector(`[name="arr[${i}].selected_debrid"]`).value
selected_debrid: document.querySelector(`[name="arr[${i}].selected_debrid"]`).value,
source: document.querySelector(`[name="arr[${i}].source"]`).value
};
if (arr.name && arr.host) {