/
/
/
1"""Tests for CUE sheet parser."""
2
3from music_assistant.helpers.cue_sheet import parse_cue_sheet
4
5SAMPLE_CUE = """\
6REM GENRE Rock
7REM DATE 1995
8REM MUSICBRAINZ_ALBUMID 4591f427-4632-474c-9f2d-b4f009e53096
9PERFORMER "The Artist"
10TITLE "The Album"
11FILE "album.flac" WAVE
12 TRACK 01 AUDIO
13 TITLE "First Track"
14 PERFORMER "The Artist"
15 ISRC USRC17607839
16 REM MUSICBRAINZ_TRACKID d5c4a1b0-1234-5678-9abc-def012345678
17 INDEX 01 00:00:00
18 TRACK 02 AUDIO
19 TITLE "Second Track"
20 PERFORMER "Featured Artist"
21 INDEX 00 03:45:50
22 INDEX 01 03:46:00
23 TRACK 03 AUDIO
24 TITLE "Third Track"
25 INDEX 01 07:30:25
26"""
27
28
29def test_parse_basic_cue_sheet() -> None:
30 """Test parsing a standard CUE sheet."""
31 result = parse_cue_sheet(SAMPLE_CUE)
32
33 assert result.title == "The Album"
34 assert result.performers == ["The Artist"]
35 assert result.file_path == "album.flac"
36 assert result.date == "1995"
37 assert result.genres == ["Rock"]
38 assert result.musicbrainz_albumid == "4591f427-4632-474c-9f2d-b4f009e53096"
39 assert len(result.tracks) == 3
40
41
42def test_parse_track_metadata() -> None:
43 """Test track-level metadata parsing."""
44 result = parse_cue_sheet(SAMPLE_CUE)
45
46 track1 = result.tracks[0]
47 assert track1.number == 1
48 assert track1.title == "First Track"
49 assert track1.performers == ["The Artist"]
50 assert track1.isrcs == ["USRC17607839"]
51 # REM MUSICBRAINZ_TRACKID follows Picard's %musicbrainz_trackid% (release-track MBID)
52 assert track1.musicbrainz_releasetrackid == "d5c4a1b0-1234-5678-9abc-def012345678"
53
54 track2 = result.tracks[1]
55 assert track2.number == 2
56 assert track2.title == "Second Track"
57 assert track2.performers == ["Featured Artist"]
58
59
60def test_parse_timestamps() -> None:
61 """Test CUE timestamp to seconds conversion."""
62 result = parse_cue_sheet(SAMPLE_CUE)
63
64 # Track 1: INDEX 01 00:00:00 = 0.0 seconds
65 assert result.tracks[0].start_position == 0.0
66
67 # Track 2: INDEX 01 03:46:00 = 3*60 + 46 + 0/75 = 226.0 seconds
68 # (INDEX 00 at 03:45:50 should be ignored, INDEX 01 is used)
69 assert result.tracks[1].start_position == 226.0
70
71 # Track 3: INDEX 01 07:30:25 = 7*60 + 30 + 25/75 = 450.333... seconds
72 assert abs(result.tracks[2].start_position - (7 * 60 + 30 + 25 / 75)) < 0.001
73
74
75def test_parse_empty_cue() -> None:
76 """Test parsing an empty CUE sheet."""
77 result = parse_cue_sheet("")
78 assert result.tracks == []
79 assert result.title is None
80 assert result.file_path is None
81
82
83def test_parse_minimal_cue() -> None:
84 """Test parsing a minimal CUE sheet with just tracks."""
85 cue = """\
86FILE "music.mp3" MP3
87 TRACK 01 AUDIO
88 INDEX 01 00:00:00
89 TRACK 02 AUDIO
90 INDEX 01 05:00:00
91"""
92 result = parse_cue_sheet(cue)
93
94 assert result.file_path == "music.mp3"
95 assert len(result.tracks) == 2
96 assert result.tracks[0].number == 1
97 assert result.tracks[0].title is None
98 assert result.tracks[0].start_position == 0.0
99 assert result.tracks[1].number == 2
100 assert result.tracks[1].start_position == 300.0 # 5 minutes
101
102
103def test_parse_unquoted_filename() -> None:
104 """Test parsing FILE command with unquoted filename."""
105 cue = """\
106FILE music.flac WAVE
107 TRACK 01 AUDIO
108 INDEX 01 00:00:00
109"""
110 result = parse_cue_sheet(cue)
111 assert result.file_path == "music.flac"
112
113
114def test_parse_no_performer() -> None:
115 """Test parsing CUE sheet without PERFORMER tags."""
116 cue = """\
117TITLE "My Album"
118FILE "album.flac" WAVE
119 TRACK 01 AUDIO
120 TITLE "Song One"
121 INDEX 01 00:00:00
122"""
123 result = parse_cue_sheet(cue)
124 assert result.performers == []
125 assert result.tracks[0].performers == []
126 assert result.tracks[0].title == "Song One"
127
128
129def test_track_without_title_defaults_none() -> None:
130 """Test that tracks without TITLE have None as title."""
131 cue = """\
132FILE "album.flac" WAVE
133 TRACK 01 AUDIO
134 INDEX 01 00:00:00
135"""
136 result = parse_cue_sheet(cue)
137 assert result.tracks[0].title is None
138
139
140def test_rem_lines_at_track_level() -> None:
141 """REM MBIDs land in their distinct fields (Picard variable naming)."""
142 cue = """\
143FILE "album.flac" WAVE
144 TRACK 01 AUDIO
145 TITLE "Track One"
146 REM MUSICBRAINZ_TRACKID rtrk-123
147 REM MUSICBRAINZ_RECORDINGID rec-456
148 ISRC GBAYE0000351
149 INDEX 01 00:00:00
150"""
151 result = parse_cue_sheet(cue)
152 # MUSICBRAINZ_TRACKID follows %musicbrainz_trackid% = release-track MBID;
153 # MUSICBRAINZ_RECORDINGID = recording MBID
154 assert result.tracks[0].musicbrainz_releasetrackid == "rtrk-123"
155 assert result.tracks[0].musicbrainz_recordingid == "rec-456"
156 assert result.tracks[0].isrcs == ["GBAYE0000351"]
157
158
159def test_multi_line_vorbis_style_fields() -> None:
160 """Repeated PERFORMER / ISRC / REM lines accumulate into lists."""
161 cue = """\
162PERFORMER "Label Artist A"
163PERFORMER "Label Artist B"
164REM GENRE "Rock"
165REM GENRE "Pop"
166REM MUSICBRAINZ_ALBUMARTISTID aa-1
167REM MUSICBRAINZ_ALBUMARTISTID aa-2
168REM ALBUMARTISTSORT "Artist A, Label"
169REM ALBUMARTISTSORT "Artist B, Label"
170REM RELEASETYPE album
171REM RELEASETYPE compilation
172TITLE "Album"
173FILE "album.flac" WAVE
174 TRACK 01 AUDIO
175 TITLE "T1"
176 PERFORMER "Duo Member One"
177 PERFORMER "Duo Member Two"
178 ISRC USRC0000001
179 ISRC USRC0000002
180 REM ARTISTSORT "Member One, Duo"
181 REM ARTISTSORT "Member Two, Duo"
182 REM MUSICBRAINZ_ARTISTID mb-1
183 REM MUSICBRAINZ_ARTISTID mb-2
184 REM GENRE "Folk"
185 REM GENRE "Blues"
186 INDEX 01 00:00:00
187"""
188 result = parse_cue_sheet(cue)
189 assert result.performers == ["Label Artist A", "Label Artist B"]
190 assert result.genres == ["Rock", "Pop"]
191 assert result.musicbrainz_albumartistids == ["aa-1", "aa-2"]
192 assert result.album_artist_sort_names == ["Artist A, Label", "Artist B, Label"]
193 assert result.album_types == ["album", "compilation"]
194
195 track = result.tracks[0]
196 assert track.performers == ["Duo Member One", "Duo Member Two"]
197 assert track.isrcs == ["USRC0000001", "USRC0000002"]
198 assert track.artist_sort_names == ["Member One, Duo", "Member Two, Duo"]
199 assert track.musicbrainz_artistids == ["mb-1", "mb-2"]
200 assert track.genres == ["Folk", "Blues"]
201
202
203def test_additional_single_value_rem_fields() -> None:
204 """REM TITLESORT / COPYRIGHT / GROUPING / COMMENT / ITUNESADVISORY + CATALOG."""
205 cue = """\
206CATALOG 0123456789012
207TITLE "Album"
208REM ALBUMSORT "Album, The"
209REM MUSICBRAINZ_RELEASEGROUPID rg-uuid
210FILE "album.flac" WAVE
211 TRACK 01 AUDIO
212 TITLE "Song, The"
213 REM TITLESORT "Song, The"
214 REM COPYRIGHT "(c) 2024 Label"
215 REM GROUPING "Movement I"
216 REM COMMENT "Live at Wembley"
217 REM ITUNESADVISORY 1
218 INDEX 01 00:00:00
219"""
220 result = parse_cue_sheet(cue)
221 assert result.sort_title == "Album, The"
222 assert result.barcode == "0123456789012"
223 assert result.musicbrainz_releasegroupid == "rg-uuid"
224
225 track = result.tracks[0]
226 assert track.sort_name == "Song, The"
227 assert track.copyright == "(c) 2024 Label"
228 assert track.grouping == "Movement I"
229 assert track.comment == "Live at Wembley"
230 assert track.explicit is True
231
232
233def test_parse_top_level_genre() -> None:
234 """Top-level GENRE lands in the same bucket as REM GENRE at both scopes."""
235 cue = """\
236GENRE "Jazz"
237TITLE "Album"
238FILE "album.flac" WAVE
239 TRACK 01 AUDIO
240 TITLE "Song"
241 GENRE "Fusion"
242 INDEX 01 00:00:00
243"""
244 result = parse_cue_sheet(cue)
245 assert result.genres == ["Jazz"]
246 assert result.tracks[0].genres == ["Fusion"]
247
248
249def test_parse_mixed_top_level_and_rem_genre() -> None:
250 """Top-level GENRE and REM GENRE both append to the same list."""
251 cue = """\
252GENRE "Rock"
253REM GENRE "Progressive"
254TITLE "Album"
255FILE "album.flac" WAVE
256 TRACK 01 AUDIO
257 TITLE "Song"
258 INDEX 01 00:00:00
259"""
260 result = parse_cue_sheet(cue)
261 assert result.genres == ["Rock", "Progressive"]
262