diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index 1d34c26..534fe17 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -1,12 +1,12 @@ from datetime import datetime, timedelta -from unittest.mock import patch, MagicMock -from django.utils import timezone -from django.contrib.auth import get_user_model +from unittest.mock import MagicMock, patch import pytest import time_machine +from django.contrib.auth import get_user_model from django.urls import reverse -from music.models import Track, Artist, Album +from django.utils import timezone +from music.models import Album, Artist, Track from podcasts.models import PodcastEpisode from scrobbles.models import Scrobble from tasks.models import Task @@ -704,3 +704,32 @@ def test_scrobble_jellyfin_track_create_new( scrobble = Scrobble.objects.get(id=1) assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion" + + +@pytest.mark.django_db +def test_get_not_allowed_from_gps(client, valid_auth_token): + url = reverse("scrobbles:gps-webhook") + headers = {"Authorization": f"Token {valid_auth_token}"} + response = client.get(url, headers=headers) + assert response.status_code == 405 + + +@pytest.mark.django_db +def test_gps_webhook_creates_location(client, valid_auth_token): + url = reverse("scrobbles:gps-webhook") + headers = {"Authorization": f"Token {valid_auth_token}"} + gps_data = { + "lat": "40.7128", + "lon": "-74.0060", + "alt": "10.5", + "time": "2024-01-14T12:00:00Z", + "prov": "gps", + } + response = client.post( + url, + gps_data, + content_type="application/json", + headers=headers, + ) + assert response.status_code == 200 + assert "scrobble_id" in response.data diff --git a/vrobbler/apps/scrobbles/management/commands/import_lastfm_historical.py b/vrobbler/apps/scrobbles/management/commands/import_lastfm_historical.py new file mode 100644 index 0000000..79dece2 --- /dev/null +++ b/vrobbler/apps/scrobbles/management/commands/import_lastfm_historical.py @@ -0,0 +1,45 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + "user_id", + type=int, + help="User ID to import historical scrobbles for", + ) + + def handle(self, *args, **options): + from scrobbles.models import LastFmImport + + user_id = options.get("user_id") + user = User.objects.filter(id=user_id).first() + + if not user: + self.stderr.write(f"Error: User with ID {user_id} not found") + return + + profile = user.profile + if not profile.lastfm_username or not profile.lastfm_password: + self.stderr.write( + f"Error: User {user} does not have LastFM credentials configured" + ) + return + + lfm_import = LastFmImport.objects.create( + user=user, + earliest_timestamp=None, + last_batch_imported=None, + import_in_progress=False, + ) + + lfm_import.start_historical_import() + + self.stdout.write( + self.style.SUCCESS( + f"Started historical LastFM import for {user} (import ID: {lfm_import.id})" + ) + ) diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index 5484644..74af584 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -1,6 +1,6 @@ from django.urls import path from scrobbles import views -from tasks.webhooks import emacs_webhook, todoist_webhook +from tasks.webhooks import EmacsWebhookView, TodoistWebhookView app_name = "scrobbles" @@ -49,26 +49,26 @@ urlpatterns = [ ), path( "webhook/web-scrobbler/", - views.web_scrobbler_webhook, + views.WebScrobblerWebhookView.as_view(), name="web-scrobbler-webhook", ), path( "webhook/gps/", - views.gps_webhook, + views.GPSWebhookView.as_view(), name="gps-webhook", ), path( "webhook/jellyfin/", - views.jellyfin_webhook, + views.JellyfinWebhookView.as_view(), name="jellyfin-webhook", ), path( "webhook/mopidy/", - views.mopidy_webhook, + views.MopidyWebhookView.as_view(), name="mopidy-webhook", ), - path("webhook/todoist/", todoist_webhook, name="todoist-webhook"), - path("webhook/emacs/", emacs_webhook, name="emacs_webhook"), + path("webhook/todoist/", TodoistWebhookView.as_view(), name="todoist-webhook"), + path("webhook/emacs/", EmacsWebhookView.as_view(), name="emacs_webhook"), path("export/", views.export, name="export"), path( "imports/", diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index ec9d629..333bec6 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -48,6 +48,7 @@ from rest_framework.decorators import ( from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from scrobbles.api import serializers from scrobbles.constants import ( LONG_PLAY_MEDIA, @@ -80,6 +81,14 @@ User = get_user_model() logger = logging.getLogger(__name__) +class WebhookView(APIView): + permission_classes = [IsAuthenticated] + + @property + def logger(self): + return logger + + class ScrobbleableListView(ListView): model = None paginate_by = 200 @@ -427,8 +436,8 @@ class KoReaderImportCreateView(LoginRequiredMixin, JsonableResponseMixin, Create return HttpResponseRedirect(self.request.META.get("HTTP_REFERER")) -@permission_classes([IsAuthenticated]) @api_view(["GET"]) +@permission_classes([IsAuthenticated]) def lastfm_import(request): lfm_import, created = LastFmImport.objects.get_or_create( user=request.user, processed_finished__isnull=True @@ -439,155 +448,143 @@ def lastfm_import(request): return HttpResponseRedirect(request.META.get("HTTP_REFERER")) -@csrf_exempt -@api_view(["POST"]) -def web_scrobbler_webhook(request): - """Note, this does not work as implemented. For some reason the web - scrobbler tool kicks back a bad response code and does not seem to POST data - as expected. +class WebScrobblerWebhookView(WebhookView): + def post(self, request): + """Note, this does not work as implemented. For some reason the web + scrobbler tool kicks back a bad response code and does not seem to POST data + as expected. - """ - user_id = request.user.id - - logger.info( - "[web_scrobbler_webhook] called", - extra={ - "post_data": request.POST, - "user_id": user_id, - }, - ) - youtube_id = request.POST.get("uniqueID") - if not youtube_id: - logger.warning( - "[web_scrobbler_webhook] failed", - extra={ - "youtube_id": youtube_id, - "user_id": user_id, - }, - ) - return JsonResponse( - {"errors": ["No youtube ID provided"]}, status=status.HTTP_200_OK - ) - - playing_state = "started" - if request.POST.get("isPlaying") == "false": - playing_state = "stopped" - scrobble = web_scrobbler_scrobble_media(youtube_id, user_id, status=playing_state) - if not scrobble: - logger.warning( - "[web_scrobbler_webhook] failed", - extra={ - "youtube_id": youtube_id, - "user_id": user_id, - }, - ) - return JsonResponse( - {"errors": ["No scrobble found for user or video"]}, - status=status.HTTP_200_OK, - ) - - logger.info( - "[web_scrobbler_webhook] finished", - extra={"scrobble_id": scrobble.id}, - ) - return JsonResponse({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) - - -@csrf_exempt -@permission_classes([IsAuthenticated]) -@api_view(["POST"]) -def jellyfin_webhook(request): - post_data = request.data - logger.info( - "[jellyfin_webhook] called", - extra={ - "post_data": post_data, - "user_id": request.user.id, - }, - ) - - in_progress = post_data.get("NotificationType", "") == "PlaybackProgress" - is_music = post_data.get("ItemType", "") == "Audio" - - # Disregard progress updates - if in_progress and is_music: - logger.info( - "[jellyfin_webhook] ignoring update of music in progress", - extra={"post_data": post_data}, - ) - return Response({}, status=status.HTTP_304_NOT_MODIFIED) - - scrobble = jellyfin_scrobble_media(post_data, request.user.id) - - if not scrobble: - return Response({}, status=status.HTTP_400_BAD_REQUEST) - - logger.info( - "[jellyfin_webhook] finished", - extra={"scrobble_id": scrobble.id}, - ) - return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) - - -@csrf_exempt -@permission_classes([IsAuthenticated]) -@api_view(["POST"]) -def mopidy_webhook(request): - try: - data_dict = json.loads(request.data) - except TypeError: - data_dict = request.data - - logger.info( - "[mopidy_webhook] called", - extra={ - "post_data": data_dict, - "user_id": request.user.id, - }, - ) - - scrobble = mopidy_scrobble_media(data_dict, request.user.id) - - if not scrobble: - return Response({}, status=status.HTTP_400_BAD_REQUEST) - - return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) - - -@csrf_exempt -@permission_classes([IsAuthenticated]) -@api_view(["POST"]) -def gps_webhook(request): - try: - data_dict = json.loads(request.data) - except TypeError: - data_dict = request.data - - logger.info( - "[geolocation_webhook] called", - extra={ - "post_data": data_dict, - "user_id": 1, - }, - ) - - # TODO Fix this so we have to authenticate! - user_id = 1 - if request.user.id: + """ user_id = request.user.id - scrobble = gpslogger_scrobble_location(data_dict, user_id) + self.logger.info( + "[web_scrobbler_webhook] called", + extra={ + "post_data": request.POST, + "user_id": user_id, + }, + ) + youtube_id = request.POST.get("uniqueID") + if not youtube_id: + self.logger.warning( + "[web_scrobbler_webhook] failed", + extra={ + "youtube_id": youtube_id, + "user_id": user_id, + }, + ) + return JsonResponse( + {"errors": ["No youtube ID provided"]}, status=status.HTTP_200_OK + ) - if not scrobble: - return Response({}, status=status.HTTP_200_OK) + playing_state = "started" + if request.POST.get("isPlaying") == "false": + playing_state = "stopped" + scrobble = web_scrobbler_scrobble_media( + youtube_id, user_id, status=playing_state + ) + if not scrobble: + self.logger.warning( + "[web_scrobbler_webhook] failed", + extra={ + "youtube_id": youtube_id, + "user_id": user_id, + }, + ) + return JsonResponse( + {"errors": ["No scrobble found for user or video"]}, + status=status.HTTP_200_OK, + ) - return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) + self.logger.info( + "[web_scrobbler_webhook] finished", + extra={"scrobble_id": scrobble.id}, + ) + return JsonResponse({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) + + +class JellyfinWebhookView(WebhookView): + def post(self, request): + post_data = request.data + self.logger.info( + "[jellyfin_webhook] called", + extra={ + "post_data": post_data, + "user_id": request.user.id, + }, + ) + + in_progress = post_data.get("NotificationType", "") == "PlaybackProgress" + is_music = post_data.get("ItemType", "") == "Audio" + + if in_progress and is_music: + self.logger.info( + "[jellyfin_webhook] ignoring update of music in progress", + extra={"post_data": post_data}, + ) + return Response({}, status=status.HTTP_304_NOT_MODIFIED) + + scrobble = jellyfin_scrobble_media(post_data, request.user.id) + + if not scrobble: + return Response({}, status=status.HTTP_400_BAD_REQUEST) + + self.logger.info( + "[jellyfin_webhook] finished", + extra={"scrobble_id": scrobble.id}, + ) + return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) + + +class MopidyWebhookView(WebhookView): + def post(self, request): + try: + data_dict = json.loads(request.data) + except TypeError: + data_dict = request.data + + self.logger.info( + "[mopidy_webhook] called", + extra={ + "post_data": data_dict, + "user_id": request.user.id, + }, + ) + + scrobble = mopidy_scrobble_media(data_dict, request.user.id) + + if not scrobble: + return Response({}, status=status.HTTP_400_BAD_REQUEST) + + return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) + + +class GPSWebhookView(WebhookView): + def post(self, request): + try: + data_dict = json.loads(request.data) + except TypeError: + data_dict = request.data + + self.logger.info( + "[geolocation_webhook] called", + extra={ + "post_data": data_dict, + "user_id": request.user.id, + }, + ) + + scrobble = gpslogger_scrobble_location(data_dict, request.user.id) + + if not scrobble: + return Response({}, status=status.HTTP_200_OK) + + return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) -@csrf_exempt -@permission_classes([IsAuthenticated]) @api_view(["POST"]) @parser_classes([MultiPartParser]) +@permission_classes([IsAuthenticated]) def import_audioscrobbler_file(request): """Takes a TSV file in the Audioscrobbler format, saves it and processes the scrobbles. @@ -603,8 +600,8 @@ def import_audioscrobbler_file(request): return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST) -@permission_classes([IsAuthenticated]) @api_view(["GET"]) +@permission_classes([IsAuthenticated]) def scrobble_start(request, uuid): logger.info( "[scrobble_start] called", @@ -694,8 +691,8 @@ def scrobble_longplay_finish(request, uuid): return HttpResponseRedirect(success_url) -@permission_classes([IsAuthenticated]) @api_view(["GET"]) +@permission_classes([IsAuthenticated]) def scrobble_finish(request, uuid): user = request.user success_url = request.META.get("HTTP_REFERER") @@ -718,8 +715,8 @@ def scrobble_finish(request, uuid): return HttpResponseRedirect(success_url) -@permission_classes([IsAuthenticated]) @api_view(["GET"]) +@permission_classes([IsAuthenticated]) def scrobble_cancel(request, uuid): user = request.user success_url = reverse_lazy("vrobbler-home") @@ -740,8 +737,8 @@ def scrobble_cancel(request, uuid): return HttpResponseRedirect(request.META.get("HTTP_REFERER")) -@permission_classes([IsAuthenticated]) @api_view(["GET"]) +@permission_classes([IsAuthenticated]) def export(request): format = request.GET.get("export_type", "csv") start = request.GET.get("start") @@ -1020,23 +1017,23 @@ class ScrobbleSearchView(LoginRequiredMixin, TemplateView): template_name = "scrobbles/scrobble_search.html" MEDIA_FIELDS = { - "Video": ("video__title", "video__overview"), - "Track": ("track__title", None), - "PodcastEpisode": ("podcast_episode__title", None), - "Book": ("book__title", "book__description"), - "Paper": ("paper__title", None), - "VideoGame": ("video_game__title", "video_game__description"), - "BoardGame": ("board_game__title", "board_game__description"), - "Beer": ("beer__name", "beer__style"), - "Food": ("food__name", "food__description"), - "Puzzle": ("puzzle__name", "puzzle__description"), - "Trail": ("trail__name", "trail__location"), - "GeoLocation": ("geo_location__name", "geo_location__description"), - "Task": ("task__title", "task__description"), - "WebPage": ("web_page__title", None), - "LifeEvent": ("life_event__title", "life_event__description"), - "Mood": ("mood__title", "mood__description"), - "BrickSet": ("brick_set__title", None), + "Video": ["video__title", "video__overview"], + "Track": ["track__title", "track__artist__name", "track__album__name"], + "PodcastEpisode": ["podcast_episode__title", None], + "Book": ["book__title", "book__summary"], + "Paper": ["paper__title", None], + "VideoGame": ["video_game__title", "video_game__summary"], + "BoardGame": ["board_game__title", "board_game__description"], + "Beer": ["beer__name", "beer__style"], + "Food": ["food__name", "food__description"], + "Puzzle": ["puzzle__name", "puzzle__description"], + "Trail": ["trail__name", "trail__location"], + "GeoLocation": ["geo_location__title", None], + "Task": ["task__title", "task__description"], + "WebPage": ["web_page__title", None], + "LifeEvent": ["life_event__title", "life_event__description"], + "Mood": ["mood__title", "mood__description"], + "BrickSet": ["brick_set__title", None], } def get(self, request, *args, **kwargs): @@ -1074,11 +1071,10 @@ class ScrobbleSearchView(LoginRequiredMixin, TemplateView): q |= Q(source__icontains=query) if media_type and media_type in self.MEDIA_FIELDS: - title_field, desc_field = self.MEDIA_FIELDS[media_type] - if title_field: - q |= Q(**{f"{title_field}__icontains": query}) - if desc_field: - q |= Q(**{f"{desc_field}__icontains": query}) + fields = self.MEDIA_FIELDS[media_type] + for field in fields: + if field: + q |= Q(**{f"{field}__icontains": query}) scrobbles = scrobbles.filter(q).distinct() else: @@ -1087,12 +1083,11 @@ class ScrobbleSearchView(LoginRequiredMixin, TemplateView): base_matches = scrobbles.filter(q).values_list("id", flat=True) all_matching_ids.update(base_matches) - for mt, (title_field, desc_field) in self.MEDIA_FIELDS.items(): + for mt, fields in self.MEDIA_FIELDS.items(): mt_queries = [] - if title_field: - mt_queries.append(Q(**{f"{title_field}__icontains": query})) - if desc_field: - mt_queries.append(Q(**{f"{desc_field}__icontains": query})) + for field in fields: + if field: + mt_queries.append(Q(**{f"{field}__icontains": query})) if mt_queries: mt_q = mt_queries[0] diff --git a/vrobbler/apps/tasks/views.py b/vrobbler/apps/tasks/views.py index 0b15481..dcecad5 100644 --- a/vrobbler/apps/tasks/views.py +++ b/vrobbler/apps/tasks/views.py @@ -24,9 +24,8 @@ class TaskDetailView(ScrobbleableDetailView): model = Task -@csrf_exempt -@permission_classes([IsAuthenticated]) @api_view(["GET"]) +@permission_classes([IsAuthenticated]) def todoist_oauth(request): logger.info( "[todoist_oauth] called", diff --git a/vrobbler/apps/tasks/webhooks.py b/vrobbler/apps/tasks/webhooks.py index 21b8fab..9d9a7c7 100644 --- a/vrobbler/apps/tasks/webhooks.py +++ b/vrobbler/apps/tasks/webhooks.py @@ -1,12 +1,11 @@ import json import logging -from django.views.decorators.csrf import csrf_exempt from profiles.models import UserProfile from rest_framework import status -from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from scrobbles.scrobblers import ( emacs_scrobble_task, emacs_scrobble_update_task, @@ -17,156 +16,158 @@ from scrobbles.scrobblers import ( logger = logging.getLogger(__name__) -@csrf_exempt -@permission_classes([IsAuthenticated]) -@api_view(["POST"]) -def todoist_webhook(request): - post_data = request.data - logger.info( - "[todoist_webhook] called", - extra={"post_data": post_data}, - ) - todoist_task = {} - todoist_note = {} - todoist_type, todoist_event = post_data.get("event_name").split(":") - event_data = post_data.get("event_data", {}) - is_item_type = todoist_type == "item" - is_note_type = todoist_tyllll = "note" - new_labels = event_data.get("labels", []) - old_labels = ( - post_data.get("event_data_extra", {}).get("old_item", {}).get("labels", []) - ) - # TODO Don't hard code status strings in here - is_updated = todoist_event in ["updated"] - is_added = todoist_event in ["added"] +class WebhookView(APIView): + permission_classes = [IsAuthenticated] - task_started = "inprogress" in new_labels and "inprogress" not in old_labels - task_stopped = "inprogress" not in new_labels and "inprogress" in old_labels + @property + def logger(self): + return logger - if is_item_type and is_updated and (task_started or task_stopped): - todoist_task = { - "todoist_id": event_data.get("id"), - "todoist_label_list": event_data.get("labels"), - "todoist_type": todoist_type, - "todoist_event": todoist_event, - "updated_at": event_data.get("updated_at"), - "todoist_project_id": event_data.get("project_id"), - "description": event_data.get("content"), - "details": event_data.get("description"), - } - if is_note_type and is_added: - task_data = event_data.get("item", {}) - todoist_note = { - "task_id": event_data.get("item_id"), - "todoist_id": event_data.get("id"), - "todoist_label_list": task_data.get("labels"), - "todoist_type": todoist_type, - "todoist_event": todoist_event, - "updated_at": task_data.get("updated_at"), - "details": task_data.get("description"), - "notes": event_data.get("content"), - "is_deleted": (True if event_data.get("is_deleted") == "true" else False), - } - if (is_added and not todoist_note) or (is_updated and not todoist_task): - logger.info( - "[todoist_webhook] ignoring wrong todoist type, event or labels", - extra={ +class TodoistWebhookView(WebhookView): + def post(self, request): + post_data = request.data + self.logger.info( + "[todoist_webhook] called", + extra={"post_data": post_data}, + ) + todoist_task = {} + todoist_note = {} + todoist_type, todoist_event = post_data.get("event_name").split(":") + event_data = post_data.get("event_data", {}) + is_item_type = todoist_type == "item" + is_note_type = todoist_type == "note" + new_labels = event_data.get("labels", []) + old_labels = ( + post_data.get("event_data_extra", {}).get("old_item", {}).get("labels", []) + ) + is_updated = todoist_event in ["updated"] + is_added = todoist_event in ["added"] + + task_started = "inprogress" in new_labels and "inprogress" not in old_labels + task_stopped = "inprogress" not in new_labels and "inprogress" in old_labels + + if is_item_type and is_updated and (task_started or task_stopped): + todoist_task = { + "todoist_id": event_data.get("id"), + "todoist_label_list": event_data.get("labels"), "todoist_type": todoist_type, "todoist_event": todoist_event, - "task_started": task_started, - "task_stopped": task_stopped, - "new_labels": new_labels, - "old_labels": old_labels, - }, + "updated_at": event_data.get("updated_at"), + "todoist_project_id": event_data.get("project_id"), + "description": event_data.get("content"), + "details": event_data.get("description"), + } + if is_note_type and is_added: + task_data = event_data.get("item", {}) + todoist_note = { + "task_id": event_data.get("item_id"), + "todoist_id": event_data.get("id"), + "todoist_label_list": task_data.get("labels"), + "todoist_type": todoist_type, + "todoist_event": todoist_event, + "updated_at": task_data.get("updated_at"), + "details": task_data.get("description"), + "notes": event_data.get("content"), + "is_deleted": ( + True if event_data.get("is_deleted") == "true" else False + ), + } + + if (is_added and not todoist_note) or (is_updated and not todoist_task): + self.logger.info( + "[todoist_webhook] ignoring wrong todoist type, event or labels", + extra={ + "todoist_type": todoist_type, + "todoist_event": todoist_event, + "task_started": task_started, + "task_stopped": task_stopped, + "new_labels": new_labels, + "old_labels": old_labels, + }, + ) + return Response({}, status=status.HTTP_304_NOT_MODIFIED) + + user_profile = UserProfile.objects.filter( + todoist_user_id=post_data.get("user_id", None) + ).first() + + scrobble = None + if todoist_task: + scrobble = todoist_scrobble_task( + todoist_task, + user_profile.user_id, + stopped=task_stopped, + user_context_list=user_profile.task_context_tags, + ) + + if todoist_note: + scrobble = todoist_scrobble_update_task(todoist_note, user_profile.user_id) + + if not scrobble: + self.logger.info( + "[todoist_webhook] finished with no note or task found", + extra={"scrobble_id": None}, + ) + return Response( + {"error": "No scrobble found to be updated"}, + status=status.HTTP_304_NOT_MODIFIED, + ) + + self.logger.info( + "[todoist_webhook] finished", + extra={"scrobble_id": scrobble.id}, ) - return Response({}, status=status.HTTP_304_NOT_MODIFIED) + return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) - user_profile = UserProfile.objects.filter( - todoist_user_id=post_data.get("user_id", None) - ).first() - scrobble = None - if todoist_task: - scrobble = todoist_scrobble_task( - todoist_task, - user_profile.user_id, - stopped=task_stopped, - user_context_list=user_profile.task_context_tags, +class EmacsWebhookView(WebhookView): + def post(self, request): + try: + post_data = json.loads(request.data) + except TypeError: + post_data = request.data + self.logger.info( + "[emacs_webhook] called", + extra={"post_data": post_data}, ) + task_in_progress = post_data.get("state") == "STRT" + task_stopped = post_data.get("state") == "DONE" + post_data["source_id"] = post_data.pop("emacs_id") - if todoist_note: - scrobble = todoist_scrobble_update_task(todoist_note, user_profile.user_id) + user_id = request.user.id - if not scrobble: - logger.info( - "[todoist_webhook] finished with no note or task found", - extra={"scrobble_id": None}, + user_profile = UserProfile.objects.filter(user_id=user_id).first() + + scrobble = None + if post_data.get("source_id"): + scrobble = emacs_scrobble_task( + post_data, + user_id, + started=task_in_progress, + stopped=task_stopped, + user_context_list=user_profile.task_context_tags, + ) + + if not scrobble: + self.logger.info( + "[emacs_webhook] finished with no note or task found", + extra={"scrobble_id": None}, + ) + return Response( + {"error": "No scrobble found to be updated"}, + status=status.HTTP_304_NOT_MODIFIED, + ) + + if task_in_progress and post_data.get("notes"): + emacs_scrobble_update_task( + post_data.get("source_id"), + post_data.get("notes"), + user_id, + ) + + self.logger.info( + "[emacs_webhook] finished", + extra={"scrobble_id": scrobble.id}, ) - return Response( - {"error": "No scrobble found to be updated"}, - status=status.HTTP_304_NOT_MODIFIED, - ) - - logger.info( - "[todoist_webhook] finished", - extra={"scrobble_id": scrobble.id}, - ) - return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) - - -@csrf_exempt -@permission_classes([IsAuthenticated]) -@api_view(["POST"]) -def emacs_webhook(request): - try: - post_data = json.loads(request.data) - except TypeError: - post_data = request.data - logger.info( - "[emacs_webhook] called", - extra={"post_data": post_data}, - ) - task_in_progress = post_data.get("state") == "STRT" - task_stopped = post_data.get("state") == "DONE" - post_data["source_id"] = post_data.pop("emacs_id") - - # TODO: Figure out why token auth is not working - user_id = request.user.id - if not user_id: - user_id = 1 - - user_profile = UserProfile.objects.filter(user_id=user_id).first() - - scrobble = None - if post_data.get("source_id"): - scrobble = emacs_scrobble_task( - post_data, - user_id, - started=task_in_progress, - stopped=task_stopped, - user_context_list=user_profile.task_context_tags, - ) - - if not scrobble: - logger.info( - "[emacs_webhook] finished with no note or task found", - extra={"scrobble_id": None}, - ) - return Response( - {"error": "No scrobble found to be updated"}, - status=status.HTTP_304_NOT_MODIFIED, - ) - - if task_in_progress and post_data.get("notes"): - emacs_scrobble_update_task( - post_data.get("source_id"), - post_data.get("notes"), - user_id, - ) - - logger.info( - "[emacs_webhook] finished", - extra={"scrobble_id": scrobble.id}, - ) - return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) + return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) diff --git a/vrobbler/apps/webpages/views.py b/vrobbler/apps/webpages/views.py index ad9ed0d..ce1df06 100644 --- a/vrobbler/apps/webpages/views.py +++ b/vrobbler/apps/webpages/views.py @@ -1,16 +1,17 @@ import datetime + from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.paginator import Paginator from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse from django.utils import timezone from django.views import generic +from scrobbles.models import Scrobble +from scrobbles.views import ScrobbleableDetailView, ScrobbleableListView from webpages.forms import WebPageReadForm from webpages.models import WebPage -from scrobbles.models import Scrobble -from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView - class WebPageListView(ScrobbleableListView): model = WebPage @@ -52,13 +53,24 @@ class WebPageListView(ScrobbleableListView): "count": domains_this_month.get(domain.id, {}).get("count", 0) + 1, } - context_data["domains_this_week"] = sorted( + domains_week_sorted = sorted( domains_this_week.values(), key=lambda x: x["count"], reverse=True ) - context_data["domains_this_month"] = sorted( + domains_month_sorted = sorted( domains_this_month.values(), key=lambda x: x["count"], reverse=True ) + domain_page = self.request.GET.get("domain_page", 1) + + context_data["domains_this_week"] = Paginator(domains_week_sorted, 10).page( + domain_page + ) + context_data["domains_this_month"] = Paginator(domains_month_sorted, 10).page( + domain_page + ) + context_data["domain_page"] = int(domain_page) + context_data["domain_paginator"] = Paginator(domains_week_sorted, 10) + return context_data diff --git a/vrobbler/templates/scrobbles/scrobble_search.html b/vrobbler/templates/scrobbles/scrobble_search.html index c09996f..28b91ff 100644 --- a/vrobbler/templates/scrobbles/scrobble_search.html +++ b/vrobbler/templates/scrobbles/scrobble_search.html @@ -101,7 +101,7 @@
- {% if scrobble.media_type == "Task" and scrobble.logdata.title %}{{ scrobble.media_obj.title }}: {{ scrobble.logdata.title }}{% else %}{{ scrobble.media_obj.title|default:scrobble.media_obj|truncatechars:100 }}{% endif %} + {{ scrobble|truncatechars:100 }}
diff --git a/vrobbler/templates/webpages/webpage_list.html b/vrobbler/templates/webpages/webpage_list.html index 86ae4a1..595b293 100644 --- a/vrobbler/templates/webpages/webpage_list.html +++ b/vrobbler/templates/webpages/webpage_list.html @@ -9,6 +9,20 @@

Domains Read

+ + {% if domains_this_week %}

This Week