import os import tempfile import unittest from pathlib import Path os.environ['MUSIC_WORKSHOP_DB_PATH'] = str( Path(tempfile.gettempdir()) / f'music_workshop_metadata_normalization_{next(tempfile._get_candidate_names())}.db' ) from backend.app.metadata_normalization import MetadataNormalizationService, can_ingest_metadata, parse_artist_string from backend.app.task_store import TaskStore class MetadataNormalizationTests(unittest.TestCase): def setUp(self): self.db_path = Path(os.environ['MUSIC_WORKSHOP_DB_PATH']) if self.db_path.exists(): self.db_path.unlink() self.task_store = TaskStore(self.db_path) self.task = self.task_store.create_task_if_idle( { 'input': '/tmp/input', 'output': '/tmp/output', 'trash': '/tmp/trash' } ) self.service = MetadataNormalizationService(self.task_store) def test_parse_artist_string_supports_common_delimiters(self): self.assertEqual(parse_artist_string('A / B')['tokens'], ['A', 'B']) self.assertEqual(parse_artist_string('A; B')['tokens'], ['A', 'B']) self.assertEqual(parse_artist_string('A & B')['tokens'], ['A', 'B']) self.assertEqual(parse_artist_string('A feat. B')['tokens'], ['A', 'B']) self.assertEqual(parse_artist_string('A、B')['tokens'], ['A', 'B']) def test_single_artist_album_derives_album_artist(self): item = self._insert_item('track-01.flac', {'title': 'Song 1', 'artist': 'Artist A', 'album': 'Album X'}) self._insert_item('track-02.flac', {'title': 'Song 2', 'artist': 'Artist A', 'album': 'Album X'}) normalized = self.service.normalize_item(item) self.assertEqual(normalized['album_artist'], 'Artist A') self.assertEqual(normalized['normalization_strategy'], 'single_artist') self.assertTrue(can_ingest_metadata({**normalized, 'title': 'Song 1'})) def test_feat_album_uses_dominant_primary_artist(self): item = self._insert_item('track-01.flac', {'title': 'Song 1', 'artist': 'Artist A feat. Guest', 'album': 'Album X'}) self._insert_item('track-02.flac', {'title': 'Song 2', 'artist': 'Artist A', 'album': 'Album X'}) self._insert_item('track-03.flac', {'title': 'Song 3', 'artist': 'Artist A & Another', 'album': 'Album X'}) normalized = self.service.normalize_item(item) self.assertEqual(normalized['album_artist'], 'Artist A') self.assertEqual(normalized['normalization_strategy'], 'main_artist_feat') def test_compilation_album_sets_various_artists(self): item = self._insert_item('track-01.flac', {'title': 'Song 1', 'artist': 'Artist A', 'album': 'Top Hits 2025'}) self._insert_item('track-02.flac', {'title': 'Song 2', 'artist': 'Artist B', 'album': 'Top Hits 2025'}) self._insert_item('track-03.flac', {'title': 'Song 3', 'artist': 'Artist C', 'album': 'Top Hits 2025'}) normalized = self.service.normalize_item(item) self.assertEqual(normalized['album_artist'], 'Various Artists') self.assertEqual(normalized['compilation'], 1) self.assertEqual(normalized['normalization_strategy'], 'compilation') def test_existing_album_artist_is_preserved(self): item = self._insert_item( 'track-01.flac', {'title': 'Song 1', 'artist': '阿信', 'album': 'Solo Album', 'album_artist': '五月天'} ) normalized = self.service.normalize_item(item) self.assertEqual(normalized['album_artist'], '五月天') self.assertEqual(normalized['normalization_strategy'], 'source_preserved') def _insert_item(self, filename: str, matched_metadata_json: dict): return self.task_store.insert_task_item( self.task['task_id'], original_path=f'/tmp/input/{filename}', current_file_path=f'/tmp/input/{filename}', relative_path=f'Artist/Album/{filename}', filename=filename, extension='.flac', size_bytes=123456, modified_at='2024-01-01T00:00:00Z', local_cover=None, local_lyric=None, scan_status='queued', scan_reason=None, scan_message=None, match_status='matched_fallback', matched_metadata_json=matched_metadata_json ) if __name__ == '__main__': unittest.main()