diff --git a/vrobbler/apps/scrobbles/migrations/0009_scrobble_uuid.py b/vrobbler/apps/scrobbles/migrations/0009_scrobble_uuid.py new file mode 100644 index 0000000..ae04511 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0009_scrobble_uuid.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.5 on 2023-01-20 18:40 + +from uuid import uuid4 +from django.db import migrations, models + + +def generate_uuids(apps, schema_editor): + """Force uuid generation for old scrobbles""" + Scrobble = apps.get_model('scrobbles', 'Scrobble') + for scrobble in Scrobble.objects.all(): + if not scrobble.uuid: + scrobble.uuid = uuid4() + scrobble.save(update_fields=['uuid']) + + +def reverse_generate_uuids(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('scrobbles', '0008_scrobble_sport_event'), + ] + + operations = [ + migrations.AddField( + model_name='scrobble', + name='uuid', + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.RunPython( + code=generate_uuids, reverse_code=reverse_generate_uuids + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 6372e79..dc855f2 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -1,5 +1,6 @@ import logging from datetime import timedelta +from uuid import uuid4 from django.contrib.auth import get_user_model from django.db import models @@ -8,8 +9,8 @@ from django_extensions.db.models import TimeStampedModel from music.models import Track from podcasts.models import Episode from scrobbles.utils import check_scrobble_for_finish -from videos.models import Video from sports.models import SportEvent +from videos.models import Video logger = logging.getLogger(__name__) User = get_user_model() @@ -17,6 +18,7 @@ BNULL = {"blank": True, "null": True} class Scrobble(TimeStampedModel): + uuid = models.UUIDField(editable=False, **BNULL) video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL) track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL) podcast_episode = models.ForeignKey( @@ -38,6 +40,22 @@ class Scrobble(TimeStampedModel): in_progress = models.BooleanField(default=True) scrobble_log = models.TextField(**BNULL) + def save(self, *args, **kwargs): + if not self.uuid: + self.uuid = uuid4() + + return super(Scrobble, self).save(*args, **kwargs) + + @property + def status(self) -> str: + if self.is_paused: + return 'paused' + if self.played_to_completion: + return 'finished' + if self.in_progress: + return 'in-progress' + return 'zombie' + @property def percent_played(self) -> int: if not self.media_obj.run_time_ticks: @@ -231,13 +249,13 @@ class Scrobble(TimeStampedModel): ) return scrobble - def stop(self) -> None: + def stop(self, force_finish=False) -> None: if not self.in_progress: logger.warning("Scrobble already stopped") return self.in_progress = False self.save(update_fields=['in_progress']) - check_scrobble_for_finish(self) + check_scrobble_for_finish(self, force_finish) def pause(self) -> None: if self.is_paused: @@ -253,6 +271,10 @@ class Scrobble(TimeStampedModel): self.in_progress = True return self.save(update_fields=["is_paused", "in_progress"]) + def cancel(self) -> None: + check_scrobble_for_finish(self, force_finish=True) + self.delete() + def update_ticks(self, data) -> None: self.playback_position_ticks = data.get("playback_position_ticks") self.playback_position = data.get("playback_position") diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index 7f551e4..c4d1258 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -4,7 +4,9 @@ from scrobbles import views app_name = 'scrobbles' urlpatterns = [ - path('', views.scrobble_endpoint, name='scrobble-list'), + path('', views.scrobble_endpoint, name='api-list'), + path('finish/', views.scrobble_finish, name='finish'), + path('cancel/', views.scrobble_cancel, name='cancel'), path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'), path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'), ] diff --git a/vrobbler/apps/scrobbles/utils.py b/vrobbler/apps/scrobbles/utils.py index f08f3c5..c7d6498 100644 --- a/vrobbler/apps/scrobbles/utils.py +++ b/vrobbler/apps/scrobbles/utils.py @@ -66,10 +66,12 @@ def parse_mopidy_uri(uri: str) -> dict: } -def check_scrobble_for_finish(scrobble: "Scrobble") -> None: +def check_scrobble_for_finish( + scrobble: "Scrobble", force_finish=False +) -> None: completion_percent = scrobble.media_obj.COMPLETION_PERCENT - if scrobble.percent_played >= completion_percent: + if scrobble.percent_played >= completion_percent or force_finish: logger.debug(f"Completion percent {completion_percent} met, finishing") scrobble.in_progress = False diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 7d73a7e..12eb9aa 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -177,3 +177,36 @@ def mopidy_websocket(request): return Response({}, status=status.HTTP_400_BAD_REQUEST) return Response({'scrobble_id': scrobble.id}, status=status.HTTP_200_OK) + + +@csrf_exempt +@api_view(['GET']) +def scrobble_finish(request, uuid): + user = request.user + if not user.is_authenticated: + return Response({}, status=status.HTTP_403_FORBIDDEN) + + scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first() + if not scrobble: + return Response({}, status=status.HTTP_404_NOT_FOUND) + scrobble.stop(force_finish=True) + return Response( + {'id': scrobble.id, 'status': scrobble.status}, + status=status.HTTP_200_OK, + ) + + +@csrf_exempt +@api_view(['GET']) +def scrobble_cancel(request, uuid): + user = request.user + if not user.is_authenticated: + return Response({}, status=status.HTTP_403_FORBIDDEN) + + scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first() + if not scrobble: + return Response({}, status=status.HTTP_404_NOT_FOUND) + scrobble.cancel() + return Response( + {'id': scrobble.id, 'status': 'cancelled'}, status=status.HTTP_200_OK + ) diff --git a/vrobbler/templates/base.html b/vrobbler/templates/base.html index c5c0af0..6631a59 100644 --- a/vrobbler/templates/base.html +++ b/vrobbler/templates/base.html @@ -212,6 +212,8 @@
+ Cancel + Finish
{% endfor %} diff --git a/vrobbler/urls.py b/vrobbler/urls.py index 156a375..9c6d2be 100644 --- a/vrobbler/urls.py +++ b/vrobbler/urls.py @@ -1,12 +1,11 @@ +import scrobbles.views as scrobbles_views from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from rest_framework import routers -import scrobbles.views as scrobbles_views -from videos import urls as video_urls - from scrobbles import urls as scrobble_urls +from videos import urls as video_urls urlpatterns = [ path("admin/", admin.site.urls),