[templates] Clean up widgets
All checks were successful
build & deploy / test (push) Successful in 1m53s
build & deploy / deploy (push) Successful in 25s

This commit is contained in:
2026-03-20 18:37:11 -04:00
parent e8f120f85e
commit 49b8b86249
7 changed files with 168 additions and 445 deletions

View File

@ -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/<int:user_id>/",
views.EmbeddableTopArtistWidget.as_view(),
"widget/top-artists/<str:period>/<int:user_id>/",
views.EmbeddableTopArtistsWidget.as_view(),
name="embeddable-top-artists",
),
path(
"widget/top-board-games/month/<int:user_id>/",
views.EmbeddableTopBoardGamesMonthWidget.as_view(),
name="embeddable-top-board-games-month",
"widget/top-board-games/<str:period>/<int:user_id>/",
views.EmbeddableTopBoardGamesWidget.as_view(),
name="embeddable-top-board-games",
),
path(
"widget/top-board-games/week/<int:user_id>/",
views.EmbeddableTopBoardGamesWeekWidget.as_view(),
name="embeddable-top-board-games-week",
),
path(
"widget/top-board-games/year/<int:user_id>/",
views.EmbeddableTopBoardGamesYearWidget.as_view(),
name="embeddable-top-board-games-year",
"widget/top-books/<str:period>/<int:user_id>/",
views.EmbeddableTopBooksWidget.as_view(),
name="embeddable-top-books",
),
path(
"manual/lookup/",

View File

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

View File

@ -111,10 +111,15 @@
<div class="widget-links">
<strong>Available Widgets:</strong>
<ul>
<li><a href="{% url 'scrobbles:embeddable-top-artists' user_id %}">Top Artists</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-board-games-week' user_id %}">Top Board Games (Week)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-board-games-month' user_id %}">Top Board Games (Month)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-board-games-year' user_id %}">Top Board Games (Year)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-artists' 'week' user_id %}">Top Artists (Week)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-artists' 'month' user_id %}">Top Artists (Month)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-artists' 'year' user_id %}">Top Artists (Year)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-board-games' 'week' user_id %}">Top Board Games (Week)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-board-games' 'month' user_id %}">Top Board Games (Month)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-board-games' 'year' user_id %}">Top Board Games (Year)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-books' 'week' user_id %}">Top Books (Week)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-books' 'month' user_id %}">Top Books (Month)</a></li>
<li><a href="{% url 'scrobbles:embeddable-top-books' 'year' user_id %}">Top Books (Year)</a></li>
</ul>
</div>
{% endif %}

View File

@ -1,90 +0,0 @@
{% load static %}
<style>
.vrobbler-widget {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 400px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
background: #fff;
}
.vrobbler-widget h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
.vrobbler-artist-list {
list-style: none;
padding: 0;
margin: 0;
}
.vrobbler-artist-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.vrobbler-artist-item:last-child {
border-bottom: none;
}
.vrobbler-rank {
width: 24px;
font-weight: bold;
color: #666;
}
.vrobbler-artist-info {
flex: 1;
}
.vrobbler-artist-name {
font-weight: 500;
color: #333;
}
.vrobbler-scrobble-count {
font-size: 12px;
color: #888;
}
.vrobbler-artist-thumb {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
margin-right: 10px;
}
.vrobbler-error {
color: #d32f2f;
}
.vrobbler-no-data {
color: #666;
font-style: italic;
}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
<div class="vrobbler-widget">
<h3>Last 7 Days Top Artists{% if user %} - {{ user.username }}{% endif %}</h3>
{% if error %}
<p class="vrobbler-error">{{ error }}</p>
{% elif top_artists %}
<ul class="vrobbler-artist-list">
{% for artist in top_artists %}
<li class="vrobbler-artist-item">
<span class="vrobbler-rank">{{ forloop.counter }}</span>
{% if artist.thumbnail %}
<img class="vrobbler-artist-thumb" src="{{ artist.thumbnail_small.url }}" alt="{{ artist.name }}">
{% else %}
<img class="vrobbler-artist-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ artist.name }}">
{% endif %}
<div class="vrobbler-artist-info">
<div class="vrobbler-artist-name">{{ artist.name }}</div>
<div class="vrobbler-scrobble-count">{{ artist.num_scrobbles }} scrobbles</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="vrobbler-no-data">No scrobbles in the last 7 days</p>
{% endif %}
</div>

View File

