[music] Fix find_or_create for tracks
This commit is contained in:
@ -14,14 +14,14 @@ from imagekit.models import ImageSpecField
|
|||||||
from imagekit.processors import ResizeToFit
|
from imagekit.processors import ResizeToFit
|
||||||
from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic
|
from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic
|
||||||
from music.bandcamp import get_bandcamp_slug
|
from music.bandcamp import get_bandcamp_slug
|
||||||
from music.musicbrainz import lookup_album_dict_from_mb, lookup_track_from_mb
|
from music.musicbrainz import lookup_album_dict_from_mb, lookup_album_from_mb, lookup_track_from_mb, lookup_artist_from_mb
|
||||||
from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb
|
from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb
|
||||||
|
from music.utils import clean_artist_name
|
||||||
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
|
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
|
|
||||||
|
|
||||||
class Artist(TimeStampedModel):
|
class Artist(TimeStampedModel):
|
||||||
"""Represents a music artist.
|
"""Represents a music artist.
|
||||||
|
|
||||||
@ -170,23 +170,20 @@ class Artist(TimeStampedModel):
|
|||||||
return f"https://bandcamp.com/search?q={artist}&item_type=b"
|
return f"https://bandcamp.com/search?q={artist}&item_type=b"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, name: str, musicbrainz_id: str = "") -> "Artist":
|
def find_or_create(cls, name: str = "", musicbrainz_id: str = "") -> "Artist":
|
||||||
from music.musicbrainz import lookup_artist_from_mb
|
keys = {}
|
||||||
from music.utils import clean_artist_name
|
if name:
|
||||||
|
name = clean_artist_name(name)
|
||||||
|
keys["name"] = name
|
||||||
|
|
||||||
if not name:
|
|
||||||
raise Exception("Must have name to lookup artist")
|
|
||||||
|
|
||||||
artist = None
|
|
||||||
name = clean_artist_name(name)
|
|
||||||
|
|
||||||
# Check for name/mbid combo, just mbid and then just name
|
|
||||||
if musicbrainz_id:
|
if musicbrainz_id:
|
||||||
artist = cls.objects.filter(
|
keys["musicbrainz_id"] = musicbrainz_id
|
||||||
name=name, musicbrainz_id=musicbrainz_id
|
|
||||||
).first()
|
if not keys:
|
||||||
if not artist:
|
raise Exception("Must have name, mb_id or both to lookup artist")
|
||||||
artist = cls.objects.filter(musicbrainz_id=musicbrainz_id).first()
|
|
||||||
|
artist = cls.objects.filter(**keys).first()
|
||||||
|
|
||||||
if not artist:
|
if not artist:
|
||||||
artist = cls.objects.filter(
|
artist = cls.objects.filter(
|
||||||
models.Q(name=name) | models.Q(alt_names__icontains=name)
|
models.Q(name=name) | models.Q(alt_names__icontains=name)
|
||||||
@ -593,87 +590,67 @@ class Track(ScrobblableMixin):
|
|||||||
url = self.album.cover_image_medium.url
|
url = self.album.cover_image_medium.url
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(
|
||||||
cls,
|
cls,
|
||||||
title: str = "",
|
title: str = "",
|
||||||
musicbrainz_id: str = "",
|
artist_name: str = "",
|
||||||
album_name: str = "",
|
musicbrainz_id: str = "",
|
||||||
artist_name: str = "",
|
album_name: str = "",
|
||||||
enrich: bool = True,
|
run_time_seconds: int = 900,
|
||||||
run_time_seconds: Optional[int] = None,
|
enrich: bool = False,
|
||||||
|
commit: bool = True
|
||||||
) -> "Track":
|
) -> "Track":
|
||||||
# TODO we can use Q to build queries here based on whether we have mbid and album name
|
"""Given a name, try to find the track by the artist from Musicbrainz.
|
||||||
track = None
|
|
||||||
# Full look up with MB ID
|
|
||||||
if album_name:
|
|
||||||
track = cls.objects.filter(
|
|
||||||
musicbrainz_id=musicbrainz_id,
|
|
||||||
title=title,
|
|
||||||
artist__name=artist_name,
|
|
||||||
album__name=album_name,
|
|
||||||
).first()
|
|
||||||
# Full look up without album
|
|
||||||
if not track:
|
|
||||||
track = cls.objects.filter(
|
|
||||||
musicbrainz_id=musicbrainz_id,
|
|
||||||
title=title,
|
|
||||||
artist__name=artist_name,
|
|
||||||
).first()
|
|
||||||
|
|
||||||
# Full look up without MB ID
|
As a basic conceit we trust the source for giving us the track and artist
|
||||||
if not track:
|
name
|
||||||
track = cls.objects.filter(
|
|
||||||
title=title,
|
Optionally, we can update any found artists with overwrite."""
|
||||||
artist__name=artist_name,
|
created = False
|
||||||
album__name=album_name,
|
if musicbrainz_id:
|
||||||
).first()
|
track = cls.objects.filter(musicbrainz_id=musicbrainz_id).first()
|
||||||
# Base look up without MB ID or album
|
artist = track.artist
|
||||||
if not track:
|
if not track and not (title and album_name):
|
||||||
track = cls.objects.filter(
|
raise Exception("Cannot find track with musicbrainz_id and no track title or artist name provided.")
|
||||||
title=title,
|
else:
|
||||||
artist__name=artist_name,
|
artist = Artist.find_or_create(artist_name)
|
||||||
).first()
|
track, created = cls.objects.get_or_create(title=title, artist=artist)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
logger.info("Found exact match for track by name and artist", extra={"title": title, "artist_name": artist_name, "track_id": track.id})
|
||||||
|
|
||||||
|
if track.album and album_name != track.album.name:
|
||||||
|
# TODO found track, but it's on a different album ... associations?
|
||||||
|
logger.info("Found track by artist, but album is different.")
|
||||||
|
album = Album.find_or_create()
|
||||||
|
|
||||||
|
if enrich:
|
||||||
|
album = None
|
||||||
|
if album_name:
|
||||||
|
album = Album.find_or_create(album_name)
|
||||||
|
|
||||||
|
if artist.musicbrainz_id:
|
||||||
|
track_dict = lookup_track_from_mb(title, artist.musicbrainz_id)
|
||||||
|
musicbrainz_id = musicbrainz_id or track_dict.get("id", "")
|
||||||
|
|
||||||
|
found_title: bool = track_dict.get("name", False)
|
||||||
|
mismatched_title: bool = title != track_dict.get("name", "")
|
||||||
|
if found_title and mismatched_title:
|
||||||
|
logger.warning("Source track title and found title do not match", extra={"title": title, "track_dict": track_dict})
|
||||||
|
|
||||||
if not track and enrich:
|
|
||||||
track_dict = lookup_track_from_mb(title, artist_name, album_name)
|
|
||||||
musicbrainz_id = musicbrainz_id or track_dict.get("id", "")
|
|
||||||
# TODO This only works some of the time
|
|
||||||
# try:
|
|
||||||
# album_name = album_name or track_dict.get("release-list")[
|
|
||||||
# 0
|
|
||||||
# ].get("title", "")
|
|
||||||
# except IndexError:
|
|
||||||
# pass
|
|
||||||
if not run_time_seconds:
|
if not run_time_seconds:
|
||||||
run_time_seconds = int(
|
run_time_seconds = int(
|
||||||
int(track_dict.get("length", 900000)) / 1000
|
int(track_dict.get("length", 900000)) / 1000
|
||||||
)
|
)
|
||||||
if title != track_dict.get("name", "") and track_dict.get(
|
|
||||||
"name", False
|
|
||||||
):
|
|
||||||
|
|
||||||
title = track_dict.get("name", "")
|
|
||||||
|
|
||||||
if musicbrainz_id:
|
track.album = album
|
||||||
track = cls.objects.filter(
|
track.artist = artist
|
||||||
musicbrainz_id=musicbrainz_id
|
track.run_time_seconds = run_time_seconds
|
||||||
).first()
|
if commit:
|
||||||
if not track:
|
track.save()
|
||||||
artist = Artist.find_or_create(name=artist_name)
|
# TODO Also set cover art and tags
|
||||||
album = None
|
|
||||||
if album_name:
|
|
||||||
album = Album.find_or_create(
|
|
||||||
name=album_name, artist_name=artist_name
|
|
||||||
)
|
|
||||||
track = cls.objects.create(
|
|
||||||
title=title,
|
|
||||||
album=album,
|
|
||||||
musicbrainz_id=musicbrainz_id,
|
|
||||||
artist=artist,
|
|
||||||
run_time_seconds=run_time_seconds,
|
|
||||||
)
|
|
||||||
# TODO maybe do this in a separate process?
|
|
||||||
track.fix_metadata()
|
|
||||||
|
|
||||||
return track
|
return track
|
||||||
|
|||||||
@ -1,21 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from music.musicbrainz import (
|
|
||||||
lookup_album_dict_from_mb,
|
|
||||||
lookup_artist_from_mb,
|
|
||||||
lookup_track_from_mb,
|
|
||||||
)
|
|
||||||
from music.constants import VARIOUS_ARTIST_DICT
|
from music.constants import VARIOUS_ARTIST_DICT
|
||||||
from scrobbles.utils import convert_to_seconds
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
from music.models import Album, Artist, Track
|
|
||||||
|
|
||||||
|
|
||||||
def clean_artist_name(name: str) -> str:
|
def clean_artist_name(name: str) -> str:
|
||||||
"""Remove featured names from artist string."""
|
"""Remove featured names from artist string."""
|
||||||
if "feat." in name.lower():
|
if "feat." in name.lower():
|
||||||
@ -27,156 +16,9 @@ def clean_artist_name(name: str) -> str:
|
|||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
def get_or_create_various_artists() -> "Artist":
|
||||||
|
from music.models import Artist
|
||||||
|
|
||||||
# TODO These are depreacted, remove them eventually
|
|
||||||
def get_or_create_artist(name: str, mbid: str = "") -> Artist:
|
|
||||||
"""Get an Artist object from the database.
|
|
||||||
|
|
||||||
Check if an artist with this name or Musicbrainz ID already exists.
|
|
||||||
Otherwise, go lookup artist data from Musicbrainz and create one.
|
|
||||||
|
|
||||||
"""
|
|
||||||
artist = None
|
|
||||||
name = clean_artist_name(name)
|
|
||||||
|
|
||||||
# Check for name/mbid combo, just mbid and then just name
|
|
||||||
artist = Artist.objects.filter(name=name, mbid=mbid).first()
|
|
||||||
if not artist:
|
|
||||||
artist = Artist.objects.filter(musicbrainz_id=mbid).first()
|
|
||||||
if not artist:
|
|
||||||
artist = Artist.objects.filter(name=name).first()
|
|
||||||
|
|
||||||
# Does not exist, look it up from Musicbrainz
|
|
||||||
if not artist:
|
|
||||||
artist_dict = lookup_artist_from_mb(name)
|
|
||||||
mbid = mbid or artist_dict.get("id", "")
|
|
||||||
|
|
||||||
if mbid:
|
|
||||||
artist = Artist.objects.filter(musicbrainz_id=mbid).first()
|
|
||||||
|
|
||||||
if not artist:
|
|
||||||
artist = Artist.objects.create(name=name, musicbrainz_id=mbid)
|
|
||||||
# TODO maybe this should be spun off into an async task?
|
|
||||||
artist.fix_metadata()
|
|
||||||
|
|
||||||
return artist
|
|
||||||
|
|
||||||
|
|
||||||
# TODO These are depreacted, remove them eventually
|
|
||||||
def get_or_create_album(
|
|
||||||
name: str, artist: Artist, mbid: str = None
|
|
||||||
) -> Optional[Album]:
|
|
||||||
album = None
|
|
||||||
album_dict = lookup_album_dict_from_mb(name, artist_name=artist.name)
|
|
||||||
|
|
||||||
name = name or album_dict.get("title", None)
|
|
||||||
if not name:
|
|
||||||
logger.debug(
|
|
||||||
f"Cannot get or create album by {artist} with no name ({name})"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
album = Album.objects.filter(
|
|
||||||
musicbrainz_id=mbid, name=name, artists__in=[artist]
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not album:
|
|
||||||
mbid_group = album_dict.get("mb_group_id")
|
|
||||||
album = Album.objects.filter(
|
|
||||||
musicbrainz_releasegroup_id=mbid_group
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not album and name:
|
|
||||||
mbid = mbid or album_dict["mb_id"]
|
|
||||||
album, album_created = Album.objects.get_or_create(musicbrainz_id=mbid)
|
|
||||||
if album_created:
|
|
||||||
album.name = name
|
|
||||||
album.year = album_dict["year"]
|
|
||||||
album.musicbrainz_releasegroup_id = album_dict["mb_group_id"]
|
|
||||||
album.musicbrainz_albumartist_id = artist.musicbrainz_id
|
|
||||||
album.save(
|
|
||||||
update_fields=[
|
|
||||||
"name",
|
|
||||||
"musicbrainz_id",
|
|
||||||
"year",
|
|
||||||
"musicbrainz_releasegroup_id",
|
|
||||||
"musicbrainz_albumartist_id",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
album.artists.add(artist)
|
|
||||||
album.fix_album_artist()
|
|
||||||
album.fetch_artwork()
|
|
||||||
album.scrape_allmusic()
|
|
||||||
|
|
||||||
if not album:
|
|
||||||
logger.warn(f"No album found for {name} and {mbid}")
|
|
||||||
|
|
||||||
album.fix_album_artist()
|
|
||||||
return album
|
|
||||||
|
|
||||||
|
|
||||||
# TODO These are depreacted, remove them eventually
|
|
||||||
def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
|
|
||||||
try:
|
|
||||||
track_run_time_seconds = int(
|
|
||||||
post_data.get(post_keys.get("RUN_TIME"), 0)
|
|
||||||
)
|
|
||||||
except ValueError: # Sometimes we get run time as a string like "01:35"
|
|
||||||
track_run_time_seconds = convert_to_seconds(
|
|
||||||
post_data.get(post_keys.get("RUN_TIME"), 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
artist_name = post_data.get(post_keys.get("ARTIST_NAME"), "")
|
|
||||||
artist_mb_id = post_data.get(post_keys.get("ARTIST_MB_ID"), "")
|
|
||||||
album_title = post_data.get(post_keys.get("ALBUM_NAME"), "")
|
|
||||||
album_mb_id = post_data.get(post_keys.get("ALBUM_MB_ID"), "")
|
|
||||||
track_title = post_data.get(post_keys.get("TRACK_TITLE"), "")
|
|
||||||
track_mb_id = post_data.get(post_keys.get("TRACK_MB_ID"), "")
|
|
||||||
|
|
||||||
artist = Artist.find_or_create(artist_name, artist_mb_id)
|
|
||||||
album = None
|
|
||||||
# We may get no album ID or title, in which case, skip
|
|
||||||
if album_mb_id or album_title:
|
|
||||||
album = Album.find_or_create(
|
|
||||||
album_title, str(artist.name), album_mb_id
|
|
||||||
)
|
|
||||||
|
|
||||||
track = None
|
|
||||||
if not track_mb_id and album:
|
|
||||||
try:
|
|
||||||
track_mb_id = lookup_track_from_mb(
|
|
||||||
track_title,
|
|
||||||
artist.musicbrainz_id,
|
|
||||||
album.musicbrainz_id,
|
|
||||||
).get("id", 0)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not track_title and not track_mb_id:
|
|
||||||
logger.info(
|
|
||||||
"Cannot find track without either title or MB ID",
|
|
||||||
extra={"post_data": post_data},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if track_mb_id:
|
|
||||||
track = Track.objects.filter(musicbrainz_id=track_mb_id).first()
|
|
||||||
|
|
||||||
if not track and track_title:
|
|
||||||
track = Track.objects.filter(title=track_title, artist=artist).first()
|
|
||||||
|
|
||||||
if not track:
|
|
||||||
track = Track.objects.create(
|
|
||||||
title=track_title,
|
|
||||||
artist=artist,
|
|
||||||
album=album,
|
|
||||||
musicbrainz_id=track_mb_id,
|
|
||||||
run_time_seconds=track_run_time_seconds,
|
|
||||||
)
|
|
||||||
return track
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_various_artists() -> Artist:
|
|
||||||
artist = Artist.objects.filter(name="Various Artists").first()
|
artist = Artist.objects.filter(name="Various Artists").first()
|
||||||
if not artist:
|
if not artist:
|
||||||
artist = Artist.objects.create(**VARIOUS_ARTIST_DICT)
|
artist = Artist.objects.create(**VARIOUS_ARTIST_DICT)
|
||||||
|
|||||||
Reference in New Issue
Block a user