From 8db46df69e257f60ef0db12a070a3d5fa12a7f38 Mon Sep 17 00:00:00 2001 From: Media Date: Wed, 12 Aug 2026 18:18:38 -0500 Subject: [PATCH] fix: enforce Player download height limit --- controller/downloads.py | 11 ++-- tests/test_downloads.py | 123 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 6 deletions(-) diff --git a/controller/downloads.py b/controller/downloads.py index e4bcae9..52da6cd 100644 --- a/controller/downloads.py +++ b/controller/downloads.py @@ -57,6 +57,12 @@ def canonical_playlist_path(media_base: Path, slot: int) -> Path: return canonical_slot_directory(media_base, slot) / "playlist.m3u" +def player_format_selector(max_height: int) -> str: + """Select the best Player media without exceeding its configured height.""" + + return f"bv*[height<={max_height}]+ba/b[height<={max_height}]" + + def title_from_filename(path: Path) -> str: stem = path.stem if " - " in stem: @@ -297,10 +303,7 @@ class DownloadManager: / "%(playlist_index)03d - %(title)s [%(id)s].%(ext)s" ), "-f", - ( - f"bv*[height<={self.config.downloads.max_height}]+ba/" - f"b[height<={self.config.downloads.max_height}]/b" - ), + player_format_selector(self.config.downloads.max_height), url, ] self._run_command( diff --git a/tests/test_downloads.py b/tests/test_downloads.py index 5c04eb9..cb73a49 100644 --- a/tests/test_downloads.py +++ b/tests/test_downloads.py @@ -8,8 +8,14 @@ import unittest from concurrent.futures import Future from pathlib import Path +from yt_dlp import YoutubeDL + from controller.config import ControllerConfig, DownloadConfig, StorageConfig -from controller.downloads import DownloadManager, canonical_playlist_path +from controller.downloads import ( + DownloadManager, + canonical_playlist_path, + player_format_selector, +) from controller.models import SlotProtectedError from controller.store import ControllerStore @@ -169,7 +175,120 @@ class DownloadManagerTests(unittest.TestCase): self.assertTrue(all(not path.exists() for path in working_paths)) self.assertEqual(self.cookie.read_text(encoding="utf-8"), "cookie data") download_arguments = runner.calls[-1][0] - self.assertIn("bv*[height<=720]+ba/b[height<=720]/b", download_arguments) + self.assertIn("bv*[height<=720]+ba/b[height<=720]", download_arguments) + + @staticmethod + def _select_player_format(max_height, formats): + with YoutubeDL({"quiet": True}) as ydl: + selector = ydl.build_format_selector( + player_format_selector(max_height) + ) + selected = list(ydl._select_formats(formats, selector)) + return selected + + def test_player_selector_chooses_separate_true_1080_video_and_audio(self): + formats = [ + { + "format_id": "22", + "url": "https://example.test/720.mp4", + "ext": "mp4", + "height": 720, + "width": 1280, + "vcodec": "avc1.64001f", + "acodec": "mp4a.40.2", + "tbr": 2500, + "filesize": 1, + }, + { + "format_id": "399", + "url": "https://example.test/1080.mp4", + "ext": "mp4", + "height": 1080, + "width": 1920, + "vcodec": "av01.0.08M.08", + "acodec": "none", + "tbr": 4500, + "filesize": 1, + }, + { + "format_id": "251", + "url": "https://example.test/audio.webm", + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "tbr": 160, + "filesize": 1, + }, + ] + + selected = self._select_player_format(1080, formats) + + self.assertEqual(len(selected), 1) + self.assertEqual(selected[0]["height"], 1080) + self.assertEqual( + [item["format_id"] for item in selected[0]["requested_formats"]], + ["399", "251"], + ) + self.assertEqual(selected[0]["vcodec"], "av01.0.08M.08") + self.assertEqual(selected[0]["acodec"], "opus") + + def test_player_selector_preserves_lower_source_resolution(self): + formats = [ + { + "format_id": "18", + "url": "https://example.test/360.mp4", + "ext": "mp4", + "height": 360, + "width": 640, + "vcodec": "avc1.42001e", + "acodec": "mp4a.40.2", + "tbr": 700, + "filesize": 1, + }, + { + "format_id": "22", + "url": "https://example.test/720.mp4", + "ext": "mp4", + "height": 720, + "width": 1280, + "vcodec": "avc1.64001f", + "acodec": "mp4a.40.2", + "tbr": 2500, + "filesize": 1, + }, + ] + + selected = self._select_player_format(1080, formats) + + self.assertEqual(len(selected), 1) + self.assertEqual(selected[0]["format_id"], "22") + self.assertEqual(selected[0]["height"], 720) + + def test_player_selector_never_uses_unfiltered_over_height_fallback(self): + formats = [ + { + "format_id": "401", + "url": "https://example.test/2160.mp4", + "ext": "mp4", + "height": 2160, + "width": 3840, + "vcodec": "av01.0.12M.08", + "acodec": "none", + "tbr": 12000, + "filesize": 1, + }, + { + "format_id": "251", + "url": "https://example.test/audio.webm", + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "tbr": 160, + "filesize": 1, + }, + ] + + self.assertEqual(self._select_player_format(1080, formats), []) def test_worker_failure_is_contained_and_persisted(self): events = []