diff --git a/vrobbler/apps/books/models.py b/vrobbler/apps/books/models.py index e1ca9a2..0be946c 100644 --- a/vrobbler/apps/books/models.py +++ b/vrobbler/apps/books/models.py @@ -12,7 +12,7 @@ from django.core.files.base import ContentFile from django.db import models from django.urls import reverse from django_extensions.db.models import TimeStampedModel -from scrobbles.mixins import ScrobblableMixin +from scrobbles.mixins import LongPlayScrobblableMixin, ScrobblableMixin from scrobbles.utils import get_scrobbles_for_media logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ class Author(TimeStampedModel): self.headshot.save(fname, ContentFile(r.content), save=True) -class Book(ScrobblableMixin): +class Book(LongPlayScrobblableMixin): COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95) AVG_PAGE_READING_SECONDS = getattr( settings, "AVERAGE_PAGE_READING_SECONDS", 60 @@ -78,6 +78,16 @@ class Book(ScrobblableMixin): def __str__(self): return f"{self.title} by {self.author}" + @property + def subtitle(self): + return f" by {self.author}" + + def get_start_url(self): + return reverse("scrobbles:start", kwargs={"uuid": self.uuid}) + + def get_absolute_url(self): + return reverse("books:book_detail", kwargs={"slug": self.uuid}) + def fix_metadata(self, force_update=False): if not self.openlibrary_id or force_update: book_dict = lookup_book_from_openlibrary(self.title, self.author) @@ -130,9 +140,6 @@ class Book(ScrobblableMixin): def author(self): return self.authors.first() - def get_absolute_url(self): - return reverse("books:book_detail", kwargs={"slug": self.uuid}) - @property def pages_for_completion(self) -> int: if not self.pages: diff --git a/vrobbler/apps/scrobbles/mixins.py b/vrobbler/apps/scrobbles/mixins.py index 22f351c..7f296ad 100644 --- a/vrobbler/apps/scrobbles/mixins.py +++ b/vrobbler/apps/scrobbles/mixins.py @@ -1,9 +1,11 @@ import logging -from typing import Dict +from typing import Optional from uuid import uuid4 from django.db import models +from django.urls import reverse from django_extensions.db.models import TimeStampedModel +from scrobbles.utils import get_scrobbles_for_media BNULL = {"blank": True, "null": True} @@ -27,3 +29,19 @@ class ScrobblableMixin(TimeStampedModel): @classmethod def find_or_create(cls): logger.warn("find_or_create() not implemented yet") + + +class LongPlayScrobblableMixin(ScrobblableMixin): + class Meta: + abstract = True + + def get_longplay_finish_url(self): + return reverse("scrobbles:longplay-finish", kwargs={"uuid": self.uuid}) + + def last_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]: + return ( + get_scrobbles_for_media(self, user) + .filter(long_play_complete=False) + .order_by("-timestamp") + .first() + ) diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index 36350a6..08c24b9 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -222,21 +222,11 @@ def manual_scrobble_video_game(data_dict: dict, user_id: Optional[int]): def manual_scrobble_book(data_dict: dict, user_id: Optional[int]): book = Book.find_or_create(data_dict) - last_scrobble = Scrobble.objects.filter( - book=book, - user_id=user_id, - played_to_completion=True, - long_play_complete=False, - ).last() - - start_playback_position = 0 - if last_scrobble: - start_playback_position = last_scrobble.playback_position or 0 scrobble_dict = { "user_id": user_id, "timestamp": timezone.now(), - "playback_position_ticks": int(start_playback_position) * 1000, - "playback_position": start_playback_position, + "playback_position_ticks": None, + "playback_position": 0, "source": "Vrobbler", "long_play_complete": False, } diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index ae98791..6a52902 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -19,8 +19,14 @@ urlpatterns = [ views.KoReaderImportCreateView.as_view(), name="koreader-file-upload", ), - path("finish/", views.scrobble_finish, name="finish"), - path("cancel/", views.scrobble_cancel, name="cancel"), + path("start//", views.scrobble_start, name="start"), + path("finish//", views.scrobble_finish, name="finish"), + path("cancel//", views.scrobble_cancel, name="cancel"), + path( + "long-play-finish//", + views.scrobble_longplay_finish, + name="longplay-finish", + ), path( "upload/", views.AudioScrobblerImportCreateView.as_view(), diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 7a399bb..1458b3e 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -4,6 +4,7 @@ import logging from datetime import datetime, timedelta import pytz +from django.apps import apps from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin @@ -31,6 +32,7 @@ from scrobbles.api import serializers from scrobbles.constants import ( JELLYFIN_AUDIO_ITEM_TYPES, JELLYFIN_VIDEO_ITEM_TYPES, + LONG_PLAY_MEDIA, ) from scrobbles.export import export_scrobbles from scrobbles.forms import ExportScrobbleForm, ScrobbleForm @@ -60,8 +62,8 @@ from sports.thesportsdb import lookup_event_from_thesportsdb from videogames.howlongtobeat import lookup_game_from_hltb from videos.imdb import lookup_video_from_imdb -from vrobbler.apps.books.openlibrary import lookup_book_from_openlibrary -from vrobbler.apps.scrobbles.utils import ( +from books.openlibrary import lookup_book_from_openlibrary +from scrobbles.utils import ( get_long_plays_completed, get_long_plays_in_progress, ) @@ -388,6 +390,84 @@ def import_audioscrobbler_file(request): ) +@permission_classes([IsAuthenticated]) +@api_view(["GET"]) +def scrobble_start(request, uuid): + user = request.user + success_url = request.META.get("HTTP_REFERER") + + if not user.is_authenticated: + return HttpResponseRedirect(success_url) + + media_obj = None + for app, model in LONG_PLAY_MEDIA.items(): + media_model = apps.get_model(app_label=app, model_name=model) + media_obj = media_model.objects.filter(uuid=uuid).first() + if media_obj: + break + + if not media_obj: + return + + scrobble = None + user_id = request.user.id + if media_obj and media_obj.__class__.__name__ == "Book": + data_dict = { + "title": media_obj.title, + "author": media_obj.author.name, + } + scrobble = manual_scrobble_book(data_dict, user_id) + if media_obj and media_obj.__class__.__name__ == "VideoGame": + data_dict = {"hltb_id": media_obj.hltb_id} + scrobble = manual_scrobble_video_game(data_dict, user_id) + + if scrobble: + messages.add_message( + request, + messages.SUCCESS, + f"Scrobble of {scrobble.media_obj} started.", + ) + else: + messages.add_message( + request, messages.ERROR, f"Media with uuid {uuid} not found." + ) + return HttpResponseRedirect(success_url) + + +@api_view(["GET"]) +def scrobble_longplay_finish(request, uuid): + user = request.user + success_url = request.META.get("HTTP_REFERER") + + if not user.is_authenticated: + return HttpResponseRedirect(success_url) + + media_obj = None + for app, model in LONG_PLAY_MEDIA.items(): + media_model = apps.get_model(app_label=app, model_name=model) + media_obj = media_model.objects.filter(uuid=uuid).first() + if media_obj: + break + + if not media_obj: + return + + last_scrobble = media_obj.last_long_play_scrobble_for_user(user) + if last_scrobble and last_scrobble.long_play_complete == False: + last_scrobble.long_play_complete = True + last_scrobble.save(update_fields=["long_play_complete"]) + messages.add_message( + request, + messages.SUCCESS, + f"Long play of {media_obj} finished.", + ) + else: + messages.add_message( + request, messages.ERROR, f"Media with uuid {uuid} not found." + ) + return HttpResponseRedirect(success_url) + + @permission_classes([IsAuthenticated]) @api_view(["GET"]) def scrobble_finish(request, uuid): diff --git a/vrobbler/apps/videogames/models.py b/vrobbler/apps/videogames/models.py index ce3966c..6644329 100644 --- a/vrobbler/apps/videogames/models.py +++ b/vrobbler/apps/videogames/models.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from uuid import uuid4 from django.conf import settings @@ -7,7 +6,7 @@ from django.contrib.auth import get_user_model from django.db import models from django.urls import reverse from django_extensions.db.models import TimeStampedModel -from scrobbles.mixins import ScrobblableMixin +from scrobbles.mixins import LongPlayScrobblableMixin from scrobbles.utils import get_scrobbles_for_media from videogames.igdb import lookup_game_id_from_gdb @@ -46,7 +45,7 @@ class VideoGameCollection(TimeStampedModel): ) -class VideoGame(ScrobblableMixin): +class VideoGame(LongPlayScrobblableMixin): COMPLETION_PERCENT = getattr(settings, "GAME_COMPLETION_PERCENT", 100) FIELDS_FROM_IGDB = [ @@ -89,6 +88,10 @@ class VideoGame(ScrobblableMixin): def __str__(self): return self.title + @property + def subtitle(self): + return f" On {self.platforms.first()}" + def get_absolute_url(self): return reverse( "videogames:videogame_detail", kwargs={"slug": self.uuid} @@ -101,6 +104,9 @@ class VideoGame(ScrobblableMixin): slug = self.title.lower().replace(" ", "-") return f"https://igdb.com/games/{slug}" + def get_start_url(self): + return reverse("scrobbles:start", kwargs={"uuid": self.uuid}) + def save(self, **kwargs): super().save(**kwargs) self.fix_metadata() diff --git a/vrobbler/templates/base.html b/vrobbler/templates/base.html index 05386ad..816483f 100644 --- a/vrobbler/templates/base.html +++ b/vrobbler/templates/base.html @@ -31,11 +31,13 @@ min-height: 3em; border-right: 1px solid #777; } + .now-playing { margin-right:20px;} .now-playing p { margin:0; } .now-playing .right { float:right; margin-right:10px; } .latest-scrobble { width: 50%; } + .now-playing img { height:75px; width: 75px; object-fit: cover; } .progress-bar { width: 100%; @@ -244,13 +246,18 @@ {% for scrobble in now_playing_list %}
{% if scrobble.media_obj.album.cover_image %} -
+
+ {% endif %} + {% if scrobble.media_obj.cover %} +
+ {% endif %} + {% if scrobble.media_obj.hltb_cover %} +
{% endif %}

{{scrobble.media_obj.title}}

{% if scrobble.media_obj.subtitle %}

{{scrobble.media_obj.subtitle}}

{% endif %} -

{{scrobble.timestamp|naturaltime}} from {{scrobble.source}}

@@ -259,6 +266,7 @@ Cancel Finish

+ {% if not forloop.last %}
{% endif %}
{% endfor %} diff --git a/vrobbler/templates/scrobbles/long_plays_in_progress.html b/vrobbler/templates/scrobbles/long_plays_in_progress.html index f9c72be..282350a 100644 --- a/vrobbler/templates/scrobbles/long_plays_in_progress.html +++ b/vrobbler/templates/scrobbles/long_plays_in_progress.html @@ -2,6 +2,15 @@ {% block title %}Long Plays{% endblock %} +{% block head_extra %} + +{% endblock %} + {% block lists %}
@@ -10,14 +19,22 @@
{% for media in in_progress %} {% if media.hltb_cover %} -
-
+
{{media.title}}
+
+
+ Resume + Finish +
{% elif media.cover %} -
-
+
{{media.title}}
+
+
+ Resume + Finish +
{% endif %} {% endfor %} @@ -45,17 +62,18 @@
{% endif %} +

Completed

{% if view == 'grid' %}
{% for media in completed %} {% if media.hltb_cover %} -
+
{{media.title}}
{% elif media.cover %} -
+
{{media.title}}
@@ -85,4 +103,4 @@
{% endif %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/vrobbler/templates/videogames/videogame_detail.html b/vrobbler/templates/videogames/videogame_detail.html index a459dfa..666d103 100644 --- a/vrobbler/templates/videogames/videogame_detail.html +++ b/vrobbler/templates/videogames/videogame_detail.html @@ -5,16 +5,24 @@ {% block title %}{{object.title}}{% endblock %} +{% block head_extra %} + +{% endblock %} + {% block lists %}
{% if object.hltb_cover%} -

- -

+
{% endif %} -
+
{% if object.summary %}

{{object.summary|safe|linebreaks|truncatewords:160}}