From 1928acf8a6c0dc5cf26b0b0998b339e04f98f169 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Fri, 29 May 2026 08:26:30 -0400 Subject: [PATCH] [music] Make artists_m2m field source of artist truth (05a24455) --- PROJECT.org | 28 ++++++++++++++++- vrobbler/apps/music/admin.py | 2 +- .../migrations/0034_backfill_album_artists.py | 29 ++++++++++++++++++ vrobbler/apps/music/models.py | 30 +++++++++++-------- vrobbler/apps/scrobbles/export.py | 2 +- vrobbler/templates/music/album_list.html | 2 +- 6 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 vrobbler/apps/music/migrations/0034_backfill_album_artists.py diff --git a/PROJECT.org b/PROJECT.org index 90244fe..08f9d2f 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -93,7 +93,7 @@ fetching and simple saving. :LOGBOOK: CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 :END: -* Backlog [32/47] :vrobbler:project:personal: +* Backlog [33/49] :vrobbler:project:personal: ** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: :PROPERTIES: :ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85 @@ -400,6 +400,20 @@ CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 :PROPERTIES: :ID: 133bcf71-078f-4efa-a029-1eae4b4d146d :END: +** TODO [#C] Fix exporting so it works reliably :exporting:project:feature: + +*** Description + +The existing export function is very naieve. It runs in the web process, takes +too long and just dumps tracks. We should make it more robust by creating one +CSV file per scrobble media type and writing them into a zip file that gets +placed in the media directory: + +`/media/exports/user_/-export.zip` + +And this should all be done in a celery task that is just kicked off by the +"Export" button on the frontend + ** TODO [#B] Add importer class for IMAP imports :vrobbler:feature:imap:importers:project:personal: :PROPERTIES: :ID: b1426d92-2feb-4d15-9738-d5b7b0594f96 @@ -475,6 +489,18 @@ to GMT to save it in the database. It would be nice to not duplicate comments that exist on a task when it's first scrobbled. +** DONE [#B] Make artists_m2m field source of artist truth for albums :music:bug:albums: +:PROPERTIES: +:ID: 05a24455-0e71-45ef-ac6e-c1b7b843047d +:END: + +*** Description + +Albums have an FK for album_artist, but like artists, the M2M should be the +source of truth. We should migrate all uses of album_artist to an `artist` +property on the Album model and use a data migration to populate artists with +the album_artist value. + ** DONE [#A] Fix various artist album problem with Superwolves (track with multiple artists) :vrobbler:project:music:bug:artists: :PROPERTIES: :ID: 590bc038-745f-710b-8272-4d8a3d2efa01 diff --git a/vrobbler/apps/music/admin.py b/vrobbler/apps/music/admin.py index 6b5d78f..57f1375 100644 --- a/vrobbler/apps/music/admin.py +++ b/vrobbler/apps/music/admin.py @@ -9,7 +9,7 @@ class AlbumAdmin(admin.ModelAdmin): list_display = ( "name", "year", - "album_artist", + "artist", "theaudiodb_genre", "theaudiodb_mood", "musicbrainz_id", diff --git a/vrobbler/apps/music/migrations/0034_backfill_album_artists.py b/vrobbler/apps/music/migrations/0034_backfill_album_artists.py new file mode 100644 index 0000000..c62c9ee --- /dev/null +++ b/vrobbler/apps/music/migrations/0034_backfill_album_artists.py @@ -0,0 +1,29 @@ +from django.db import migrations + + +def backfill_album_artist_to_artists(apps, schema_editor): + Album = apps.get_model("music", "Album") + AlbumArtist = Album.artists.through + AlbumArtist.objects.bulk_create( + [ + AlbumArtist(album_id=r["id"], artist_id=r["album_artist_id"]) + for r in Album.objects.filter(album_artist__isnull=False).values( + "id", "album_artist_id" + ) + ], + ignore_conflicts=True, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("music", "0033_backfill_track_artists"), + ] + + operations = [ + migrations.RunPython( + backfill_album_artist_to_artists, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/vrobbler/apps/music/models.py b/vrobbler/apps/music/models.py index db8c270..8c90516 100644 --- a/vrobbler/apps/music/models.py +++ b/vrobbler/apps/music/models.py @@ -294,7 +294,7 @@ class Album(TimeStampedModel): alt_names = models.TextField(**BNULL) def __str__(self) -> str: - return "{} by {}".format(self.name, self.album_artist) + return "{} by {}".format(self.name, self.artist) def get_absolute_url(self): return reverse("music:album_detail", kwargs={"slug": self.uuid}) @@ -320,6 +320,10 @@ class Album(TimeStampedModel): .order_by("-scrobble_count") ) + @property + def artist(self): + return self.artists.first() or self.album_artist + def fix_album_artist(self): from music.utils import get_or_create_various_artists @@ -338,10 +342,10 @@ class Album(TimeStampedModel): ) return - if self.album_artist and (not self.allmusic_id or force): - slug = get_allmusic_slug(self.album_artist.name, self.name) + if self.artist and (not self.allmusic_id or force): + slug = get_allmusic_slug(self.artist.name, self.name) if not slug: - logger.info(f"No allmsuic link for {self} by {self.album_artist}") + logger.info(f"No allmsuic link for {self} by {self.artist}") return self.allmusic_id = slug self.save(update_fields=["allmusic_id"]) @@ -351,7 +355,7 @@ class Album(TimeStampedModel): allmusic_data = scrape_data_from_allmusic(self.allmusic_link) if not allmusic_data: - logger.info(f"No allmsuic data for {self} by {self.album_artist}") + logger.info(f"No allmsuic data for {self} by {self.artist}") return self.allmusic_review = allmusic_data["review"] @@ -360,8 +364,8 @@ class Album(TimeStampedModel): def scrape_theaudiodb(self) -> None: artist = "Various Artists" - if self.album_artist: - artist = self.album_artist.name + if self.artist: + artist = self.artist.name album_data = lookup_album_from_tadb(self.name, artist) if not album_data.get("theaudiodb_id"): logger.info(f"No data for {self} found in TheAudioDB") @@ -374,7 +378,7 @@ class Album(TimeStampedModel): def scrape_bandcamp(self, force=False) -> None: if not self.bandcamp_id or force: - slug = get_bandcamp_slug(self.album_artist.name, self.name) + slug = get_bandcamp_slug(self.artist.name, self.name) if not slug: logger.info(f"No bandcamp link for {self}") return @@ -485,19 +489,19 @@ class Album(TimeStampedModel): @property def rym_link(self): - artist_slug = self.album_artist.name.lower().replace(" ", "-") + artist_slug = self.artist.name.lower().replace(" ", "-") album_slug = self.name.lower().replace(" ", "-") return f"https://rateyourmusic.com/release/album/{artist_slug}/{album_slug}/" @property def bandcamp_link(self): - if self.bandcamp_id and self.album_artist.bandcamp_id: - return f"https://{self.album_artist.bandcamp_id}.bandcamp.com/album/{self.bandcamp_id}" + if self.bandcamp_id and self.artist.bandcamp_id: + return f"https://{self.artist.bandcamp_id}.bandcamp.com/album/{self.bandcamp_id}" return "" @property def bandcamp_search_link(self): - artist = self.album_artist.name.lower() + artist = self.artist.name.lower() album = self.name.lower() return f"https://bandcamp.com/search?q={album} {artist}&item_type=a" @@ -565,7 +569,7 @@ class Album(TimeStampedModel): album_artist=artist, alt_names=alt_name, ) - album.artists.add(*extra_artists) + album.artists.add(artist, *extra_artists) album.fetch_artwork() return album diff --git a/vrobbler/apps/scrobbles/export.py b/vrobbler/apps/scrobbles/export.py index 2a74ee0..56b685e 100644 --- a/vrobbler/apps/scrobbles/export.py +++ b/vrobbler/apps/scrobbles/export.py @@ -50,7 +50,7 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"): track = scrobble.track track_number = 0 # TODO Add track number track_rating = "S" # TODO implement ratings? - track_artist = track.artist or track.album.album_artist + track_artist = track.artist or track.album.artist row = [ track_artist, track.album.name, diff --git a/vrobbler/templates/music/album_list.html b/vrobbler/templates/music/album_list.html index f19070a..76158ab 100644 --- a/vrobbler/templates/music/album_list.html +++ b/vrobbler/templates/music/album_list.html @@ -56,7 +56,7 @@ {{album.scrobbles.count}} {{album}} - {{album.album_artist}} + {{album.artist}} {% endfor %}