From f435e60b804b9c2430446c9f88b464177b2bf96c Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Thu, 12 Jan 2023 11:10:25 -0500 Subject: [PATCH] Add very rudimentary fetching of art and metadata from MB --- poetry.lock | 21 ++++- pyproject.toml | 1 + vrobbler/apps/music/admin.py | 3 + .../migrations/0005_album_cover_image.py | 23 ++++++ .../music/migrations/0006_album_artists.py | 20 +++++ vrobbler/apps/music/models.py | 78 ++++++++++++++----- 6 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 vrobbler/apps/music/migrations/0005_album_cover_image.py create mode 100644 vrobbler/apps/music/migrations/0006_album_artists.py diff --git a/poetry.lock b/poetry.lock index 3efd83b..ed84b9e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -625,6 +625,14 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "musicbrainzngs" +version = "0.7.1" +description = "Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "mypy" version = "0.961" @@ -1369,7 +1377,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "6105971e3adba942edffa16bd54f5822cdcabcd1e55dfecfc67410cf486a1a71" +content-hash = "f8d6b69cfb8ac53b7e03533e8fede68d323363bd73b9c7902d958dc433b1210e" [metadata.files] amqp = [ @@ -1719,6 +1727,10 @@ mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +musicbrainzngs = [ + {file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"}, + {file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"}, +] mypy = [ {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"}, {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"}, @@ -1769,6 +1781,13 @@ pbr = [ {file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"}, ] pillow = [ + {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, + {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, + {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, + {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, + {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, + {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, + {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, diff --git a/pyproject.toml b/pyproject.toml index 619885b..0e4a377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ django-markdownify = "^0.9.1" gunicorn = "^20.1.0" django-simple-history = "^3.1.1" whitenoise = "^6.3.0" +musicbrainzngs = "^0.7.1" [tool.poetry.dev-dependencies] Werkzeug = "2.0.3" diff --git a/vrobbler/apps/music/admin.py b/vrobbler/apps/music/admin.py index e67cff1..57acfa4 100644 --- a/vrobbler/apps/music/admin.py +++ b/vrobbler/apps/music/admin.py @@ -9,6 +9,9 @@ class AlbumAdmin(admin.ModelAdmin): list_display = ("name", "year", "musicbrainz_id") list_filter = ("year",) ordering = ("name",) + filter_horizontal = [ + 'artists', + ] @admin.register(Artist) diff --git a/vrobbler/apps/music/migrations/0005_album_cover_image.py b/vrobbler/apps/music/migrations/0005_album_cover_image.py new file mode 100644 index 0000000..2efa991 --- /dev/null +++ b/vrobbler/apps/music/migrations/0005_album_cover_image.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.5 on 2023-01-12 04:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + 'music', + '0004_alter_artist_options_alter_album_musicbrainz_id_and_more', + ), + ] + + operations = [ + migrations.AddField( + model_name='album', + name='cover_image', + field=models.ImageField( + blank=True, null=True, upload_to='albums/' + ), + ), + ] diff --git a/vrobbler/apps/music/migrations/0006_album_artists.py b/vrobbler/apps/music/migrations/0006_album_artists.py new file mode 100644 index 0000000..8c7a9bf --- /dev/null +++ b/vrobbler/apps/music/migrations/0006_album_artists.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.5 on 2023-01-12 05:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('music', '0005_album_cover_image'), + ] + + operations = [ + migrations.AddField( + model_name='album', + name='artists', + field=models.ManyToManyField( + blank=True, null=True, to='music.artist' + ), + ), + ] diff --git a/vrobbler/apps/music/models.py b/vrobbler/apps/music/models.py index 59d1e8d..654201c 100644 --- a/vrobbler/apps/music/models.py +++ b/vrobbler/apps/music/models.py @@ -1,9 +1,10 @@ import logging from typing import Dict, Optional from uuid import uuid4 +import musicbrainzngs from django.apps.config import cached_property - +from django.core.files.base import ContentFile from django.db import models from django.utils.translation import gettext_lazy as _ from django_extensions.db.models import TimeStampedModel @@ -12,29 +13,13 @@ logger = logging.getLogger(__name__) BNULL = {"blank": True, "null": True} -class Album(TimeStampedModel): - uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) - name = models.CharField(max_length=255) - year = models.IntegerField(**BNULL) - musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL) - musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL) - musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL) - - def __str__(self): - return self.name - - @property - def mb_link(self): - return f"https://musicbrainz.org/release/{self.musicbrainz_id}" - - class Artist(TimeStampedModel): uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) name = models.CharField(max_length=255) musicbrainz_id = models.CharField(max_length=255, **BNULL) class Meta: - unique_together=[['name', 'musicbrainz_id']] + unique_together = [['name', 'musicbrainz_id']] def __str__(self): return self.name @@ -44,6 +29,59 @@ class Artist(TimeStampedModel): return f"https://musicbrainz.org/artist/{self.musicbrainz_id}" +class Album(TimeStampedModel): + uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) + name = models.CharField(max_length=255) + artists = models.ManyToManyField(Artist, **BNULL) + year = models.IntegerField(**BNULL) + musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL) + musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL) + musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL) + cover_image = models.ImageField(upload_to="albums/", **BNULL) + + def __str__(self): + return self.name + + @property + def primary_artist(self): + return self.artists.first() + + def fix_metadata(self): + musicbrainzngs.set_useragent('vrobbler', '0.3.0') + mb_data = musicbrainzngs.get_release_by_id( + self.musicbrainz_id, includes=['artists'] + ) + if not self.musicbrainz_albumartist_id: + self.musicbrainz_albumartist_id = mb_data['release'][ + 'artist-credit' + ][0]['artist']['id'] + if not self.year: + self.year = mb_data['release']['date'][0:4] + self.save(update_fields=['musicbrainz_albumartist_id', 'year']) + + new_artist = Artist.objects.filter( + musicbrainz_id=self.musicbrainz_albumartist_id + ).first() + if self.musicbrainz_albumartist_id and new_artist: + self.artists.add(new_artist) + if not new_artist: + for t in self.track_set.all(): + self.artists.add(t.artist) + + def fetch_artwork(self): + try: + img_data = musicbrainzngs.get_image_front(self.musicbrainz_id) + name = f"{self.name}_{self.uuid}.jpg" + self.cover_image = ContentFile(img_data, name=name) + self.save() + except musicbrainzngs.ResponseError: + logger.warning(f'No cover art found for {self.name}') + + @property + def mb_link(self): + return f"https://musicbrainz.org/release/{self.musicbrainz_id}" + + class Track(TimeStampedModel): class Opinion(models.IntegerChoices): DOWN = -1, 'Thumbs down' @@ -83,7 +121,7 @@ class Track(TimeStampedModel): 'musicbrainz_id' ): logger.warning( - f"No artist or artist musicbrainz ID found in message from Jellyfin, not scrobbling" + f"No artist or artist musicbrainz ID found in message from source, not scrobbling" ) return artist, artist_created = Artist.objects.get_or_create(**artist_dict) @@ -97,6 +135,8 @@ class Track(TimeStampedModel): logger.debug(f"Created new album {album}") else: logger.debug(f"Found album {album}") + album.fix_metadata() + album.fetch_artwork() track_dict['album_id'] = getattr(album, "id", None) track_dict['artist_id'] = artist.id