/
/
1"""Tests for LocalFileSystemProvider._parse_playlist_line path resolution."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6from unittest.mock import AsyncMock, MagicMock
7
8import pytest
9
10from music_assistant.providers.filesystem_local import LocalFileSystemProvider
11
12if TYPE_CHECKING:
13 from pathlib import Path
14
15
16@pytest.fixture
17def music_dir(tmp_path: Path) -> Path:
18 """Create a music folder with a playlist next to an artist folder."""
19 (tmp_path / "Playlists").mkdir()
20 (tmp_path / "Playlists" / "list.m3u").write_text("../Artist/track.mp3\n")
21 (tmp_path / "Artist").mkdir()
22 (tmp_path / "Artist" / "track.mp3").write_bytes(b"audio-bytes")
23 return tmp_path
24
25
26@pytest.fixture
27def provider(
28 music_dir: Path, monkeypatch: pytest.MonkeyPatch
29) -> tuple[LocalFileSystemProvider, MagicMock]:
30 """Create a provider on the music folder with tag parsing and track building stubbed."""
31 provider = LocalFileSystemProvider.__new__(LocalFileSystemProvider)
32 provider.base_path = str(music_dir)
33 provider.logger = MagicMock()
34 monkeypatch.setattr(
35 "music_assistant.providers.filesystem_local.async_parse_tags",
36 AsyncMock(return_value=MagicMock()),
37 )
38 track = MagicMock(name="track")
39 provider._parse_track = AsyncMock(return_value=track) # type: ignore[method-assign]
40 return provider, track
41
42
43async def test_line_relative_to_playlist_folder(
44 provider: tuple[LocalFileSystemProvider, MagicMock],
45) -> None:
46 """A ../ line resolves against the playlist's own folder."""
47 fs_provider, track = provider
48
49 result = await fs_provider._parse_playlist_line("../Artist/track.mp3", "Playlists")
50
51 assert result is track
52 file_item = fs_provider._parse_track.await_args.args[0] # type: ignore[attr-defined]
53 assert file_item.relative_path == "Artist/track.mp3"
54
55
56async def test_line_relative_to_base_path(
57 provider: tuple[LocalFileSystemProvider, MagicMock],
58) -> None:
59 """A base-path-relative line resolves after the playlist-folder candidate misses."""
60 fs_provider, track = provider
61
62 assert await fs_provider._parse_playlist_line("Artist/track.mp3", "Playlists") is track
63
64
65async def test_absolute_line(
66 provider: tuple[LocalFileSystemProvider, MagicMock], music_dir: Path
67) -> None:
68 """An absolute path in the playlist still resolves."""
69 fs_provider, track = provider
70 absolute_line = str(music_dir / "Artist" / "track.mp3")
71
72 assert await fs_provider._parse_playlist_line(absolute_line, "Playlists") is track
73
74
75async def test_file_uri_line(
76 provider: tuple[LocalFileSystemProvider, MagicMock], music_dir: Path
77) -> None:
78 """A file:// uri with url-encoded characters resolves."""
79 fs_provider, track = provider
80 line = f"file://{music_dir}/Artist/track%2Emp3"
81
82 assert await fs_provider._parse_playlist_line(line, "Playlists") is track
83
84
85async def test_missing_file_returns_none(
86 provider: tuple[LocalFileSystemProvider, MagicMock],
87) -> None:
88 """A line pointing at a nonexistent file logs a warning instead of raising."""
89 fs_provider, _ = provider
90
91 assert await fs_provider._parse_playlist_line("Missing/nope.mp3", "Playlists") is None
92 fs_provider.logger.warning.assert_called_once() # type: ignore[attr-defined]
93