From 49b8b8624969a8d1e41e4ce5fc873604b544f396 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Fri, 20 Mar 2026 18:37:11 -0400 Subject: [PATCH] [templates] Clean up widgets --- vrobbler/apps/scrobbles/urls.py | 23 +- vrobbler/apps/scrobbles/views.py | 267 +++++++++--------- .../templates/profiles/settings_form.html | 13 +- .../scrobbles/embeddable_top_artist.html | 90 ------ .../embeddable_top_board_games_week.html | 90 ------ .../embeddable_top_board_games_year.html | 90 ------ ...s_month.html => embeddable_top_media.html} | 40 +-- 7 files changed, 168 insertions(+), 445 deletions(-) delete mode 100644 vrobbler/templates/scrobbles/embeddable_top_artist.html delete mode 100644 vrobbler/templates/scrobbles/embeddable_top_board_games_week.html delete mode 100644 vrobbler/templates/scrobbles/embeddable_top_board_games_year.html rename vrobbler/templates/scrobbles/{embeddable_top_board_games_month.html => embeddable_top_media.html} (55%) diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index a820560..d0c9d94 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -1,30 +1,25 @@ from django.urls import path from scrobbles import views -from tasks.webhooks import todoist_webhook, emacs_webhook +from tasks.webhooks import emacs_webhook, todoist_webhook app_name = "scrobbles" urlpatterns = [ path("status/", views.ScrobbleStatusView.as_view(), name="status"), path( - "widget/top-artists//", - views.EmbeddableTopArtistWidget.as_view(), + "widget/top-artists///", + views.EmbeddableTopArtistsWidget.as_view(), name="embeddable-top-artists", ), path( - "widget/top-board-games/month//", - views.EmbeddableTopBoardGamesMonthWidget.as_view(), - name="embeddable-top-board-games-month", + "widget/top-board-games///", + views.EmbeddableTopBoardGamesWidget.as_view(), + name="embeddable-top-board-games", ), path( - "widget/top-board-games/week//", - views.EmbeddableTopBoardGamesWeekWidget.as_view(), - name="embeddable-top-board-games-week", - ), - path( - "widget/top-board-games/year//", - views.EmbeddableTopBoardGamesYearWidget.as_view(), - name="embeddable-top-board-games-year", + "widget/top-books///", + views.EmbeddableTopBooksWidget.as_view(), + name="embeddable-top-books", ), path( "manual/lookup/", diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 394c938..b7c6b5d 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -8,12 +8,17 @@ import pytz from dateutil.relativedelta import relativedelta from django.apps import apps from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth import get_user_model +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Count, Max, Q from django.db.models.query import QuerySet -from django.http import FileResponse, Http404, HttpResponseRedirect, JsonResponse +from django.http import ( + FileResponse, + Http404, + HttpResponseRedirect, + JsonResponse, +) from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils import timezone @@ -24,13 +29,15 @@ from django.views.generic.list import ListView from moods.models import Mood from music.aggregators import ( artist_scrobble_count, - week_of_scrobbles, - scrobble_counts, live_charts, live_tv_charts, live_youtube_channel_charts, + scrobble_counts, + week_of_scrobbles, ) from pendulum.parsing.exceptions import ParserError +from profiles.models import UserProfile +from profiles.utils import now_user_timezone from rest_framework import status from rest_framework.decorators import ( api_view, @@ -67,8 +74,6 @@ from scrobbles.utils import ( get_long_plays_completed, get_long_plays_in_progress, ) -from profiles.models import UserProfile -from profiles.utils import now_user_timezone User = get_user_model() @@ -1019,170 +1024,156 @@ class ScrobbleDetailView(DetailView): return context -class EmbeddableTopArtistWidget(TemplateView): - template_name = "scrobbles/embeddable_top_artist.html" +class BaseEmbeddableWidget(TemplateView): + template_name = "scrobbles/embeddable_top_media.html" - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) + media_type = "Media" + count_label = "plays" + no_data_message = "No scrobbles" + model = None + scrobble_filter = {} + + def get_user_and_profile(self): user_id = self.kwargs.get("user_id") - - try: - user = User.objects.get(id=user_id) - except User.DoesNotExist: - context["error"] = "User not found" - return context - + user = User.objects.get(id=user_id) profile = user.profile if not profile.enable_public_widgets: raise Http404("Public widgets are not enabled for this user") + return user, profile + + def get_date_range(self, user): + now = timezone.now() + if user.is_authenticated: + now = now_user_timezone(user.profile) + now = now.date() + + period = self.kwargs.get("period", "week") + if period == "month": + start = now.replace(day=1) + label = now.strftime("%B %Y") + elif period == "year": + start = now.replace(month=1, day=1) + label = now.strftime("%Y") + else: + start = now - timedelta(days=now.isoweekday() % 7) + label = "This Week" + + return start, label + + def get_items(self, user, start_date, model=None): + from django.db.models import Count + + model = model or self.model + queryset = ( + model.objects.filter( + scrobble__user=user, + scrobble__timestamp__gte=start_date, + **self.scrobble_filter, + ) + .annotate(count=Count("scrobble", distinct=True)) + .order_by("-count")[:10] + ) + + items = list(queryset) + for item in items: + if not hasattr(item, "name") and hasattr(item, "title"): + item.name = item.title + return items + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + user, profile = self.get_user_and_profile() context["user"] = user context["custom_css"] = profile.widget_custom_css + context["media_type"] = self.media_type + context["count_label"] = self.count_label - artist = { - "user": user, - "media_type": "Artist", - "limit": 10, - } - context["top_artists"] = list(live_charts(**artist, chart_period="last7")) + start_date, period_label = self.get_date_range(user) + context["period_label"] = period_label + + items = self.get_items(user, start_date) + context["items"] = items + context["no_data_message"] = self.no_data_message return context -class EmbeddableTopBoardGamesMonthWidget(TemplateView): - template_name = "scrobbles/embeddable_top_board_games_month.html" +class EmbeddableTopArtistsWidget(TemplateView): + template_name = "scrobbles/embeddable_top_media.html" + media_type = "Artists" + count_label = "scrobbles" + no_data_message = "No scrobbles" + + def get_user_and_profile(self): + user_id = self.kwargs.get("user_id") + user = User.objects.get(id=user_id) + profile = user.profile + if not profile.enable_public_widgets: + raise Http404("Public widgets are not enabled for this user") + return user, profile def get_context_data(self, **kwargs): - from django.db.models import Count - from django.utils import timezone + context = super().get_context_data(**kwargs) + user, profile = self.get_user_and_profile() + + context["user"] = user + context["custom_css"] = profile.widget_custom_css + context["media_type"] = self.media_type + context["count_label"] = self.count_label + context["no_data_message"] = self.no_data_message + + period = self.kwargs.get("period", "week") + period_map = {"week": "last7", "month": "last30", "year": "year"} + chart_period = period_map.get(period, "last7") + + artist = {"user": user, "media_type": "Artist", "limit": 10} + items = list(live_charts(**artist, chart_period=chart_period)) + + for item in items: + item.count = item.num_scrobbles + + context["items"] = items + from datetime import timedelta - from boardgames.models import BoardGame - context = super().get_context_data(**kwargs) - user_id = self.kwargs.get("user_id") - - try: - user = User.objects.get(id=user_id) - except User.DoesNotExist: - context["error"] = "User not found" - return context - - profile = user.profile - if not profile.enable_public_widgets: - raise Http404("Public widgets are not enabled for this user") - - context["user"] = user - context["custom_css"] = profile.widget_custom_css - - now = timezone.now() - if user.is_authenticated: - now = now_user_timezone(user.profile) - now = now.date() - start_day_of_month = now.replace(day=1) - - top_games = ( - BoardGame.objects.filter( - scrobble__user=user, - scrobble__timestamp__gte=start_day_of_month, - scrobble__played_to_completion=True, - ) - .annotate(num_plays=Count("scrobble", distinct=True)) - .order_by("-num_plays")[:10] - ) - - context["top_board_games"] = list(top_games) - context["month"] = now.strftime("%B %Y") - - return context - - -class EmbeddableTopBoardGamesWeekWidget(TemplateView): - template_name = "scrobbles/embeddable_top_board_games_week.html" - - def get_context_data(self, **kwargs): - from django.db.models import Count from django.utils import timezone - from datetime import timedelta - from boardgames.models import BoardGame - - context = super().get_context_data(**kwargs) - user_id = self.kwargs.get("user_id") - - try: - user = User.objects.get(id=user_id) - except User.DoesNotExist: - context["error"] = "User not found" - return context - - profile = user.profile - if not profile.enable_public_widgets: - raise Http404("Public widgets are not enabled for this user") - - context["user"] = user - context["custom_css"] = profile.widget_custom_css now = timezone.now() if user.is_authenticated: now = now_user_timezone(user.profile) now = now.date() - start_day_of_week = now - timedelta(days=now.isoweekday() % 7) - top_games = ( - BoardGame.objects.filter( - scrobble__user=user, - scrobble__timestamp__gte=start_day_of_week, - scrobble__played_to_completion=True, - ) - .annotate(num_plays=Count("scrobble", distinct=True)) - .order_by("-num_plays")[:10] - ) - - context["top_board_games"] = list(top_games) + if period == "month": + context["period_label"] = now.strftime("%B %Y") + elif period == "year": + context["period_label"] = now.strftime("%Y") + else: + start = now - timedelta(days=now.isoweekday() % 7) + context["period_label"] = "This Week" return context -class EmbeddableTopBoardGamesYearWidget(TemplateView): - template_name = "scrobbles/embeddable_top_board_games_year.html" +class EmbeddableTopBoardGamesWidget(BaseEmbeddableWidget): + media_type = "Board Games" + count_label = "plays" + no_data_message = "No board games played" + scrobble_filter = {"scrobble__played_to_completion": True} - def get_context_data(self, **kwargs): - from django.db.models import Count - from django.utils import timezone + def get_items(self, user, start_date): from boardgames.models import BoardGame - context = super().get_context_data(**kwargs) - user_id = self.kwargs.get("user_id") + return super().get_items(user, start_date, BoardGame) - try: - user = User.objects.get(id=user_id) - except User.DoesNotExist: - context["error"] = "User not found" - return context - profile = user.profile - if not profile.enable_public_widgets: - raise Http404("Public widgets are not enabled for this user") +class EmbeddableTopBooksWidget(BaseEmbeddableWidget): + media_type = "Books" + count_label = "reads" + no_data_message = "No books read" + scrobble_filter = {"scrobble__long_play_complete": True} - context["user"] = user - context["custom_css"] = profile.widget_custom_css + def get_items(self, user, start_date): + from books.models import Book - now = timezone.now() - if user.is_authenticated: - now = now_user_timezone(user.profile) - now = now.date() - start_day_of_year = now.replace(month=1, day=1) - - top_games = ( - BoardGame.objects.filter( - scrobble__user=user, - scrobble__timestamp__gte=start_day_of_year, - scrobble__played_to_completion=True, - ) - .annotate(num_plays=Count("scrobble", distinct=True)) - .order_by("-num_plays")[:10] - ) - - context["top_board_games"] = list(top_games) - context["year"] = now.strftime("%Y") - - return context + return super().get_items(user, start_date, Book) diff --git a/vrobbler/templates/profiles/settings_form.html b/vrobbler/templates/profiles/settings_form.html index 22b0899..2e22b4c 100644 --- a/vrobbler/templates/profiles/settings_form.html +++ b/vrobbler/templates/profiles/settings_form.html @@ -111,10 +111,15 @@ {% endif %} diff --git a/vrobbler/templates/scrobbles/embeddable_top_artist.html b/vrobbler/templates/scrobbles/embeddable_top_artist.html deleted file mode 100644 index e6c1789..0000000 --- a/vrobbler/templates/scrobbles/embeddable_top_artist.html +++ /dev/null @@ -1,90 +0,0 @@ -{% load static %} - - -
-

Last 7 Days Top Artists{% if user %} - {{ user.username }}{% endif %}

- - {% if error %} -

{{ error }}

- {% elif top_artists %} -
    - {% for artist in top_artists %} -
  • - {{ forloop.counter }} - {% if artist.thumbnail %} - {{ artist.name }} - {% else %} - {{ artist.name }} - {% endif %} -
    -
    {{ artist.name }}
    -
    {{ artist.num_scrobbles }} scrobbles
    -
    -
  • - {% endfor %} -
- {% else %} -

No scrobbles in the last 7 days

- {% endif %} -
diff --git a/vrobbler/templates/scrobbles/embeddable_top_board_games_week.html b/vrobbler/templates/scrobbles/embeddable_top_board_games_week.html deleted file mode 100644 index b5e1c12..0000000 --- a/vrobbler/templates/scrobbles/embeddable_top_board_games_week.html +++ /dev/null @@ -1,90 +0,0 @@ -{% load static %} - - -
-

Top Board Games - This Week{% if user %} - {{ user.username }}{% endif %}

- - {% if error %} -

{{ error }}

- {% elif top_board_games %} -
    - {% for game in top_board_games %} -
  • - {{ forloop.counter }} - {% if game.cover %} - {{ game.title }} - {% else %} - {{ game.title }} - {% endif %} -
    -
    {{ game.title }}
    -
    {{ game.num_plays }} plays
    -
    -
  • - {% endfor %} -
- {% else %} -

No board games played this week

- {% endif %} -
\ No newline at end of file diff --git a/vrobbler/templates/scrobbles/embeddable_top_board_games_year.html b/vrobbler/templates/scrobbles/embeddable_top_board_games_year.html deleted file mode 100644 index ae1ad6f..0000000 --- a/vrobbler/templates/scrobbles/embeddable_top_board_games_year.html +++ /dev/null @@ -1,90 +0,0 @@ -{% load static %} - - -
-

Top Board Games - {{ year }}{% if user %} - {{ user.username }}{% endif %}

- - {% if error %} -

{{ error }}

- {% elif top_board_games %} -
    - {% for game in top_board_games %} -
  • - {{ forloop.counter }} - {% if game.cover %} - {{ game.title }} - {% else %} - {{ game.title }} - {% endif %} -
    -
    {{ game.title }}
    -
    {{ game.num_plays }} plays
    -
    -
  • - {% endfor %} -
- {% else %} -

No board games played this year

- {% endif %} -
\ No newline at end of file diff --git a/vrobbler/templates/scrobbles/embeddable_top_board_games_month.html b/vrobbler/templates/scrobbles/embeddable_top_media.html similarity index 55% rename from vrobbler/templates/scrobbles/embeddable_top_board_games_month.html rename to vrobbler/templates/scrobbles/embeddable_top_media.html index 8b7f566..4cf52b4 100644 --- a/vrobbler/templates/scrobbles/embeddable_top_board_games_month.html +++ b/vrobbler/templates/scrobbles/embeddable_top_media.html @@ -13,18 +13,18 @@ font-size: 16px; color: #333; } - .vrobbler-board-game-list { + .vrobbler-media-list { list-style: none; padding: 0; margin: 0; } - .vrobbler-board-game-item { + .vrobbler-media-item { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; } - .vrobbler-board-game-item:last-child { + .vrobbler-media-item:last-child { border-bottom: none; } .vrobbler-rank { @@ -32,18 +32,18 @@ font-weight: bold; color: #666; } - .vrobbler-board-game-info { + .vrobbler-media-info { flex: 1; } - .vrobbler-board-game-title { + .vrobbler-media-title { font-weight: 500; color: #333; } - .vrobbler-play-count { + .vrobbler-media-count { font-size: 12px; color: #888; } - .vrobbler-board-game-thumb { + .vrobbler-media-thumb { width: 40px; height: 40px; border-radius: 4px; @@ -63,28 +63,30 @@
-

Top Board Games - {{ month }}{% if user %} - {{ user.username }}{% endif %}

+

Top {{ media_type }} - {{ period_label }}{% if user %} - {{ user.username }}{% endif %}

{% if error %}

{{ error }}

- {% elif top_board_games %} -
    - {% for game in top_board_games %} -
  • + {% elif items %} +
      + {% for item in items %} +
    • {{ forloop.counter }} - {% if game.cover %} - {{ game.title }} + {% if item.cover %} + {{ item.name }} + {% elif item.thumbnail %} + {{ item.name }} {% else %} - {{ game.title }} + {{ item.name }} {% endif %} -
      -
      {{ game.title }}
      -
      {{ game.num_plays }} plays
      +
      +
      {{ item.name }}
      +
      {{ item.count }} {{ count_label }}
    • {% endfor %}
    {% else %} -

    No board games played this month

    +

    {{ no_data_message }}

    {% endif %}
\ No newline at end of file