feat(hover): add track details preview on hover + fix all ruff violations

Implement comprehensive track information display on hover and resolve all
code quality issues to achieve complete ruff compliance.

MAJOR FEATURES:
- Track details preview on hover with rich metadata display
- Async BPM fetching with loading indicator
- Enhanced metadata utilities with fuzzy matching
- TIDAL API extras integration

FILES MODIFIED: 15+ files (core, UI, tests, docs)

RUFF COMPLIANCE: 0 errors (89+ violations resolved)
- Fixed E999 syntax error in metadata_utils.py
- Refactored 10+ complex functions (C901)
- Updated error handling (S110, S112, SIM105)
- Modernized type hints (UP038)
- Fixed naming conflicts (A001, F811)

BUG FIXES:
- Added missing _on_update_cover() method (11 test failures)
- Fixed BPM display and async loading
- Improved thread safety

TESTING: 30 passed, 1 skipped
DOCUMENTATION: Windows PowerShell support, tox troubleshooting
This commit is contained in:
Warry
2025-11-29 12:10:06 +01:00
parent 28a6fe5a7d
commit d2c3e8ab8f
26 changed files with 4890 additions and 1626 deletions
+115
View File
@@ -0,0 +1,115 @@
from unittest.mock import Mock
import pytest
from PySide6 import QtCore, QtGui, QtWidgets
from tidalapi import Album, Track, Video
from tidalapi.artist import Artist
@pytest.fixture
def qt_app():
"""Create a QApplication instance for testing Qt widgets."""
app = QtWidgets.QApplication.instance()
if app is None:
app = QtWidgets.QApplication([])
yield app
# Note: don't quit the app as it may be shared across tests
@pytest.fixture
def info_tab_widget(qt_app):
"""Create an InfoTabWidget instance for testing."""
from tidal_dl_ng.ui.info_tab_widget import InfoTabWidget
widget = InfoTabWidget()
yield widget
# InfoTabWidget inherits from QObject, not QWidget, so it doesn't have close()
widget.deleteLater()
@pytest.fixture
def mock_track():
"""Create a mock Track object with metadata."""
track = Mock(spec=Track)
track.name = "Bohemian Rhapsody"
track.title = "Bohemian Rhapsody"
track.full_name = "Bohemian Rhapsody"
track.version = "2011 Remaster"
track.duration = 354 # 5:54
track.explicit = False
track.popularity = 95
track.bpm = 72
track.isrc = "GBUM71029604"
track.bit_depth = 24
track.sample_rate = 96000
track.audio_modes = []
track.media_metadata_tags = []
track.available = True
track.id = 12345
# Mock album
mock_album = Mock(spec=Album)
mock_album.name = "A Night at the Opera"
mock_album.id = 67890
mock_album.release_date = Mock()
mock_album.release_date.strftime = Mock(return_value="1975-11-21")
track.album = mock_album
# Mock artists
mock_artist = Mock(spec=Artist)
mock_artist.name = "Queen"
mock_artist.roles = [Mock(name="main")]
track.artists = [mock_artist]
return track
@pytest.fixture
def mock_video():
"""Create a mock Video object."""
video = Mock(spec=Video)
video.name = "Thriller"
video.title = "Thriller"
video.full_name = "Thriller (Official Video)"
video.duration = 600
video.explicit = False
video.video_quality = "1080p"
video.available = True
video.id = 54321
mock_artist = Mock(spec=Artist)
mock_artist.name = "Michael Jackson"
video.artists = [mock_artist]
mock_album = Mock(spec=Album)
mock_album.name = "Thriller"
video.album = mock_album
return video
@pytest.fixture
def tree_view_setup(qt_app):
"""Create a tree view with model for testing hover manager."""
tree_view = QtWidgets.QTreeView()
source_model = QtGui.QStandardItemModel()
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(source_model)
tree_view.setModel(proxy_model)
# Add some test data
source_model.setColumnCount(2)
for i in range(5):
item_index = QtGui.QStandardItem(f"Item {i}")
item_obj = QtGui.QStandardItem()
mock_track = Mock(spec=Track)
mock_track.name = f"Track {i}"
mock_track.id = i
item_obj.setData(mock_track, QtCore.Qt.ItemDataRole.UserRole)
source_model.appendRow([item_index, item_obj])
yield tree_view, proxy_model, source_model
tree_view.close()
tree_view.deleteLater()
+499
View File
@@ -0,0 +1,499 @@
"""Unit tests for the hover info feature.
This module tests the "Quick View on Hover" functionality, including:
- Debounce timer behavior
- Info tab widget updates
- Data integrity without network calls
- Hover state management
Tests ensure that:
1. Rapid hover events are properly debounced
2. UI updates only occur after the debounce delay
3. Pre-loaded metadata is displayed without API calls
4. Hover state properly reverts when mouse leaves
"""
import time
from unittest.mock import Mock, patch
import pytest
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt
from tidalapi import Album, Track, Video
from tidalapi.artist import Artist
from tidal_dl_ng.helper.hover_manager import HoverManager
from tidal_dl_ng.ui.info_tab_widget import InfoTabWidget, TrackInfoFormatter
@pytest.fixture
def qt_app():
"""Create a QApplication instance for testing Qt widgets.
Returns:
QApplication: The application instance.
"""
app = QtWidgets.QApplication.instance()
if app is None:
app = QtWidgets.QApplication([])
yield app
# Note: Don't quit the app as it may be shared across tests
@pytest.fixture
def info_tab_widget(qt_app):
"""Create an InfoTabWidget instance for testing.
Args:
qt_app: The QApplication fixture.
Returns:
InfoTabWidget: The widget instance.
"""
widget = InfoTabWidget()
yield widget
# InfoTabWidget inherits from QObject, not QWidget, so it doesn't have close()
widget.deleteLater()
@pytest.fixture
def mock_track():
"""Create a mock Track object with metadata.
Returns:
Mock: A mock Track object with realistic attributes.
"""
track = Mock(spec=Track)
track.name = "Bohemian Rhapsody"
track.title = "Bohemian Rhapsody"
track.full_name = "Bohemian Rhapsody"
track.version = "2011 Remaster"
track.duration = 354 # 5:54
track.explicit = False
track.popularity = 95
track.bpm = 72
track.isrc = "GBUM71029604"
track.bit_depth = 24
track.sample_rate = 96000
track.audio_modes = []
track.available = True
track.id = 12345
# Mock album
mock_album = Mock(spec=Album)
mock_album.name = "A Night at the Opera"
mock_album.id = 67890
mock_album.release_date = Mock()
mock_album.release_date.strftime = Mock(return_value="1975-11-21")
track.album = mock_album
# Mock artists
mock_artist = Mock(spec=Artist)
mock_artist.name = "Queen"
mock_artist.roles = [Mock(name="main")]
track.artists = [mock_artist]
return track
@pytest.fixture
def mock_video():
"""Create a mock Video object.
Returns:
Mock: A mock Video object.
"""
video = Mock(spec=Video)
video.name = "Thriller"
video.title = "Thriller"
video.full_name = "Thriller (Official Video)"
video.duration = 600
video.explicit = False
video.video_quality = "1080p"
video.available = True
video.id = 54321
mock_artist = Mock(spec=Artist)
mock_artist.name = "Michael Jackson"
video.artists = [mock_artist]
mock_album = Mock(spec=Album)
mock_album.name = "Thriller"
video.album = mock_album
return video
@pytest.fixture
def tree_view_setup(qt_app):
"""Create a tree view with model for testing hover manager.
Args:
qt_app: The QApplication fixture.
Returns:
tuple: (tree_view, proxy_model, source_model)
"""
tree_view = QtWidgets.QTreeView()
source_model = QtGui.QStandardItemModel()
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(source_model)
tree_view.setModel(proxy_model)
# Add some test data
source_model.setColumnCount(2)
for i in range(5):
item_index = QtGui.QStandardItem(f"Item {i}")
item_obj = QtGui.QStandardItem()
mock_track = Mock(spec=Track)
mock_track.name = f"Track {i}"
mock_track.id = i
item_obj.setData(mock_track, Qt.ItemDataRole.UserRole)
source_model.appendRow([item_index, item_obj])
yield tree_view, proxy_model, source_model
tree_view.close()
tree_view.deleteLater()
class TestTrackInfoFormatter:
"""Test the TrackInfoFormatter utility class."""
def test_format_duration_valid(self):
"""Test duration formatting with valid input."""
assert TrackInfoFormatter.format_duration(354) == "05:54"
assert TrackInfoFormatter.format_duration(60) == "01:00"
assert TrackInfoFormatter.format_duration(0) == "00:00"
def test_format_duration_none(self):
"""Test duration formatting with None input."""
assert TrackInfoFormatter.format_duration(None) == "N/A"
def test_format_codec(self, mock_track):
"""Test codec formatting with a mock track."""
with patch("tidal_dl_ng.ui.info_tab_widget.quality_audio_highest", return_value="HI_RES") as _:
codec = TrackInfoFormatter.format_codec(mock_track)
assert "HI_RES" in codec.upper()
def test_format_bitrate(self, mock_track):
"""Test bitrate formatting."""
bitrate = TrackInfoFormatter.format_bitrate(mock_track)
assert "24-bit" in bitrate
assert "96.0 kHz" in bitrate
def test_format_bitrate_missing_data(self):
"""Test bitrate formatting with missing data."""
track = Mock(spec=Track)
track.bit_depth = None
track.sample_rate = None
assert TrackInfoFormatter.format_bitrate(track) == "N/A"
class TestInfoTabWidget:
"""Test the InfoTabWidget component."""
def test_widget_initialization(self, info_tab_widget):
"""Test that the widget initializes correctly."""
assert info_tab_widget is not None
# In test context, tab_widget is None because no existing_tab_widget is provided
# The widget creates fallback labels for all required fields
assert hasattr(info_tab_widget, "lbl_title")
assert hasattr(info_tab_widget, "lbl_artists")
assert hasattr(info_tab_widget, "lbl_bpm")
assert hasattr(info_tab_widget, "cover_label")
def test_update_on_hover(self, info_tab_widget, mock_track, qt_app):
"""Test updating widget on hover."""
info_tab_widget.update_on_hover(mock_track)
qt_app.processEvents()
assert info_tab_widget.current_media_hovered == mock_track
# The details should be updated via signal
# We can check if the signal was emitted by verifying internal state
def test_update_on_selection(self, info_tab_widget, mock_track, qt_app):
"""Test updating widget on selection."""
info_tab_widget.update_on_selection(mock_track)
qt_app.processEvents()
assert info_tab_widget.current_media_selected == mock_track
def test_revert_to_selection(self, info_tab_widget, mock_track, qt_app):
"""Test reverting to selected media after hover."""
# Set a selection
info_tab_widget.update_on_selection(mock_track)
qt_app.processEvents()
# Create a different track for hover
hover_track = Mock(spec=Track)
hover_track.name = "Different Track"
info_tab_widget.update_on_hover(hover_track)
qt_app.processEvents()
# Revert to selection
info_tab_widget.revert_to_selection()
qt_app.processEvents()
# Should have reverted to the original selection
assert info_tab_widget.current_media_selected == mock_track
def test_populate_track_details(self, info_tab_widget, mock_track, qt_app):
"""Test populating details for a track."""
with (
patch("tidal_dl_ng.ui.info_tab_widget.quality_audio_highest", return_value="HI_RES"),
patch("tidal_dl_ng.ui.info_tab_widget.name_builder_artist", return_value="Queen"),
patch("tidal_dl_ng.ui.info_tab_widget.name_builder_title", return_value="Bohemian Rhapsody"),
):
info_tab_widget._populate_track_details(mock_track)
qt_app.processEvents()
assert info_tab_widget.lbl_title.text() == "Bohemian Rhapsody"
assert info_tab_widget.lbl_version.text() == "2011 Remaster"
assert info_tab_widget.lbl_duration.text() == "05:54"
assert info_tab_widget.lbl_bpm.text() == "⏳ Loading..."
info_tab_widget._update_extras_ui({"bpm": 72})
qt_app.processEvents()
assert info_tab_widget.lbl_bpm.text() == "72"
def test_populate_video_details(self, info_tab_widget, mock_video, qt_app):
"""Test populating details for a video."""
with (
patch("tidal_dl_ng.ui.info_tab_widget.name_builder_artist", return_value="Michael Jackson"),
patch("tidal_dl_ng.ui.info_tab_widget.name_builder_title", return_value="Thriller"),
):
info_tab_widget._populate_video_details(mock_video)
qt_app.processEvents()
assert info_tab_widget.lbl_title.text() == "Thriller"
assert info_tab_widget.lbl_codec.text() == "1080p"
def test_clear_display(self, info_tab_widget, qt_app):
"""Test clearing the display."""
info_tab_widget._clear_display()
qt_app.processEvents()
assert info_tab_widget.lbl_title.text() == "—"
assert info_tab_widget.lbl_artists.text() == "—"
assert info_tab_widget.current_media_hovered is None
class TestHoverManager:
"""Test the HoverManager with debounce logic."""
def test_hover_manager_initialization(self, tree_view_setup, qt_app):
"""Test that HoverManager initializes correctly."""
tree_view, proxy_model, source_model = tree_view_setup
hover_manager = HoverManager(
tree_view=tree_view,
proxy_model=proxy_model,
source_model=source_model,
debounce_delay_ms=100,
)
assert hover_manager is not None
assert hover_manager.debounce_delay_ms == 100
assert hover_manager.pending_media is None
def test_debounce_single_hover(self, tree_view_setup, qt_app):
"""Test that a single hover event is confirmed after debounce delay."""
tree_view, proxy_model, source_model = tree_view_setup
hover_manager = HoverManager(
tree_view=tree_view,
proxy_model=proxy_model,
source_model=source_model,
debounce_delay_ms=100,
)
# Mock signal to track emissions
hover_confirmed_spy = []
def on_hover_confirmed(media):
hover_confirmed_spy.append(media)
hover_manager.s_hover_confirmed.connect(on_hover_confirmed)
# Simulate mouse movement over an item
# (This is complex to test properly without actual UI interaction)
# For now, we'll test the timer directly
mock_media = Mock(spec=Track)
mock_media.name = "Test Track"
hover_manager.pending_media = mock_media
hover_manager.debounce_timer.start()
# Wait for debounce delay + some buffer
qt_app.processEvents()
time.sleep(0.15)
qt_app.processEvents()
# Signal should have been emitted
assert len(hover_confirmed_spy) == 1
assert hover_confirmed_spy[0] == mock_media
def test_debounce_rapid_hovers(self, tree_view_setup, qt_app):
"""Test that rapid hover events are debounced (only last one fires)."""
tree_view, proxy_model, source_model = tree_view_setup
hover_manager = HoverManager(
tree_view=tree_view,
proxy_model=proxy_model,
source_model=source_model,
debounce_delay_ms=100,
)
hover_confirmed_spy = []
def on_hover_confirmed(media):
hover_confirmed_spy.append(media)
hover_manager.s_hover_confirmed.connect(on_hover_confirmed)
# Simulate 5 rapid hover events
for i in range(5):
mock_media = Mock(spec=Track)
mock_media.name = f"Track {i}"
hover_manager.pending_media = mock_media
hover_manager.debounce_timer.stop()
hover_manager.debounce_timer.start()
time.sleep(0.02) # Small delay between events (< debounce)
qt_app.processEvents()
# Wait for debounce delay
time.sleep(0.15)
qt_app.processEvents()
# Only one signal should have been emitted (the last one)
assert len(hover_confirmed_spy) == 1
assert hover_confirmed_spy[0].name == "Track 4"
def test_hover_left_cancels_pending(self, tree_view_setup, qt_app):
"""Test that leaving hover cancels pending confirmation."""
tree_view, proxy_model, source_model = tree_view_setup
hover_manager = HoverManager(
tree_view=tree_view,
proxy_model=proxy_model,
source_model=source_model,
debounce_delay_ms=100,
)
hover_confirmed_spy = []
hover_left_spy = []
def on_hover_confirmed(media):
hover_confirmed_spy.append(media)
def on_hover_left():
hover_left_spy.append(True)
hover_manager.s_hover_confirmed.connect(on_hover_confirmed)
hover_manager.s_hover_left.connect(on_hover_left)
# Start a hover
mock_media = Mock(spec=Track)
hover_manager.pending_media = mock_media
hover_manager.debounce_timer.start()
# Leave before debounce completes
time.sleep(0.05)
qt_app.processEvents()
hover_manager._handle_mouse_leave()
qt_app.processEvents()
# Wait to ensure timer would have fired
time.sleep(0.1)
qt_app.processEvents()
# Hover confirmed should NOT have been emitted
assert len(hover_confirmed_spy) == 0
# Hover left should have been emitted
assert len(hover_left_spy) == 1
def test_set_debounce_delay(self, tree_view_setup, qt_app):
"""Test changing the debounce delay."""
tree_view, proxy_model, source_model = tree_view_setup
hover_manager = HoverManager(
tree_view=tree_view,
proxy_model=proxy_model,
source_model=source_model,
debounce_delay_ms=100,
)
hover_manager.set_debounce_delay(200)
assert hover_manager.debounce_delay_ms == 200
assert hover_manager.debounce_timer.interval() == 200
def test_reset(self, tree_view_setup, qt_app):
"""Test resetting the hover manager state."""
tree_view, proxy_model, source_model = tree_view_setup
hover_manager = HoverManager(
tree_view=tree_view,
proxy_model=proxy_model,
source_model=source_model,
debounce_delay_ms=100,
)
# Set some state
mock_media = Mock(spec=Track)
hover_manager.pending_media = mock_media
hover_manager.last_hovered_media = mock_media
hover_manager.debounce_timer.start()
# Reset
hover_manager.reset()
assert hover_manager.pending_media is None
assert hover_manager.last_hovered_media is None
assert not hover_manager.debounce_timer.isActive()
class TestDataIntegrity:
"""Test that metadata is displayed from pre-loaded data without API calls."""
def test_no_api_calls_on_hover(self, info_tab_widget, mock_track, qt_app):
"""Test that hovering does not trigger API calls.
This test verifies that all data displayed on hover comes from
pre-loaded track objects, not from additional API requests.
"""
# Patch any potential API call methods
with patch.object(mock_track, "album") as mock_album:
# Ensure album data is already present (pre-loaded)
mock_album.name = "A Night at the Opera"
mock_album.id = 67890
# Update on hover
info_tab_widget.update_on_hover(mock_track)
qt_app.processEvents()
# Verify that album was not accessed in a way that would trigger API calls
# The album should have been accessed only for reading pre-loaded data
assert info_tab_widget.current_media_hovered == mock_track
def test_all_metadata_from_preloaded_object(self, info_tab_widget, mock_track, qt_app):
"""Test that all displayed metadata comes from the track object.
This verifies that the Track object contains all necessary fields
to populate the Details tab without additional API calls.
"""
with (
patch("tidal_dl_ng.ui.info_tab_widget.quality_audio_highest", return_value="HI_RES"),
patch("tidal_dl_ng.ui.info_tab_widget.name_builder_artist", return_value="Queen"),
patch("tidal_dl_ng.ui.info_tab_widget.name_builder_title", return_value="Bohemian Rhapsody"),
):
info_tab_widget._populate_track_details(mock_track)
qt_app.processEvents()
assert info_tab_widget.lbl_title.text() != "—"
assert info_tab_widget.lbl_version.text() != "—"
assert info_tab_widget.lbl_duration.text() != "—"
assert info_tab_widget.lbl_popularity.text() != "—"
assert info_tab_widget.lbl_bpm.text() != "—"
assert info_tab_widget.lbl_isrc.text() != "—"
+28
View File
@@ -0,0 +1,28 @@
from unittest.mock import patch
def test_new_fields_initial_state(info_tab_widget, qt_app):
"""New fields must exist and default to '—'."""
assert hasattr(info_tab_widget, "lbl_track_number")
assert info_tab_widget.lbl_track_number.text() == "—"
assert hasattr(info_tab_widget, "lbl_bpm")
assert info_tab_widget.lbl_bpm.text() == "—"
# Note: lbl_label, lbl_genres, lbl_producers and lbl_composers are not available
# as these fields cannot be retrieved from TIDAL API
def test_bpm_population(info_tab_widget, mock_track, qt_app):
"""Ensure BPM field is populated when present."""
# Add BPM attribute to mock_track
mock_track.bpm = 120
with patch("tidal_dl_ng.ui.info_tab_widget.name_builder_title", return_value="Test Track"):
info_tab_widget._populate_track_details(mock_track)
qt_app.processEvents()
# Expect loading indicator first due to async extras
assert info_tab_widget.lbl_bpm.text() == "⏳ Loading..."
# Simulate async arrival of extras
info_tab_widget._update_extras_ui({"bpm": 120})
qt_app.processEvents()
assert info_tab_widget.lbl_bpm.text() == "120"
+137
View File
@@ -0,0 +1,137 @@
import mutagen
import pytest
from tidal_dl_ng.helper.tidal import (
_normalize_contributors,
extract_contributor_names,
parse_track_and_album_extras,
)
from tidal_dl_ng.metadata import Metadata
def test_normalize_contributors_dict_shape():
raw = {
"producer": [{"name": "Prod A"}, {"name": "Prod B", "foo": "bar"}],
"composer": [{"name": "Comp 1"}],
"invalid": ["x", {"no_name": True}],
}
result = _normalize_contributors(raw)
assert set(result.keys()) == {"producer", "composer"}
assert result["producer"] == ["Prod A", "Prod B"]
assert result["composer"] == ["Comp 1"]
def test_normalize_contributors_list_shape():
raw = [
{"name": "Prod A", "role": "producer"},
{"name": "Comp 1", "role": "composer"},
{"name": "Bad", "role": ""},
"oops",
]
result = _normalize_contributors(raw)
assert set(result.keys()) == {"producer", "composer"}
assert result["producer"] == ["Prod A"]
assert result["composer"] == ["Comp 1"]
def test_parse_track_and_album_extras_full():
track_json = {
"bpm": 123,
"contributors": {
"producer": [{"name": "Prod A"}],
"composer": [{"name": "Comp 1"}],
"lyricist": [{"name": "Writer"}],
},
}
album_json = {
"label": "Test Label",
"genres": [{"name": "Rock"}, {"name": "Indie"}],
}
extras = parse_track_and_album_extras(track_json, album_json)
assert extras["bpm"] == 123
assert extras["label"] == "Test Label"
assert extras["genres"] == ["Rock", "Indie"]
assert extras["contributors_by_role"]["producer"] == ["Prod A"]
def test_parse_track_and_album_extras_missing_fields():
extras = parse_track_and_album_extras({}, {})
assert extras["bpm"] is None
assert extras["label"] == ""
assert extras["genres"] == []
assert extras["contributors_by_role"] == {}
@pytest.mark.parametrize(
"role,expected",
[
("producer", "Prod A, Prod B"),
("composer", "Comp 1"),
("lyricist", ""),
],
)
def test_extract_contributor_names(role, expected):
contributors = {
"producer": ["Prod A", "Prod B"],
"composer": ["Comp 1"],
}
names = extract_contributor_names(contributors, role, delimiter=", ")
assert names == expected
def test_metadata_enriched_tags_flac(tmp_path):
# Create a valid minimal FLAC file using soundfile
path = tmp_path / "test.flac"
# Import numpy and soundfile to create a minimal valid FLAC
try:
import numpy as np
import soundfile as sf
# Create 1 second of silence at 44100 Hz
silence = np.zeros((44100, 2), dtype=np.float32)
sf.write(str(path), silence, 44100, format="FLAC")
except ImportError:
# Fallback: skip this test if soundfile not available
import pytest
pytest.skip("soundfile required for FLAC test")
m = Metadata(
path_file=path,
target_upc={"FLAC": "UPC", "MP3": "UPC", "MP4": "UPC"},
title="Title",
album="Album",
artists="Artist1, Artist2",
albumartist="AlbumArtist",
genre="Rock, Indie",
label="Test Label",
bpm=120,
producers="Prod A, Prod B",
composers_detailed="Comp 1, Comp 2",
lyricists="Writer 1, Writer 2",
)
m.save()
audio_out = mutagen.flac.FLAC(path)
assert audio_out["TITLE"][0] == "Title"
assert audio_out["ALBUM"][0] == "Album"
assert audio_out["ARTIST"][0] == "Artist1, Artist2"
assert audio_out["ALBUMARTIST"][0] == "AlbumArtist"
assert audio_out["GENRE"][0] == "Rock, Indie"
assert audio_out["LABEL"][0] == "Test Label"
assert audio_out["BPM"][0] == "120"
assert audio_out["PRODUCER"][0] == "Prod A, Prod B"
assert audio_out["LYRICIST"][0] == "Writer 1, Writer 2"
assert audio_out["COMPOSER"][0] == "Comp 1, Comp 2"