[notifications] Add a class for notifications
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
18
vrobbler/apps/scrobbles/management/commands/dedup_tracks.py
Normal file
18
vrobbler/apps/scrobbles/management/commands/dedup_tracks.py
Normal 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")
|
||||||
@ -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")
|
||||||
@ -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:
|
||||||
|
|||||||
55
vrobbler/apps/scrobbles/notifications.py
Normal file
55
vrobbler/apps/scrobbles/notifications.py
Normal 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
@ -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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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")),
|
||||||
|
|||||||
Reference in New Issue
Block a user