/
/
/
1"""Test Tidal Streaming Manager."""
2
3from collections.abc import Coroutine
4from sqlite3 import OperationalError
5from typing import Any
6from unittest.mock import AsyncMock, MagicMock, Mock
7
8import pytest
9from music_assistant_models.enums import ContentType, ExternalID, StreamType
10from music_assistant_models.errors import MediaNotFoundError
11from music_assistant_models.media_items import AudioFormat, Track
12
13from music_assistant.providers.tidal.streaming import TidalStreamingManager
14
15
16@pytest.fixture
17def provider_mock() -> Mock:
18 """Return a mock provider."""
19 provider = Mock()
20 provider.domain = "tidal"
21 provider.instance_id = "tidal_instance"
22 provider.config.get_value.return_value = "HIGH"
23 provider.api = AsyncMock()
24 provider.api.OPEN_API_URL = "https://openapi.tidal.com/v2"
25
26 # Mock throttler bypass as async context manager using MagicMock
27 bypass_ctx = MagicMock()
28 bypass_ctx.__aenter__ = AsyncMock(return_value=None)
29 bypass_ctx.__aexit__ = AsyncMock(return_value=None)
30 provider.api.throttler = Mock()
31 provider.api.throttler.bypass = Mock(return_value=bypass_ctx)
32
33 provider.get_track = AsyncMock()
34
35 # Mock mass
36 provider.mass = Mock()
37 provider.mass.cache.get = AsyncMock(return_value=None)
38 provider.mass.cache.set = AsyncMock()
39 provider.mass.cache.delete = AsyncMock()
40 provider.mass.music.tracks.get_library_item_by_prov_id = AsyncMock(return_value=None)
41
42 return provider
43
44
45@pytest.fixture
46def streaming_manager(provider_mock: Mock) -> TidalStreamingManager:
47 """Return a TidalStreamingManager instance."""
48 return TidalStreamingManager(provider_mock)
49
50
51@pytest.fixture
52def mock_track() -> Mock:
53 """Return a mock track."""
54 track = Mock(spec=Track)
55 track.item_id = "123"
56 track.duration = 180
57 return track
58
59
60async def test_get_stream_details_lossless(
61 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
62) -> None:
63 """Test get_stream_details with LOSSLESS quality."""
64 provider_mock.get_track.return_value = mock_track
65 provider_mock.api.get.return_value = {
66 "manifestMimeType": "application/vnd.tidal.bts",
67 "urls": ["https://example.com/stream.flac"],
68 "audioQuality": "LOSSLESS",
69 "sampleRate": 44100,
70 "bitDepth": 16,
71 }
72
73 stream_details = await streaming_manager.get_stream_details("123")
74
75 assert stream_details.item_id == "123"
76 assert stream_details.provider == "tidal_instance"
77 assert stream_details.audio_format.content_type == ContentType.FLAC
78 assert stream_details.audio_format.sample_rate == 44100
79 assert stream_details.audio_format.bit_depth == 16
80 assert stream_details.stream_type == StreamType.HTTP
81 assert stream_details.path == "https://example.com/stream.flac"
82 assert stream_details.can_seek is True
83
84 provider_mock.get_track.assert_called_with("123")
85 provider_mock.api.get.assert_called_with(
86 "tracks/123/playbackinfopostpaywall",
87 params={
88 "playbackmode": "STREAM",
89 "assetpresentation": "FULL",
90 "audioquality": "HIGH",
91 },
92 )
93
94
95async def test_get_stream_details_hires(
96 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
97) -> None:
98 """Test get_stream_details with HIRES_LOSSLESS quality."""
99 provider_mock.get_track.return_value = mock_track
100 provider_mock.api.get.return_value = {
101 "urls": ["https://example.com/stream.flac"],
102 "audioQuality": "HIRES_LOSSLESS",
103 "sampleRate": 96000,
104 "bitDepth": 24,
105 }
106
107 stream_details = await streaming_manager.get_stream_details("123")
108
109 assert stream_details.audio_format.content_type == ContentType.FLAC
110 assert stream_details.audio_format.sample_rate == 96000
111 assert stream_details.audio_format.bit_depth == 24
112
113
114async def test_get_stream_details_with_dash_manifest(
115 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
116) -> None:
117 """Test get_stream_details with DASH manifest served via HTTP route."""
118 provider_mock.get_track.return_value = mock_track
119 provider_mock.api.get.return_value = {
120 "manifestMimeType": "application/dash+xml",
121 "manifest": "bWFuaWZlc3REYXRh",
122 "audioQuality": "HIGH",
123 "sampleRate": 44100,
124 "bitDepth": 16,
125 }
126 # Mock the stream server's dynamic route registration
127 provider_mock.mass.streams.register_dynamic_route = Mock(return_value=lambda: None)
128 provider_mock.mass.streams.base_url = "http://localhost:8097"
129
130 stream_details = await streaming_manager.get_stream_details("123")
131
132 assert isinstance(stream_details.path, str)
133 assert stream_details.path.startswith("http://localhost:8097/tidal-dash/")
134 assert "base64" not in stream_details.path
135 # Verify the route was registered
136 provider_mock.mass.streams.register_dynamic_route.assert_called_once()
137 # Verify cleanup was scheduled with duration-based TTL (180s track + 300s buffer = 480s)
138 provider_mock.mass.call_later.assert_called_with(
139 480,
140 streaming_manager._remove_dash_route,
141 "/tidal-dash/a3aca34e43c1737c7738507842972fa9",
142 task_id="tidal-dash-cleanup-a3aca34e43c1737c7738507842972fa9",
143 )
144
145
146async def test_get_stream_details_with_dash_manifest_handler(
147 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
148) -> None:
149 """Test the DASH manifest handler returns correct body and content type."""
150 provider_mock.get_track.return_value = mock_track
151 provider_mock.api.get.return_value = {
152 "manifestMimeType": "application/dash+xml",
153 "manifest": "bWFuaWZlc3REYXRh",
154 "audioQuality": "HIGH",
155 "sampleRate": 44100,
156 "bitDepth": 16,
157 }
158 # Capture the registered handler
159 register_mock = Mock()
160 provider_mock.mass.streams.register_dynamic_route = register_mock
161 provider_mock.mass.streams.base_url = "http://localhost:8097"
162
163 await streaming_manager.get_stream_details("123")
164
165 # Extract the handler that was registered as second argument
166 handler = register_mock.call_args[0][1]
167 response = await handler(Mock())
168
169 assert response.body == b"manifestData" # base64 decoded
170 assert response.content_type == "application/dash+xml"
171 # Verify Cache-Control header is set to prevent proxy caching
172 assert response.headers.get("Cache-Control") == "no-cache"
173
174
175async def test_get_stream_details_with_dash_manifest_duplicate_registration(
176 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
177) -> None:
178 """Test cleanup is still scheduled when reusing an already-registered route."""
179 provider_mock.get_track.return_value = mock_track
180 provider_mock.api.get.return_value = {
181 "manifestMimeType": "application/dash+xml",
182 "manifest": "bWFuaWZlc3REYXRh",
183 "audioQuality": "HIGH",
184 "sampleRate": 44100,
185 "bitDepth": 16,
186 }
187 # First call succeeds, second raises RuntimeError (duplicate route)
188 provider_mock.mass.streams.register_dynamic_route = Mock(
189 side_effect=[None, RuntimeError("duplicate")]
190 )
191 provider_mock.mass.streams.base_url = "http://localhost:8097"
192 # call_later must count calls
193 call_later_mock = Mock()
194 provider_mock.mass.call_later = call_later_mock
195
196 # Call get_stream_details twice (same track → same manifest hash)
197 await streaming_manager.get_stream_details("123")
198 await streaming_manager.get_stream_details("123")
199
200 # route registration was attempted twice
201 assert provider_mock.mass.streams.register_dynamic_route.call_count == 2
202 # cleanup was scheduled both times (not skipped on duplicate)
203 assert call_later_mock.call_count == 2
204
205
206async def test_remove_dash_route(
207 streaming_manager: TidalStreamingManager, provider_mock: Mock
208) -> None:
209 """Test _remove_dash_route calls unregister on the stream server."""
210 unregister_mock = Mock()
211 provider_mock.mass.streams.unregister_dynamic_route = unregister_mock
212
213 streaming_manager._remove_dash_route("/tidal-dash/abc123")
214
215 unregister_mock.assert_called_once_with("/tidal-dash/abc123", method="GET")
216
217
218async def test_remove_dash_route_handles_runtime_error(
219 streaming_manager: TidalStreamingManager, provider_mock: Mock
220) -> None:
221 """Test _remove_dash_route silently swallows RuntimeError."""
222 provider_mock.mass.streams.unregister_dynamic_route = Mock(
223 side_effect=RuntimeError("not found")
224 )
225
226 # Should not raise
227 streaming_manager._remove_dash_route("/tidal-dash/abc123")
228
229
230async def test_get_stream_details_with_codec(
231 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
232) -> None:
233 """Test get_stream_details with codec specified."""
234 provider_mock.get_track.return_value = mock_track
235 provider_mock.api.get.return_value = {
236 "urls": ["https://example.com/stream.aac"],
237 "audioQuality": "HIGH",
238 "codec": "AAC",
239 "sampleRate": 44100,
240 "bitDepth": 16,
241 }
242
243 stream_details = await streaming_manager.get_stream_details("123")
244
245 assert stream_details.audio_format.content_type == ContentType.AAC
246
247
248async def test_get_stream_details_defaults_to_mp4(
249 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
250) -> None:
251 """Test get_stream_details defaults to MP4 when no quality/codec."""
252 provider_mock.get_track.return_value = mock_track
253 provider_mock.api.get.return_value = {
254 "urls": ["https://example.com/stream.m4a"],
255 "sampleRate": 44100,
256 "bitDepth": 16,
257 }
258
259 stream_details = await streaming_manager.get_stream_details("123")
260
261 assert stream_details.audio_format.content_type == ContentType.MP4
262
263
264async def test_get_stream_details_no_urls_raises_error(
265 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
266) -> None:
267 """Test get_stream_details raises error when no URLs."""
268 provider_mock.get_track.return_value = mock_track
269 provider_mock.api.get.return_value = {
270 "audioQuality": "HIGH",
271 "sampleRate": 44100,
272 "bitDepth": 16,
273 }
274
275 with pytest.raises(MediaNotFoundError, match="No stream URL found"):
276 await streaming_manager.get_stream_details("123")
277
278
279async def test_get_stream_details_track_not_found_no_isrc(
280 streaming_manager: TidalStreamingManager, provider_mock: Mock
281) -> None:
282 """Test get_stream_details when track not found and no ISRC fallback."""
283 provider_mock.get_track.side_effect = MediaNotFoundError("Track not found")
284 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = None
285
286 with pytest.raises(MediaNotFoundError, match="Track 123 not found"):
287 await streaming_manager.get_stream_details("123")
288
289
290async def test_get_track_by_isrc_from_cache(
291 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
292) -> None:
293 """Test _get_track_by_isrc returns cached result."""
294 provider_mock.mass.cache.get.return_value = "cached_track_456"
295 provider_mock.get_track.return_value = mock_track
296
297 result = await streaming_manager._get_track_by_isrc("123")
298
299 assert result == mock_track
300 provider_mock.mass.cache.get.assert_called_with(
301 "123",
302 provider="tidal_instance",
303 category=2, # CACHE_CATEGORY_ISRC_MAP
304 )
305 provider_mock.get_track.assert_called_with("cached_track_456")
306
307
308async def test_get_track_by_isrc_cache_miss_lookup_success(
309 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
310) -> None:
311 """Test _get_track_by_isrc performs ISRC lookup on cache miss."""
312 # Cache miss
313 provider_mock.mass.cache.get.return_value = None
314
315 # Library item with ISRC
316 lib_track = Mock()
317 lib_track.external_ids = [(ExternalID.ISRC, "US1234567890")]
318 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
319
320 # API lookup
321 provider_mock.api.get.return_value = {"data": [{"id": 456}]}
322
323 # Final track fetch
324 provider_mock.get_track.return_value = mock_track
325
326 result = await streaming_manager._get_track_by_isrc("123")
327
328 assert result == mock_track
329
330 # Verify API call
331 provider_mock.api.get.assert_called_with(
332 "tracks",
333 params={"filter[isrc]": "US1234567890"},
334 base_url=provider_mock.api.OPEN_API_URL,
335 )
336
337 # Verify cache set
338 provider_mock.mass.cache.set.assert_called_with(
339 key="123",
340 data="456",
341 provider="tidal_instance",
342 category=2, # CACHE_CATEGORY_ISRC_MAP
343 persistent=True,
344 expiration=86400 * 90,
345 )
346
347 # Verify final track fetch
348 provider_mock.get_track.assert_called_with("456")
349
350
351async def test_get_track_by_isrc_no_library_item(
352 streaming_manager: TidalStreamingManager, provider_mock: Mock
353) -> None:
354 """Test _get_track_by_isrc returns None when no library item."""
355 provider_mock.mass.cache.get.return_value = None
356 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = None
357
358 result = await streaming_manager._get_track_by_isrc("123")
359
360 assert result is None
361
362
363async def test_get_track_by_isrc_no_isrc_external_id(
364 streaming_manager: TidalStreamingManager, provider_mock: Mock
365) -> None:
366 """Test _get_track_by_isrc returns None when library item has no ISRC."""
367 provider_mock.mass.cache.get.return_value = None
368
369 lib_track = Mock()
370 lib_track.external_ids = [(ExternalID.BARCODE, "some-id")]
371 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
372
373 result = await streaming_manager._get_track_by_isrc("123")
374
375 assert result is None
376
377
378async def test_get_track_by_isrc_api_returns_empty(
379 streaming_manager: TidalStreamingManager, provider_mock: Mock
380) -> None:
381 """Test _get_track_by_isrc returns None when API returns no data."""
382 provider_mock.mass.cache.get.return_value = None
383
384 lib_track = Mock()
385 lib_track.external_ids = [(ExternalID.ISRC, "US1234567890")]
386 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
387
388 provider_mock.api.get.return_value = {"data": []}
389
390 result = await streaming_manager._get_track_by_isrc("123")
391
392 assert result is None
393
394
395async def test_get_track_by_isrc_cached_track_not_found(
396 streaming_manager: TidalStreamingManager, provider_mock: Mock
397) -> None:
398 """Test _get_track_by_isrc deletes cache when cached track not found."""
399 provider_mock.mass.cache.get.return_value = "cached_track_999"
400 provider_mock.get_track.side_effect = MediaNotFoundError("Track not found")
401
402 # Should continue with ISRC lookup
403 lib_track = Mock()
404 lib_track.external_ids = [(ExternalID.ISRC, "US1234567890")]
405 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
406
407 provider_mock.api.get.return_value = {"data": []}
408
409 result = await streaming_manager._get_track_by_isrc("123")
410
411 # Should delete invalid cache entry
412 provider_mock.mass.cache.delete.assert_called_with(
413 "123",
414 provider="tidal_instance",
415 category=2, # CACHE_CATEGORY_ISRC_MAP
416 )
417
418 assert result is None
419
420
421async def test_get_stream_details_with_isrc_fallback(
422 streaming_manager: TidalStreamingManager, provider_mock: Mock, mock_track: Mock
423) -> None:
424 """Test get_stream_details uses ISRC fallback when direct lookup fails."""
425 # Direct lookup fails
426 provider_mock.get_track.side_effect = [
427 MediaNotFoundError("Track not found"), # First call
428 mock_track, # Second call from ISRC lookup
429 mock_track, # Third call for stream details
430 ]
431
432 # ISRC lookup succeeds
433 lib_track = Mock()
434 lib_track.external_ids = [(ExternalID.ISRC, "US1234567890")]
435 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
436
437 provider_mock.api.get.side_effect = [
438 {"data": [{"id": 456}]}, # ISRC lookup
439 { # Stream details
440 "urls": ["https://example.com/stream.flac"],
441 "audioQuality": "LOSSLESS",
442 "sampleRate": 44100,
443 "bitDepth": 16,
444 },
445 ]
446
447 stream_details = await streaming_manager.get_stream_details("123")
448
449 assert stream_details.item_id == "123"
450 assert stream_details.path == "https://example.com/stream.flac"
451
452
453async def test_get_stream_details_schedules_background_mapping_update(
454 streaming_manager: TidalStreamingManager,
455 provider_mock: Mock,
456 mock_track: Mock,
457 monkeypatch: pytest.MonkeyPatch,
458) -> None:
459 """Ensure get_stream_details schedules the background mapping update task."""
460 provider_mock.get_track.return_value = mock_track
461 provider_mock.api.get.return_value = {
462 "urls": ["https://example.com/stream.flac"],
463 "audioQuality": "LOSSLESS",
464 "sampleRate": 44100,
465 "bitDepth": 16,
466 }
467
468 created: list[tuple[str, AudioFormat]] = []
469
470 async def _fake_worker(provider_track_id: str, resolved_audio_format: AudioFormat) -> None:
471 created.append((provider_track_id, resolved_audio_format))
472
473 # Patch the worker method so we can validate the coroutine is created with expected args
474 monkeypatch.setattr(
475 streaming_manager, "_async_update_provider_mapping_audio_format", _fake_worker
476 )
477
478 captured_coros: list[Coroutine[Any, Any, None]] = []
479
480 def _fake_create_task(coro: Coroutine[Any, Any, None]) -> None:
481 # Don't schedule; just capture the coroutine so the test can await it.
482 captured_coros.append(coro)
483
484 provider_mock.mass.create_task = _fake_create_task
485
486 stream_details = await streaming_manager.get_stream_details("123")
487
488 assert len(captured_coros) == 1
489
490 # Execute the captured coroutine (safe because we patched the worker)
491 await captured_coros[0]
492
493 assert created == [("123", stream_details.audio_format)]
494
495
496async def test_async_update_provider_mapping_audio_format_no_library_item(
497 streaming_manager: TidalStreamingManager, provider_mock: Mock
498) -> None:
499 """Ensure no update occurs when no library item is found."""
500 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = None
501 provider_mock.mass.music.tracks.update_provider_mapping = AsyncMock()
502
503 await streaming_manager._async_update_provider_mapping_audio_format(
504 provider_track_id="123",
505 resolved_audio_format=AudioFormat(
506 content_type=ContentType.FLAC, sample_rate=44100, bit_depth=16
507 ),
508 )
509
510 provider_mock.mass.music.tracks.update_provider_mapping.assert_not_called()
511
512
513async def test_async_update_provider_mapping_audio_format_no_mapping(
514 streaming_manager: TidalStreamingManager, provider_mock: Mock
515) -> None:
516 """Ensure no update occurs when no provider mapping is found."""
517 lib_track = Mock()
518 lib_track.item_id = 1
519 lib_track.provider_mappings = set()
520 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
521 provider_mock.mass.music.tracks.update_provider_mapping = AsyncMock()
522
523 await streaming_manager._async_update_provider_mapping_audio_format(
524 provider_track_id="123",
525 resolved_audio_format=AudioFormat(
526 content_type=ContentType.FLAC, sample_rate=44100, bit_depth=16
527 ),
528 )
529
530 provider_mock.mass.music.tracks.update_provider_mapping.assert_not_called()
531
532
533async def test_async_update_provider_mapping_audio_format_same_format_no_update(
534 streaming_manager: TidalStreamingManager, provider_mock: Mock
535) -> None:
536 """Ensure no update occurs when the audio format is unchanged."""
537 fmt = AudioFormat(content_type=ContentType.FLAC, sample_rate=44100, bit_depth=16)
538 mapping = Mock()
539 mapping.provider_instance = provider_mock.instance_id
540 mapping.item_id = "123"
541 mapping.audio_format = fmt
542
543 lib_track = Mock()
544 lib_track.item_id = 1
545 lib_track.provider_mappings = {mapping}
546 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
547 provider_mock.mass.music.tracks.update_provider_mapping = AsyncMock()
548
549 await streaming_manager._async_update_provider_mapping_audio_format(
550 provider_track_id="123",
551 resolved_audio_format=fmt,
552 )
553
554 provider_mock.mass.music.tracks.update_provider_mapping.assert_not_called()
555
556
557async def test_async_update_provider_mapping_audio_format_different_format_updates(
558 streaming_manager: TidalStreamingManager, provider_mock: Mock
559) -> None:
560 """Ensure update occurs when the audio format is different."""
561 old_fmt = AudioFormat(content_type=ContentType.MP4, sample_rate=44100, bit_depth=16)
562 new_fmt = AudioFormat(content_type=ContentType.FLAC, sample_rate=44100, bit_depth=16)
563
564 mapping = Mock()
565 mapping.provider_instance = provider_mock.instance_id
566 mapping.item_id = "123"
567 mapping.audio_format = old_fmt
568
569 lib_track = Mock()
570 lib_track.item_id = 1
571 lib_track.provider_mappings = {mapping}
572 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
573 provider_mock.mass.music.tracks.update_provider_mapping = AsyncMock()
574
575 await streaming_manager._async_update_provider_mapping_audio_format(
576 provider_track_id="123",
577 resolved_audio_format=new_fmt,
578 )
579
580 provider_mock.mass.music.tracks.update_provider_mapping.assert_awaited_once()
581 provider_mock.mass.music.tracks.update_provider_mapping.assert_awaited_with(
582 item_id=1,
583 provider_instance_id=provider_mock.instance_id,
584 provider_item_id="123",
585 audio_format=new_fmt,
586 )
587
588
589async def test_async_update_provider_mapping_audio_format_sqlite_operational_error_logs_debug(
590 streaming_manager: TidalStreamingManager, provider_mock: Mock
591) -> None:
592 """Ensure OperationalError is logged at debug level."""
593 provider_mock.logger = Mock()
594 provider_mock.mass.music.tracks.get_library_item_by_prov_id.side_effect = OperationalError(
595 "database is locked"
596 )
597
598 await streaming_manager._async_update_provider_mapping_audio_format(
599 provider_track_id="123",
600 resolved_audio_format=AudioFormat(
601 content_type=ContentType.FLAC, sample_rate=44100, bit_depth=16
602 ),
603 )
604
605 provider_mock.logger.debug.assert_called()
606
607
608async def test_async_update_provider_mapping_audio_format_unexpected_error_logs_exception(
609 streaming_manager: TidalStreamingManager, provider_mock: Mock
610) -> None:
611 """Ensure unexpected errors are logged at exception level."""
612 provider_mock.logger = Mock()
613
614 lib_track = Mock()
615 lib_track.item_id = 1
616 lib_track.provider_mappings = set()
617 provider_mock.mass.music.tracks.get_library_item_by_prov_id.return_value = lib_track
618
619 # Force an unexpected error after resolving lib_track
620 provider_mock.mass.music.tracks.update_provider_mapping = AsyncMock(
621 side_effect=RuntimeError("boom")
622 )
623
624 # Create a mapping that triggers the update path
625 mapping = Mock()
626 mapping.provider_instance = provider_mock.instance_id
627 mapping.item_id = "123"
628 mapping.audio_format = AudioFormat(
629 content_type=ContentType.MP4, sample_rate=44100, bit_depth=16
630 )
631 lib_track.provider_mappings = {mapping}
632
633 await streaming_manager._async_update_provider_mapping_audio_format(
634 provider_track_id="123",
635 resolved_audio_format=AudioFormat(
636 content_type=ContentType.FLAC, sample_rate=44100, bit_depth=16
637 ),
638 )
639
640 provider_mock.logger.exception.assert_called()
641