Explorar o código

Add ability to cancel and finish manual scrobbles

Colin Powell %!s(int64=2) %!d(string=hai) anos
pai
achega
646c7ab99c

+ 35 - 0
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
+        ),
+    ]

+ 25 - 3
vrobbler/apps/scrobbles/models.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 from datetime import timedelta
 from datetime import timedelta
+from uuid import uuid4
 
 
 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
@@ -8,8 +9,8 @@ from django_extensions.db.models import TimeStampedModel
 from music.models import Track
 from music.models import Track
 from podcasts.models import Episode
 from podcasts.models import Episode
 from scrobbles.utils import check_scrobble_for_finish
 from scrobbles.utils import check_scrobble_for_finish
-from videos.models import Video
 from sports.models import SportEvent
 from sports.models import SportEvent
+from videos.models import Video
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 User = get_user_model()
 User = get_user_model()
@@ -17,6 +18,7 @@ BNULL = {"blank": True, "null": True}
 
 
 
 
 class Scrobble(TimeStampedModel):
 class Scrobble(TimeStampedModel):
+    uuid = models.UUIDField(editable=False, **BNULL)
     video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
     video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
     track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
     track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
     podcast_episode = models.ForeignKey(
     podcast_episode = models.ForeignKey(
@@ -38,6 +40,22 @@ class Scrobble(TimeStampedModel):
     in_progress = models.BooleanField(default=True)
     in_progress = models.BooleanField(default=True)
     scrobble_log = models.TextField(**BNULL)
     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
     @property
     def percent_played(self) -> int:
     def percent_played(self) -> int:
         if not self.media_obj.run_time_ticks:
         if not self.media_obj.run_time_ticks:
@@ -231,13 +249,13 @@ class Scrobble(TimeStampedModel):
         )
         )
         return scrobble
         return scrobble
 
 
-    def stop(self) -> None:
+    def stop(self, force_finish=False) -> None:
         if not self.in_progress:
         if not self.in_progress:
             logger.warning("Scrobble already stopped")
             logger.warning("Scrobble already stopped")
             return
             return
         self.in_progress = False
         self.in_progress = False
         self.save(update_fields=['in_progress'])
         self.save(update_fields=['in_progress'])
-        check_scrobble_for_finish(self)
+        check_scrobble_for_finish(self, force_finish)
 
 
     def pause(self) -> None:
     def pause(self) -> None:
         if self.is_paused:
         if self.is_paused:
@@ -253,6 +271,10 @@ class Scrobble(TimeStampedModel):
             self.in_progress = True
             self.in_progress = True
             return self.save(update_fields=["is_paused", "in_progress"])
             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:
     def update_ticks(self, data) -> None:
         self.playback_position_ticks = data.get("playback_position_ticks")
         self.playback_position_ticks = data.get("playback_position_ticks")
         self.playback_position = data.get("playback_position")
         self.playback_position = data.get("playback_position")

+ 3 - 1
vrobbler/apps/scrobbles/urls.py

@@ -4,7 +4,9 @@ from scrobbles import views
 app_name = 'scrobbles'
 app_name = 'scrobbles'
 
 
 urlpatterns = [
 urlpatterns = [
-    path('', views.scrobble_endpoint, name='scrobble-list'),
+    path('', views.scrobble_endpoint, name='api-list'),
+    path('finish/<slug:uuid>', views.scrobble_finish, name='finish'),
+    path('cancel/<slug:uuid>', views.scrobble_cancel, name='cancel'),
     path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
     path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
     path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'),
     path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'),
 ]
 ]

+ 4 - 2
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
     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")
         logger.debug(f"Completion percent {completion_percent} met, finishing")
 
 
         scrobble.in_progress = False
         scrobble.in_progress = False

+ 33 - 0
vrobbler/apps/scrobbles/views.py

@@ -177,3 +177,36 @@ def mopidy_websocket(request):
         return Response({}, status=status.HTTP_400_BAD_REQUEST)
         return Response({}, status=status.HTTP_400_BAD_REQUEST)
 
 
     return Response({'scrobble_id': scrobble.id}, status=status.HTTP_200_OK)
     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
+    )

+ 2 - 0
vrobbler/templates/base.html

@@ -212,6 +212,8 @@
                                 <div class="progress-bar" style="margin-right:5px;">
                                 <div class="progress-bar" style="margin-right:5px;">
                                     <span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
                                     <span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
                                 </div>
                                 </div>
+                                <a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>
+                                <a href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>
                             </div>
                             </div>
                             <hr/>
                             <hr/>
                             {% endfor %}
                             {% endfor %}

+ 2 - 3
vrobbler/urls.py

@@ -1,12 +1,11 @@
+import scrobbles.views as scrobbles_views
 from django.conf import settings
 from django.conf import settings
 from django.conf.urls.static import static
 from django.conf.urls.static import static
 from django.contrib import admin
 from django.contrib import admin
 from django.urls import include, path
 from django.urls import include, path
 from rest_framework import routers
 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 scrobbles import urls as scrobble_urls
+from videos import urls as video_urls
 
 
 urlpatterns = [
 urlpatterns = [
     path("admin/", admin.site.urls),
     path("admin/", admin.site.urls),