From 83058489b61121983a613dd2fa9b77ee09b16c71 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Wed, 27 Aug 2025 13:02:43 +0100 Subject: [PATCH] Add callback URL for post-processing --- internal/config/config.go | 1 + internal/request/discord.go | 2 + pkg/debrid/store/repair.go | 2 + pkg/web/api.go | 35 +------- pkg/web/assets/build/js/config.js | 2 +- pkg/web/assets/js/config.js | 1 + pkg/web/templates/config.html | 128 ++++++++++++++++-------------- pkg/wire/request.go | 3 + 8 files changed, 80 insertions(+), 94 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 243ac6f..75c3ca0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -152,6 +152,7 @@ type Config struct { Auth *Auth `json:"-"` DiscordWebhook string `json:"discord_webhook_url,omitempty"` RemoveStalledAfter string `json:"remove_stalled_after,omitzero"` + CallbackURL string `json:"callback_url,omitempty"` } func (c *Config) JsonFile() string { diff --git a/internal/request/discord.go b/internal/request/discord.go index 7e618ff..0ecc6c0 100644 --- a/internal/request/discord.go +++ b/internal/request/discord.go @@ -45,6 +45,8 @@ func getDiscordHeader(event string) string { return "[Decypharr] Repair Completed, Awaiting action" case "repair_complete": return "[Decypharr] Repair Complete" + case "repair_cancelled": + return "[Decypharr] Repair Cancelled" default: // split the event string and capitalize the first letter of each word evs := strings.Split(event, "_") diff --git a/pkg/debrid/store/repair.go b/pkg/debrid/store/repair.go index dccf0d8..9afe804 100644 --- a/pkg/debrid/store/repair.go +++ b/pkg/debrid/store/repair.go @@ -59,6 +59,8 @@ func (c *Cache) markAsSuccessfullyReinserted(torrentId string) { } } +// GetBrokenFiles checks the files in the torrent for broken links. +// It also attempts to reinsert the torrent if any files are broken. func (c *Cache) GetBrokenFiles(t *CachedTorrent, filenames []string) []string { files := make(map[string]types.File) repairStrategy := config.Get().Repair.Strategy diff --git a/pkg/web/api.go b/pkg/web/api.go index 817bb1d..25c1215 100644 --- a/pkg/web/api.go +++ b/pkg/web/api.go @@ -246,35 +246,6 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { return } - // Get the current configuration - currentConfig := config.Get() - - // Update fields that can be changed - currentConfig.LogLevel = updatedConfig.LogLevel - currentConfig.MinFileSize = updatedConfig.MinFileSize - currentConfig.MaxFileSize = updatedConfig.MaxFileSize - currentConfig.RemoveStalledAfter = updatedConfig.RemoveStalledAfter - currentConfig.AllowedExt = updatedConfig.AllowedExt - currentConfig.DiscordWebhook = updatedConfig.DiscordWebhook - - // Should this be added? - currentConfig.URLBase = updatedConfig.URLBase - currentConfig.BindAddress = updatedConfig.BindAddress - currentConfig.Port = updatedConfig.Port - - // Update QBitTorrent config - currentConfig.QBitTorrent = updatedConfig.QBitTorrent - - // Update Repair config - currentConfig.Repair = updatedConfig.Repair - currentConfig.Rclone = updatedConfig.Rclone - - // Update Debrids - if len(updatedConfig.Debrids) > 0 { - currentConfig.Debrids = updatedConfig.Debrids - // Clear legacy single debrid if using array - } - // Update Arrs through the service storage := wire.Get() arrStorage := storage.Arr() @@ -287,10 +258,10 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { } newConfigArrs = append(newConfigArrs, a) } - currentConfig.Arrs = newConfigArrs + updatedConfig.Arrs = newConfigArrs // Add config arr into the config - for _, a := range currentConfig.Arrs { + for _, a := range updatedConfig.Arrs { if a.Host == "" || a.Token == "" { continue // Skip empty arrs } @@ -312,7 +283,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { } } - if err := currentConfig.Save(); err != nil { + if err := updatedConfig.Save(); err != nil { http.Error(w, "Error saving config: "+err.Error(), http.StatusInternalServerError) return } diff --git a/pkg/web/assets/build/js/config.js b/pkg/web/assets/build/js/config.js index 4b384ec..c650f23 100644 --- a/pkg/web/assets/build/js/config.js +++ b/pkg/web/assets/build/js/config.js @@ -1 +1 @@ -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"].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","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
\n
\n
\n

\n \n Debrid Service #${e+1}\n

\n \n
\n
\n
\n \n \n
\n\n
\n \n
\n \n \n
\n
\n API key for the debrid service\n
\n
\n
\n\n
\n
\n
\n \n
\n \n \n
\n
\n Multiple API keys for downloads - leave empty to use main API key\n
\n
\n
\n
\n
\n
\n \n \n
\n Path where debrid files are mounted\n
\n
\n
\n \n \n
\n Custom mount path for this debrid service. If empty, uses global rclone mount path.\n
\n
\n \n
\n
\n
\n \n \n
\n API rate limit for this service\n
\n
\n
\n \n \n
\n This proxy is used for this debrid account\n
\n
\n
\n \n \n
\n Minimum free slot for this debrid\n
\n
\n
\n \n
\n
\n\n
\n
\n \n
\n Create internal WebDAV server\n
\n
\n\n
\n \n
\n Download uncached files\n
\n
\n\n
\n \n
\n Include sample files\n
\n
\n\n
\n \n
\n Preprocess RAR files\n
\n
\n
\n\n \n
\n
\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
\n
\n
\n
Virtual Directory
\n \n
\n\n
\n \n \n
\n\n
\n
\n
\n Filters\n \n
\n
\n\n
\n
\n\n
\n \n\n \n\n \n\n \n
\n
\n
\n
\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
\n
\n ${r.label}\n
\n \n
\n \n
\n \n
\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 \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
\n
\n
\n

\n \n Arr Service #${e+1}\n ${t?'
Auto-detected
':""}\n

\n ${t?"":'\n \n '}\n
\n\n \n\n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n
\n \n \n
\n
\n
\n\n
\n
\n \n \n
\n Which debrid service this Arr should prefer\n
\n
\n\n
\n
\n
\n \n
\n\n
\n \n
\n\n
\n \n
\n
\n
\n
\n
\n
\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,debrids:this.collectDebridConfigs(),qbittorrent:this.collectQBittorrentConfig(),arrs:this.collectArrConfigs(),repair:this.collectRepairConfig(),rclone:this.collectRcloneConfig()}}collectDebridConfigs(){const e=[];for(let n=0;ne.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{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"),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='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='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)}} \ No newline at end of file +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"].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","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
\n
\n
\n

\n \n Debrid Service #${e+1}\n

\n \n
\n
\n
\n \n \n
\n\n
\n \n
\n \n \n
\n
\n API key for the debrid service\n
\n
\n
\n\n
\n
\n
\n \n
\n \n \n
\n
\n Multiple API keys for downloads - leave empty to use main API key\n
\n
\n
\n
\n
\n
\n \n \n
\n Path where debrid files are mounted\n
\n
\n
\n \n \n
\n Custom mount path for this debrid service. If empty, uses global rclone mount path.\n
\n
\n \n
\n
\n
\n \n \n
\n API rate limit for this service\n
\n
\n
\n \n \n
\n This proxy is used for this debrid account\n
\n
\n
\n \n \n
\n Minimum free slot for this debrid\n
\n
\n
\n \n
\n
\n\n
\n
\n \n
\n Create internal WebDAV server\n
\n
\n\n
\n \n
\n Download uncached files\n
\n
\n\n
\n \n
\n Include sample files\n
\n
\n\n
\n \n
\n Preprocess RAR files\n
\n
\n
\n\n \n
\n
\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
\n
\n
\n
Virtual Directory
\n \n
\n\n
\n \n \n
\n\n
\n
\n
\n Filters\n \n
\n
\n\n
\n
\n\n
\n \n\n \n\n \n\n \n
\n
\n
\n
\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
\n
\n ${r.label}\n
\n \n
\n \n
\n \n
\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 \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
\n
\n
\n

\n \n Arr Service #${e+1}\n ${t?'
Auto-detected
':""}\n

\n ${t?"":'\n \n '}\n
\n\n \n\n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n
\n \n \n
\n
\n
\n\n
\n
\n \n \n
\n Which debrid service this Arr should prefer\n
\n
\n\n
\n
\n
\n \n
\n\n
\n \n
\n\n
\n \n
\n
\n
\n
\n
\n
\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;ne.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{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"),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='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='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)}} \ No newline at end of file diff --git a/pkg/web/assets/js/config.js b/pkg/web/assets/js/config.js index 1c36992..df1379a 100644 --- a/pkg/web/assets/js/config.js +++ b/pkg/web/assets/js/config.js @@ -1085,6 +1085,7 @@ class ConfigManager { 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, // Debrid configurations debrids: this.collectDebridConfigs(), diff --git a/pkg/web/templates/config.html b/pkg/web/templates/config.html index eee0ef1..fb8a7f6 100644 --- a/pkg/web/templates/config.html +++ b/pkg/web/templates/config.html @@ -120,7 +120,7 @@ -
+
+
+ + +
+ Optional callback URL for download status updates +
+
@@ -364,71 +373,68 @@
-
-
- - -
- How often to run repair (e.g., 24h, 1d, 03:00, or crontab) -
-
- -
- - -
- Number of concurrent repair workers -
-
- -
- - -
- How to handle repairs -
+
+ + +
+ How often to run repair (e.g., 24h, 1d, 03:00, or crontab)
-
-
- - -
- Optional Zurg instance to speed up repairs +
+ + +
+ Number of concurrent repair workers +
+
+ +
+ + +
+ How to handle repairs +
+
+
+ + +
+ Optional Zurg instance to speed up repairs +
+
+
+ +
+
+
+ +
-
- -
- -
- -
+
+
diff --git a/pkg/wire/request.go b/pkg/wire/request.go index 748b84b..70ef532 100644 --- a/pkg/wire/request.go +++ b/pkg/wire/request.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "github.com/google/uuid" + "github.com/sirrobot01/decypharr/internal/config" "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/pkg/arr" @@ -43,6 +44,8 @@ type ImportRequest struct { } func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet, arr *arr.Arr, action string, downloadUncached bool, callBackUrl string, importType ImportType) *ImportRequest { + cfg := config.Get() + callBackUrl = cmp.Or(callBackUrl, cfg.CallbackURL) return &ImportRequest{ Id: uuid.New().String(), Status: "started",