diff --git a/config/settings.py b/config/settings.py index ff232ad6..f1482081 100644 --- a/config/settings.py +++ b/config/settings.py @@ -508,6 +508,22 @@ class ConfigManager: # its partial data, fail the download so the next source can # try) or "pause" (pause in the client, leave for the user). "torrent_stall_action": "abandon", + # Where THIS container can read completed torrent/usenet + # downloads (#857). The downloader (qBit/SAB) reports a save + # path from inside ITS OWN container — often a category folder + # like /data/downloads/music — which may be mounted at a + # different point here. Set these to the in-container path(s) + # where SoulSync sees those finished downloads; the resolver + # then finds the release by name under them. Empty = fall back + # to the soulseek download/transfer dirs (the shared-volume + # default). See core.download_plugins.album_bundle.resolve_reported_save_path. + "torrent_download_path": "", + "usenet_download_path": "", + # Explicit remote→local prefix mappings for non-shared / oddly + # mounted layouts (Sonarr/Radarr "Remote Path Mapping" style): + # a list of {"from": "", "to": ""}. + # Tried before the basename fallback above. + "usenet_path_mappings": [], }, "post_processing": { # When a download is quarantined (AcoustID mismatch, integrity / diff --git a/tests/test_album_bundle.py b/tests/test_album_bundle.py index 8b85e6c4..5c7e0eed 100644 --- a/tests/test_album_bundle.py +++ b/tests/test_album_bundle.py @@ -551,6 +551,26 @@ def test_resolve_skips_mapping_when_target_missing_then_tries_basename(tmp_path: assert resolved == str(tmp_path / "MyAlbum") +def test_resolve_uses_custom_torrent_download_path(tmp_path: Path) -> None: + """#857: the user's torrent client saves to a category folder (e.g. a + 'Music' category) mounted here at a custom in-container path. Setting + download_source.torrent_download_path lets SoulSync find the release there.""" + music_mount = tmp_path / "downloads" / "music" + (music_mount / "MyAlbum").mkdir(parents=True) + cfg = _cfg({'download_source.torrent_download_path': str(music_mount)}) + resolved = resolve_reported_save_path('/data/Downloads/Music/MyAlbum', config_get=cfg) + assert resolved == str(music_mount / "MyAlbum") + + +def test_resolve_uses_custom_usenet_download_path(tmp_path: Path) -> None: + """#857: same for the usenet source's custom completed-downloads path.""" + nzb_mount = tmp_path / "nzb" / "music" + (nzb_mount / "MyAlbum").mkdir(parents=True) + cfg = _cfg({'download_source.usenet_download_path': str(nzb_mount)}) + resolved = resolve_reported_save_path('/config/Downloads/complete/MyAlbum', config_get=cfg) + assert resolved == str(nzb_mount / "MyAlbum") + + # --------------------------------------------------------------------------- # poll_album_download — lifted poll loop for both torrent + usenet plugins. # --------------------------------------------------------------------------- diff --git a/webui/index.html b/webui/index.html index 6a37eede..8518816d 100644 --- a/webui/index.html +++ b/webui/index.html @@ -5431,6 +5431,13 @@ Override where the torrent client writes downloads. This path is on the torrent client's machine, not SoulSync's. +
+ + +
+ Where SoulSync can read finished torrents — its own in-container path. Set this when your client saves to a category folder (e.g. a "Music" category at /data/downloads/music) that's mounted here at a different path. SoulSync finds the release by name under this folder. Blank = use the Soulseek download/transfer folders. +
+
@@ -5511,6 +5518,13 @@ SoulSync tags every NZB with this category so it ends up in a predictable post-processing folder.
+
+ + +
+ Where SoulSync can read finished NZB downloads — its own in-container path. Set this when SABnzbd/NZBGet writes that category to a folder (e.g. /data/downloads/music) mounted here at a different path. SoulSync finds the release by name under this folder. Blank = use the Soulseek download/transfer folders. +
+
diff --git a/webui/static/settings.js b/webui/static/settings.js index 8ba12156..5091fb40 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -1179,6 +1179,8 @@ async function loadSettingsData() { _tcStall.value = (secs === undefined || secs === null) ? 10 : Math.round(Number(secs) / 60); } if (_tcStallAct) _tcStallAct.value = settings.download_source?.torrent_stall_action || 'abandon'; + const _tcDlPath = document.getElementById('torrent-download-path'); + if (_tcDlPath) _tcDlPath.value = settings.download_source?.torrent_download_path || ''; const _ucType = document.getElementById('usenet-client-type'); const _ucUrl = document.getElementById('usenet-client-url'); const _ucKey = document.getElementById('usenet-client-api-key'); @@ -1191,6 +1193,8 @@ async function loadSettingsData() { if (_ucUser) _ucUser.value = settings.usenet_client?.username || ''; if (_ucPass) _ucPass.value = settings.usenet_client?.password || ''; if (_ucCat) _ucCat.value = settings.usenet_client?.category || 'soulsync'; + const _ucDlPath = document.getElementById('usenet-download-path'); + if (_ucDlPath) _ucDlPath.value = settings.download_source?.usenet_download_path || ''; if (typeof updateUsenetClientUI === 'function') updateUsenetClientUI(); // Sync ARL to connections tab field + bidirectional listeners const _connArl = document.getElementById('deezer-connection-arl'); @@ -3001,6 +3005,10 @@ async function saveSettings(quiet = false) { return (Number.isFinite(m) && m >= 0 ? m : 10) * 60; })(), torrent_stall_action: document.getElementById('torrent-stall-action')?.value || 'abandon', + // In-container path(s) where SoulSync reads finished torrent/usenet + // downloads (#857). Rendered in the torrent/usenet client sections. + torrent_download_path: document.getElementById('torrent-download-path')?.value || '', + usenet_download_path: document.getElementById('usenet-download-path')?.value || '', }, tidal_download: { quality: document.getElementById('tidal-download-quality').value || 'lossless',