diff --git a/vrobbler/apps/boardgames/sources/lichess.py b/vrobbler/apps/boardgames/sources/lichess.py index 20800d6..2ea30db 100644 --- a/vrobbler/apps/boardgames/sources/lichess.py +++ b/vrobbler/apps/boardgames/sources/lichess.py @@ -3,7 +3,7 @@ from boardgames.models import BoardGame from django.conf import settings from django.contrib.auth import get_user_model from scrobbles.models import Scrobble -from scrobbles.utils import send_start_notifications_for_scrobble +from scrobbles.notifications import NtfyNotification User = get_user_model() @@ -124,5 +124,5 @@ def import_chess_games_for_all_users(): if scrobbles_to_create: created = Scrobble.objects.bulk_create(scrobbles_to_create) for scrobble in created: - send_start_notifications_for_scrobble(scrobble.id) + NtfyNotification(scrobble).send() return scrobbles_to_create diff --git a/vrobbler/apps/books/koreader.py b/vrobbler/apps/books/koreader.py index 8ac67f6..6c80b0e 100644 --- a/vrobbler/apps/books/koreader.py +++ b/vrobbler/apps/books/koreader.py @@ -9,8 +9,8 @@ import requests from books.constants import BOOKS_TITLES_TO_IGNORE from django.apps import apps from django.contrib.auth import get_user_model -from scrobbles.utils import send_start_notifications_for_scrobble from stream_sqlite import stream_sqlite +from scrobbles.notifications import NtfyNotification from webdav.client import get_webdav_client logger = logging.getLogger(__name__) @@ -443,7 +443,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list: if new_scrobbles: created = Scrobble.objects.bulk_create(new_scrobbles) if created: - send_start_notifications_for_scrobble(created[-1].id) + NtfyNotification(created[-1]).send() fix_long_play_stats_for_scrobbles(created) logger.info( f"Created {len(created)} scrobbles", diff --git a/vrobbler/apps/scrobbles/management/commands/dedup_tracks.py b/vrobbler/apps/scrobbles/management/commands/dedup_tracks.py new file mode 100644 index 0000000..b552b28 --- /dev/null +++ b/vrobbler/apps/scrobbles/management/commands/dedup_tracks.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand +from vrobbler.apps.scrobbles.utils import deduplicate_tracks + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + "--commit", + action="store_true", + help="Commit changes", + ) + + def handle(self, *args, **options): + commit = False + if options["commit"]: + commit = True + dups = deduplicate_tracks(commit=commit) + print(f"Deduplicated {dups} music tracks") diff --git a/vrobbler/apps/scrobbles/management/commands/send_notification_for_in_progress.py b/vrobbler/apps/scrobbles/management/commands/send_notification_for_in_progress.py new file mode 100644 index 0000000..9e64850 --- /dev/null +++ b/vrobbler/apps/scrobbles/management/commands/send_notification_for_in_progress.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand +from vrobbler.apps.scrobbles.utils import ( + send_stop_notifications_for_in_progress_scrobbles, +) + + +class Command(BaseCommand): + def handle(self, *args, **options): + sent_count = send_stop_notifications_for_in_progress_scrobbles() + print(f"Sent {sent_count} in progress notifications") diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index b552351..a99f852 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -42,7 +42,6 @@ from scrobbles.stats import build_charts from scrobbles.utils import ( get_file_md5_hash, media_class_to_foreign_key, - send_start_notifications_for_scrobble, ) from sports.models import SportEvent from tasks.models import Task @@ -50,6 +49,7 @@ from trails.models import Trail from videogames import retroarch from videogames.models import VideoGame from videos.models import Series, Video +from scrobbles.notifications import NtfyNotification from webpages.models import WebPage logger = logging.getLogger(__name__) @@ -1213,7 +1213,7 @@ class Scrobble(TimeStampedModel): scrobble_data: dict, ) -> "Scrobble": scrobble = cls.objects.create(**scrobble_data) - send_start_notifications_for_scrobble(scrobble.id) + NtfyNotification(scrobble).send() return scrobble def stop(self, timestamp=None, force_finish=False) -> None: diff --git a/vrobbler/apps/scrobbles/notifications.py b/vrobbler/apps/scrobbles/notifications.py new file mode 100644 index 0000000..073453c --- /dev/null +++ b/vrobbler/apps/scrobbles/notifications.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod +import requests + +from django.conf import settings +from django.contrib.sites.models import Site + +class Notification(ABC): + scrobble: "Scrobble" + ntfy_headers: dict = {} + ntfy_url: str = "" + + def __init__(self, scrobble: "Scrobble"): + self.scrobble = scrobble + self.user = scrobble.user + self.media_obj = scrobble.media_obj + protocol = "http" if settings.DEBUG else "https" + domain = Site.objects.get_current().domain + self.url_tmpl = f'{protocol}://{domain}' + '{path}' + + + + @abstractmethod + def send(self) -> None: + pass + + +class NtfyNotification(Notification): + def __init__(self, scrobble, **kwargs): + super().__init__(scrobble) + self.ntfy_str: str = f"{self.scrobble.media_obj}" + self.click_url = self.url_tmpl.format(path=self.media_obj.get_absolute_url()) + if kwargs.get("end", False): + self.click_url = self.url_tmpl.format(path=self.scrobble.finish_url) + self.title = "Finish " + self.media_obj.strings.verb.lower() + "?" + + if self.scrobble.log and isinstance(self.scrobble.log, dict) and self.scrobble.log.get("description"): + self.ntfy_str += f" - {self.scrobble.log.get('description')}" + + def send(self): + if ( + self.user + and self.user.profile + and self.user.profile.ntfy_enabled + and self.user.profile.ntfy_url + ): + requests.post( + self.user.profile.ntfy_url, + data=self.ntfy_str.encode(encoding="utf-8"), + headers={ + "Title": self.title, + "Priority": self.media_obj.strings.priority, + "Tags": self.media_obj.strings.tags, + "Click": self.click_url, + }, + ) diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index 87956f5..1f9f5c0 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -88,7 +88,7 @@ urlpatterns = [ views.ScrobbleLongPlaysView.as_view(), name="long-plays", ), - path("/start/", views.scrobble_start, name="start"), - path("/finish/", views.scrobble_finish, name="finish"), - path("/cancel/", views.scrobble_cancel, name="cancel"), + path("scrobble//start/", views.scrobble_start, name="start"), + path("scrobble//finish/", views.scrobble_finish, name="finish"), + path("scrobble//cancel/", views.scrobble_cancel, name="cancel"), ] diff --git a/vrobbler/apps/scrobbles/utils.py b/vrobbler/apps/scrobbles/utils.py index 3ab79da..2b83bcd 100644 --- a/vrobbler/apps/scrobbles/utils.py +++ b/vrobbler/apps/scrobbles/utils.py @@ -17,6 +17,7 @@ from scrobbles.tasks import ( process_lastfm_import, process_retroarch_import, ) +from vrobbler.apps.scrobbles.notifications import NtfyNotification from webdav.client import get_webdav_client logger = logging.getLogger(__name__) @@ -66,8 +67,7 @@ def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet: return Scrobble.objects.filter(media_query, user=user) -def get_recently_played_board_games(user: User) -> dict: - ... +def get_recently_played_board_games(user: User) -> dict: ... def get_long_plays_in_progress(user: User) -> dict: @@ -272,7 +272,7 @@ def get_file_md5_hash(file_path: str) -> str: return file_hash.hexdigest() -def deduplicate_tracks(commit=False): +def deduplicate_tracks(commit=False) -> int: from music.models import Track # TODO This whole thing should iterate over users @@ -291,54 +291,27 @@ def deduplicate_tracks(commit=False): other.scrobble_set.update(track=first) print("deleting ", other.id, " - ", other) other.delete() + return len(dups) -def send_start_notifications_for_scrobble(scrobble_id): +def send_stop_notifications_for_in_progress_scrobbles() -> int: + """Get all inprogress scrobbles and check if they're passed their media obj length. + + If so, send out a notification to offer to stop the scrobble.""" from scrobbles.models import Scrobble - scrobble = Scrobble.objects.get(id=scrobble_id) - profile = scrobble.user.profile - if profile and profile.ntfy_enabled and profile.ntfy_url: - # TODO allow prority and tags to be configured in the profile - notify_str = f"{scrobble.media_obj}" - if scrobble.log and scrobble.log.get("description"): - notify_str += f" - {scrobble.log.get('description')}" - requests.post( - profile.ntfy_url, - data=notify_str.encode(encoding="utf-8"), - headers={ - "Title": scrobble.media_obj.strings.verb, - "Priority": scrobble.media_obj.strings.priority, - "Tags": scrobble.media_obj.strings.tags, - "Click": scrobble.media_obj.get_absolute_url(), - }, - ) + scrobbles_in_progress_qs = Scrobble.objects.filter( + played_to_completion=False, in_progress=True + ) + notifications_sent = 0 + for scrobble in scrobbles_in_progress_qs: + elapsed_scrobble_seconds = ( + timezone.now() - scrobble.timestamp + ).seconds -def send_stop_notifications_for_scrobble(scrobble_id): - from scrobbles.models import Scrobble + if elapsed_scrobble_seconds > scrobble.media_obj.run_time_seconds: + NtfyNotification(scrobble, end=True).send() + notifications_sent += 1 - scrobble = Scrobble.objects.get(id=scrobble_id) - scrobble_surpassed_media_runtime = ( - timezone.now() - scrobble.timestamp - ).seconds > scrobble.media_obj.run_time_seconds - if not scrobble_surpassed_media_runtime: - logger.info("scrobble not surpassed media runtime") - return - - profile = scrobble.user.profile - if profile and profile.ntfy_enabled and profile.ntfy_url: - # TODO allow prority and tags to be configured in the profile - notify_str = f"{scrobble.media_obj}" - if scrobble.log and scrobble.log.get("description"): - notify_str += f" - {scrobble.log.get('description')}" - requests.post( - profile.ntfy_url, - data=notify_str.encode(encoding="utf-8"), - headers={ - "Title": "Stop " scrobble.media_obj.strings.verb.lower() + "?", - "Priority": scrobble.media_obj.strings.priority, - "Tags": scrobble.media_obj.strings.tags, - "Click": scrobble.finish_url, - }, - ) + return notifications_sent diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 0371fc9..b1aa083 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -546,6 +546,8 @@ def scrobble_longplay_finish(request, uuid): def scrobble_finish(request, uuid): user = request.user success_url = request.META.get("HTTP_REFERER") + if not success_url: + success_url = reverse_lazy("vrobbler-home") if not user.is_authenticated: return HttpResponseRedirect(success_url) diff --git a/vrobbler/urls.py b/vrobbler/urls.py index 4e75ada..edbdac9 100644 --- a/vrobbler/urls.py +++ b/vrobbler/urls.py @@ -44,6 +44,7 @@ from vrobbler.apps.videogames import urls as videogame_urls from vrobbler.apps.videos import urls as video_urls from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet from vrobbler.apps.webpages import urls as webpages_urls +from vrobbler.apps.modern_ui import urls as modern_ui_urls router = routers.DefaultRouter() router.register(r"scrobbles", ScrobbleViewSet) @@ -72,6 +73,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), path("o/", include(oauth2_urls)), + path("modern_ui/", include(modern_ui_urls, namespace="modern_ui")), path("", include(music_urls, namespace="music")), path("", include(book_urls, namespace="books")), path("", include(video_urls, namespace="videos")),