video downloads: share the INPUT folder with music; add libraries to compose
- Input/download folder is now the SAME shared config key the music side uses (soulseek.download_path via config_manager) — change it on either side and both follow, one physical download dir, simpler Docker mounts. Output libraries stay video-specific in video.db. ZERO music code touched (read/write the shared key only). - docker-compose: documented the shared ./downloads input mount, and added the three video library OUTPUT mounts (./Movies→/media/movies, ./TV→/media/tv, ./YouTube→/media/youtube) matching the in-app placeholders. - Test monkeypatches config_manager: asserts the input folder writes the shared soulseek.download_path (music sees it) and is NOT in video.db; libraries persist to video.db; legacy migration intact. ruff + guard + balance clean.
This commit is contained in:
parent
89bedaf140
commit
84fac1f80a
4 changed files with 55 additions and 17 deletions
|
|
@ -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
|
→ search → grab) is a later roadmap phase; these endpoints just store/serve the
|
||||||
config the Settings → Downloads tab edits.
|
config the Settings → Downloads tab edits.
|
||||||
|
|
||||||
Keys persisted here (all under video.db):
|
Folders:
|
||||||
- ``download_path`` : input folder a video download lands in (shared by all types)
|
- INPUT (download) folder is SHARED with the music side — it's the same
|
||||||
- ``movies_path`` : Movies library (finished movies move here)
|
``config_manager`` key the music Download Settings use (``soulseek.download_path``),
|
||||||
- ``tv_path`` : TV Shows library
|
so changing it on either side changes both (one physical download dir, simpler
|
||||||
- ``youtube_path`` : YouTube library (kept separate from real TV)
|
Docker mounts). We only READ/WRITE that shared key; no music code is touched.
|
||||||
The engine routes a finished download to the path matching its type. (Legacy single
|
- OUTPUT (library) folders are video-specific and live in video.db, one per type:
|
||||||
``transfer_path`` is migrated into ``movies_path`` on first read.)
|
``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
|
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
|
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")
|
logger = get_logger("video_api.downloads")
|
||||||
|
|
||||||
# Video-specific path keys (vs. the shared connection settings): one shared input
|
# Video-specific OUTPUT library folders (video.db). The INPUT folder is the shared
|
||||||
# folder + a separate library folder per content type.
|
# music key below — not in this list.
|
||||||
_PATH_KEYS = ("download_path", "movies_path", "tv_path", "youtube_path")
|
_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
|
# 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.
|
# 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():
|
def video_downloads_config():
|
||||||
from . import get_video_db
|
from . import get_video_db
|
||||||
from core.video.download_config import load as load_source
|
from core.video.download_config import load as load_source
|
||||||
|
from config.settings import config_manager
|
||||||
db = get_video_db()
|
db = get_video_db()
|
||||||
out = {k: db.get_setting(k) or "" for k in _PATH_KEYS}
|
out = {k: db.get_setting(k) or "" for k in _PATH_KEYS}
|
||||||
if not out["movies_path"]: # migrate the legacy single transfer folder → Movies
|
if not out["movies_path"]: # migrate the legacy single transfer folder → Movies
|
||||||
out["movies_path"] = db.get_setting("transfer_path") or ""
|
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
|
out.update(load_source(db)) # download_mode + hybrid_order
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
|
|
@ -71,6 +77,9 @@ def register_routes(bp):
|
||||||
for key in _PATH_KEYS:
|
for key in _PATH_KEYS:
|
||||||
if key in body:
|
if key in body:
|
||||||
db.set_setting(key, (str(body.get(key) or "")).strip())
|
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)
|
save_source(db, body) # download_mode + hybrid_order (validated)
|
||||||
return jsonify({"status": "saved"})
|
return jsonify({"status": "saved"})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,20 @@ services:
|
||||||
# Persistent data volumes
|
# Persistent data volumes
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- ./logs:/app/logs
|
- ./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
|
- ./downloads:/app/downloads
|
||||||
- ./Staging:/app/Staging
|
- ./Staging:/app/Staging
|
||||||
- ./MusicVideos:/app/MusicVideos
|
- ./MusicVideos:/app/MusicVideos
|
||||||
- ./scripts:/app/scripts
|
- ./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)
|
# Use named volume for database persistence (separate from host database)
|
||||||
# NOTE: Changed from /app/database to /app/data to avoid overwriting Python package
|
# NOTE: Changed from /app/database to /app/data to avoid overwriting Python package
|
||||||
- soulsync_database:/app/data
|
- soulsync_database:/app/data
|
||||||
|
|
|
||||||
|
|
@ -356,21 +356,35 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
||||||
videoapi._video_db = None
|
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 api.video as videoapi
|
||||||
|
import config.settings as cfg
|
||||||
from database.video_database import VideoDatabase
|
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"))
|
db = VideoDatabase(database_path=str(tmp_path / "video_library.db"))
|
||||||
videoapi._video_db = db
|
videoapi._video_db = db
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video")
|
app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video")
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
try:
|
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() == {
|
assert client.get("/api/video/downloads/config").get_json() == {
|
||||||
"download_path": "", "movies_path": "", "tv_path": "", "youtube_path": "",
|
"download_path": "", "movies_path": "", "tv_path": "", "youtube_path": "",
|
||||||
"download_mode": "soulseek", "hybrid_order": ["soulseek"]}
|
"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",
|
client.post("/api/video/downloads/config",
|
||||||
json={"download_path": " /mnt/v/dl ", "movies_path": "/media/movies",
|
json={"download_path": " /mnt/v/dl ", "movies_path": "/media/movies",
|
||||||
"tv_path": "/media/tv", "youtube_path": "/media/yt",
|
"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
|
"download_path": "/mnt/v/dl", "movies_path": "/media/movies", # trimmed
|
||||||
"tv_path": "/media/tv", "youtube_path": "/media/yt",
|
"tv_path": "/media/tv", "youtube_path": "/media/yt",
|
||||||
"download_mode": "hybrid", "hybrid_order": ["torrent", "usenet"]}
|
"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.
|
# Legacy single transfer_path migrates into Movies when movies_path is unset.
|
||||||
db.set_setting("movies_path", "")
|
db.set_setting("movies_path", "")
|
||||||
db.set_setting("transfer_path", "/old/lib")
|
db.set_setting("transfer_path", "/old/lib")
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Download Folder (Input — shared):</label>
|
<label>Download Folder (Input — shared with Music):</label>
|
||||||
<input type="text" id="video-download-path" placeholder="./video_downloads">
|
<input type="text" id="video-download-path" placeholder="./downloads">
|
||||||
<div class="setting-help-text">Where downloads land. Point this at the same folder as your slskd / torrent client.</div>
|
<div class="setting-help-text">Where downloads land — point this at the same folder as your slskd / torrent client. This is the SAME folder as the Music side; changing it here changes it there too.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>🎬 Movies Library:</label>
|
<label>🎬 Movies Library:</label>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue