Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e4501cad3 | |||
| 71e4ff28c8 | |||
| 9f272df99c | |||
| 8ba8ceefb8 | |||
| 9590cd0f60 | |||
| 5e7c8ff137 | |||
| fae59849f8 | |||
| 837e1280bd | |||
| 8f9c825903 | |||
| 541073aae3 | |||
| b63ec6b15f | |||
| 117157e3ae | |||
| 0c10e78d5e |
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "0.8.1"
|
||||
version = "0.8.6"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
|
||||
@ -72,11 +72,19 @@ class Album(TimeStampedModel):
|
||||
return self.artists.first()
|
||||
|
||||
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')
|
||||
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:
|
||||
self.musicbrainz_albumartist_id = mb_data['release'][
|
||||
'artist-credit'
|
||||
@ -89,7 +97,13 @@ class Album(TimeStampedModel):
|
||||
except IndexError:
|
||||
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(
|
||||
musicbrainz_id=self.musicbrainz_albumartist_id
|
||||
@ -99,6 +113,11 @@ class Album(TimeStampedModel):
|
||||
if not new_artist:
|
||||
for t in self.track_set.all():
|
||||
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):
|
||||
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'
|
||||
)
|
||||
|
||||
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:
|
||||
img_data = musicbrainzngs.get_release_group_image_front(
|
||||
self.musicbrainz_releasegroup_id
|
||||
@ -131,8 +153,6 @@ class Album(TimeStampedModel):
|
||||
logger.debug(
|
||||
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()
|
||||
|
||||
@property
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import pytz
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
@ -16,3 +18,7 @@ class UserProfile(TimeStampedModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"User profile for {self.user}"
|
||||
|
||||
@property
|
||||
def tzinfo(self):
|
||||
return pytz.timezone(self.timezone)
|
||||
|
||||
@ -52,8 +52,9 @@ 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.primary_artist
|
||||
row = [
|
||||
track.album.primary_artist.name,
|
||||
track_artist,
|
||||
track.album.name,
|
||||
track.title,
|
||||
track_number,
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -24,6 +24,7 @@ class AudioScrobblerTSVImport(TimeStampedModel):
|
||||
uuid = instance.uuid
|
||||
return f'audioscrobbler-uploads/{uuid}.{extension}'
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, **BNULL)
|
||||
uuid = models.UUIDField(editable=False, default=uuid4)
|
||||
tsv_file = models.FileField(upload_to=get_path, **BNULL)
|
||||
processed_on = models.DateTimeField(**BNULL)
|
||||
@ -48,7 +49,12 @@ class AudioScrobblerTSVImport(TimeStampedModel):
|
||||
logger.info(f"{self} already processed on {self.processed_on}")
|
||||
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:
|
||||
self.process_log = f"Created {len(scrobbles)} scrobbles"
|
||||
for scrobble in scrobbles:
|
||||
@ -255,7 +261,7 @@ class Scrobble(TimeStampedModel):
|
||||
scrobble_data['video_id'] = media.id
|
||||
if media.__class__.__name__ == 'Episode':
|
||||
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':
|
||||
media_query = models.Q(sport_event=media)
|
||||
scrobble_data['sport_event_id'] = media.id
|
||||
|
||||
87
vrobbler/apps/scrobbles/musicbrainz.py
Normal file
87
vrobbler/apps/scrobbles/musicbrainz.py
Normal 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"]
|
||||
@ -6,12 +6,19 @@ import pytz
|
||||
from music.models import Album, Artist, Track
|
||||
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__)
|
||||
|
||||
|
||||
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"""
|
||||
new_scrobbles = []
|
||||
if not user_tz:
|
||||
user_tz = pytz.utc
|
||||
|
||||
with open(file_path) as infile:
|
||||
source = 'Audioscrobbler File'
|
||||
@ -30,9 +37,8 @@ def process_audioscrobbler_tsv_file(file_path):
|
||||
continue
|
||||
artist, artist_created = Artist.objects.get_or_create(name=row[0])
|
||||
if artist_created:
|
||||
logger.debug(f"Created artist {artist}")
|
||||
else:
|
||||
logger.debug(f"Found artist {artist}")
|
||||
artist.musicbrainz_id = lookup_artist_id_from_mb(artist.name)
|
||||
artist.save(update_fields=["musicbrainz_id"])
|
||||
|
||||
album = None
|
||||
album_created = False
|
||||
@ -50,9 +56,23 @@ def process_audioscrobbler_tsv_file(file_path):
|
||||
album.artists.add(artist)
|
||||
|
||||
if album_created:
|
||||
logger.debug(f"Created album {album}")
|
||||
else:
|
||||
logger.debug(f"Found album {album}")
|
||||
album_dict = lookup_album_dict_from_mb(
|
||||
album.name, artist_name=artist.name
|
||||
)
|
||||
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(
|
||||
title=row[2],
|
||||
@ -60,17 +80,16 @@ def process_audioscrobbler_tsv_file(file_path):
|
||||
album=album,
|
||||
)
|
||||
|
||||
if track_created:
|
||||
logger.debug(f"Created track {track}")
|
||||
else:
|
||||
logger.debug(f"Found track {track}")
|
||||
|
||||
if track_created:
|
||||
track.musicbrainz_id = row[7]
|
||||
track.run_time = int(row[4])
|
||||
track.run_time_ticks = int(row[4]) * 1000
|
||||
track.save()
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(int(row[6])).replace(
|
||||
tzinfo=pytz.utc
|
||||
timestamp = (
|
||||
datetime.utcfromtimestamp(int(row[6]))
|
||||
.replace(tzinfo=user_tz)
|
||||
.astimezone(pytz.utc)
|
||||
)
|
||||
source = 'Audioscrobbler File'
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models.fields import timezone
|
||||
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
@ -160,12 +161,20 @@ class JsonableResponseMixin:
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
class AudioScrobblerImportCreateView(JsonableResponseMixin, CreateView):
|
||||
class AudioScrobblerImportCreateView(
|
||||
LoginRequiredMixin, JsonableResponseMixin, CreateView
|
||||
):
|
||||
model = AudioScrobblerTSVImport
|
||||
fields = ['tsv_file']
|
||||
template_name = 'scrobbles/upload_form.html'
|
||||
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
|
||||
@api_view(['GET'])
|
||||
|
||||
Reference in New Issue
Block a user