diff --git a/api/video/downloads.py b/api/video/downloads.py index 065d6638..6fd867ce 100644 --- a/api/video/downloads.py +++ b/api/video/downloads.py @@ -7,8 +7,12 @@ never share a folder or collide. The actual download fulfillment engine (wishlis config the Settings → Downloads tab edits. Keys persisted here (all under video.db): - - ``download_path`` : input folder a video download lands in - - ``transfer_path`` : output folder finished video files move to (video library) + - ``download_path`` : input folder a video download lands in (shared by all types) + - ``movies_path`` : Movies library (finished movies move here) + - ``tv_path`` : TV Shows library + - ``youtube_path`` : YouTube library (kept separate from real TV) + The engine routes a finished download to the path matching its type. (Legacy single + ``transfer_path`` is migrated into ``movies_path`` on first read.) Connection settings that are genuinely SHARED with music (the slskd instance, the torrent/usenet clients, Prowlarr indexers) are NOT stored here — those live in the @@ -24,8 +28,9 @@ from utils.logging_config import get_logger logger = get_logger("video_api.downloads") -# Video-specific path keys (vs. the shared connection settings). -_PATH_KEYS = ("download_path", "transfer_path") +# Video-specific path keys (vs. the shared connection settings): one shared input +# folder + a separate library folder per content type. +_PATH_KEYS = ("download_path", "movies_path", "tv_path", "youtube_path") # slskd CONNECTION settings genuinely SHARED with music — one slskd instance serves # both sides, so these live in the app-wide config_manager (soulseek.*), NOT video.db. @@ -52,6 +57,8 @@ def register_routes(bp): from core.video.download_config import load as load_source db = get_video_db() out = {k: db.get_setting(k) or "" for k in _PATH_KEYS} + if not out["movies_path"]: # migrate the legacy single transfer folder → Movies + out["movies_path"] = db.get_setting("transfer_path") or "" out.update(load_source(db)) # download_mode + hybrid_order return jsonify(out) diff --git a/tests/test_video_api.py b/tests/test_video_api.py index d711303e..fea9ee48 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -366,18 +366,24 @@ def test_downloads_config_save_load(tmp_path): app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video") client = app.test_client() try: - # Defaults: empty folders, soulseek mode. + # Defaults: empty folders, soulseek mode. One shared input + per-type libraries. assert client.get("/api/video/downloads/config").get_json() == { - "download_path": "", "transfer_path": "", + "download_path": "", "movies_path": "", "tv_path": "", "youtube_path": "", "download_mode": "soulseek", "hybrid_order": ["soulseek"]} # Round-trips + persists to video.db (separate from any music config). client.post("/api/video/downloads/config", - json={"download_path": " /mnt/v/dl ", "transfer_path": "/mnt/v/lib", + json={"download_path": " /mnt/v/dl ", "movies_path": "/media/movies", + "tv_path": "/media/tv", "youtube_path": "/media/yt", "download_mode": "hybrid", "hybrid_order": ["torrent", "usenet"]}) assert client.get("/api/video/downloads/config").get_json() == { - "download_path": "/mnt/v/dl", "transfer_path": "/mnt/v/lib", # trimmed + "download_path": "/mnt/v/dl", "movies_path": "/media/movies", # trimmed + "tv_path": "/media/tv", "youtube_path": "/media/yt", "download_mode": "hybrid", "hybrid_order": ["torrent", "usenet"]} assert db.get_setting("download_path") == "/mnt/v/dl" + # Legacy single transfer_path migrates into Movies when movies_path is unset. + db.set_setting("movies_path", "") + db.set_setting("transfer_path", "/old/lib") + assert client.get("/api/video/downloads/config").get_json()["movies_path"] == "/old/lib" finally: videoapi._video_db = None diff --git a/webui/index.html b/webui/index.html index 39fcd772..94ff63d2 100644 --- a/webui/index.html +++ b/webui/index.html @@ -6017,15 +6017,24 @@

Video Download Folders

- Where the video side lands downloads and where it moves finished files — SEPARATE from your Music paths. + One shared download folder (where slskd / torrent drop files), then a separate library folder per type — finished files are sorted to the right one automatically. SEPARATE from your Music paths.
- + +
Where downloads land. Point this at the same folder as your slskd / torrent client.
- - + + +
+
+ + +
+
+ +
diff --git a/webui/static/video/video-settings.js b/webui/static/video/video-settings.js index dbc3c469..4c153ecf 100644 --- a/webui/static/video/video-settings.js +++ b/webui/static/video/video-settings.js @@ -281,10 +281,11 @@ .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { if (!d) return; - var dl = document.getElementById('video-download-path'); - if (dl && d.download_path != null) dl.value = d.download_path; - var tr = document.getElementById('video-transfer-path'); - if (tr && d.transfer_path != null) tr.value = d.transfer_path; + var setP = function (id, v) { var el = document.getElementById(id); if (el && v != null) el.value = v; }; + setP('video-download-path', d.download_path); + setP('video-movies-path', d.movies_path); + setP('video-tv-path', d.tv_path); + setP('video-youtube-path', d.youtube_path); _videoMode = d.download_mode || 'soulseek'; _videoHybrid = (d.hybrid_order && d.hybrid_order.length) ? d.hybrid_order : ['soulseek']; var ms = document.getElementById('video-download-mode'); @@ -296,13 +297,14 @@ } function saveDownloads(silent) { - var dl = document.getElementById('video-download-path'); - var tr = document.getElementById('video-transfer-path'); + var val = function (id) { var el = document.getElementById(id); return el ? el.value : ''; }; return fetch(DOWNLOADS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ - download_path: dl ? dl.value : '', - transfer_path: tr ? tr.value : '', + download_path: val('video-download-path'), + movies_path: val('video-movies-path'), + tv_path: val('video-tv-path'), + youtube_path: val('video-youtube-path'), download_mode: _videoMode, hybrid_order: _videoHybrid, }) @@ -440,7 +442,7 @@ }); } // Folder inputs save on change too. - ['video-download-path', 'video-transfer-path'].forEach(function (id) { + ['video-download-path', 'video-movies-path', 'video-tv-path', 'video-youtube-path'].forEach(function (id) { var el = document.getElementById(id); if (el && !el._vdWired) { el._vdWired = true; el.addEventListener('change', function () { saveDownloads(true); }); } });