Files
unshackle/tests/core/test_music_wanted.py
T
imSp4rky 68c7d95173 refactor(music)!: make Song an ordinary Title on the generic path
Delete the dead grouped-download block, the planner, and the music service hooks; Song now flows through the one download loop, gets tagged via tag_file, and answers -w with {disc}x{track} keys.

BREAKING CHANGE: GROUP_AUDIO_DOWNLOADS, get_music_track_options and get_music_collection_label are gone. A music service returns one Audio track per quality and lets the framework choose.
2026-08-18 15:05:30 -06:00

127 lines
4.0 KiB
Python

"""`-w`/`--wanted` track selection for music.
A song shares the episode key space, its disc reading as the season and its track as the
episode. These check the whole path a user takes: the tokens they type, the keys the
click type makes of them, and the titles the download loop keeps.
"""
from __future__ import annotations
import click
import pytest
from unshackle.commands.dl import title_wanted
from unshackle.core.titles.movie import Movie
from unshackle.core.titles.music import Song
from unshackle.core.utils.click_types import SeasonRange
class DummyService:
pass
def make_song(track: int = 1, disc: int = 1) -> Song:
return Song(
id_=f"track-{disc:02}{track:04}",
service=DummyService,
name=f"Example Track {track}",
artist="Example Artist",
album="Example Album",
track=track,
disc=disc,
year=2025,
album_artist="Example Artist",
)
def make_movie() -> Movie:
return Movie(id_="movie-0001", service=DummyService, name="Example Movie", year=2025)
def select(tokens: str, songs: list[Song]) -> list[int]:
"""Track numbers kept, given what a user types after -w."""
wanted = SeasonRange().convert(tokens)
return [song.track for song in songs if title_wanted(song, wanted)]
def single_disc(count: int = 8) -> list[Song]:
return [make_song(track=n) for n in range(1, count + 1)]
def multi_disc(per_disc: int = 4) -> list[Song]:
return [make_song(track=n, disc=d) for d in (1, 2) for n in range(1, per_disc + 1)]
def test_a_bare_number_takes_one_track():
assert select("3", single_disc()) == [3]
def test_a_range_takes_the_tracks_inside_it():
assert select("2-5", single_disc()) == [2, 3, 4, 5]
def test_a_comma_list_takes_each_track():
assert select("1,3,7", single_disc()) == [1, 3, 7]
def test_a_leading_minus_removes_a_track_from_a_range():
assert select("1-5,-3", single_disc()) == [1, 2, 4, 5]
def test_the_disc_form_takes_one_track_of_that_disc():
songs = multi_disc()
kept = [(s.disc, s.track) for s in songs if title_wanted(s, SeasonRange().convert("2x3"))]
assert kept == [(2, 3)]
def test_the_disc_form_ranges_inside_one_disc():
songs = multi_disc()
kept = [(s.disc, s.track) for s in songs if title_wanted(s, SeasonRange().convert("2x1-2x3"))]
assert kept == [(2, 1), (2, 2), (2, 3)]
def test_a_bare_number_on_a_multi_disc_release_is_disc_one():
"""An omitted disc is disc 1, the number the tracklist shows for a one-disc release."""
songs = multi_disc()
kept = [(s.disc, s.track) for s in songs if title_wanted(s, SeasonRange().convert("1-2"))]
assert kept == [(1, 1), (1, 2)]
def test_discs_mix_in_one_selection():
songs = multi_disc()
kept = [(s.disc, s.track) for s in songs if title_wanted(s, SeasonRange().convert("1,2x4"))]
assert kept == [(1, 1), (2, 4)]
def test_a_selection_that_matches_nothing_keeps_no_track():
assert select("40-45", single_disc()) == []
def test_the_whole_release_downloads_without_a_selection():
songs = single_disc()
assert [s.track for s in songs if title_wanted(s, [])] == [1, 2, 3, 4, 5, 6, 7, 8]
def test_a_movie_stays_selected_whatever_the_tokens_are():
"""A movie has no selection key, so -w must not remove it."""
assert title_wanted(make_movie(), SeasonRange().convert("1-5")) is True
def test_a_range_across_discs_takes_the_rest_of_the_first_disc():
"""Same rule as a season range: the left disc runs to its end before the right one starts."""
songs = multi_disc()
kept = [(s.disc, s.track) for s in songs if title_wanted(s, SeasonRange().convert("1x3-2x2"))]
assert kept == [(1, 3), (1, 4), (2, 1), (2, 2)]
@pytest.mark.parametrize("token", ["1x", "x1", "1x2x3", "1.2"])
def test_bad_music_tokens_fail(token):
with pytest.raises(click.UsageError):
SeasonRange().parse_tokens(token)
def test_song_matches_only_its_own_key():
song = make_song(track=3, disc=2)
assert song.matches_wanted(["2x3"]) is True
assert song.matches_wanted(["1x3", "2x4"]) is False