diff --git a/vrobbler/apps/scrobbles/admin.py b/vrobbler/apps/scrobbles/admin.py index 5a3833c..f91df68 100644 --- a/vrobbler/apps/scrobbles/admin.py +++ b/vrobbler/apps/scrobbles/admin.py @@ -13,7 +13,7 @@ class ScrobbleInline(admin.TabularInline): @admin.register(AudioScrobblerTSVImport) class AudioScrobblerTSVImportAdmin(admin.ModelAdmin): date_hierarchy = "created" - list_display = ("id", "tsv_file", "created") + list_display = ("uuid", "created", "process_count", "tsv_file") ordering = ("-created",) diff --git a/vrobbler/apps/scrobbles/migrations/0014_audioscrobblertsvimport_process_log.py b/vrobbler/apps/scrobbles/migrations/0014_audioscrobblertsvimport_process_log.py new file mode 100644 index 0000000..631cc01 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0014_audioscrobblertsvimport_process_log.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-02-03 22:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scrobbles', '0013_audioscrobblertsvimport_processed_on'), + ] + + operations = [ + migrations.AddField( + model_name='audioscrobblertsvimport', + name='process_log', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/vrobbler/apps/scrobbles/migrations/0015_audioscrobblertsvimport_uuid_and_more.py b/vrobbler/apps/scrobbles/migrations/0015_audioscrobblertsvimport_uuid_and_more.py new file mode 100644 index 0000000..0dd3234 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0015_audioscrobblertsvimport_uuid_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.5 on 2023-02-03 23:36 + +from django.db import migrations, models +import scrobbles.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('scrobbles', '0014_audioscrobblertsvimport_process_log'), + ] + + operations = [ + migrations.AddField( + model_name='audioscrobblertsvimport', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + migrations.AlterField( + model_name='audioscrobblertsvimport', + name='tsv_file', + field=models.FileField( + blank=True, + null=True, + upload_to=scrobbles.models.AudioScrobblerTSVImport.get_path, + ), + ), + ] diff --git a/vrobbler/apps/scrobbles/migrations/0016_audioscrobblertsvimport_process_count.py b/vrobbler/apps/scrobbles/migrations/0016_audioscrobblertsvimport_process_count.py new file mode 100644 index 0000000..1022bd2 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0016_audioscrobblertsvimport_process_count.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-02-03 23:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scrobbles', '0015_audioscrobblertsvimport_uuid_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='audioscrobblertsvimport', + name='process_count', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index dc3887d..8ae7d97 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -18,18 +18,27 @@ BNULL = {"blank": True, "null": True} class AudioScrobblerTSVImport(TimeStampedModel): - tsv_file = models.FileField( - upload_to="audioscrobbler-uploads/%Y/%m-%d/", **BNULL - ) + def get_path(instance, filename): + extension = filename.split('.')[-1] + uuid = instance.uuid + return f'audioscrobbler-uploads/{uuid}.{extension}' + + uuid = models.UUIDField(editable=False, default=uuid4) + tsv_file = models.FileField(upload_to=get_path, **BNULL) processed_on = models.DateTimeField(**BNULL) + process_log = models.TextField(**BNULL) + process_count = models.IntegerField(**BNULL) def __str__(self): - return f"Audioscrobbler TSV upload: {self.tsv_file.path}" + if self.tsv_file: + return f"Audioscrobbler TSV upload: {self.tsv_file.path}" + return f"Audioscrobbler TSV upload {self.id}" def save(self, **kwargs): """On save, attempt to import the TSV file""" - - return super().save(**kwargs) + super().save(**kwargs) + self.process() + return def process(self, force=False): from scrobbles.tsv import process_audioscrobbler_tsv_file @@ -38,9 +47,21 @@ class AudioScrobblerTSVImport(TimeStampedModel): logger.info(f"{self} already processed on {self.processed_on}") return - process_audioscrobbler_tsv_file(self.tsv_file.path) + scrobbles = process_audioscrobbler_tsv_file(self.tsv_file.path) + if scrobbles: + self.process_log = f"Created {len(scrobbles)} scrobbles" + for scrobble in scrobbles: + scrobble_str = f"{scrobble.id}\t{scrobble.timestamp}\t{scrobble.track.title}\t" + self.process_log += f"\n{scrobble_str}" + self.process_count = len(scrobbles) + else: + self.process_log = f"Created no new scrobbles" + self.process_count = 0 + self.processed_on = timezone.now() - self.save(update_fields=['processed_on']) + self.save( + update_fields=['processed_on', 'process_count', 'process_log'] + ) class ChartRecord(TimeStampedModel): diff --git a/vrobbler/apps/scrobbles/serializers.py b/vrobbler/apps/scrobbles/serializers.py index bf4ce38..08771e0 100644 --- a/vrobbler/apps/scrobbles/serializers.py +++ b/vrobbler/apps/scrobbles/serializers.py @@ -1,5 +1,11 @@ from rest_framework import serializers -from scrobbles.models import Scrobble +from scrobbles.models import Scrobble, AudioScrobblerTSVImport + + +class AudioScrobblerTSVImportSerializer(serializers.ModelSerializer): + class Meta: + model = AudioScrobblerTSVImport + fields = ('tsv_file',) class ScrobbleSerializer(serializers.ModelSerializer): diff --git a/vrobbler/apps/scrobbles/tsv.py b/vrobbler/apps/scrobbles/tsv.py index e40baaf..8483983 100644 --- a/vrobbler/apps/scrobbles/tsv.py +++ b/vrobbler/apps/scrobbles/tsv.py @@ -96,3 +96,4 @@ def process_audioscrobbler_tsv_file(file_path): f"Created {len(created)} scrobbles", extra={'created_scrobbles': created}, ) + return created diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index c4d1258..bad4d8f 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -7,6 +7,11 @@ urlpatterns = [ path('', views.scrobble_endpoint, name='api-list'), path('finish/', views.scrobble_finish, name='finish'), path('cancel/', views.scrobble_cancel, name='cancel'), + path( + 'audioscrobbler-file-upload/', + views.import_audioscrobbler_file, + name='audioscrobbler-file-upload', + ), path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'), path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'), ] diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 8476e99..3e2c3f2 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -11,7 +11,12 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import FormView from django.views.generic.list import ListView from rest_framework import status -from rest_framework.decorators import api_view, permission_classes +from rest_framework.decorators import ( + api_view, + parser_classes, + permission_classes, +) +from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from scrobbles.constants import ( @@ -29,7 +34,10 @@ from scrobbles.scrobblers import ( mopidy_scrobble_podcast, mopidy_scrobble_track, ) -from scrobbles.serializers import ScrobbleSerializer +from scrobbles.serializers import ( + AudioScrobblerTSVImportSerializer, + ScrobbleSerializer, +) from scrobbles.thesportsdb import lookup_event_from_thesportsdb from vrobbler.apps.music.aggregators import ( @@ -187,6 +195,29 @@ def mopidy_websocket(request): return Response({'scrobble_id': scrobble.id}, status=status.HTTP_200_OK) +@csrf_exempt +@permission_classes([IsAuthenticated]) +@api_view(['POST']) +@parser_classes([MultiPartParser]) +def import_audioscrobbler_file(request): + """Takes a TSV file in the Audioscrobbler format, saves it and processes the + scrobbles. + """ + scrobbles_created = [] + # tsv_file = request.FILES[0] + + file_serializer = AudioScrobblerTSVImportSerializer(data=request.data) + if file_serializer.is_valid(): + import_file = file_serializer.save() + return Response( + {'scrobble_ids': scrobbles_created}, status=status.HTTP_200_OK + ) + else: + return Response( + file_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + @csrf_exempt @permission_classes([IsAuthenticated]) @api_view(['GET']) diff --git a/vrobbler/templates/scrobbles/scrobble_list.html b/vrobbler/templates/scrobbles/scrobble_list.html index a3a7290..f17ad62 100644 --- a/vrobbler/templates/scrobbles/scrobble_list.html +++ b/vrobbler/templates/scrobbles/scrobble_list.html @@ -179,7 +179,11 @@ {% for scrobble in object_list %} {{scrobble.timestamp|naturaltime}} + {% if scrobble.track.album.cover_image %} + {% else %} + {{scrobble.track.album.name}} + {% endif %} {{scrobble.track.title}} {{scrobble.track.artist.name}}