/
/
1"""Tests for filesystem provider artist path resolution with stale library paths."""
2
3import os
4from collections.abc import AsyncGenerator
5from pathlib import Path
6from unittest.mock import AsyncMock, MagicMock
7
8import pytest
9from music_assistant_models.media_items import Artist, ProviderMapping
10
11from music_assistant.helpers.tags import AudioTags
12from music_assistant.providers.filesystem_local import LocalFileSystemProvider
13
14INSTANCE_ID = "filesystem_local--test"
15
16ARTIST_FOLDER = "Simone, Nina"
17ALBUM_FOLDER = "1987 Live At Ronnie Scott's"
18TRACK_FILE = "12. My Baby Just Cares for Me.mp3"
19
20
21def _make_tags(artist: str, album: str) -> AudioTags:
22 return AudioTags(
23 raw={},
24 sample_rate=44100,
25 channels=2,
26 bits_per_sample=16,
27 format="mp3",
28 bit_rate=320,
29 duration=240.0,
30 tags={
31 "artist": artist,
32 "albumartist": artist,
33 "album": album,
34 "title": "My Baby Just Cares for Me",
35 "track": "12",
36 },
37 has_cover_image=False,
38 filename=os.path.join(ARTIST_FOLDER, ALBUM_FOLDER, TRACK_FILE),
39 )
40
41
42def _make_provider(base_path: str, lib_artists: list[Artist]) -> LocalFileSystemProvider:
43 provider = LocalFileSystemProvider.__new__(LocalFileSystemProvider)
44 provider.base_path = base_path
45 provider.logger = MagicMock()
46 provider.write_access = False
47 provider.media_content_type = "music"
48 provider.config = MagicMock()
49 provider.config.instance_id = INSTANCE_ID
50 provider.config.get_value = MagicMock(return_value="various_artists")
51 provider.manifest = MagicMock()
52 provider.manifest.domain = "filesystem_local"
53 provider.cache = MagicMock()
54 provider.cache.get = AsyncMock(return_value=None)
55 provider.cache.set = AsyncMock(return_value=None)
56
57 async def iter_library_items(
58 search: str | None = None, # noqa: ARG001
59 provider: str | None = None, # noqa: ARG001
60 ) -> AsyncGenerator[Artist]:
61 for lib_artist in lib_artists:
62 yield lib_artist
63
64 provider.mass = MagicMock()
65 provider.mass.music.artists.iter_library_items = iter_library_items
66 provider.mass.get_provider = MagicMock(return_value=None)
67 provider.mass.create_task = MagicMock()
68 return provider
69
70
71def _lib_artist(name: str, url: str | None) -> Artist:
72 return Artist(
73 item_id="1",
74 provider="library",
75 name=name,
76 provider_mappings={
77 ProviderMapping(
78 item_id=url or name,
79 provider_domain="filesystem_local",
80 provider_instance=INSTANCE_ID,
81 url=url,
82 in_library=True,
83 )
84 },
85 )
86
87
88@pytest.fixture
89def music_tree(tmp_path: Path) -> str:
90 """Create a sort-name style artist folder with one track file."""
91 track_dir = tmp_path / ARTIST_FOLDER / ALBUM_FOLDER
92 track_dir.mkdir(parents=True)
93 (track_dir / TRACK_FILE).write_bytes(b"\x00" * 128)
94 return str(tmp_path)
95
96
97async def test_stale_artist_path_does_not_fail_track_parse(music_tree: str) -> None:
98 """
99 A stale artist path stored in the library must not fail parsing the track.
100
101 Regression test: the artist folder was renamed from display-name style
102 ("Nina Simone") to sort-name style ("Simone, Nina"), but the library still
103 holds the old path in the provider mapping url. Parsing any track by that
104 artist raised FileNotFoundError and aborted the sync for that track.
105 """
106 provider = _make_provider(music_tree, [_lib_artist("Nina Simone", "Nina Simone")])
107 file_item = await provider.resolve(os.path.join(ARTIST_FOLDER, ALBUM_FOLDER, TRACK_FILE))
108
109 track = await provider._parse_track(file_item, _make_tags("Nina Simone", ALBUM_FOLDER))
110
111 assert [a.name for a in track.artists] == ["Nina Simone"]
112
113
114async def test_valid_artist_path_still_resolved(music_tree: str) -> None:
115 """A valid stored artist path keeps being used as the artist's item_id."""
116 provider = _make_provider(music_tree, [_lib_artist("Nina Simone", ARTIST_FOLDER)])
117 file_item = await provider.resolve(os.path.join(ARTIST_FOLDER, ALBUM_FOLDER, TRACK_FILE))
118
119 track = await provider._parse_track(file_item, _make_tags("Nina Simone", ALBUM_FOLDER))
120
121 assert [a.item_id for a in track.artists] == [ARTIST_FOLDER]
122