mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-22 17:07:23 +00:00
feat(dl): add --merge-video to merge video language variants
Group selected videos by (resolution, range, codec) and mux each group into one MKV; only language is collapsed, so ranges/codecs/resolutions stay in separate files. Adds --merge-video flag + muxing.merge_video config (global and per-service), docs, and tests.
This commit is contained in:
129
tests/orchestration/test_merge_video.py
Normal file
129
tests/orchestration/test_merge_video.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Tests for ``--merge-video`` track grouping.
|
||||
|
||||
``group_videos_by_variant`` (``unshackle/commands/dl.py``) decides which selected video
|
||||
tracks share one MKV when merge mode is on. The rule: group by ``(resolution, range,
|
||||
codec)`` so only language varies within a file; resolutions, ranges and codecs stay
|
||||
separate. With ``merge=False`` every track is its own group (one file per track).
|
||||
|
||||
These lock the pure grouping unit; the surrounding mux loop is Click-command orchestration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unshackle.commands.dl import group_videos_by_variant
|
||||
from unshackle.core.tracks import Video
|
||||
|
||||
|
||||
def make_video(
|
||||
track_id: str,
|
||||
*,
|
||||
range_: Video.Range,
|
||||
height: int,
|
||||
codec: Video.Codec,
|
||||
language: str = "en",
|
||||
) -> Video:
|
||||
return Video(
|
||||
id_=track_id,
|
||||
url=f"https://example.test/{track_id}.m3u8",
|
||||
language=language,
|
||||
codec=codec,
|
||||
range_=range_,
|
||||
width=int(height * 16 / 9),
|
||||
height=height,
|
||||
bitrate=1_000_000,
|
||||
)
|
||||
|
||||
|
||||
HEVC = Video.Codec.HEVC
|
||||
AVC = Video.Codec.AVC
|
||||
SDR = Video.Range.SDR
|
||||
HDR10 = Video.Range.HDR10
|
||||
DV = Video.Range.DV
|
||||
|
||||
|
||||
def test_merge_collapses_language_only() -> None:
|
||||
"""Same (height, range, codec), different language → one group."""
|
||||
videos = [
|
||||
make_video("en", range_=SDR, height=1080, codec=HEVC, language="en"),
|
||||
make_video("fr", range_=SDR, height=1080, codec=HEVC, language="fr"),
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=True)
|
||||
assert len(groups) == 1
|
||||
assert [v.id for v in groups[0]] == ["en", "fr"]
|
||||
|
||||
|
||||
def test_merge_splits_on_codec() -> None:
|
||||
"""H264 vs H265 of the same resolution+range → separate groups."""
|
||||
videos = [
|
||||
make_video("hevc", range_=SDR, height=1080, codec=HEVC),
|
||||
make_video("avc", range_=SDR, height=1080, codec=AVC),
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=True)
|
||||
assert len(groups) == 2
|
||||
assert all(len(g) == 1 for g in groups)
|
||||
|
||||
|
||||
def test_merge_splits_on_range() -> None:
|
||||
"""SDR vs HDR10 of the same resolution+codec → separate groups."""
|
||||
videos = [
|
||||
make_video("sdr", range_=SDR, height=1080, codec=HEVC),
|
||||
make_video("hdr10", range_=HDR10, height=1080, codec=HEVC),
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=True)
|
||||
assert len(groups) == 2
|
||||
|
||||
|
||||
def test_merge_splits_on_resolution() -> None:
|
||||
"""1080p vs 2160p of the same range+codec → separate groups."""
|
||||
videos = [
|
||||
make_video("1080", range_=SDR, height=1080, codec=HEVC),
|
||||
make_video("2160", range_=SDR, height=2160, codec=HEVC),
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=True)
|
||||
assert len(groups) == 2
|
||||
|
||||
|
||||
def test_merge_multi_range_yields_one_group_per_range() -> None:
|
||||
"""Regression guard: -r HYBRID,DV,HDR10,SDR must never collapse into one file.
|
||||
|
||||
HYBRID is resolved upstream into a DV deliverable plus the requested standalone
|
||||
ranges; here the four selected single-range tracks must stay in four groups.
|
||||
"""
|
||||
videos = [
|
||||
make_video("sdr", range_=SDR, height=2160, codec=HEVC),
|
||||
make_video("hdr10", range_=HDR10, height=2160, codec=HEVC),
|
||||
make_video("dv", range_=DV, height=2160, codec=HEVC),
|
||||
make_video("dv-hybrid", range_=DV, height=1080, codec=HEVC), # different height
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=True)
|
||||
assert len(groups) == 4
|
||||
|
||||
|
||||
def test_no_merge_yields_one_group_per_track() -> None:
|
||||
"""merge=False reproduces today's per-track behaviour exactly."""
|
||||
videos = [
|
||||
make_video("en", range_=SDR, height=1080, codec=HEVC, language="en"),
|
||||
make_video("fr", range_=SDR, height=1080, codec=HEVC, language="fr"),
|
||||
make_video("avc", range_=SDR, height=1080, codec=AVC),
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=False)
|
||||
assert len(groups) == 3
|
||||
assert all(len(g) == 1 for g in groups)
|
||||
|
||||
|
||||
def test_merge_preserves_first_seen_order() -> None:
|
||||
"""Group order follows first-seen track order, for stable output filenames."""
|
||||
videos = [
|
||||
make_video("hevc-en", range_=SDR, height=1080, codec=HEVC, language="en"),
|
||||
make_video("avc-en", range_=SDR, height=1080, codec=AVC, language="en"),
|
||||
make_video("hevc-fr", range_=SDR, height=1080, codec=HEVC, language="fr"),
|
||||
]
|
||||
groups = group_videos_by_variant(videos, merge=True)
|
||||
# HEVC group seen first (and gathers both languages), AVC group second.
|
||||
assert [v.id for v in groups[0]] == ["hevc-en", "hevc-fr"]
|
||||
assert [v.id for v in groups[1]] == ["avc-en"]
|
||||
|
||||
|
||||
def test_empty_input_returns_empty() -> None:
|
||||
assert group_videos_by_variant([], merge=True) == []
|
||||
assert group_videos_by_variant([], merge=False) == []
|
||||
Reference in New Issue
Block a user