soulsync/api/video/downloads.py
BoulderBadgeDad ddde6cba68 video downloads: separate YouTube quality profile (yt-dlp)
Boulder: 'youtube should have its own quality profile — there are not many options.'
Right — YouTube is grabbed with yt-dlp, not scene/p2p releases, so the Radarr-style
ladder/cutoff/rejects/HDR-audio tiers are meaningless. Added a small, separate profile:

  - core/video/youtube_quality.py (pure, isolated): max_resolution ceiling
    (best…360p), video_codec (any/av1/vp9/h264, soft), container (mp4/mkv/webm),
    prefer_60fps, allow_hdr. normalize/load/save → video.db youtube_quality_profile.
  - api/video/downloads.py: GET/POST /downloads/youtube-quality.
  - UI: a 'YouTube Quality' card on the Downloads tab (data-video-only) — resolution
    dropdown + codec/container segmented + 60fps/HDR checks, reusing the vq-* styles.
    Wired into onPageShown + the save-all chain.

The (later-phase) yt-dlp downloader maps these to a format/format_sort selection.
9 new tests green, isolation guard green, ruff clean, JS/HTML balance clean.
2026-06-19 10:45:38 -07:00

111 lines
4.9 KiB
Python

"""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")
# 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.
# Deliberately excludes the music download/transfer PATHS and source mode/quality —
# those are video-specific (stored in video.db). Maps the video field name -> the
# shared config key + default. (config_manager is shared app config, not music code.)
_SLSKD_KEYS = {
"slskd_url": ("soulseek.slskd_url", "http://localhost:5030"),
"api_key": ("soulseek.api_key", ""),
"search_timeout": ("soulseek.search_timeout", 60),
"search_timeout_buffer": ("soulseek.search_timeout_buffer", 15),
"search_min_delay_seconds": ("soulseek.search_min_delay_seconds", 0),
"min_peer_upload_speed": ("soulseek.min_peer_upload_speed", 0),
"max_peer_queue": ("soulseek.max_peer_queue", 0),
"download_timeout": ("soulseek.download_timeout", 600), # seconds (UI shows minutes)
"auto_clear_searches": ("soulseek.auto_clear_searches", True),
}
def register_routes(bp):
@bp.route("/downloads/config", methods=["GET"])
def video_downloads_config():
from . import get_video_db
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}
out.update(load_source(db)) # download_mode + hybrid_order
return jsonify(out)
@bp.route("/downloads/config", methods=["POST"])
def video_downloads_config_save():
from . import get_video_db
from core.video.download_config import save as save_source
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())
save_source(db, body) # download_mode + hybrid_order (validated)
return jsonify({"status": "saved"})
@bp.route("/downloads/quality", methods=["GET"])
def video_quality_profile():
from . import get_video_db
from core.video.quality_profile import load
return jsonify(load(get_video_db()))
@bp.route("/downloads/quality", methods=["POST"])
def video_quality_profile_save():
from . import get_video_db
from core.video.quality_profile import save
body = request.get_json(silent=True) or {}
return jsonify(save(get_video_db(), body))
@bp.route("/downloads/youtube-quality", methods=["GET"])
def video_youtube_quality():
# Separate, smaller profile — YouTube is yt-dlp, not scene/p2p releases.
from . import get_video_db
from core.video.youtube_quality import load
return jsonify(load(get_video_db()))
@bp.route("/downloads/youtube-quality", methods=["POST"])
def video_youtube_quality_save():
from . import get_video_db
from core.video.youtube_quality import save
body = request.get_json(silent=True) or {}
return jsonify(save(get_video_db(), body))
@bp.route("/downloads/slskd", methods=["GET"])
def video_slskd_config():
# SHARED with music — same slskd instance. Reads the app-wide config_manager.
from config.settings import config_manager
return jsonify({k: config_manager.get(cfg, default)
for k, (cfg, default) in _SLSKD_KEYS.items()})
@bp.route("/downloads/slskd", methods=["POST"])
def video_slskd_config_save():
from config.settings import config_manager
body = request.get_json(silent=True) or {}
for k, (cfg, _default) in _SLSKD_KEYS.items():
if k in body:
config_manager.set(cfg, body.get(k))
return jsonify({"status": "saved", "shared": True})