mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 21:45:04 +00:00
refactor: simplify music metadata processing by removing unused code and improving album signal detection
This commit is contained in:
@@ -1,105 +1,71 @@
|
||||
"""Tests for conservative audio metadata enrichment and tag mappings."""
|
||||
"""Tests for conservative audio metadata enrichment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from mutagen.id3 import APIC, ID3, TPE1, TPE2, TRCK, TXXX
|
||||
|
||||
from music_metadata import (
|
||||
_ARTISTS_KEY,
|
||||
_TRACK_NUMBER_KEY,
|
||||
_TRACK_TOTAL_KEY,
|
||||
MusicMetadataPreProcessor,
|
||||
MusicMetadataWriterPostProcessor,
|
||||
_write_m4a,
|
||||
_write_vorbis,
|
||||
is_confirmed_music_album,
|
||||
structured_track_artists,
|
||||
write_music_tags,
|
||||
)
|
||||
from music_metadata import MusicMetadataPreProcessor
|
||||
|
||||
|
||||
def _preprocess(source_url, source_entry, info):
|
||||
processor = MusicMetadataPreProcessor(
|
||||
source_url=source_url,
|
||||
source_entry=source_entry,
|
||||
)
|
||||
def _preprocess(source_entry, info):
|
||||
processor = MusicMetadataPreProcessor(source_entry=source_entry)
|
||||
_, result = processor.run(info)
|
||||
return result
|
||||
|
||||
|
||||
def test_album_detection_requires_mpre_or_olak_signal():
|
||||
assert is_confirmed_music_album(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{'playlist': 'OLAK5uy_example'},
|
||||
)
|
||||
assert is_confirmed_music_album(
|
||||
'https://music.youtube.com/browse/MPREb_example',
|
||||
{},
|
||||
)
|
||||
assert not is_confirmed_music_album(
|
||||
'https://music.youtube.com/playlist?list=PLexample',
|
||||
{'playlist': 'PLexample'},
|
||||
)
|
||||
assert not is_confirmed_music_album(
|
||||
'https://www.youtube.com/playlist?list=PLexample',
|
||||
{'playlist': 'PLexample'},
|
||||
)
|
||||
|
||||
|
||||
def test_album_uses_existing_order_and_total_when_track_number_is_missing():
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{
|
||||
'playlist': 'OLAK5uy_example',
|
||||
'playlist_index': '03',
|
||||
'playlist_count': 12,
|
||||
'playlist_title': 'Example Album',
|
||||
},
|
||||
{'title': 'Track', 'artists': ['Artist One']},
|
||||
{'title': 'Track', 'album': 'Example Album'},
|
||||
)
|
||||
|
||||
assert result['track_number'] == 3
|
||||
assert result[_TRACK_NUMBER_KEY] == 3
|
||||
assert result[_TRACK_TOTAL_KEY] == 12
|
||||
assert result['track_number'] == '3/12'
|
||||
assert result['album'] == 'Example Album'
|
||||
|
||||
|
||||
def test_official_track_number_wins_over_album_order():
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{
|
||||
'playlist': 'OLAK5uy_example',
|
||||
'playlist_index': 3,
|
||||
'playlist_count': 12,
|
||||
},
|
||||
{'playlist_index': 3, 'playlist_count': 12},
|
||||
{'track_number': 7, 'album': 'Official Album'},
|
||||
)
|
||||
|
||||
assert result['track_number'] == 7
|
||||
assert result[_TRACK_NUMBER_KEY] == 7
|
||||
assert result[_TRACK_TOTAL_KEY] == 12
|
||||
assert result['track_number'] == '7/12'
|
||||
assert result['album'] == 'Official Album'
|
||||
|
||||
|
||||
def test_inline_official_track_total_is_preserved():
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{'playlist': 'OLAK5uy_example', 'playlist_count': 12},
|
||||
{'playlist_count': 12},
|
||||
{'track_number': '4/10', 'album': 'Official Album'},
|
||||
)
|
||||
|
||||
assert result['track_number'] == '4/10'
|
||||
assert result[_TRACK_NUMBER_KEY] == 4
|
||||
assert result[_TRACK_TOTAL_KEY] == 10
|
||||
|
||||
|
||||
def test_youtube_music_playlist_does_not_infer_album_or_track_number():
|
||||
def test_source_track_number_and_total_are_retained_from_flat_extraction():
|
||||
result = _preprocess(
|
||||
{'track_number': 2, 'track_count': 9, 'playlist_index': 4},
|
||||
{'title': 'Track'},
|
||||
)
|
||||
|
||||
assert result['track_number'] == '2/9'
|
||||
|
||||
|
||||
def test_album_title_falls_back_to_source_playlist_title():
|
||||
result = _preprocess(
|
||||
{'playlist_title': 'Example Album'},
|
||||
{'track_number': 4},
|
||||
)
|
||||
|
||||
assert result['album'] == 'Example Album'
|
||||
assert result['track_number'] == 4
|
||||
|
||||
|
||||
def test_playlist_without_extractor_album_signals_is_not_changed():
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{
|
||||
'playlist': 'PLexample',
|
||||
'playlist_index': 3,
|
||||
'playlist_count': 12,
|
||||
'playlist_title': 'Example Playlist',
|
||||
@@ -109,33 +75,24 @@ def test_youtube_music_playlist_does_not_infer_album_or_track_number():
|
||||
|
||||
assert 'album' not in result
|
||||
assert 'track_number' not in result
|
||||
assert _TRACK_NUMBER_KEY not in result
|
||||
assert _TRACK_TOTAL_KEY not in result
|
||||
|
||||
|
||||
def test_regular_youtube_playlist_and_video_are_not_changed():
|
||||
def test_regular_video_artwork_is_not_changed():
|
||||
thumbnails = [
|
||||
{'url': 'square.jpg', 'width': 500, 'height': 500},
|
||||
{'url': 'landscape.jpg', 'width': 1280, 'height': 720},
|
||||
]
|
||||
info = {'title': 'Regular Video', 'thumbnails': thumbnails.copy()}
|
||||
|
||||
result = _preprocess(
|
||||
'https://www.youtube.com/watch?v=video',
|
||||
{'playlist': 'PLexample', 'playlist_index': 2, 'playlist_count': 5},
|
||||
info,
|
||||
)
|
||||
result = _preprocess({}, {'title': 'Regular Video', 'thumbnails': thumbnails.copy()})
|
||||
|
||||
assert result['thumbnails'] == thumbnails
|
||||
assert 'album' not in result
|
||||
assert 'track_number' not in result
|
||||
assert 'thumbnail' not in result
|
||||
|
||||
|
||||
def test_music_audio_prefers_largest_existing_square_thumbnail():
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{'playlist': 'PLexample'},
|
||||
{},
|
||||
{
|
||||
'track': 'Track',
|
||||
'thumbnails': [
|
||||
{'url': 'small-square.jpg', 'width': 200, 'height': 200},
|
||||
{'url': 'large-square.jpg', 'width': 1000, 'height': 1000},
|
||||
@@ -154,153 +111,9 @@ def test_landscape_only_music_artwork_keeps_existing_order():
|
||||
{'url': 'large.jpg', 'width': 1280, 'height': 720},
|
||||
]
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{'playlist': 'PLexample'},
|
||||
{'thumbnails': thumbnails.copy()},
|
||||
{},
|
||||
{'track': 'Track', 'thumbnails': thumbnails.copy()},
|
||||
)
|
||||
|
||||
assert result['thumbnails'] == thumbnails
|
||||
assert 'thumbnail' not in result
|
||||
|
||||
|
||||
def test_structured_artists_split_only_spaced_middle_dot():
|
||||
assert structured_track_artists({
|
||||
'artists': ['Artist One \u00b7 Artist Two', 'Earth, Wind & Fire', 'half\u00b7alive'],
|
||||
}) == ['Artist One', 'Artist Two', 'Earth, Wind & Fire', 'half\u00b7alive']
|
||||
assert structured_track_artists({'artist': 'Artist One, Artist Two'}) == []
|
||||
|
||||
|
||||
def test_preprocessor_retains_separate_structured_artists():
|
||||
result = _preprocess(
|
||||
'https://music.youtube.com/watch?v=track',
|
||||
{},
|
||||
{'artist': 'Artist One, Artist Two', 'artists': ['Artist One', 'Artist Two']},
|
||||
)
|
||||
|
||||
assert result['artist'] == 'Artist One, Artist Two'
|
||||
assert result[_ARTISTS_KEY] == ['Artist One', 'Artist Two']
|
||||
|
||||
|
||||
def test_mp3_mapping_uses_plural_artists_and_fractional_track():
|
||||
tags = MagicMock()
|
||||
with patch('music_metadata.ID3', return_value=tags):
|
||||
write_music_tags('track.mp3', 'mp3', ['Artist One', 'Artist Two'], 3, 12)
|
||||
|
||||
artist_frame = tags.add.call_args.args[0]
|
||||
assert isinstance(artist_frame, TXXX)
|
||||
assert artist_frame.desc == 'Artists'
|
||||
assert artist_frame.text == ['Artist One', 'Artist Two']
|
||||
track_frame = tags.setall.call_args.args[1][0]
|
||||
assert isinstance(track_frame, TRCK)
|
||||
assert track_frame.text == ['3/12']
|
||||
tags.save.assert_called_once_with('track.mp3', v2_version=4)
|
||||
|
||||
|
||||
def test_mp3_round_trip_preserves_display_artist_artwork_and_album_artist(tmp_path):
|
||||
path = tmp_path / 'track.mp3'
|
||||
initial = ID3()
|
||||
initial.add(TPE1(encoding=3, text=['Artist One, Artist Two']))
|
||||
initial.add(TPE2(encoding=3, text=['Existing Album Artist']))
|
||||
initial.add(APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=b'cover'))
|
||||
initial.save(path)
|
||||
|
||||
write_music_tags(str(path), 'mp3', ['Artist One', 'Artist Two'], 3, 12)
|
||||
|
||||
result = ID3(path)
|
||||
assert result.getall('TPE1')[0].text == ['Artist One, Artist Two']
|
||||
assert result.getall('TPE2')[0].text == ['Existing Album Artist']
|
||||
assert result.getall('APIC')[0].data == b'cover'
|
||||
assert result.getall('TXXX:Artists')[0].text == ['Artist One', 'Artist Two']
|
||||
assert result.getall('TRCK')[0].text == ['3/12']
|
||||
|
||||
|
||||
class _FakeM4A:
|
||||
def __init__(self):
|
||||
self.tags = {
|
||||
'\xa9ART': ['Artist One, Artist Two'],
|
||||
'covr': [b'cover'],
|
||||
'aART': ['Existing Album Artist'],
|
||||
}
|
||||
self.saved = False
|
||||
|
||||
def add_tags(self):
|
||||
self.tags = {}
|
||||
|
||||
def save(self):
|
||||
self.saved = True
|
||||
|
||||
|
||||
def test_m4a_mapping_preserves_display_artist_artwork_and_album_artist():
|
||||
audio = _FakeM4A()
|
||||
with patch('music_metadata.MP4', return_value=audio):
|
||||
_write_m4a('track.m4a', ['Artist One', 'Artist Two'], 3, 12)
|
||||
|
||||
assert audio.tags['\xa9ART'] == ['Artist One, Artist Two']
|
||||
assert audio.tags['covr'] == [b'cover']
|
||||
assert audio.tags['aART'] == ['Existing Album Artist']
|
||||
assert [
|
||||
bytes(value) for value in audio.tags['----:com.apple.iTunes:ARTISTS']
|
||||
] == [b'Artist One', b'Artist Two']
|
||||
assert audio.tags['trkn'] == [(3, 12)]
|
||||
assert audio.saved
|
||||
|
||||
|
||||
class _FakeVorbis(dict):
|
||||
def __init__(self):
|
||||
super().__init__({
|
||||
'ARTIST': ['Artist One, Artist Two'],
|
||||
'ALBUMARTIST': ['Existing Album Artist'],
|
||||
'METADATA_BLOCK_PICTURE': ['cover'],
|
||||
})
|
||||
self.saved = False
|
||||
|
||||
def save(self):
|
||||
self.saved = True
|
||||
|
||||
|
||||
def test_flac_mapping_preserves_singular_artist_artwork_and_album_artist():
|
||||
audio = _FakeVorbis()
|
||||
_write_vorbis(audio, ['Artist One', 'Artist Two'], 3, 12)
|
||||
|
||||
assert audio['ARTIST'] == ['Artist One, Artist Two']
|
||||
assert audio['ALBUMARTIST'] == ['Existing Album Artist']
|
||||
assert audio['METADATA_BLOCK_PICTURE'] == ['cover']
|
||||
assert audio['ARTISTS'] == ['Artist One', 'Artist Two']
|
||||
assert audio['TRACKNUMBER'] == ['3']
|
||||
assert audio['TRACKTOTAL'] == ['12']
|
||||
assert audio['TOTALTRACKS'] == ['12']
|
||||
assert audio.saved
|
||||
|
||||
|
||||
def test_after_move_writer_ignores_unsupported_and_empty_metadata():
|
||||
processor = MusicMetadataWriterPostProcessor()
|
||||
empty = {'filepath': 'track.mp3', 'ext': 'mp3'}
|
||||
unsupported = {
|
||||
'filepath': 'track.wav',
|
||||
'ext': 'wav',
|
||||
_TRACK_NUMBER_KEY: 3,
|
||||
}
|
||||
|
||||
with patch('music_metadata.write_music_tags') as writer:
|
||||
processor.run(empty)
|
||||
processor.run(unsupported)
|
||||
|
||||
writer.assert_not_called()
|
||||
|
||||
|
||||
def test_after_move_writer_dispatches_enriched_metadata():
|
||||
processor = MusicMetadataWriterPostProcessor()
|
||||
info = {
|
||||
'filepath': 'track.flac',
|
||||
'ext': 'flac',
|
||||
_ARTISTS_KEY: ['Artist One', 'Artist Two'],
|
||||
_TRACK_NUMBER_KEY: 3,
|
||||
_TRACK_TOTAL_KEY: 12,
|
||||
}
|
||||
|
||||
with patch('music_metadata.write_music_tags') as writer:
|
||||
processor.run(info)
|
||||
|
||||
writer.assert_called_once_with(
|
||||
'track.flac', 'flac', ['Artist One', 'Artist Two'], 3, 12
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user