mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
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.
127 lines
4.0 KiB
Python
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
|