video downloads (phase 1): video-specific download + transfer folders
Foundation for the isolated video download settings. The Downloads tab is almost entirely music-specific, so on the video side the music download sections are hidden (video-side.css) and a data-video-only 'Video Download Folders' section takes their place — an input (download) and output (transfer/library) folder, stored SEPARATELY from the music soulseek.* paths in video.db's video_settings KV table. - api/video/downloads.py: GET/POST /api/video/downloads/config (download_path, transfer_path), registered in the video blueprint. Imports nothing from music. - video-settings.js: loadDownloads/saveDownloads wired into onPageShown + the video save-button chain. - The shared 'Indexers & Downloaders' tab is left untouched (identical for both). 2 tests (round-trip + the isolation guard). Quality profile, video hybrid, and the shared slskd block are the next phases.
This commit is contained in:
parent
8c2f66bea9
commit
8392de9207
6 changed files with 125 additions and 1 deletions
|
|
@ -50,6 +50,7 @@ def create_video_blueprint() -> Blueprint:
|
|||
from .watchlist import register_routes as reg_watchlist
|
||||
from .wishlist import register_routes as reg_wishlist
|
||||
from .youtube import register_routes as reg_youtube
|
||||
from .downloads import register_routes as reg_downloads
|
||||
reg_dashboard(bp)
|
||||
reg_scan(bp)
|
||||
reg_library(bp)
|
||||
|
|
@ -63,5 +64,6 @@ def create_video_blueprint() -> Blueprint:
|
|||
reg_watchlist(bp)
|
||||
reg_wishlist(bp)
|
||||
reg_youtube(bp)
|
||||
reg_downloads(bp)
|
||||
|
||||
return bp
|
||||
|
|
|
|||
46
api/video/downloads.py
Normal file
46
api/video/downloads.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Video-side download SETTINGS (isolated).
|
||||
|
||||
Persists the video download configuration in video.db's ``video_settings`` KV
|
||||
table — fully separate from the music ``soulseek.*`` paths so the two libraries
|
||||
never share a folder or collide. The actual download fulfillment engine (wishlist
|
||||
→ 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
|
||||
- ``transfer_path`` : output folder finished video files move to (video library)
|
||||
|
||||
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
|
||||
music config_manager and are surfaced on the shared Indexers tab + shared slskd
|
||||
block (a deliberate shared boundary, since they're one physical resource).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import jsonify, request
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def register_routes(bp):
|
||||
@bp.route("/downloads/config", methods=["GET"])
|
||||
def video_downloads_config():
|
||||
from . import get_video_db
|
||||
db = get_video_db()
|
||||
return jsonify({k: db.get_setting(k) or "" for k in _PATH_KEYS})
|
||||
|
||||
@bp.route("/downloads/config", methods=["POST"])
|
||||
def video_downloads_config_save():
|
||||
from . import get_video_db
|
||||
db = get_video_db()
|
||||
body = request.get_json(silent=True) or {}
|
||||
for key in _PATH_KEYS:
|
||||
if key in body:
|
||||
db.set_setting(key, (str(body.get(key) or "")).strip())
|
||||
return jsonify({"status": "saved"})
|
||||
|
|
@ -356,6 +356,29 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
|||
videoapi._video_db = None
|
||||
|
||||
|
||||
def test_downloads_config_save_load(tmp_path):
|
||||
import api.video as videoapi
|
||||
from database.video_database import VideoDatabase
|
||||
|
||||
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 are empty.
|
||||
assert client.get("/api/video/downloads/config").get_json() == {
|
||||
"download_path": "", "transfer_path": ""}
|
||||
# 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"})
|
||||
assert client.get("/api/video/downloads/config").get_json() == {
|
||||
"download_path": "/mnt/v/dl", "transfer_path": "/mnt/v/lib"} # trimmed
|
||||
assert db.get_setting("download_path") == "/mnt/v/dl"
|
||||
finally:
|
||||
videoapi._video_db = None
|
||||
|
||||
|
||||
def test_video_api_imports_nothing_from_music():
|
||||
base = Path(__file__).resolve().parent.parent / "api" / "video"
|
||||
for py in base.glob("*.py"):
|
||||
|
|
|
|||
|
|
@ -6011,6 +6011,23 @@
|
|||
|
||||
<!-- Right Column - Download Settings, Database, Metadata, Logging -->
|
||||
<div class="settings-right-column">
|
||||
<!-- VIDEO Downloads (isolated; shown only on the video side). The
|
||||
music download sections in this tab are hidden on the video side
|
||||
via video-side.css; these data-video-only ones take their place. -->
|
||||
<div class="settings-group" data-stg="downloads" data-video-only>
|
||||
<h3>Video Download Folders</h3>
|
||||
<div class="setting-help-text">
|
||||
Where the video side lands downloads and where it moves finished files. These are SEPARATE from your Music paths — point them at wherever your video library lives.
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Download Folder (Input):</label>
|
||||
<input type="text" id="video-download-path" placeholder="./video_downloads">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Transfer Folder (Output — Video Library):</label>
|
||||
<input type="text" id="video-transfer-path" placeholder="./VideoLibrary">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Download Settings -->
|
||||
<div class="settings-group" data-stg="downloads">
|
||||
<h3>Download Settings</h3>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
var CONFIG_URL = '/api/video/enrichment/config';
|
||||
var SERVER_URL = '/api/video/server';
|
||||
var CONN_URL = '/api/video/server-config';
|
||||
var DOWNLOADS_URL = '/api/video/downloads/config';
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
|
@ -251,6 +252,33 @@
|
|||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
// ── Downloads tab: video-specific input/output folders ──
|
||||
function loadDownloads() {
|
||||
fetch(DOWNLOADS_URL, { headers: { 'Accept': 'application/json' } })
|
||||
.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;
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function saveDownloads(silent) {
|
||||
var dl = document.getElementById('video-download-path');
|
||||
var tr = document.getElementById('video-transfer-path');
|
||||
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 : '',
|
||||
})
|
||||
}).then(function () { if (!silent) toast('Download folders saved', 'success'); })
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function saveKeys(silent) {
|
||||
var t = document.getElementById('tmdb-api-key');
|
||||
var v = document.getElementById('tvdb-api-key');
|
||||
|
|
@ -308,6 +336,7 @@
|
|||
loadConn();
|
||||
load();
|
||||
loadKeys();
|
||||
loadDownloads();
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
|
@ -377,7 +406,7 @@
|
|||
if (!e.target.closest('#save-settings')) return;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
Promise.all([saveConn(true), save(true), saveKeys(true), savePrefs(true)])
|
||||
Promise.all([saveConn(true), save(true), saveKeys(true), savePrefs(true), saveDownloads(true)])
|
||||
.then(function () { toast('Settings saved', 'success'); })
|
||||
.catch(function () { toast('Some settings could not be saved', 'error'); });
|
||||
}, true);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@ body[data-side="video"] [data-stg="library"]:not([data-video-only]) {
|
|||
body[data-side="video"] .settings-group[data-stg="connections"]:not([data-video-only]) {
|
||||
display: none !important;
|
||||
}
|
||||
/* The Downloads tab is almost entirely music-specific (streaming sources, music
|
||||
quality profile, retry logic). Hide the music download sections on the video
|
||||
side; the video-only ones (folders, video hybrid, video quality) replace them.
|
||||
The shared "Indexers & Downloaders" tab is NOT hidden — it's identical for both. */
|
||||
body[data-side="video"] .settings-group[data-stg="downloads"]:not([data-video-only]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── Dashboard header: match music, minus the sweep animation ──────────── */
|
||||
/* The header reuses music's .dashboard-header markup/CSS for an identical look;
|
||||
|
|
|
|||
Loading…
Reference in a new issue