From 9e7730567543656e3a963dfc73a4a3039056b3e4 Mon Sep 17 00:00:00 2001 From: infojunkie Date: Wed, 10 Jan 2024 22:13:35 -0800 Subject: [PATCH] Parse extra artists, remove duplicate artists count, add tests --- pyproject.toml | 2 +- src/discogs_tag/cli.py | 50 ++- tests/release.json | 789 ++++++++++++++++++++++++++++++++++++++ tests/test_discogs_tag.py | 21 +- 4 files changed, 841 insertions(+), 21 deletions(-) create mode 100644 tests/release.json diff --git a/pyproject.toml b/pyproject.toml index b57b3cf..5924073 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "discogs-tag" -version = "0.1.7" +version = "0.1.8" description = "A rudimentary audio tagger based on Discogs metadata." authors = ["infojunkie "] readme = "README.md" diff --git a/src/discogs_tag/cli.py b/src/discogs_tag/cli.py index 71e2798..a70c6a4 100644 --- a/src/discogs_tag/cli.py +++ b/src/discogs_tag/cli.py @@ -5,9 +5,11 @@ import os from pprint import pprint import glob +import sys +import re from discogs_tag import __NAME__, __VERSION__ -def tag(release, dir = './', dry = False): +def tag(release, dir = './', dry = False, ignore = False): """Tag the audio files in dir with the given Discogs release.""" request = urllib.request.Request(f'https://api.discogs.com/releases/{release}', headers = { 'User-Agent': f'{__NAME__} {__VERSION__}' @@ -18,39 +20,57 @@ def tag(release, dir = './', dry = False): glob.glob(os.path.join(glob.escape(dir), '**', '*.flac'), recursive=True) + glob.glob(os.path.join(glob.escape(dir), '**', '*.mp3'), recursive=True) ) - tracks = list(filter(lambda t: t['type_'] == 'track', data['tracklist'])) - if (len(files) != len(tracks)): + apply_metadata(data, files, dry, ignore) + +def apply_metadata(data, files, dry, ignore): + tracks = list(filter(lambda t: t['type_'] == 'track', data['tracklist'])) + if (len(files) != len(tracks)): + if (not ignore): raise Exception(f'Expecting {len(tracks)} files but found {len(files)}. Aborting.') - for n, track in enumerate(tracks): + else: + print(f'Expecting {len(tracks)} files but found {len(files)}. Ignoring.', file=sys.stderr) + + for n, track in enumerate(tracks): + try: audio = mutagen.File(files[n], easy=True) - apply_metadata(track, audio) + merge_metadata(track, audio) if (dry): pprint(audio) else: audio.save() + except Exception as e: + if (not ignore): + raise e + else: + print(e, file=sys.stderr) - if (not dry): - print(f'Processed {len(files)} audio files.') + if (not dry): + print(f'Processed {len(files)} audio files.') -def apply_metadata(track, audio): +def merge_metadata(track, audio): audio['title'] = track['title'] + artists = [] if 'artists' in track: - audio['artist'] = ', '.join([artist_name(artist) for artist in track['artists']]) + artists += [artist_name(artist) for artist in track['artists']] + if 'extraartists' in track: + artists += [artist_name(artist) for artist in filter(lambda a: a['role'].casefold() != 'Written-By'.casefold(), track['extraartists'])] + if (artists): + audio['artist'] = ', '.join(artists) positions = track['position'].split('-') audio['tracknumber'] = positions[-1] if (len(positions) > 1): audio['discnumber'] = positions[0] - composers = ', '.join([artist_name(composer) for composer in filter(lambda a: a['role'].casefold() == 'Written-By'.casefold(), track['extraartists'])]) if 'extraartists' in track else None + composers = [artist_name(composer) for composer in filter(lambda a: a['role'].casefold() == 'Written-By'.casefold(), track['extraartists'])] if 'extraartists' in track else None if (composers): - audio['composer'] = composers + audio['composer'] = ', '.join(composers) def artist_name(artist): + name = None if 'anv' in artist and artist['anv']: - return artist['anv'] + name = artist['anv'] elif 'name' in artist and artist['name']: - return artist['name'] - else: - return None + name = artist['name'] + return re.sub(r"\s+\(\d+\)$", '', name) if name else None def cli(): fire.Fire(tag) diff --git a/tests/release.json b/tests/release.json new file mode 100644 index 0000000..13f7bc3 --- /dev/null +++ b/tests/release.json @@ -0,0 +1,789 @@ +{ + "id": 18051880, + "status": "Accepted", + "year": 2002, + "resource_url": "https://api.discogs.com/releases/18051880", + "uri": "https://www.discogs.com/release/18051880-%C3%89venk-Chants-Rituels-Des-Nomades-De-La-Ta%C3%AFga-Ritual-Songs-Of-The-Nomadic-Taiga-People", + "artists": [ + { + "name": "Evenks", + "anv": "Évenk", + "join": "", + "role": "", + "tracks": "", + "id": 9046156, + "resource_url": "https://api.discogs.com/artists/9046156" + } + ], + "artists_sort": "Evenks", + "labels": [ + { + "name": "Buda Records", + "catno": "3015792", + "entity_type": "1", + "entity_type_name": "Label", + "id": 103197, + "resource_url": "https://api.discogs.com/labels/103197" + } + ], + "series": [ + { + "name": "Musique Du Monde", + "catno": "", + "entity_type": "2", + "entity_type_name": "Series", + "id": 231838, + "resource_url": "https://api.discogs.com/labels/231838" + }, + { + "name": "Collection Dominique Buscail", + "catno": "", + "entity_type": "2", + "entity_type_name": "Series", + "id": 235937, + "resource_url": "https://api.discogs.com/labels/235937" + }, + { + "name": "Sibérie", + "catno": "8", + "entity_type": "2", + "entity_type_name": "Series", + "id": 1075549, + "resource_url": "https://api.discogs.com/labels/1075549" + } + ], + "companies": [ + { + "name": "Buda Musique", + "catno": "", + "entity_type": "4", + "entity_type_name": "Record Company", + "id": 60725, + "resource_url": "https://api.discogs.com/labels/60725" + }, + { + "name": "Universal France", + "catno": "", + "entity_type": "9", + "entity_type_name": "Distributed By", + "id": 49807, + "resource_url": "https://api.discogs.com/labels/49807" + }, + { + "name": "MPO", + "catno": "", + "entity_type": "17", + "entity_type_name": "Pressed By", + "id": 56025, + "resource_url": "https://api.discogs.com/labels/56025" + } + ], + "formats": [ + { + "name": "CD", + "qty": "1" + } + ], + "data_quality": "Needs Vote", + "community": { + "have": 10, + "want": 4, + "rating": { + "count": 1, + "average": 5 + }, + "submitter": { + "username": "GrahamStewart", + "resource_url": "https://api.discogs.com/users/GrahamStewart" + }, + "contributors": [ + { + "username": "GrahamStewart", + "resource_url": "https://api.discogs.com/users/GrahamStewart" + } + ], + "data_quality": "Needs Vote", + "status": "Accepted" + }, + "format_quantity": 1, + "date_added": "2021-03-28T10:08:00-07:00", + "date_changed": "2021-03-28T12:49:34-07:00", + "num_for_sale": 0, + "lowest_price": null, + "title": "Chants Rituels Des Nomades De La Taïga = Ritual Songs Of The Nomadic Taiga People", + "country": "France", + "released": "2002", + "notes": "Durée totale: 71:59.\nRecorded between 1997 and 2000 in Iengra, Ust'-Njukža, Čita, Tjanja, Tura, Bajkit,and Džilinda, Russia.\nMade in France.\nComes with a 36 page booklet with liner notes in French and English.\nRelease date taken from data in CD matrix.\n", + "released_formatted": "2002", + "identifiers": [ + { + "type": "Barcode", + "value": "3 259130 157925" + }, + { + "type": "Matrix / Runout", + "value": "[MPO logo] 3015792 @ 01 IFPI L039 12/09/02 12:21:58" + }, + { + "type": "Mastering SID Code", + "value": "IFPI L039" + }, + { + "type": "Mould SID Code", + "value": "IFPI 121D" + }, + { + "type": "Rights Society", + "value": "SACEM SACD SDRM SGDL" + } + ], + "genres": [ + "Folk, World, & Country" + ], + "tracklist": [ + { + "position": "", + "type_": "heading", + "title": "Chants De Vœux = Wishing Songs", + "duration": "" + }, + { + "position": "1", + "type_": "track", + "artists": [ + { + "name": "Galina Andreevna Lekhanova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046159, + "resource_url": "https://api.discogs.com/artists/9046159" + } + ], + "title": "Ogol", + "duration": "3:26" + }, + { + "position": "2", + "type_": "track", + "artists": [ + { + "name": "Nina Prokop'evna Pudova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046162, + "resource_url": "https://api.discogs.com/artists/9046162" + } + ], + "title": "Kumalakan", + "duration": "1:20" + }, + { + "position": "3", + "type_": "track", + "artists": [ + { + "name": "Elizaveta Petrovna Kirilova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046165, + "resource_url": "https://api.discogs.com/artists/9046165" + } + ], + "title": "Ogol", + "duration": "2:50" + }, + { + "position": "4", + "type_": "track", + "artists": [ + { + "name": "Vladimir Andreev (3)", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046168, + "resource_url": "https://api.discogs.com/artists/9046168" + } + ], + "title": "Untitled", + "duration": "4:45" + }, + { + "position": "5", + "type_": "track", + "artists": [ + { + "name": "Anfissa Pavlovna Avelova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046171, + "resource_url": "https://api.discogs.com/artists/9046171" + } + ], + "title": "Ogol", + "duration": "2:13" + }, + { + "position": "6", + "type_": "track", + "artists": [ + { + "name": "Galina Andreevna Abramova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046174, + "resource_url": "https://api.discogs.com/artists/9046174" + } + ], + "title": "Ègèj", + "duration": "1:50" + }, + { + "position": "7", + "type_": "track", + "artists": [ + { + "name": "Prokopij Egorovič Nikolaev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046177, + "resource_url": "https://api.discogs.com/artists/9046177" + } + ], + "title": "Untitled", + "duration": "1:06" + }, + { + "position": "", + "type_": "heading", + "title": "Rondes = The Round Dances", + "duration": "" + }, + { + "position": "8", + "type_": "track", + "artists": [ + { + "name": "Prokopij Egorovič Nikolaev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046177, + "resource_url": "https://api.discogs.com/artists/9046177" + } + ], + "title": "Dèlèhinčo", + "duration": "1:24" + }, + { + "position": "9", + "type_": "track", + "artists": [ + { + "name": "Valentina Stepanovna Enokhova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046180, + "resource_url": "https://api.discogs.com/artists/9046180" + } + ], + "title": "Mončoraj", + "duration": "1:04" + }, + { + "position": "10", + "type_": "track", + "artists": [ + { + "name": "Evgenja Alekseevna Kurejskaja", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046183, + "resource_url": "https://api.discogs.com/artists/9046183" + } + ], + "title": "Hèno", + "duration": "0:59" + }, + { + "position": "11", + "type_": "track", + "artists": [ + { + "name": "Maria Enokhova", + "anv": "", + "join": "Et", + "role": "", + "tracks": "", + "id": 9046186, + "resource_url": "https://api.discogs.com/artists/9046186" + }, + { + "name": "Oktjabrina Vladimirovna Naumova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046189, + "resource_url": "https://api.discogs.com/artists/9046189" + } + ], + "title": "Dèvèjdè", + "duration": "2:58" + }, + { + "position": "", + "type_": "heading", + "title": "Chants Narratifs = Narrative Songs", + "duration": "" + }, + { + "position": "12", + "type_": "track", + "artists": [ + { + "name": "Ivan Egorovič Galjujskij", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046192, + "resource_url": "https://api.discogs.com/artists/9046192" + } + ], + "title": "Comptine En Forme De Dialogue = Nursery Rhyme In The Form Of A Dialogue", + "duration": "0:45" + }, + { + "position": "13", + "type_": "track", + "artists": [ + { + "name": "Uljana Kikolaevna Vasileava", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046195, + "resource_url": "https://api.discogs.com/artists/9046195" + } + ], + "title": "U-juju", + "duration": "0:54" + }, + { + "position": "14", + "type_": "track", + "artists": [ + { + "name": "Oktjabrina Vladimirovna Naumova", + "anv": "Oktjabrina Vladimirovna", + "join": "Et", + "role": "", + "tracks": "", + "id": 9046189, + "resource_url": "https://api.discogs.com/artists/9046189" + }, + { + "name": "Svetlana Naumova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046198, + "resource_url": "https://api.discogs.com/artists/9046198" + } + ], + "title": "Jangduli", + "duration": "2:05" + }, + { + "position": "15", + "type_": "track", + "artists": [ + { + "name": "Galina Andreevna Lekhanova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046159, + "resource_url": "https://api.discogs.com/artists/9046159" + } + ], + "title": "Gudjajè", + "duration": "1:54" + }, + { + "position": "16", + "type_": "track", + "artists": [ + { + "name": "Elena Dmitrieva Tikhonova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046201, + "resource_url": "https://api.discogs.com/artists/9046201" + } + ], + "title": "Untitled", + "duration": "1:04" + }, + { + "position": "17", + "type_": "track", + "artists": [ + { + "name": "Galina Andreevna Abramova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046174, + "resource_url": "https://api.discogs.com/artists/9046174" + } + ], + "title": "Adyn Gačin", + "duration": "2:37" + }, + { + "position": "", + "type_": "heading", + "title": "Chants Chamaniques = Shamanic Songs", + "duration": "" + }, + { + "position": "18", + "type_": "track", + "artists": [ + { + "name": "Evdokja Egorovna Lekhanova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046204, + "resource_url": "https://api.discogs.com/artists/9046204" + } + ], + "title": "Untitled", + "duration": "4:41" + }, + { + "position": "19", + "type_": "track", + "artists": [ + { + "name": "Oktjabrina Vladimirovna Naumova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046189, + "resource_url": "https://api.discogs.com/artists/9046189" + } + ], + "title": "Buga", + "duration": "1:27" + }, + { + "position": "20", + "type_": "track", + "artists": [ + { + "name": "Savelij Vasilev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046207, + "resource_url": "https://api.discogs.com/artists/9046207" + } + ], + "title": "Extrait D'un Rituel Chamanique = An Excerpt From A Shamanic Ritual", + "duration": "2:04" + }, + { + "position": "21", + "type_": "track", + "artists": [ + { + "name": "Savelij Vasilev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046207, + "resource_url": "https://api.discogs.com/artists/9046207" + } + ], + "title": "Suide De Rituel = Continuation Of The Ritual", + "duration": "3:52" + }, + { + "position": "22", + "type_": "track", + "artists": [ + { + "name": "Savelij Vasilev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046207, + "resource_url": "https://api.discogs.com/artists/9046207" + } + ], + "title": "Suide De Rituel = Continuation Of The Ritual", + "duration": "1:37" + }, + { + "position": "23", + "type_": "track", + "artists": [ + { + "name": "Savelij Vasilev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046207, + "resource_url": "https://api.discogs.com/artists/9046207" + } + ], + "title": "Suide De Rituel = Continuation Of The Ritual", + "duration": "5:07" + }, + { + "position": "24", + "type_": "track", + "artists": [ + { + "name": "Prokopij Egorovič Nikolaev", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046177, + "resource_url": "https://api.discogs.com/artists/9046177" + } + ], + "title": "Gokaj", + "duration": "3:35" + }, + { + "position": "", + "type_": "heading", + "title": "Berceuses = Lullabies", + "duration": "" + }, + { + "position": "25", + "type_": "track", + "artists": [ + { + "name": "Galina Andreevna Abramova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046174, + "resource_url": "https://api.discogs.com/artists/9046174" + } + ], + "title": "Ogol", + "duration": "4:26" + }, + { + "position": "26", + "type_": "track", + "artists": [ + { + "name": "Elena Dmitrieva Tikhonova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046201, + "resource_url": "https://api.discogs.com/artists/9046201" + } + ], + "title": "Untitled", + "duration": "1:15" + }, + { + "position": "27", + "type_": "track", + "artists": [ + { + "name": "Oktjabrina Vladimirovna Naumova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046189, + "resource_url": "https://api.discogs.com/artists/9046189" + } + ], + "title": "Sagadjam", + "duration": "5:47" + }, + { + "position": "28", + "type_": "track", + "artists": [ + { + "name": "Margarita Kolesova", + "anv": "", + "join": "", + "role": "", + "tracks": "", + "id": 9046210, + "resource_url": "https://api.discogs.com/artists/9046210" + } + ], + "title": "Ba-lju-lju", + "duration": "3:55" + } + ], + "extraartists": [ + { + "name": "Claudine Combalier", + "anv": "", + "join": "", + "role": "Design Concept [Conception Graphique]", + "tracks": "", + "id": 2161557, + "resource_url": "https://api.discogs.com/artists/2161557" + }, + { + "name": "Gilles Fruchaux", + "anv": "", + "join": "", + "role": "Editor [Collection Dominique Buscail Dirigée Par]", + "tracks": "", + "id": 1284842, + "resource_url": "https://api.discogs.com/artists/1284842" + }, + { + "name": "Henri Lecomte", + "anv": "", + "join": "", + "role": "Editor [Série Dirigée Par]", + "tracks": "", + "id": 1293259, + "resource_url": "https://api.discogs.com/artists/1293259" + }, + { + "name": "Dominique Bach", + "anv": "", + "join": "", + "role": "Liner Notes [English Translation]", + "tracks": "", + "id": 1987331, + "resource_url": "https://api.discogs.com/artists/1987331" + }, + { + "name": "Alexandra Lavrillier", + "anv": "", + "join": "", + "role": "Liner Notes [Texte De Présentation], Liner Notes [Traductions À Partir De L'évenk]", + "tracks": "", + "id": 4233210, + "resource_url": "https://api.discogs.com/artists/4233210" + }, + { + "name": "Henri Lecomte", + "anv": "", + "join": "", + "role": "Photography By [Pages 1, 19]", + "tracks": "", + "id": 1293259, + "resource_url": "https://api.discogs.com/artists/1293259" + }, + { + "name": "Alexandra Lavrillier", + "anv": "", + "join": "", + "role": "Photography By [Pages 17, 18, 20, 36]", + "tracks": "", + "id": 4233210, + "resource_url": "https://api.discogs.com/artists/4233210" + }, + { + "name": "Alexandra Lavrillier", + "anv": "", + "join": "", + "role": "Recorded By", + "tracks": "4, 5, 7 to 11, 13, 19 to 24", + "id": 4233210, + "resource_url": "https://api.discogs.com/artists/4233210" + }, + { + "name": "Henri Lecomte", + "anv": "", + "join": "", + "role": "Recorded By", + "tracks": "1 to 3, 6, 12, 14 to 18, 25 to 28", + "id": 1293259, + "resource_url": "https://api.discogs.com/artists/1293259" + } + ], + "images": [ + { + "type": "primary", + "uri": "", + "resource_url": "", + "uri150": "", + "width": 600, + "height": 588 + }, + { + "type": "secondary", + "uri": "", + "resource_url": "", + "uri150": "", + "width": 600, + "height": 583 + }, + { + "type": "secondary", + "uri": "", + "resource_url": "", + "uri150": "", + "width": 600, + "height": 524 + }, + { + "type": "secondary", + "uri": "", + "resource_url": "", + "uri150": "", + "width": 600, + "height": 554 + } + ], + "thumb": "", + "estimated_weight": 85, + "blocked_from_sale": false +} \ No newline at end of file diff --git a/tests/test_discogs_tag.py b/tests/test_discogs_tag.py index 8acd1f5..9bb83ae 100644 --- a/tests/test_discogs_tag.py +++ b/tests/test_discogs_tag.py @@ -1,8 +1,10 @@ -from discogs_tag.cli import apply_metadata +from discogs_tag.cli import merge_metadata, apply_metadata +import pytest +import json -def test_apply_metadata(): +def test_merge_metadata(): audio = {} - apply_metadata({ + merge_metadata({ 'title': 'Title', 'artists': [{ 'anv': 'Artist 1' @@ -10,7 +12,7 @@ def test_apply_metadata(): 'name': 'Artist 2' }, { 'anv': '', - 'name': 'Artist 3' + 'name': 'Artist 3 (56)' }], 'position': '1-02', 'extraartists': [{ @@ -22,7 +24,16 @@ def test_apply_metadata(): }] }, audio) assert audio['title'] == 'Title' - assert audio['artist'] == 'Artist 1, Artist 2, Artist 3' + assert audio['artist'] == 'Artist 1, Artist 2, Artist 3, Guitarist' assert audio['discnumber'] == '1' assert audio['tracknumber'] == '02' assert audio['composer'] == 'Composer' + +def test_apply_metadata(): + with open('tests/release.json') as release: + data = json.load(release) + + # Test that files must match API results. + with pytest.raises(Exception) as e1: + apply_metadata(data, [], False, False) + assert "Expecting 28 files" in str(e1.value)