[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 django.urls import path
from scrobbles import views from scrobbles import views
from tasks.webhooks import todoist_webhook, emacs_webhook from tasks.webhooks import emacs_webhook, todoist_webhook
app_name = "scrobbles" app_name = "scrobbles"
urlpatterns = [ urlpatterns = [
path("status/", views.ScrobbleStatusView.as_view(), name="status"), path("status/", views.ScrobbleStatusView.as_view(), name="status"),
path( path(
"widget/top-artists/<int:user_id>/", "widget/top-artists/<str:period>/<int:user_id>/",
views.EmbeddableTopArtistWidget.as_view(), views.EmbeddableTopArtistsWidget.as_view(),
name="embeddable-top-artists", name="embeddable-top-artists",
), ),
path( path(
"widget/top-board-games/month/<int:user_id>/", "widget/top-board-games/<str:period>/<int:user_id>/",
views.EmbeddableTopBoardGamesMonthWidget.as_view(), views.EmbeddableTopBoardGamesWidget.as_view(),
name="embeddable-top-board-games-month", name="embeddable-top-board-games",
), ),
path( path(
"widget/top-board-games/week/<int:user_id>/", "widget/top-books/<str:period>/<int:user_id>/",
views.EmbeddableTopBoardGamesWeekWidget.as_view(), views.EmbeddableTopBooksWidget.as_view(),
name="embeddable-top-board-games-week", name="embeddable-top-books",
),
path(
"widget/top-board-games/year/<int:user_id>/",
views.EmbeddableTopBoardGamesYearWidget.as_view(),
name="embeddable-top-board-games-year",
), ),
path( path(
"manual/lookup/", "manual/lookup/",

View File

@ -8,12 +8,17 @@ import pytz
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.apps import apps from django.apps import apps
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import get_user_model 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.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Count, Max, Q from django.db.models import Count, Max, Q
from django.db.models.query import QuerySet 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.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
@ -24,13 +29,15 @@ from django.views.generic.list import ListView
from moods.models import Mood from moods.models import Mood
from music.aggregators import ( from music.aggregators import (
artist_scrobble_count, artist_scrobble_count,
week_of_scrobbles,
scrobble_counts,
live_charts, live_charts,
live_tv_charts, live_tv_charts,
live_youtube_channel_charts, live_youtube_channel_charts,
scrobble_counts,
week_of_scrobbles,
) )
from pendulum.parsing.exceptions import ParserError 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 import status
from rest_framework.decorators import ( from rest_framework.decorators import (
api_view, api_view,
@ -67,8 +74,6 @@ from scrobbles.utils import (
get_long_plays_completed, get_long_plays_completed,
get_long_plays_in_progress, get_long_plays_in_progress,
) )
from profiles.models import UserProfile
from profiles.utils import now_user_timezone
User = get_user_model() User = get_user_model()
@ -1019,170 +1024,156 @@ class ScrobbleDetailView(DetailView):
return context return context
class EmbeddableTopArtistWidget(TemplateView): class BaseEmbeddableWidget(TemplateView):
template_name = "scrobbles/embeddable_top_artist.html" template_name = "scrobbles/embeddable_top_media.html"
def get_context_data(self, **kwargs): media_type = "Media"
context = super().get_context_data(**kwargs) 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") user_id = self.kwargs.get("user_id")
user = User.objects.get(id=user_id)
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
context["error"] = "User not found"
return context
profile = user.profile profile = user.profile
if not profile.enable_public_widgets: if not profile.enable_public_widgets:
raise Http404("Public widgets are not enabled for this user") 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["user"] = user
context["custom_css"] = profile.widget_custom_css context["custom_css"] = profile.widget_custom_css
context["media_type"] = self.media_type
context["count_label"] = self.count_label
artist = { start_date, period_label = self.get_date_range(user)
"user": user, context["period_label"] = period_label
"media_type": "Artist",
"limit": 10, items = self.get_items(user, start_date)
} context["items"] = items
context["top_artists"] = list(live_charts(**artist, chart_period="last7")) context["no_data_message"] = self.no_data_message
return context return context
class EmbeddableTopBoardGamesMonthWidget(TemplateView): class EmbeddableTopArtistsWidget(TemplateView):
template_name = "scrobbles/embeddable_top_board_games_month.html" 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): def get_context_data(self, **kwargs):
from django.db.models import Count context = super().get_context_data(**kwargs)
from django.utils import timezone 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 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 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() now = timezone.now()
if user.is_authenticated: if user.is_authenticated:
now = now_user_timezone(user.profile) now = now_user_timezone(user.profile)
now = now.date() now = now.date()
start_day_of_week = now - timedelta(days=now.isoweekday() % 7)
top_games = ( if period == "month":
BoardGame.objects.filter( context["period_label"] = now.strftime("%B %Y")
scrobble__user=user, elif period == "year":
scrobble__timestamp__gte=start_day_of_week, context["period_label"] = now.strftime("%Y")
scrobble__played_to_completion=True, else:
) start = now - timedelta(days=now.isoweekday() % 7)
.annotate(num_plays=Count("scrobble", distinct=True)) context["period_label"] = "This Week"
.order_by("-num_plays")[:10]
)
context["top_board_games"] = list(top_games)
return context return context
class EmbeddableTopBoardGamesYearWidget(TemplateView): class EmbeddableTopBoardGamesWidget(BaseEmbeddableWidget):
template_name = "scrobbles/embeddable_top_board_games_year.html" 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): def get_items(self, user, start_date):
from django.db.models import Count
from django.utils import timezone
from boardgames.models import BoardGame from boardgames.models import BoardGame
context = super().get_context_data(**kwargs) return super().get_items(user, start_date, BoardGame)
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 class EmbeddableTopBooksWidget(BaseEmbeddableWidget):
if not profile.enable_public_widgets: media_type = "Books"
raise Http404("Public widgets are not enabled for this user") count_label = "reads"
no_data_message = "No books read"
scrobble_filter = {"scrobble__long_play_complete": True}
context["user"] = user def get_items(self, user, start_date):
context["custom_css"] = profile.widget_custom_css from books.models import Book
now = timezone.now() return super().get_items(user, start_date, Book)
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

View File

@ -111,10 +111,15 @@
<div class="widget-links"> <div class="widget-links">
<strong>Available Widgets:</strong> <strong>Available Widgets:</strong>
<ul> <ul>
<li><a href="{% url 'scrobbles:embeddable-top-artists' user_id %}">Top Artists</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-board-games-week' user_id %}">Top Board Games (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-board-games-month' user_id %}">Top Board Games (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-year' user_id %}">Top Board Games (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> </ul>
</div> </div>
{% endif %} {% 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; font-size: 16px;
color: #333; color: #333;
} }
.vrobbler-board-game-list { .vrobbler-media-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.vrobbler-board-game-item { .vrobbler-media-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 0; padding: 8px 0;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
} }
.vrobbler-board-game-item:last-child { .vrobbler-media-item:last-child {
border-bottom: none; border-bottom: none;
} }
.vrobbler-rank { .vrobbler-rank {
@ -32,18 +32,18 @@
font-weight: bold; font-weight: bold;
color: #666; color: #666;
} }
.vrobbler-board-game-info { .vrobbler-media-info {
flex: 1; flex: 1;
} }
.vrobbler-board-game-title { .vrobbler-media-title {
font-weight: 500; font-weight: 500;
color: #333; color: #333;
} }
.vrobbler-play-count { .vrobbler-media-count {
font-size: 12px; font-size: 12px;
color: #888; color: #888;
} }
.vrobbler-board-game-thumb { .vrobbler-media-thumb {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 4px; border-radius: 4px;
@ -63,28 +63,30 @@
</style> </style>
<div class="vrobbler-widget"> <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 %} {% if error %}
<p class="vrobbler-error">{{ error }}</p> <p class="vrobbler-error">{{ error }}</p>
{% elif top_board_games %} {% elif items %}
<ul class="vrobbler-board-game-list"> <ul class="vrobbler-media-list">
{% for game in top_board_games %} {% for item in items %}
<li class="vrobbler-board-game-item"> <li class="vrobbler-media-item">
<span class="vrobbler-rank">{{ forloop.counter }}</span> <span class="vrobbler-rank">{{ forloop.counter }}</span>
{% if game.cover %} {% if item.cover %}
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}"> <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 %} {% 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 %} {% endif %}
<div class="vrobbler-board-game-info"> <div class="vrobbler-media-info">
<div class="vrobbler-board-game-title">{{ game.title }}</div> <div class="vrobbler-media-title">{{ item.name }}</div>
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div> <div class="vrobbler-media-count">{{ item.count }} {{ count_label }}</div>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<p class="vrobbler-no-data">No board games played this month</p> <p class="vrobbler-no-data">{{ no_data_message }}</p>
{% endif %} {% endif %}
</div> </div>