Compare commits

..

13 Commits
0.8.1 ... 0.8.6

Author SHA1 Message Date
0e4501cad3 Bump version to 0.8.6 2023-02-08 20:01:09 -05:00
71e4ff28c8 Add musicbrainz utilities 2023-02-08 20:00:46 -05:00
9f272df99c Fix fetching release group and cover images 2023-02-08 20:00:09 -05:00
8ba8ceefb8 Bump version to 0.8.5 2023-02-07 01:30:38 -05:00
9590cd0f60 Fix fetching artwork when importing tsv 2023-02-07 01:30:04 -05:00
5e7c8ff137 Bump version to 0.8.4 2023-02-07 00:52:11 -05:00
fae59849f8 Add mb lookups to TSV imports 2023-02-07 00:51:43 -05:00
837e1280bd Bump version to 0.8.3 2023-02-06 23:30:14 -05:00
8f9c825903 Fix user timezones in scrobbler files 2023-02-06 23:28:57 -05:00
541073aae3 Bump version to 0.8.2 2023-02-06 19:32:15 -05:00
b63ec6b15f Fix bug in export when artist does not exist 2023-02-06 19:31:25 -05:00
117157e3ae Fix audioscrobbler import bug
Issue was not having a user so we couldn't set a timezone. All fixed now
2023-02-06 19:30:58 -05:00
0c10e78d5e Fix bug in unified scrobbler for podcasts 2023-02-06 17:54:48 -05:00
9 changed files with 199 additions and 25 deletions

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "vrobbler" name = "vrobbler"
version = "0.8.1" version = "0.8.6"
description = "" description = ""
authors = ["Colin Powell <colin@unbl.ink>"] authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -72,11 +72,19 @@ class Album(TimeStampedModel):
return self.artists.first() return self.artists.first()
def fix_metadata(self): def fix_metadata(self):
if not self.musicbrainz_albumartist_id or not self.year: if (
not self.musicbrainz_albumartist_id
or not self.year
or not self.musicbrainz_releasegroup_id
):
musicbrainzngs.set_useragent('vrobbler', '0.3.0') musicbrainzngs.set_useragent('vrobbler', '0.3.0')
mb_data = musicbrainzngs.get_release_by_id( mb_data = musicbrainzngs.get_release_by_id(
self.musicbrainz_id, includes=['artists'] self.musicbrainz_id, includes=['artists', 'release-groups']
) )
if not self.musicbrainz_releasegroup_id:
self.musicbrainz_releasegroup_id = mb_data['release'][
'release-group'
]['id']
if not self.musicbrainz_albumartist_id: if not self.musicbrainz_albumartist_id:
self.musicbrainz_albumartist_id = mb_data['release'][ self.musicbrainz_albumartist_id = mb_data['release'][
'artist-credit' 'artist-credit'
@ -89,7 +97,13 @@ class Album(TimeStampedModel):
except IndexError: except IndexError:
pass pass
self.save(update_fields=['musicbrainz_albumartist_id', 'year']) self.save(
update_fields=[
'musicbrainz_albumartist_id',
'musicbrainz_releasegroup_id',
'year',
]
)
new_artist = Artist.objects.filter( new_artist = Artist.objects.filter(
musicbrainz_id=self.musicbrainz_albumartist_id musicbrainz_id=self.musicbrainz_albumartist_id
@ -99,6 +113,11 @@ class Album(TimeStampedModel):
if not new_artist: if not new_artist:
for t in self.track_set.all(): for t in self.track_set.all():
self.artists.add(t.artist) self.artists.add(t.artist)
if (
not self.cover_image
or self.cover_image == 'default-image-replace-me'
):
self.fetch_artwork()
def fetch_artwork(self, force=False): def fetch_artwork(self, force=False):
if not self.cover_image and not force: if not self.cover_image and not force:
@ -115,7 +134,10 @@ class Album(TimeStampedModel):
f'No cover art found for {self.name} by release' f'No cover art found for {self.name} by release'
) )
if not self.cover_image and self.musicbrainz_releasegroup_id: if (
not self.cover_image
or self.cover_image == "default-image-replace-me"
) and self.musicbrainz_releasegroup_id:
try: try:
img_data = musicbrainzngs.get_release_group_image_front( img_data = musicbrainzngs.get_release_group_image_front(
self.musicbrainz_releasegroup_id self.musicbrainz_releasegroup_id
@ -131,8 +153,6 @@ class Album(TimeStampedModel):
logger.debug( logger.debug(
f"No cover art found for release or release group for {self.name}, setting to default" f"No cover art found for release or release group for {self.name}, setting to default"
) )
# TODO Get a placeholder image in here
self.cover_image = 'default-image-replace-me'
self.save() self.save()
@property @property

View File

@ -1,3 +1,5 @@
import pytz
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
@ -16,3 +18,7 @@ class UserProfile(TimeStampedModel):
def __str__(self): def __str__(self):
return f"User profile for {self.user}" return f"User profile for {self.user}"
@property
def tzinfo(self):
return pytz.timezone(self.timezone)

View File

@ -52,8 +52,9 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"):
track = scrobble.track track = scrobble.track
track_number = 0 # TODO Add track number track_number = 0 # TODO Add track number
track_rating = "S" # TODO implement ratings? track_rating = "S" # TODO implement ratings?
track_artist = track.artist or track.album.primary_artist
row = [ row = [
track.album.primary_artist.name, track_artist,
track.album.name, track.album.name,
track.title, track.title,
track_number, track_number,

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.5 on 2023-02-07 00:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('scrobbles', '0016_audioscrobblertsvimport_process_count'),
]
operations = [
migrations.AddField(
model_name='audioscrobblertsvimport',
name='user',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -24,6 +24,7 @@ class AudioScrobblerTSVImport(TimeStampedModel):
uuid = instance.uuid uuid = instance.uuid
return f'audioscrobbler-uploads/{uuid}.{extension}' return f'audioscrobbler-uploads/{uuid}.{extension}'
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, **BNULL)
uuid = models.UUIDField(editable=False, default=uuid4) uuid = models.UUIDField(editable=False, default=uuid4)
tsv_file = models.FileField(upload_to=get_path, **BNULL) tsv_file = models.FileField(upload_to=get_path, **BNULL)
processed_on = models.DateTimeField(**BNULL) processed_on = models.DateTimeField(**BNULL)
@ -48,7 +49,12 @@ class AudioScrobblerTSVImport(TimeStampedModel):
logger.info(f"{self} already processed on {self.processed_on}") logger.info(f"{self} already processed on {self.processed_on}")
return return
scrobbles = process_audioscrobbler_tsv_file(self.tsv_file.path) tz = None
if self.user:
tz = self.user.profile.tzinfo
scrobbles = process_audioscrobbler_tsv_file(
self.tsv_file.path, user_tz=tz
)
if scrobbles: if scrobbles:
self.process_log = f"Created {len(scrobbles)} scrobbles" self.process_log = f"Created {len(scrobbles)} scrobbles"
for scrobble in scrobbles: for scrobble in scrobbles:
@ -255,7 +261,7 @@ class Scrobble(TimeStampedModel):
scrobble_data['video_id'] = media.id scrobble_data['video_id'] = media.id
if media.__class__.__name__ == 'Episode': if media.__class__.__name__ == 'Episode':
media_query = models.Q(podcast_episode=media) media_query = models.Q(podcast_episode=media)
scrobble_data['podcast_id'] = media.id scrobble_data['podcast_episode_id'] = media.id
if media.__class__.__name__ == 'SportEvent': if media.__class__.__name__ == 'SportEvent':
media_query = models.Q(sport_event=media) media_query = models.Q(sport_event=media)
scrobble_data['sport_event_id'] = media.id scrobble_data['sport_event_id'] = media.id

View File

@ -0,0 +1,87 @@
import logging
import musicbrainzngs
from dateutil.parser import parse
logger = logging.getLogger(__name__)
def lookup_album_from_mb(musicbrainz_id: str) -> dict:
release_dict = {}
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
release_data = musicbrainzngs.get_release_by_id(
musicbrainz_id,
includes=['artists', 'release-groups', 'recordings'],
).get('release')
if not release_data:
return release_dict
primary_artist = release_data.get('artist-credit')[0]
release_dict = {
'artist': {
'name': primary_artist.get('name'),
'musicbrainz_id': primary_artist.get('id'),
},
'album': {
'name': release_data.get('title'),
'musicbrainz_id': musicbrainz_id,
'musicbrainz_releasegroup_id': release_data.get(
'release-group'
).get('id'),
'musicbrainz_albumaritist_id': primary_artist.get('id'),
'year': release_data.get('year')[0:4],
},
}
release_dict['tracks'] = []
for track in release_data.get('medium-list')[0]['track-list']:
recording = track['recording']
release_dict['tracks'].append(
{
'title': recording['title'],
'musicbrainz_id': recording['id'],
'run_time_ticks': track['length'],
}
)
return release_dict
def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
top_result = musicbrainzngs.search_releases(
release_name, artist=artist_name
)['release-list'][0]
score = int(top_result.get('ext:score'))
if score < 85:
logger.debug(
"Album lookup score below 85 threshold",
extra={"result": top_result},
)
return {}
print(top_result)
return {
"year": parse(top_result["date"]).year,
"mb_id": top_result["id"],
"mb_group_id": top_result["release-group"]["id"],
}
def lookup_artist_id_from_mb(artist_name: str) -> str:
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
top_result = musicbrainzngs.search_artists(artist=artist_name)[
'artist-list'
][0]
score = int(top_result.get('ext:score'))
if score < 85:
logger.debug(
"Artist lookup score below 85 threshold",
extra={"result": top_result},
)
return ""
return top_result["id"]

View File

@ -6,12 +6,19 @@ import pytz
from music.models import Album, Artist, Track from music.models import Album, Artist, Track
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from vrobbler.apps.scrobbles.musicbrainz import (
lookup_album_dict_from_mb,
lookup_artist_id_from_mb,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def process_audioscrobbler_tsv_file(file_path): def process_audioscrobbler_tsv_file(file_path, user_tz=None):
"""Takes a path to a file of TSV data and imports it as past scrobbles""" """Takes a path to a file of TSV data and imports it as past scrobbles"""
new_scrobbles = [] new_scrobbles = []
if not user_tz:
user_tz = pytz.utc
with open(file_path) as infile: with open(file_path) as infile:
source = 'Audioscrobbler File' source = 'Audioscrobbler File'
@ -30,9 +37,8 @@ def process_audioscrobbler_tsv_file(file_path):
continue continue
artist, artist_created = Artist.objects.get_or_create(name=row[0]) artist, artist_created = Artist.objects.get_or_create(name=row[0])
if artist_created: if artist_created:
logger.debug(f"Created artist {artist}") artist.musicbrainz_id = lookup_artist_id_from_mb(artist.name)
else: artist.save(update_fields=["musicbrainz_id"])
logger.debug(f"Found artist {artist}")
album = None album = None
album_created = False album_created = False
@ -50,9 +56,23 @@ def process_audioscrobbler_tsv_file(file_path):
album.artists.add(artist) album.artists.add(artist)
if album_created: if album_created:
logger.debug(f"Created album {album}") album_dict = lookup_album_dict_from_mb(
else: album.name, artist_name=artist.name
logger.debug(f"Found album {album}") )
album.year = album_dict["year"]
album.musicbrainz_id = album_dict["mb_id"]
album.musicbrainz_releasegroup_id = album_dict["mb_group_id"]
album.musicbrainz_albumartist_id = artist.musicbrainz_id
album.save(
update_fields=[
"year",
"musicbrainz_id",
"musicbrainz_releasegroup_id",
"musicbrainz_albumartist_id",
]
)
album.artists.add(artist)
album.fetch_artwork()
track, track_created = Track.objects.get_or_create( track, track_created = Track.objects.get_or_create(
title=row[2], title=row[2],
@ -60,17 +80,16 @@ def process_audioscrobbler_tsv_file(file_path):
album=album, album=album,
) )
if track_created:
logger.debug(f"Created track {track}")
else:
logger.debug(f"Found track {track}")
if track_created: if track_created:
track.musicbrainz_id = row[7] track.musicbrainz_id = row[7]
track.run_time = int(row[4])
track.run_time_ticks = int(row[4]) * 1000
track.save() track.save()
timestamp = datetime.utcfromtimestamp(int(row[6])).replace( timestamp = (
tzinfo=pytz.utc datetime.utcfromtimestamp(int(row[6]))
.replace(tzinfo=user_tz)
.astimezone(pytz.utc)
) )
source = 'Audioscrobbler File' source = 'Audioscrobbler File'

View File

@ -4,6 +4,7 @@ from datetime import datetime
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models.fields import timezone from django.db.models.fields import timezone
from django.http import FileResponse, HttpResponseRedirect, JsonResponse from django.http import FileResponse, HttpResponseRedirect, JsonResponse
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -160,12 +161,20 @@ class JsonableResponseMixin:
return JsonResponse(data) return JsonResponse(data)
class AudioScrobblerImportCreateView(JsonableResponseMixin, CreateView): class AudioScrobblerImportCreateView(
LoginRequiredMixin, JsonableResponseMixin, CreateView
):
model = AudioScrobblerTSVImport model = AudioScrobblerTSVImport
fields = ['tsv_file'] fields = ['tsv_file']
template_name = 'scrobbles/upload_form.html' template_name = 'scrobbles/upload_form.html'
success_url = reverse_lazy('vrobbler-home') success_url = reverse_lazy('vrobbler-home')
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
return HttpResponseRedirect(self.get_success_url())
@csrf_exempt @csrf_exempt
@api_view(['GET']) @api_view(['GET'])