1 line
47 KiB
JavaScript
1 line
47 KiB
JavaScript
class ConfigManager{constructor(){this.debridCount=0,this.arrCount=0,this.debridDirectoryCounts={},this.directoryFilterCounts={},this.refs={configForm:document.getElementById("configForm"),loadingOverlay:document.getElementById("loadingOverlay"),debridConfigs:document.getElementById("debridConfigs"),arrConfigs:document.getElementById("arrConfigs"),addDebridBtn:document.getElementById("addDebridBtn"),addArrBtn:document.getElementById("addArrBtn")},this.init()}init(){this.bindEvents(),this.loadConfiguration(),this.setupMagnetHandler(),this.checkIncompleteConfig()}checkIncompleteConfig(){const e=new URLSearchParams(window.location.search);if(e.has("inco")){const n=e.get("inco");window.decypharrUtils.createToast(`Incomplete configuration: ${n}`,"warning")}}bindEvents(){this.refs.configForm.addEventListener("submit",e=>this.saveConfiguration(e)),this.refs.addDebridBtn.addEventListener("click",()=>this.addDebridConfig()),this.refs.addArrBtn.addEventListener("click",()=>this.addArrConfig()),document.addEventListener("change",e=>{e.target.classList.contains("useWebdav")&&this.toggleWebDAVSection(e.target)})}async loadConfiguration(){try{const e=await window.decypharrUtils.fetcher("/api/config");if(!e.ok)throw new Error("Failed to load configuration");const n=await e.json();this.populateForm(n)}catch(e){console.error("Error loading configuration:",e),window.decypharrUtils.createToast("Error loading configuration","error")}}populateForm(e){this.populateGeneralSettings(e),e.debrids&&Array.isArray(e.debrids)&&e.debrids.forEach(e=>this.addDebridConfig(e)),this.populateQBittorrentSettings(e.qbittorrent),e.arrs&&Array.isArray(e.arrs)&&e.arrs.forEach(e=>this.addArrConfig(e)),this.populateRepairSettings(e.repair)}populateGeneralSettings(e){["log_level","url_base","bind_address","port","discord_webhook_url","min_file_size","max_file_size","remove_stalled_after"].forEach(n=>{const t=document.querySelector(`[name="${n}"]`);t&&void 0!==e[n]&&(t.value=e[n])}),e.allowed_file_types&&Array.isArray(e.allowed_file_types)&&(document.querySelector('[name="allowed_file_types"]').value=e.allowed_file_types.join(", "))}populateQBittorrentSettings(e){if(!e)return;["download_folder","refresh_interval","max_downloads","skip_pre_cache"].forEach(n=>{const t=document.querySelector(`[name="qbit.${n}"]`);t&&void 0!==e[n]&&("checkbox"===t.type?t.checked=e[n]:t.value=e[n])})}populateRepairSettings(e){if(!e)return;["enabled","interval","workers","zurg_url","strategy","use_webdav","auto_process"].forEach(n=>{const t=document.querySelector(`[name="repair.${n}"]`);t&&void 0!==e[n]&&("checkbox"===t.type?t.checked=e[n]:t.value=e[n])})}addDebridConfig(e={}){const n=this.getDebridTemplate(this.debridCount,e);this.refs.debridConfigs.insertAdjacentHTML("beforeend",n);const t=this.refs.debridConfigs.lastElementChild.querySelector(".useWebdav");e.use_webdav&&this.toggleWebDAVSection(t,!0),Object.keys(e).length>0&&this.populateDebridData(this.debridCount,e),this.debridDirectoryCounts[this.debridCount]=0,e.directories&&Object.entries(e.directories).forEach(([e,n])=>{const t=this.addDirectory(this.debridCount,{name:e,...n});n.filters&&Object.entries(n.filters).forEach(([e,n])=>{this.addFilter(this.debridCount,t,e,n)})}),this.debridCount++}populateDebridData(e,n){Object.entries(n).forEach(([n,t])=>{const a=document.querySelector(`[name="debrid[${e}].${n}"]`);a&&("checkbox"===a.type?a.checked=t:"download_api_keys"===n&&Array.isArray(t)?(a.value=t.join("\n"),"textarea"===a.tagName.toLowerCase()&&(a.style.webkitTextSecurity="disc",a.style.textSecurity="disc",a.setAttribute("data-password-visible","false"))):a.value=t)})}getDebridTemplate(e,n={}){return`\n <div class="card bg-base-100 border border-base-300 shadow-sm debrid-config" data-index="${e}">\n <div class="card-body">\n <div class="flex justify-between items-start mb-4">\n <h3 class="card-title text-lg">\n <i class="bi bi-cloud mr-2 text-secondary"></i>\n Debrid Service #${e+1}\n </h3>\n <button type="button" class="btn btn-error btn-sm" onclick="this.closest('.debrid-config').remove();">\n <i class="bi bi-trash"></i>\n </button>\n </div>\n <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">\n <div class="form-control">\n <label class="label" for="debrid[${e}].name">\n <span class="label-text font-medium">Service Type</span>\n </label>\n <select class="select select-bordered" name="debrid[${e}].name" id="debrid[${e}].name" required>\n <option value="realdebrid">Real Debrid</option>\n <option value="alldebrid">AllDebrid</option>\n <option value="debrid_link">Debrid Link</option>\n <option value="torbox">Torbox</option>\n </select>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].api_key">\n <span class="label-text font-medium">API Key</span>\n </label>\n <div class="password-toggle-container">\n <input type="password" class="input input-bordered input-has-toggle" \n name="debrid[${e}].api_key" id="debrid[${e}].api_key" required>\n <button type="button" class="password-toggle-btn">\n <i class="bi bi-eye" id="debrid[${e}].api_key_icon"></i>\n </button>\n </div>\n <div class="label">\n <span class="label-text-alt">API key for the debrid service</span>\n </div>\n </div>\n </div>\n\n <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">\n <div class="flex flex-col">\n <div class="form-control flex-1">\n <label class="label" for="debrid[${e}].download_api_keys">\n <span class="label-text font-medium">Download API Keys</span>\n <span class="badge badge-ghost badge-sm">Optional</span>\n </label>\n <div class="password-toggle-container">\n <textarea class="textarea textarea-bordered has-toggle font-mono h-full min-h-[200px]" \n name="debrid[${e}].download_api_keys" \n id="debrid[${e}].download_api_keys" \n placeholder="Multiple API keys for download (one per line). If empty, main API key will be used."></textarea>\n <button type="button" class="password-toggle-btn textarea-toggle">\n <i class="bi bi-eye" id="debrid[${e}].download_api_keys_icon"></i>\n </button>\n </div>\n <div class="label">\n <span class="label-text-alt">Multiple API keys for downloads - leave empty to use main API key</span>\n </div>\n </div>\n </div>\n <div class="space-y-4">\n <div class="form-control">\n <label class="label" for="debrid[${e}].folder">\n <span class="label-text font-medium">Mount/Rclone Folder</span>\n </label>\n <input type="text" class="input input-bordered" \n name="debrid[${e}].folder" id="debrid[${e}].folder" \n placeholder="/mnt/remote/realdebrid" required>\n <div class="label">\n <span class="label-text-alt">Path where debrid files are mounted</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].rate_limit">\n <span class="label-text font-medium">Rate Limit</span>\n </label>\n <input type="text" class="input input-bordered" \n name="debrid[${e}].rate_limit" id="debrid[${e}].rate_limit" \n placeholder="250/minute" value="250/minute">\n <div class="label">\n <span class="label-text-alt">API rate limit for this service</span>\n </div>\n </div>\n </div>\n </div>\n\n \x3c!-- Options Grid - Full Width Below --\x3e\n <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-6">\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox useWebdav" \n name="debrid[${e}].use_webdav" id="debrid[${e}].use_webdav">\n <span class="label-text font-medium">Enable WebDAV</span>\n </label>\n <div class="label">\n <span class="label-text-alt">Create internal WebDAV server</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox" \n name="debrid[${e}].download_uncached" id="debrid[${e}].download_uncached">\n <span class="label-text font-medium">Download Uncached</span>\n </label>\n <div class="label">\n <span class="label-text-alt">Download uncached files</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox" \n name="debrid[${e}].add_samples" id="debrid[${e}].add_samples">\n <span class="label-text font-medium">Add Samples</span>\n </label>\n <div class="label">\n <span class="label-text-alt">Include sample files</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox" \n name="debrid[${e}].unpack_rar" id="debrid[${e}].unpack_rar">\n <span class="label-text font-medium">Unpack RAR</span>\n </label>\n <div class="label">\n <span class="label-text-alt">Preprocess RAR files</span>\n </div>\n </div>\n </div>\n\n \x3c!-- WebDAV Configuration (Initially Hidden) --\x3e\n <div class="webdav-section hidden mt-6" id="webdav-section-${e}">\n <div class="divider">\n <span class="text-lg font-semibold">WebDAV Settings</span>\n </div>\n \n <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">\n \x3c!-- WebDAV Basic Settings --\x3e\n <div class="form-control">\n <label class="label" for="debrid[${e}].torrents_refresh_interval">\n <span class="label-text font-medium">Torrents Refresh Interval</span>\n </label>\n <input type="text" class="input input-bordered webdav-field" \n name="debrid[${e}].torrents_refresh_interval" \n id="debrid[${e}].torrents_refresh_interval" \n placeholder="15s" value="15s">\n <div class="label">\n <span class="label-text-alt">How often to refresh torrents list</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].download_links_refresh_interval">\n <span class="label-text font-medium">Links Refresh Interval</span>\n </label>\n <input type="text" class="input input-bordered webdav-field" \n name="debrid[${e}].download_links_refresh_interval" \n id="debrid[${e}].download_links_refresh_interval" \n placeholder="40m" value="40m">\n <div class="label">\n <span class="label-text-alt">How often to refresh download links</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].auto_expire_links_after">\n <span class="label-text font-medium">Expire Links After</span>\n </label>\n <input type="text" class="input input-bordered webdav-field" \n name="debrid[${e}].auto_expire_links_after" \n id="debrid[${e}].auto_expire_links_after" \n placeholder="3d" value="3d">\n <div class="label">\n <span class="label-text-alt">How long to keep links in WebDAV</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].workers">\n <span class="label-text font-medium">Workers</span>\n </label>\n <input type="number" class="input input-bordered webdav-field" \n name="debrid[${e}].workers" id="debrid[${e}].workers" \n placeholder="50" value="50" min="1" max="200">\n <div class="label">\n <span class="label-text-alt">Number of concurrent workers</span>\n </div>\n </div>\n \n <div class="form-control">\n <label class="label" for="debrid[${e}].folder_naming">\n <span class="label-text font-medium">Folder Naming</span>\n </label>\n <select class="select select-bordered webdav-field" \n name="debrid[${e}].folder_naming" id="debrid[${e}].folder_naming">\n <option value="original_no_ext" selected>Original name (No Extension)</option>\n <option value="original">Original name</option>\n <option value="filename">File name</option>\n <option value="filename_no_ext">File name (No Extension)</option>\n <option value="id">Use ID</option>\n <option value="infohash">Use Infohash</option>\n </select>\n <div class="label">\n <span class="label-text-alt">How to name torrent directories</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].rc_url">\n <span class="label-text font-medium">Rclone RC URL</span>\n </label>\n <input type="url" class="input input-bordered webdav-field" \n name="debrid[${e}].rc_url" id="debrid[${e}].rc_url" \n placeholder="http://localhost:9990">\n <div class="label">\n <span class="label-text-alt">Rclone RC URL (speeds up imports)</span>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].rc_refresh_dirs">\n <span class="label-text font-medium">RC Refresh Directories</span>\n </label>\n <input type="text" class="input input-bordered webdav-field" \n name="debrid[${e}].rc_refresh_dirs" id="debrid[${e}].rc_refresh_dirs" \n placeholder="__all__, torrents">\n <div class="label">\n <span class="label-text-alt">Comma-separated directory list</span>\n </div>\n </div>\n <div class="form-control">\n <label class="label" for="debrid[${e}].rc_user">\n <span class="label-text font-medium">RC User</span>\n </label>\n <input type="text" class="input input-bordered webdav-field" \n name="debrid[${e}].rc_user" id="debrid[${e}].rc_user">\n </div>\n\n <div class="form-control">\n <label class="label" for="debrid[${e}].rc_pass">\n <span class="label-text font-medium">RC Password</span>\n </label>\n <div class="password-toggle-container">\n <input type="password" class="input input-bordered webdav-field input-has-toggle" \n name="debrid[${e}].rc_pass" id="debrid[${e}].rc_pass">\n <button type="button" class="password-toggle-btn">\n <i class="bi bi-eye" id="debrid[${e}].rc_pass_icon"></i>\n </button>\n </div>\n </div>\n\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox webdav-field" \n name="debrid[${e}].serve_from_rclone" id="debrid[${e}].serve_from_rclone">\n <span class="label-text font-medium">Serve From Rclone</span>\n </label>\n <div class="label">\n <span class="label-text-alt">Let Rclone handle serving/streaming</span>\n </div>\n </div>\n </div>\n\n \x3c!-- Virtual Directories --\x3e\n <div class="mt-6">\n <div class="flex justify-between items-center mb-4">\n <h4 class="text-lg font-semibold">Virtual Directories</h4>\n <button type="button" class="btn btn-secondary btn-sm" onclick="configManager.addDirectory(${e});">\n <i class="bi bi-plus mr-2"></i>Add Directory\n </button>\n </div>\n <p class="text-sm text-base-content/70 mb-4">Create virtual directories with filters to organize your content</p>\n <div class="directories-container space-y-4" id="debrid[${e}].directories">\n \x3c!-- Dynamic directories will be added here --\x3e\n </div>\n </div>\n </div>\n </div>\n </div>\n `}toggleWebDAVSection(e,n=!1){const t=e.closest(".debrid-config"),a=t.dataset.index,r=t.querySelector(`#webdav-section-${a}`),i=r.querySelectorAll(".webdav-field");e.checked||n?(r.classList.remove("hidden"),r.querySelectorAll('input[name$=".torrents_refresh_interval"]').forEach(e=>e.required=!0),r.querySelectorAll('input[name$=".download_links_refresh_interval"]').forEach(e=>e.required=!0),r.querySelectorAll('input[name$=".auto_expire_links_after"]').forEach(e=>e.required=!0),r.querySelectorAll('input[name$=".workers"]').forEach(e=>e.required=!0)):(r.classList.add("hidden"),i.forEach(e=>e.required=!1))}addDirectory(e,n={}){this.debridDirectoryCounts[e]||(this.debridDirectoryCounts[e]=0);const t=this.debridDirectoryCounts[e],a=document.getElementById(`debrid[${e}].directories`),r=this.getDirectoryTemplate(e,t);a.insertAdjacentHTML("beforeend",r);const i=`${e}-${t}`;if(this.directoryFilterCounts[i]=0,n.name){const a=document.querySelector(`[name="debrid[${e}].directory[${t}].name"]`);a&&(a.value=n.name)}return this.debridDirectoryCounts[e]++,t}getDirectoryTemplate(e,n){return`\n <div class="card bg-base-200 border border-base-300 directory-item">\n <div class="card-body">\n <div class="flex justify-between items-start mb-4">\n <h5 class="text-lg font-medium">Virtual Directory</h5>\n <button type="button" class="btn btn-error btn-xs" onclick="this.closest('.directory-item').remove();">\n <i class="bi bi-trash"></i>\n </button>\n </div>\n\n <div class="form-control mb-4">\n <label class="label">\n <span class="label-text font-medium">Directory Name</span>\n </label>\n <input type="text" class="input input-bordered webdav-field"\n name="debrid[${e}].directory[${n}].name"\n placeholder="Movies, TV Shows, Collections, etc.">\n </div>\n\n <div class="space-y-4">\n <div class="flex justify-between items-center">\n <h6 class="font-medium flex items-center">\n Filters\n <button type="button" class="btn btn-ghost btn-xs ml-2" onclick="configManager.showFilterHelp();">\n <i class="bi bi-question-circle"></i>\n </button>\n </h6>\n </div>\n\n <div class="filters-container space-y-2" id="debrid[${e}].directory[${n}].filters">\n \x3c!-- Filters will be added here --\x3e\n </div>\n\n <div class="flex flex-wrap gap-2">\n <div class="dropdown">\n <div tabindex="0" role="button" class="btn btn-outline btn-sm">\n <i class="bi bi-plus mr-1"></i>Text Filter\n <i class="bi bi-chevron-down ml-1"></i>\n </div>\n <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-48 p-2 shadow">\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'include');">Include</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'exclude');">Exclude</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'starts_with');">Starts With</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'not_starts_with');">Not Starts With</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'ends_with');">Ends With</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'not_ends_with');">Not Ends With</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'exact_match');">Exact Match</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'not_exact_match');">Not Exact Match</a></li>\n </ul>\n </div>\n\n <div class="dropdown">\n <div tabindex="0" role="button" class="btn btn-outline btn-sm">\n <i class="bi bi-code mr-1"></i>Regex Filter\n <i class="bi bi-chevron-down ml-1"></i>\n </div>\n <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-48 p-2 shadow">\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'regex');">Regex Match</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'not_regex');">Regex Doesn't Match</a></li>\n </ul>\n </div>\n\n <div class="dropdown">\n <div tabindex="0" role="button" class="btn btn-outline btn-sm">\n <i class="bi bi-hdd mr-1"></i>Size Filter\n <i class="bi bi-chevron-down ml-1"></i>\n </div>\n <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-48 p-2 shadow">\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'size_gt');">Size Greater Than</a></li>\n <li><a onclick="configManager.addFilter(${e}, ${n}, 'size_lt');">Size Less Than</a></li>\n </ul>\n </div>\n\n <button type="button" class="btn btn-outline btn-sm" onclick="configManager.addFilter(${e}, ${n}, 'last_added');">\n <i class="bi bi-clock mr-1"></i>Last Added Filter\n </button>\n </div>\n </div>\n </div>\n </div>\n `}addFilter(e,n,t,a=""){const r=`${e}-${n}`;this.directoryFilterCounts[r]||(this.directoryFilterCounts[r]=0);const i=this.directoryFilterCounts[r],l=document.getElementById(`debrid[${e}].directory[${n}].filters`);if(l){const s=this.getFilterTemplate(e,n,i,t);if(l.insertAdjacentHTML("beforeend",s),a){const t=l.querySelector(`[name="debrid[${e}].directory[${n}].filter[${i}].value"]`);t&&(t.value=a)}this.directoryFilterCounts[r]++}}getFilterTemplate(e,n,t,a){const r=this.getFilterConfig(a);return`\n <div class="filter-item flex items-center gap-3 p-3 bg-base-100 rounded-lg border border-base-300">\n <div class="badge ${r.badgeClass} badge-sm">\n ${r.label}\n </div>\n <input type="hidden"\n name="debrid[${e}].directory[${n}].filter[${t}].type"\n value="${a}">\n <div class="flex-1">\n <input type="text" \n class="input input-bordered input-sm w-full webdav-field"\n name="debrid[${e}].directory[${n}].filter[${t}].value"\n placeholder="${r.placeholder}">\n </div>\n <button type="button" class="btn btn-error btn-xs" onclick="this.closest('.filter-item').remove();">\n <i class="bi bi-x"></i>\n </button>\n </div>\n `}getFilterConfig(e){return{include:{label:"Include",placeholder:"Text that should be included in filename",badgeClass:"badge-primary"},exclude:{label:"Exclude",placeholder:"Text that should not be in filename",badgeClass:"badge-error"},regex:{label:"Regex Match",placeholder:"Regular expression pattern",badgeClass:"badge-warning"},not_regex:{label:"Regex Not Match",placeholder:"Regular expression pattern that should not match",badgeClass:"badge-error"},exact_match:{label:"Exact Match",placeholder:"Exact text to match",badgeClass:"badge-primary"},not_exact_match:{label:"Not Exact Match",placeholder:"Exact text that should not match",badgeClass:"badge-error"},starts_with:{label:"Starts With",placeholder:"Text that filename starts with",badgeClass:"badge-primary"},not_starts_with:{label:"Not Starts With",placeholder:"Text that filename should not start with",badgeClass:"badge-error"},ends_with:{label:"Ends With",placeholder:"Text that filename ends with",badgeClass:"badge-primary"},not_ends_with:{label:"Not Ends With",placeholder:"Text that filename should not end with",badgeClass:"badge-error"},size_gt:{label:"Size Greater Than",placeholder:"Size in bytes, KB, MB, GB (e.g. 700MB)",badgeClass:"badge-success"},size_lt:{label:"Size Less Than",placeholder:"Size in bytes, KB, MB, GB (e.g. 700MB)",badgeClass:"badge-warning"},last_added:{label:"Added in the last",placeholder:"Time duration (e.g. 24h, 7d, 30d)",badgeClass:"badge-info"}}[e]||{label:e.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase()),placeholder:"Filter value",badgeClass:"badge-ghost"}}showFilterHelp(){const e=document.createElement("dialog");e.className="modal",e.innerHTML='\n <div class="modal-box max-w-2xl">\n <form method="dialog">\n <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>\n </form>\n <h3 class="font-bold text-lg mb-4">Directory Filter Types</h3>\n <div class="space-y-4">\n <div>\n <h4 class="font-semibold text-primary">Text Filters</h4>\n <ul class="list-disc list-inside text-sm space-y-1 ml-4">\n <li><strong>Include/Exclude:</strong> Simple text inclusion/exclusion</li>\n <li><strong>Starts/Ends With:</strong> Matches beginning or end of filename</li>\n <li><strong>Exact Match:</strong> Match the entire filename</li>\n </ul>\n </div>\n <div>\n <h4 class="font-semibold text-warning">Regex Filters</h4>\n <ul class="list-disc list-inside text-sm space-y-1 ml-4">\n <li><strong>Regex:</strong> Use regular expressions for complex patterns</li>\n <li>Example: <code>.*\\.mkv$</code> matches files ending with .mkv</li>\n </ul>\n </div>\n <div>\n <h4 class="font-semibold text-success">Size Filters</h4>\n <ul class="list-disc list-inside text-sm space-y-1 ml-4">\n <li><strong>Size Greater/Less Than:</strong> Filter by file size</li>\n <li>Examples: 1GB, 500MB, 2.5GB</li>\n </ul>\n </div>\n <div>\n <h4 class="font-semibold text-info">Time Filters</h4>\n <ul class="list-disc list-inside text-sm space-y-1 ml-4">\n <li><strong>Last Added:</strong> Show only recently added content</li>\n <li>Examples: 24h, 7d, 30d</li>\n </ul>\n </div>\n <div class="alert alert-info">\n <i class="bi bi-info-circle"></i>\n <span>Negative filters (Not...) will exclude matches instead of including them.</span>\n </div>\n </div>\n </div>\n ',document.body.appendChild(e),e.showModal(),e.addEventListener("close",()=>{document.body.removeChild(e)})}addArrConfig(e={}){const n=this.getArrTemplate(this.arrCount,e);this.refs.arrConfigs.insertAdjacentHTML("beforeend",n),Object.keys(e).length>0&&this.populateArrData(this.arrCount,e),this.arrCount++}populateArrData(e,n){Object.entries(n).forEach(([n,t])=>{const a=document.querySelector(`[name="arr[${e}].${n}"]`);a&&("checkbox"===a.type?a.checked=t:a.value=t)})}getArrTemplate(e,n={}){const t="auto"===n.source;return`\n <div class="card bg-base-100 border border-base-300 shadow-sm arr-config ${t?"border-info":""}" data-index="${e}">\n <div class="card-body">\n <div class="flex justify-between items-start mb-4">\n <h3 class="card-title text-lg">\n <i class="bi bi-collection mr-2 text-warning"></i>\n Arr Service #${e+1}\n ${t?'<div class="badge badge-info badge-sm ml-2">Auto-detected</div>':""}\n </h3>\n ${t?"":'\n <button type="button" class="btn btn-error btn-sm" onclick="this.closest(\'.arr-config\').remove();">\n <i class="bi bi-trash"></i>\n </button>\n '}\n </div>\n\n <input type="hidden" name="arr[${e}].source" value="${n.source||""}">\n\n <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">\n <div class="form-control">\n <label class="label" for="arr[${e}].name">\n <span class="label-text font-medium">Service Name</span>\n </label>\n <input type="text" class="input input-bordered ${t?"input-disabled":""}" \n name="arr[${e}].name" id="arr[${e}].name" \n ${t?"readonly":"required"} \n placeholder="sonarr, radarr, etc.">\n </div>\n\n <div class="form-control">\n <label class="label" for="arr[${e}].host">\n <span class="label-text font-medium">Host URL</span>\n </label>\n <input type="url" class="input input-bordered ${t?"input-disabled":""}" \n name="arr[${e}].host" id="arr[${e}].host" \n ${t?"readonly":"required"} \n placeholder="http://localhost:8989">\n </div>\n\n <div class="form-control">\n <label class="label" for="arr[${e}].token">\n <span class="label-text font-medium">API Token</span>\n </label>\n <div class="password-toggle-container">\n <input type="password" class="input input-bordered input-has-toggle ${t?"input-disabled":""}" \n name="arr[${e}].token" id="arr[${e}].token" \n ${t?"readonly":"required"}>\n <button type="button" class="password-toggle-btn ${t?"opacity-50 cursor-not-allowed":""}"\n ${t?"disabled":""}>\n <i class="bi bi-eye" id="arr[${e}].token_icon"></i>\n </button>\n </div>\n </div>\n </div>\n\n <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">\n <div class="form-control">\n <label class="label" for="arr[${e}].selected_debrid">\n <span class="label-text font-medium">Preferred Debrid Service</span>\n </label>\n <select class="select select-bordered" name="arr[${e}].selected_debrid" id="arr[${e}].selected_debrid">\n <option value="" selected>Auto-select</option>\n <option value="realdebrid">Real Debrid</option>\n <option value="alldebrid">AllDebrid</option>\n <option value="debrid_link">Debrid Link</option>\n <option value="torbox">Torbox</option>\n </select>\n <div class="label">\n <span class="label-text-alt">Which debrid service this Arr should prefer</span>\n </div>\n </div>\n\n <div class="flex flex-col justify-end">\n <div class="grid grid-cols-3 gap-2">\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox checkbox-sm" \n name="arr[${e}].cleanup" id="arr[${e}].cleanup">\n <span class="label-text text-sm">Cleanup Queue</span>\n </label>\n </div>\n\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox checkbox-sm" \n name="arr[${e}].skip_repair" id="arr[${e}].skip_repair">\n <span class="label-text text-sm">Skip Repair</span>\n </label>\n </div>\n\n <div class="form-control">\n <label class="label cursor-pointer justify-start gap-2">\n <input type="checkbox" class="checkbox checkbox-sm" \n name="arr[${e}].download_uncached" id="arr[${e}].download_uncached">\n <span class="label-text text-sm">Download Uncached</span>\n </label>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n `}async saveConfiguration(e){e.preventDefault(),this.refs.loadingOverlay.classList.remove("hidden");try{const e=this.collectFormData(),n=this.validateConfiguration(e);if(!n.valid)throw new Error(n.errors.join("\n"));const t=await window.decypharrUtils.fetcher("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok){const e=await t.text();throw new Error(e||"Failed to save configuration")}window.decypharrUtils.createToast("Configuration saved successfully! Services are restarting...","success"),setTimeout(()=>{window.location.reload()},2e3)}catch(e){console.error("Error saving configuration:",e),window.decypharrUtils.createToast(`Error saving configuration: ${e.message}`,"error"),this.refs.loadingOverlay.classList.add("hidden")}}validateConfiguration(e){const n=[];return e.port&&(e.port<1||e.port>65535)&&n.push("Port must be between 1 and 65535"),e.debrids.forEach((e,t)=>{e.name&&e.api_key&&e.folder||n.push(`Debrid service #${t+1}: Name, API key, and folder are required`)}),e.arrs.forEach((e,t)=>{e.name&&e.host||n.push(`Arr service #${t+1}: Name and host are required`),e.host&&!this.isValidUrl(e.host)&&n.push(`Arr service #${t+1}: Invalid host URL format`)}),e.repair.enabled&&(e.repair.interval||n.push("Repair interval is required when repair is enabled"),e.repair.workers&&(e.repair.workers<1||e.repair.workers>50)&&n.push("Repair workers must be between 1 and 50")),{valid:0===n.length,errors:n}}isValidUrl(e){try{return new URL(e),!0}catch(e){return!1}}collectFormData(){return{log_level:document.getElementById("log-level").value,url_base:document.getElementById("urlBase").value,bind_address:document.getElementById("bindAddress").value,port:document.getElementById("port").value?parseInt(document.getElementById("port").value):null,discord_webhook_url:document.getElementById("discordWebhookUrl").value,allowed_file_types:document.getElementById("allowedExtensions").value.split(",").map(e=>e.trim()).filter(Boolean),min_file_size:document.getElementById("minFileSize").value,max_file_size:document.getElementById("maxFileSize").value,remove_stalled_after:document.getElementById("removeStalledAfter").value,debrids:this.collectDebridConfigs(),qbittorrent:this.collectQBittorrentConfig(),arrs:this.collectArrConfigs(),repair:this.collectRepairConfig()}}collectDebridConfigs(){const e=[];for(let n=0;n<this.debridCount;n++){const t=document.querySelector(`[name="debrid[${n}].name"]`);if(!t||!t.closest(".debrid-config"))continue;const a={name:t.value,api_key:document.querySelector(`[name="debrid[${n}].api_key"]`).value,folder:document.querySelector(`[name="debrid[${n}].folder"]`).value,rate_limit:document.querySelector(`[name="debrid[${n}].rate_limit"]`).value,download_uncached:document.querySelector(`[name="debrid[${n}].download_uncached"]`).checked,unpack_rar:document.querySelector(`[name="debrid[${n}].unpack_rar"]`).checked,add_samples:document.querySelector(`[name="debrid[${n}].add_samples"]`).checked,use_webdav:document.querySelector(`[name="debrid[${n}].use_webdav"]`).checked},r=document.querySelector(`[name="debrid[${n}].download_api_keys"]`);if(r&&r.value.trim()&&(a.download_api_keys=r.value.split("\n").map(e=>e.trim()).filter(e=>e.length>0)),a.use_webdav){a.torrents_refresh_interval=document.querySelector(`[name="debrid[${n}].torrents_refresh_interval"]`).value,a.download_links_refresh_interval=document.querySelector(`[name="debrid[${n}].download_links_refresh_interval"]`).value,a.auto_expire_links_after=document.querySelector(`[name="debrid[${n}].auto_expire_links_after"]`).value,a.folder_naming=document.querySelector(`[name="debrid[${n}].folder_naming"]`).value,a.workers=parseInt(document.querySelector(`[name="debrid[${n}].workers"]`).value),a.rc_url=document.querySelector(`[name="debrid[${n}].rc_url"]`).value,a.rc_user=document.querySelector(`[name="debrid[${n}].rc_user"]`).value,a.rc_pass=document.querySelector(`[name="debrid[${n}].rc_pass"]`).value,a.rc_refresh_dirs=document.querySelector(`[name="debrid[${n}].rc_refresh_dirs"]`).value,a.serve_from_rclone=document.querySelector(`[name="debrid[${n}].serve_from_rclone"]`).checked,a.directories={};const e=this.debridDirectoryCounts[n]||0;for(let t=0;t<e;t++){const e=document.querySelector(`[name="debrid[${n}].directory[${t}].name"]`);if(e&&e.value&&e.closest(".directory-item")){const r=e.value;a.directories[r]={filters:{}};const i=`${n}-${t}`,l=this.directoryFilterCounts[i]||0;for(let e=0;e<l;e++){const i=document.querySelector(`[name="debrid[${n}].directory[${t}].filter[${e}].type"]`),l=document.querySelector(`[name="debrid[${n}].directory[${t}].filter[${e}].value"]`);if(i&&l&&l.value&&l.closest(".filter-item")){const e=i.value;a.directories[r].filters[e]=l.value}}}}}a.name&&a.api_key&&e.push(a)}return e}collectQBittorrentConfig(){return{download_folder:document.querySelector('[name="qbit.download_folder"]').value,refresh_interval:parseInt(document.querySelector('[name="qbit.refresh_interval"]').value)||30,max_downloads:parseInt(document.querySelector('[name="qbit.max_downloads"]').value)||0,skip_pre_cache:document.querySelector('[name="qbit.skip_pre_cache"]').checked}}collectArrConfigs(){const e=[];for(let n=0;n<this.arrCount;n++){const t=document.querySelector(`[name="arr[${n}].name"]`);if(!t||!t.closest(".arr-config"))continue;const a={name:t.value,host:document.querySelector(`[name="arr[${n}].host"]`).value,token:document.querySelector(`[name="arr[${n}].token"]`).value,cleanup:document.querySelector(`[name="arr[${n}].cleanup"]`).checked,skip_repair:document.querySelector(`[name="arr[${n}].skip_repair"]`).checked,download_uncached:document.querySelector(`[name="arr[${n}].download_uncached"]`).checked,selected_debrid:document.querySelector(`[name="arr[${n}].selected_debrid"]`).value,source:document.querySelector(`[name="arr[${n}].source"]`).value};a.name&&a.host&&e.push(a)}return e}collectRepairConfig(){return{enabled:document.querySelector('[name="repair.enabled"]').checked,interval:document.querySelector('[name="repair.interval"]').value,zurg_url:document.querySelector('[name="repair.zurg_url"]').value,strategy:document.querySelector('[name="repair.strategy"]').value,workers:parseInt(document.querySelector('[name="repair.workers"]').value)||1,use_webdav:document.querySelector('[name="repair.use_webdav"]').checked,auto_process:document.querySelector('[name="repair.auto_process"]').checked}}setupMagnetHandler(){if(window.registerMagnetLinkHandler=()=>{if("registerProtocolHandler"in navigator)try{navigator.registerProtocolHandler("magnet",`${window.location.origin}${window.urlBase}download?magnet=%s`,"Decypharr"),localStorage.setItem("magnetHandler","true");const e=document.getElementById("registerMagnetLink");e.innerHTML='<i class="bi bi-check-circle mr-2"></i>Magnet Handler Registered',e.classList.remove("btn-primary"),e.classList.add("btn-success"),e.disabled=!0,window.decypharrUtils.createToast("Magnet link handler registered successfully")}catch(e){console.error("Failed to register magnet link handler:",e),window.decypharrUtils.createToast("Failed to register magnet link handler","error")}else window.decypharrUtils.createToast("Magnet link registration not supported in this browser","warning")},"true"===localStorage.getItem("magnetHandler")){const e=document.getElementById("registerMagnetLink");e&&(e.innerHTML='<i class="bi bi-check-circle mr-2"></i>Magnet Handler Registered',e.classList.remove("btn-primary"),e.classList.add("btn-success"),e.disabled=!0)}}}document.addEventListener("DOMContentLoaded",()=>{window.configManager=new ConfigManager}); |