/
/
/
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 provider.sync_running = False
57 provider._sync_nfo_by_dir = {}
58 provider._sync_nfo_index_ready = False
59
60 async def iter_library_items(
61 search: str | None = None, # noqa: ARG001
62 provider: str | None = None, # noqa: ARG001
63 ) -> AsyncGenerator[Artist]:
64 for lib_artist in lib_artists:
65 yield lib_artist
66
67 provider.mass = MagicMock()
68 provider.mass.music.artists.iter_library_items = iter_library_items
69 provider.mass.get_provider = MagicMock(return_value=None)
70 provider.mass.create_task = MagicMock()
71 return provider
72
73
74def _lib_artist(name: str, url: str | None) -> Artist:
75 return Artist(
76 item_id="1",
77 provider="library",
78 name=name,
79 provider_mappings={
80 ProviderMapping(
81 item_id=url or name,
82 provider_domain="filesystem_local",
83 provider_instance=INSTANCE_ID,
84 url=url,
85 in_library=True,
86 )
87 },
88 )
89
90
91@pytest.fixture
92def music_tree(tmp_path: Path) -> str:
93 """Create a sort-name style artist folder with one track file."""
94 track_dir = tmp_path / ARTIST_FOLDER / ALBUM_FOLDER
95 track_dir.mkdir(parents=True)
96 (track_dir / TRACK_FILE).write_bytes(b"\x00" * 128)
97 return str(tmp_path)
98
99
100async def test_stale_artist_path_does_not_fail_track_parse(music_tree: str) -> None:
101 """
102 A stale artist path stored in the library must not fail parsing the track.
103
104 Regression test: the artist folder was renamed from display-name style
105 ("Nina Simone") to sort-name style ("Simone, Nina"), but the library still
106 holds the old path in the provider mapping url. Parsing any track by that
107 artist raised FileNotFoundError and aborted the sync for that track.
108 """
109 provider = _make_provider(music_tree, [_lib_artist("Nina Simone", "Nina Simone")])
110 file_item = await provider.resolve(os.path.join(ARTIST_FOLDER, ALBUM_FOLDER, TRACK_FILE))
111
112 track = await provider._parse_track(file_item, _make_tags("Nina Simone", ALBUM_FOLDER))
113
114 assert [a.name for a in track.artists] == ["Nina Simone"]
115
116
117async def test_valid_artist_path_still_resolved(music_tree: str) -> None:
118 """A valid stored artist path keeps being used as the artist's item_id."""
119 provider = _make_provider(music_tree, [_lib_artist("Nina Simone", ARTIST_FOLDER)])
120 file_item = await provider.resolve(os.path.join(ARTIST_FOLDER, ALBUM_FOLDER, TRACK_FILE))
121
122 track = await provider._parse_track(file_item, _make_tags("Nina Simone", ALBUM_FOLDER))
123
124 assert [a.item_id for a in track.artists] == [ARTIST_FOLDER]
125