diff --git a/api/video/downloads.py b/api/video/downloads.py index 6fd867ce..392d3491 100644 --- a/api/video/downloads.py +++ b/api/video/downloads.py @@ -6,13 +6,15 @@ never share a folder or collide. The actual download fulfillment engine (wishlis → search → grab) is a later roadmap phase; these endpoints just store/serve the config the Settings → Downloads tab edits. -Keys persisted here (all under video.db): - - ``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.) +Folders: + - INPUT (download) folder is SHARED with the music side — it's the same + ``config_manager`` key the music Download Settings use (``soulseek.download_path``), + so changing it on either side changes both (one physical download dir, simpler + Docker mounts). We only READ/WRITE that shared key; no music code is touched. + - OUTPUT (library) folders are video-specific and live in video.db, one per type: + ``movies_path`` / ``tv_path`` / ``youtube_path``. + The engine routes a finished download to the library path matching its type. (Legacy + single video ``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 @@ -28,9 +30,11 @@ from utils.logging_config import get_logger logger = get_logger("video_api.downloads") -# 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") +# Video-specific OUTPUT library folders (video.db). The INPUT folder is the shared +# music key below — not in this list. +_PATH_KEYS = ("movies_path", "tv_path", "youtube_path") +# The shared input/download dir — the SAME config key the music side reads/writes. +_SHARED_DOWNLOAD_KEY = "soulseek.download_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. @@ -55,10 +59,12 @@ def register_routes(bp): def video_downloads_config(): from . import get_video_db from core.video.download_config import load as load_source + from config.settings import config_manager 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["download_path"] = config_manager.get(_SHARED_DOWNLOAD_KEY, "") or "" # shared w/ music out.update(load_source(db)) # download_mode + hybrid_order return jsonify(out) @@ -71,6 +77,9 @@ def register_routes(bp): for key in _PATH_KEYS: if key in body: db.set_setting(key, (str(body.get(key) or "")).strip()) + if "download_path" in body: # SHARED with music — write the same config key + from config.settings import config_manager + config_manager.set(_SHARED_DOWNLOAD_KEY, (str(body.get("download_path") or "")).strip()) save_source(db, body) # download_mode + hybrid_order (validated) return jsonify({"status": "saved"}) diff --git a/docker-compose.yml b/docker-compose.yml index fe5ed945..e7857811 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,10 +32,20 @@ services: # Persistent data volumes - ./config:/app/config - ./logs:/app/logs + # Shared INPUT folder — slskd / torrent / usenet drop downloads here, and BOTH + # the music and video sides read from it (Settings → Downloads uses one path). - ./downloads:/app/downloads - ./Staging:/app/Staging - ./MusicVideos:/app/MusicVideos - ./scripts:/app/scripts + # ─── Video libraries (OUTPUT) — one per content type ───────────────── + # The video Downloads tab sorts finished files into these by type. Point each + # at where Plex/Jellyfin reads from, then set the matching path on + # Settings → Downloads (the in-app placeholders match the in-container paths): + # Movies → /media/movies · TV → /media/tv · YouTube → /media/youtube + - ./Movies:/media/movies + - ./TV:/media/tv + - ./YouTube:/media/youtube # Use named volume for database persistence (separate from host database) # NOTE: Changed from /app/database to /app/data to avoid overwriting Python package - soulsync_database:/app/data diff --git a/tests/test_video_api.py b/tests/test_video_api.py index fea9ee48..4a850148 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -356,21 +356,35 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch): videoapi._video_db = None -def test_downloads_config_save_load(tmp_path): +def test_downloads_config_save_load(tmp_path, monkeypatch): import api.video as videoapi + import config.settings as cfg from database.video_database import VideoDatabase + class _Cfg: # fake shared app config so the test never touches real music config + def __init__(self): + self._d = {} + + def get(self, key, default=None): + return self._d.get(key, default) + + def set(self, key, value): + self._d[key] = value + + fake = _Cfg() + monkeypatch.setattr(cfg, "config_manager", fake, raising=False) + db = VideoDatabase(database_path=str(tmp_path / "video_library.db")) videoapi._video_db = db app = Flask(__name__) app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video") client = app.test_client() try: - # Defaults: empty folders, soulseek mode. One shared input + per-type libraries. + # Defaults: empty folders, soulseek mode. Shared input + per-type libraries. assert client.get("/api/video/downloads/config").get_json() == { "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). + # Round-trips: libraries → video.db, the INPUT folder → the SHARED music key. client.post("/api/video/downloads/config", json={"download_path": " /mnt/v/dl ", "movies_path": "/media/movies", "tv_path": "/media/tv", "youtube_path": "/media/yt", @@ -379,7 +393,12 @@ def test_downloads_config_save_load(tmp_path): "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" + # The input folder is the SHARED soulseek.download_path (so music sees it too); + # it is NOT stored in video.db. + assert fake.get("soulseek.download_path") == "/mnt/v/dl" + assert db.get_setting("download_path") is None + # Library paths DO persist to video.db. + assert db.get_setting("movies_path") == "/media/movies" # Legacy single transfer_path migrates into Movies when movies_path is unset. db.set_setting("movies_path", "") db.set_setting("transfer_path", "/old/lib") diff --git a/webui/index.html b/webui/index.html index 94ff63d2..1da6651c 100644 --- a/webui/index.html +++ b/webui/index.html @@ -6020,9 +6020,9 @@ 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.