[notifications] Add a class for notifications

This commit is contained in:
2025-03-25 09:50:14 -04:00
parent 0a50bca622
commit 676c40176c
10 changed files with 116 additions and 56 deletions

View File

@ -3,7 +3,7 @@ from boardgames.models import BoardGame
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.utils import send_start_notifications_for_scrobble from scrobbles.notifications import NtfyNotification
User = get_user_model() User = get_user_model()
@ -124,5 +124,5 @@ def import_chess_games_for_all_users():
if scrobbles_to_create: if scrobbles_to_create:
created = Scrobble.objects.bulk_create(scrobbles_to_create) created = Scrobble.objects.bulk_create(scrobbles_to_create)
for scrobble in created: for scrobble in created:
send_start_notifications_for_scrobble(scrobble.id) NtfyNotification(scrobble).send()
return scrobbles_to_create return scrobbles_to_create

View File

@ -9,8 +9,8 @@ import requests
from books.constants import BOOKS_TITLES_TO_IGNORE from books.constants import BOOKS_TITLES_TO_IGNORE
from django.apps import apps from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from scrobbles.utils import send_start_notifications_for_scrobble
from stream_sqlite import stream_sqlite from stream_sqlite import stream_sqlite
from scrobbles.notifications import NtfyNotification
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -443,7 +443,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
if new_scrobbles: if new_scrobbles:
created = Scrobble.objects.bulk_create(new_scrobbles) created = Scrobble.objects.bulk_create(new_scrobbles)
if created: if created:
send_start_notifications_for_scrobble(created[-1].id) NtfyNotification(created[-1]).send()
fix_long_play_stats_for_scrobbles(created) fix_long_play_stats_for_scrobbles(created)
logger.info( logger.info(
f"Created {len(created)} scrobbles", f"Created {len(created)} scrobbles",

View File

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

View File

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

View File

@ -42,7 +42,6 @@ from scrobbles.stats import build_charts
from scrobbles.utils import ( from scrobbles.utils import (
get_file_md5_hash, get_file_md5_hash,
media_class_to_foreign_key, media_class_to_foreign_key,
send_start_notifications_for_scrobble,
) )
from sports.models import SportEvent from sports.models import SportEvent
from tasks.models import Task from tasks.models import Task
@ -50,6 +49,7 @@ from trails.models import Trail
from videogames import retroarch from videogames import retroarch
from videogames.models import VideoGame from videogames.models import VideoGame
from videos.models import Series, Video from videos.models import Series, Video
from scrobbles.notifications import NtfyNotification
from webpages.models import WebPage from webpages.models import WebPage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -1213,7 +1213,7 @@ class Scrobble(TimeStampedModel):
scrobble_data: dict, scrobble_data: dict,
) -> "Scrobble": ) -> "Scrobble":
scrobble = cls.objects.create(**scrobble_data) scrobble = cls.objects.create(**scrobble_data)
send_start_notifications_for_scrobble(scrobble.id) NtfyNotification(scrobble).send()
return scrobble return scrobble
def stop(self, timestamp=None, force_finish=False) -> None: def stop(self, timestamp=None, force_finish=False) -> None:

View File

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

View File

@ -88,7 +88,7 @@ urlpatterns = [
views.ScrobbleLongPlaysView.as_view(), views.ScrobbleLongPlaysView.as_view(),
name="long-plays", name="long-plays",
), ),
path("<slug:uuid>/start/", views.scrobble_start, name="start"), path("scrobble/<slug:uuid>/start/", views.scrobble_start, name="start"),
path("<slug:uuid>/finish/", views.scrobble_finish, name="finish"), path("scrobble/<slug:uuid>/finish/", views.scrobble_finish, name="finish"),
path("<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"), path("scrobble/<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"),
] ]

View File

@ -17,6 +17,7 @@ from scrobbles.tasks import (
process_lastfm_import, process_lastfm_import,
process_retroarch_import, process_retroarch_import,
) )
from vrobbler.apps.scrobbles.notifications import NtfyNotification
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
logger = logging.getLogger(__name__) 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) 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: 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() return file_hash.hexdigest()
def deduplicate_tracks(commit=False): def deduplicate_tracks(commit=False) -> int:
from music.models import Track from music.models import Track
# TODO This whole thing should iterate over users # TODO This whole thing should iterate over users
@ -291,54 +291,27 @@ def deduplicate_tracks(commit=False):
other.scrobble_set.update(track=first) other.scrobble_set.update(track=first)
print("deleting ", other.id, " - ", other) print("deleting ", other.id, " - ", other)
other.delete() 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 from scrobbles.models import Scrobble
scrobble = Scrobble.objects.get(id=scrobble_id) scrobbles_in_progress_qs = Scrobble.objects.filter(
profile = scrobble.user.profile played_to_completion=False, in_progress=True
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(),
},
)
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): if elapsed_scrobble_seconds > scrobble.media_obj.run_time_seconds:
from scrobbles.models import Scrobble NtfyNotification(scrobble, end=True).send()
notifications_sent += 1
scrobble = Scrobble.objects.get(id=scrobble_id) return notifications_sent
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,
},
)

View File

@ -546,6 +546,8 @@ def scrobble_longplay_finish(request, uuid):
def scrobble_finish(request, uuid): def scrobble_finish(request, uuid):
user = request.user user = request.user
success_url = request.META.get("HTTP_REFERER") success_url = request.META.get("HTTP_REFERER")
if not success_url:
success_url = reverse_lazy("vrobbler-home")
if not user.is_authenticated: if not user.is_authenticated:
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)

View File

@ -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 import urls as video_urls
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
from vrobbler.apps.webpages import urls as webpages_urls from vrobbler.apps.webpages import urls as webpages_urls
from vrobbler.apps.modern_ui import urls as modern_ui_urls
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r"scrobbles", ScrobbleViewSet) router.register(r"scrobbles", ScrobbleViewSet)
@ -72,6 +73,7 @@ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("o/", include(oauth2_urls)), path("o/", include(oauth2_urls)),
path("modern_ui/", include(modern_ui_urls, namespace="modern_ui")),
path("", include(music_urls, namespace="music")), path("", include(music_urls, namespace="music")),
path("", include(book_urls, namespace="books")), path("", include(book_urls, namespace="books")),
path("", include(video_urls, namespace="videos")), path("", include(video_urls, namespace="videos")),