Add ability to cancel and finish manual scrobbles

This commit is contained in:
2023-01-20 14:03:23 -05:00
parent 7fc3705455
commit 646c7ab99c
7 changed files with 104 additions and 9 deletions

View File

@ -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
),
]

View File

@ -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")

View File

@ -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'),
] ]

View File

@ -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

View File

@ -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
)

View File

@ -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 %}

View File

@ -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),