fix: enforce Player download height limit
This commit is contained in:
@@ -57,6 +57,12 @@ def canonical_playlist_path(media_base: Path, slot: int) -> Path:
|
|||||||
return canonical_slot_directory(media_base, slot) / "playlist.m3u"
|
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:
|
def title_from_filename(path: Path) -> str:
|
||||||
stem = path.stem
|
stem = path.stem
|
||||||
if " - " in stem:
|
if " - " in stem:
|
||||||
@@ -297,10 +303,7 @@ class DownloadManager:
|
|||||||
/ "%(playlist_index)03d - %(title)s [%(id)s].%(ext)s"
|
/ "%(playlist_index)03d - %(title)s [%(id)s].%(ext)s"
|
||||||
),
|
),
|
||||||
"-f",
|
"-f",
|
||||||
(
|
player_format_selector(self.config.downloads.max_height),
|
||||||
f"bv*[height<={self.config.downloads.max_height}]+ba/"
|
|
||||||
f"b[height<={self.config.downloads.max_height}]/b"
|
|
||||||
),
|
|
||||||
url,
|
url,
|
||||||
]
|
]
|
||||||
self._run_command(
|
self._run_command(
|
||||||
|
|||||||
+121
-2
@@ -8,8 +8,14 @@ import unittest
|
|||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from yt_dlp import YoutubeDL
|
||||||
|
|
||||||
from controller.config import ControllerConfig, DownloadConfig, StorageConfig
|
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.models import SlotProtectedError
|
||||||
from controller.store import ControllerStore
|
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.assertTrue(all(not path.exists() for path in working_paths))
|
||||||
self.assertEqual(self.cookie.read_text(encoding="utf-8"), "cookie data")
|
self.assertEqual(self.cookie.read_text(encoding="utf-8"), "cookie data")
|
||||||
download_arguments = runner.calls[-1][0]
|
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):
|
def test_worker_failure_is_contained_and_persisted(self):
|
||||||
events = []
|
events = []
|
||||||
|
|||||||
Reference in New Issue
Block a user