- Remove trackers from torrenst/magnet URI --------- Co-authored-by: Mukhtar Akere <akeremukhtar10@gmail.com>
1 line
51 KiB
JavaScript
1 line
51 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),this.populateRcloneSettings(e.rclone),this.populateAPIToken(e)}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","always_rm_tracker_urls"].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])})}populateRcloneSettings(e){if(!e)return;["enabled","rc_port","mount_path","cache_dir","transfers","vfs_cache_mode","vfs_cache_max_size","vfs_cache_max_age","vfs_cache_poll_interval","vfs_read_chunk_size","vfs_read_chunk_size_limit","buffer_size","bw_limit","uid","gid","vfs_read_ahead","attr_timeout","dir_cache_time","poll_interval","umask","no_modtime","no_checksum","log_level","vfs_cache_min_free_space","vfs_fast_fingerprint","vfs_read_chunk_streams","async_read","use_mmap"].forEach(n=>{const t=document.querySelector(`[name="rclone.${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="debridlink">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 </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="grid grid-cols-1 lg:grid-cols-2 gap-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/__all__" required>\n <div class="label">\n <span class="label-text-alt">Path where debrid files are mounted</span>\n </div>\n </div>\n <div class="form-control">\n <label class="label" for="debrid[${e}].rclone_mount_path">\n <span class="label-text font-medium">Custom Rclone Mount Path</span>\n <span class="badge badge-ghost badge-sm">Optional</span>\n </label>\n <input type="text" class="input input-bordered" \n name="debrid[${e}].rclone_mount_path" id="debrid[${e}].rclone_mount_path" \n placeholder="/custom/mount/path (leave empty for global mount path)">\n <div class="label">\n <span class="label-text-alt">Custom mount path for this debrid service. If empty, uses global rclone mount path.</span>\n </div>\n </div>\n \n </div>\n <div class="grid grid-cols-2 lg:grid-cols-3 gap-3">\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 class="form-control">\n <label class="label" for="debrid[${e}].proxy">\n <span class="label-text font-medium">Proxy</span>\n </label>\n <input type="text" class="input input-bordered" \n name="debrid[${e}].proxy" id="debrid[${e}].proxy" \n placeholder="socks4, socks5, https proxy">\n <div class="label">\n <span class="label-text-alt">This proxy is used for this debrid account</span>\n </div>\n </div>\n <div class="form-control">\n <label class="label" for="debrid[${e}].minimum_free_slot">\n <span class="label-text font-medium">Minimum Free Slot</span>\n </label>\n <input type="number" class="input input-bordered" \n name="debrid[${e}].minimum_free_slot" id="debrid[${e}].minimum_free_slot" \n placeholder="1" value="1">\n <div class="label">\n <span class="label-text-alt">Minimum free slot for this debrid</span>\n </div>\n </div>\n </div>\n \n </div>\n </div>\n\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 <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 <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">\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 <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 </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}`),l=r.querySelectorAll(".webdav-field");e.checked||n?r.classList.remove("hidden"):(r.classList.add("hidden"),l.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 l=`${e}-${t}`;if(this.directoryFilterCounts[l]=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 </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 l=this.directoryFilterCounts[r],i=document.getElementById(`debrid[${e}].directory[${n}].filters`);if(i){const s=this.getFilterTemplate(e,n,l,t);if(i.insertAdjacentHTML("beforeend",s),a){const t=i.querySelector(`[name="debrid[${e}].directory[${n}].filter[${l}].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-2 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 \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="debridlink">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 </div>\n\n <div class="grid grid-cols-3 gap-4">\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 `}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.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.rclone.enabled&&""===e.rclone.mount_path&&n.push("Rclone mount path is required when Rclone is enabled"),{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?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,callback_url:document.getElementById("callbackUrl").value,debrids:this.collectDebridConfigs(),qbittorrent:this.collectQBittorrentConfig(),arrs:this.collectArrConfigs(),repair:this.collectRepairConfig(),rclone:this.collectRcloneConfig()}}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,minimum_free_slot:parseInt(document.querySelector(`[name="debrid[${n}].minimum_free_slot"]`).value)||0,rclone_mount_path:document.querySelector(`[name="debrid[${n}].rclone_mount_path"]`).value,proxy:document.querySelector(`[name="debrid[${n}].proxy"]`).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 l=`${n}-${t}`,i=this.directoryFilterCounts[l]||0;for(let e=0;e<i;e++){const l=document.querySelector(`[name="debrid[${n}].directory[${t}].filter[${e}].type"]`),i=document.querySelector(`[name="debrid[${n}].directory[${t}].filter[${e}].value"]`);if(l&&i&&i.value&&i.closest(".filter-item")){const e=l.value;a.directories[r].filters[e]=i.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,always_rm_tracker_urls:document.querySelector('[name="qbit.always_rm_tracker_urls"]').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}}collectRcloneConfig(){const e=(e,n="")=>{const t=document.querySelector(`[name="rclone.${e}"]`);if(!t)return n;if("checkbox"===t.type)return t.checked;if("number"===t.type){const e=parseInt(t.value);return isNaN(e)?0:e}return t.value||n};return{enabled:e("enabled",!1),rc_port:e("rc_port","5572"),mount_path:e("mount_path"),buffer_size:e("buffer_size"),bw_limit:e("bw_limit"),cache_dir:e("cache_dir"),transfers:e("transfers",8),vfs_cache_mode:e("vfs_cache_mode","off"),vfs_cache_max_age:e("vfs_cache_max_age","1h"),vfs_cache_max_size:e("vfs_cache_max_size"),vfs_cache_poll_interval:e("vfs_cache_poll_interval","1m"),vfs_read_chunk_size:e("vfs_read_chunk_size","128M"),vfs_read_chunk_size_limit:e("vfs_read_chunk_size_limit","off"),vfs_cache_min_free_space:e("vfs_cache_min_free_space",""),vfs_fast_fingerprint:e("vfs_fast_fingerprint",!1),vfs_read_chunk_streams:e("vfs_read_chunk_streams",0),use_mmap:e("use_mmap",!1),async_read:e("async_read",!0),uid:e("uid",0),gid:e("gid",0),umask:e("umask",""),vfs_read_ahead:e("vfs_read_ahead","128k"),attr_timeout:e("attr_timeout","1s"),dir_cache_time:e("dir_cache_time","5m"),no_modtime:e("no_modtime",!1),no_checksum:e("no_checksum",!1),log_level:e("log_level","INFO")}}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)}}populateAPIToken(e){const n=document.getElementById("api-token-display");n&&(n.value=e.api_token||"****");const t=document.getElementById("auth-username");t&&e.auth_username&&(t.value=e.auth_username)}} |