mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-22 17:07:23 +00:00
feat(music): native music core - shared helpers, album folder template, display cleanup (#125)
* Add native Music core workflow
* feat(music): add shared music core helpers, album folder template, and UX cleanup
Consolidate duplicated music-service logic into core and extend the native music workflow.
- add core/music/extract.py: shared stateless helpers (first_text, first_number, year/duration/name formatting, classify_release_kind, dedupe_track_options, build_music_from_songs) so services stop carrying their own copies
- add core/music/display.py: shared rich rendering (render_track_panel, render_album_header, render_artwork_preview) with TrackRow / MusicHeaderInfo data holders
- export the new helpers from core/music/__init__.py
- add dedicated `albums` folder template kind (output_template.folder.albums) resolving albums -> songs -> "{artist} - {album} ({year})"; whitelist music template variables (album_artist, track_total, disc_total, release_type, genre, explicit, isrc, upc, label)
- fix song filename crash: config.get_output_template(...) did not exist; use config.output_template.get("songs") with a sane default
- strip emojis from music output (renderer, dl.py music branch) to match unshackle UX; remove dead MusicSongPlan import and music_icon logic
- document the albums folder key in unshackle-example.yaml
- add tests for extract, display, and the folder template
---------
Co-authored-by: MrMovies-Dev <MrMovies-Dev@users.noreply.github.com>
This commit is contained in:
93
tests/core/test_music_folder_template.py
Normal file
93
tests/core/test_music_folder_template.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Tests for the dedicated album-folder template (`output_template.folder.albums`)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from unshackle.core.config import Config, config
|
||||
from unshackle.core.titles.music import Song
|
||||
from unshackle.core.utilities import sanitize_filename
|
||||
|
||||
|
||||
class DummyService:
|
||||
pass
|
||||
|
||||
|
||||
class StubMediaInfo:
|
||||
"""Minimal MediaInfo stand-in: the template context only reads track lists."""
|
||||
|
||||
video_tracks: list = []
|
||||
audio_tracks: list = []
|
||||
|
||||
|
||||
def make_song(**overrides) -> Song:
|
||||
kwargs = dict(
|
||||
id_="track-0001",
|
||||
service=DummyService,
|
||||
name="NUEVAYoL",
|
||||
artist="Bad Bunny",
|
||||
album="DeBI TiRAR MaS FOToS",
|
||||
track=1,
|
||||
disc=1,
|
||||
year=2025,
|
||||
album_artist="Bad Bunny",
|
||||
)
|
||||
kwargs.update(overrides)
|
||||
return Song(**kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_folder_config():
|
||||
"""Save/restore the global config's template attributes around each test."""
|
||||
saved = (config.folder_templates, config.folder_template, config.output_template)
|
||||
config.folder_templates = {}
|
||||
config.folder_template = ""
|
||||
config.output_template = {}
|
||||
yield config
|
||||
config.folder_templates, config.folder_template, config.output_template = saved
|
||||
|
||||
|
||||
def test_folder_fallback_when_no_templates(reset_folder_config):
|
||||
song = make_song()
|
||||
result = song.get_filename(StubMediaInfo(), folder=True)
|
||||
assert result == sanitize_filename("Bad Bunny - DeBI TiRAR MaS FOToS (2025)", " ")
|
||||
|
||||
|
||||
def test_albums_template_used(reset_folder_config):
|
||||
reset_folder_config.folder_templates = {"albums": "{album_artist} - {album} ({year})"}
|
||||
result = make_song().get_filename(StubMediaInfo(), folder=True)
|
||||
assert result == sanitize_filename("Bad Bunny - DeBI TiRAR MaS FOToS (2025)", " ")
|
||||
# Album folder must NOT carry per-track info like the song file name does.
|
||||
assert "01" not in result
|
||||
|
||||
|
||||
def test_albums_preferred_over_songs(reset_folder_config):
|
||||
reset_folder_config.folder_templates = {"albums": "AA-{album}", "songs": "SS-{album}"}
|
||||
result = make_song().get_filename(StubMediaInfo(), folder=True)
|
||||
assert result.startswith("AA-")
|
||||
|
||||
|
||||
def test_songs_folder_used_when_no_albums(reset_folder_config):
|
||||
# Backward compatibility: the legacy "songs" folder kind still names the album folder.
|
||||
reset_folder_config.folder_templates = {"songs": "SS-{album}"}
|
||||
result = make_song().get_filename(StubMediaInfo(), folder=True)
|
||||
assert result.startswith("SS-")
|
||||
|
||||
|
||||
def test_validation_accepts_music_variables_and_albums_kind():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error") # any warning becomes a test failure
|
||||
Config(
|
||||
output_template={
|
||||
"songs": "{track_number}. {title}",
|
||||
"folder": {"albums": "{album_artist} - {album} ({year?})"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_validation_warns_on_unknown_folder_kind():
|
||||
with pytest.warns(UserWarning, match="Unknown folder template kind"):
|
||||
# A non-folder key is required so output-template validation actually runs.
|
||||
Config(output_template={"songs": "{track_number}. {title}", "folder": {"bogus": "{album}"}})
|
||||
Reference in New Issue
Block a user