@ -1,90 +0,0 @@
{% load static %}
<style>
.vrobbler-widget {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 400px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
background: #fff;
}
.vrobbler-widget h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
.vrobbler-board-game-list {
list-style: none;
padding: 0;
margin: 0;
}
.vrobbler-board-game-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.vrobbler-board-game-item:last-child {
border-bottom: none;
}
.vrobbler-rank {
width: 24px;
font-weight: bold;
color: #666;
}
.vrobbler-board-game-info {
flex: 1;
}
.vrobbler-board-game-title {
font-weight: 500;
color: #333;
}
.vrobbler-play-count {
font-size: 12px;
color: #888;
}
.vrobbler-board-game-thumb {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
margin-right: 10px;
}
.vrobbler-error {
color: #d32f2f;
}
.vrobbler-no-data {
color: #666;
font-style: italic;
}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
<div class="vrobbler-widget">
<h3>Top Board Games - This Week{% if user %} - {{ user.username }}{% endif %}</h3>
{% if error %}
<p class="vrobbler-error">{{ error }}</p>
{% elif top_board_games %}
<ul class="vrobbler-board-game-list">
{% for game in top_board_games %}
<li class="vrobbler-board-game-item">
<span class="vrobbler-rank">{{ forloop.counter }}</span>
{% if game.cover %}
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}">
{% else %}
<img class="vrobbler-board-game-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ game.title }}">
{% endif %}
<div class="vrobbler-board-game-info">
<div class="vrobbler-board-game-title">{{ game.title }}</div>
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="vrobbler-no-data">No board games played this week</p>
{% endif %}
</div>

View File

@ -1,90 +0,0 @@
{% load static %}
<style>
.vrobbler-widget {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 400px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
background: #fff;
}
.vrobbler-widget h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
.vrobbler-board-game-list {
list-style: none;
padding: 0;
margin: 0;
}
.vrobbler-board-game-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.vrobbler-board-game-item:last-child {
border-bottom: none;
}
.vrobbler-rank {
width: 24px;
font-weight: bold;
color: #666;
}
.vrobbler-board-game-info {
flex: 1;
}
.vrobbler-board-game-title {
font-weight: 500;
color: #333;
}
.vrobbler-play-count {
font-size: 12px;
color: #888;
}
.vrobbler-board-game-thumb {
width: 40px;
height: 40px;
border-radius: 4px;
object-fit: cover;
margin-right: 10px;
}
.vrobbler-error {
color: #d32f2f;
}
.vrobbler-no-data {
color: #666;
font-style: italic;
}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
<div class="vrobbler-widget">
<h3>Top Board Games - {{ year }}{% if user %} - {{ user.username }}{% endif %}</h3>
{% if error %}
<p class="vrobbler-error">{{ error }}</p>
{% elif top_board_games %}
<ul class="vrobbler-board-game-list">
{% for game in top_board_games %}
<li class="vrobbler-board-game-item">
<span class="vrobbler-rank">{{ forloop.counter }}</span>
{% if game.cover %}
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}">
{% else %}
<img class="vrobbler-board-game-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ game.title }}">
{% endif %}
<div class="vrobbler-board-game-info">
<div class="vrobbler-board-game-title">{{ game.title }}</div>
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="vrobbler-no-data">No board games played this year</p>
{% endif %}
</div>

View File

@ -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 @@
</style>
<div class="vrobbler-widget">
<h3>Top Board Games - {{ month }}{% if user %} - {{ user.username }}{% endif %}</h3>
<h3>Top {{ media_type }} - {{ period_label }}{% if user %} - {{ user.username }}{% endif %}</h3>
{% if error %}
<p class="vrobbler-error">{{ error }}</p>
{% elif top_board_games %}
<ul class="vrobbler-board-game-list">
{% for game in top_board_games %}
<li class="vrobbler-board-game-item">
{% elif items %}
<ul class="vrobbler-media-list">
{% for item in items %}
<li class="vrobbler-media-item">
<span class="vrobbler-rank">{{ forloop.counter }}</span>
{% if game.cover %}
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}">
{% if item.cover %}
<img class="vrobbler-media-thumb" src="{{ item.cover_small.url }}" alt="{{ item.name }}">
{% elif item.thumbnail %}
<img class="vrobbler-media-thumb" src="{{ item.thumbnail_small.url }}" alt="{{ item.name }}">
{% else %}
<img class="vrobbler-board-game-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ game.title }}">
<img class="vrobbler-media-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ item.name }}">
{% endif %}
<div class="vrobbler-board-game-info">
<div class="vrobbler-board-game-title">{{ game.title }}</div>
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div>
<div class="vrobbler-media-info">
<div class="vrobbler-media-title">{{ item.name }}</div>
<div class="vrobbler-media-count">{{ item.count }} {{ count_label }}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="vrobbler-no-data">No board games played this month</p>
<p class="vrobbler-no-data">{{ no_data_message }}</p>
{% endif %}
</div>