Compare commits

...

5 Commits
58.3 ... 58.5

Author SHA1 Message Date
ec4c190e6c [release] Bump to version 58.5
All checks were successful
build / test (push) Successful in 2m15s
deploy / test (push) Successful in 2m14s
deploy / build-and-deploy (push) Successful in 56s
- The maloja style charts are messed up
2026-06-30 15:02:16 -04:00
58126928c7 [charts] Fix maloja charts acting weird
Some checks failed
build / test (push) Has been cancelled
2026-06-30 15:01:57 -04:00
c0d2881585 [release] Bump to version 58.4
All checks were successful
build / test (push) Successful in 2m13s
deploy / test (push) Successful in 2m10s
deploy / build-and-deploy (push) Successful in 53s
- Allow people all trends or individual trends
- Fix a bug in board game scorelog data
2026-06-25 20:02:34 -04:00
41a68291a4 [trends] Allow disabling one or many or all trends
All checks were successful
build / test (push) Successful in 2m22s
2026-06-25 18:58:23 -04:00
0a411bedf4 [boardgames] Fix bug in logdata
All checks were successful
build / test (push) Successful in 2m23s
2026-06-24 19:09:43 -04:00
18 changed files with 385 additions and 271 deletions

View File

@ -88,7 +88,7 @@ fetching and simple saving.
*** Metadata sources
**** Scraper
* Backlog [0/22] :vrobbler:project:personal:
* Backlog [0/23] :vrobbler:project:personal:
** TODO [#C] After transition to linux add curl_cffi as webpage scrapper again :webpages:metadata:
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
:PROPERTIES:
@ -605,6 +605,22 @@ independent of the email flow it was originally creatdd for
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
* Version 58.5 [1/1]
** DONE [#A] The maloja style charts are messed up :templates:charts:
:PROPERTIES:
:ID: 987397a2-7e74-4eb1-87cc-4c8bbe1c7b23
:END:
* Version 58.4 [2/2]
** DONE [#B] Allow people all trends or individual trends :trends:profiles:
:PROPERTIES:
:ID: 1d081152-abd1-73c2-a625-903565a10c6c
:END:
** DONE [#A] Fix a bug in board game scorelog data :boardgames:logdata:
:PROPERTIES:
:ID: 014bab30-13bf-fae7-e678-4666a8d38ae4
:END:
* Version 58.3 [1/1]
** DONE [#A] Remove curl-cffi as it doesn't work on FreeBSD :webpages:deps:
:PROPERTIES:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "58.3"
version = "58.5"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -120,7 +120,12 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
def player_log(self) -> str:
if self.players:
return ", ".join(
[BoardGameScoreLogData(**player).__str__() for player in self.players]
[
BoardGameScoreLogData(
**{k: v for k, v in player.items() if k in BoardGameScoreLogData.__dataclass_fields__}
).__str__()
for player in self.players
]
)
return ""
@ -136,7 +141,9 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
if self.players:
players_html = []
for player_data in self.players:
player = BoardGameScoreLogData(**player_data)
player = BoardGameScoreLogData(
**{k: v for k, v in player_data.items() if k in BoardGameScoreLogData.__dataclass_fields__}
)
player_info = player.name
if player.score:
player_info += f" ({player.score})"

View File

@ -114,6 +114,51 @@ class ChartRecordView(TemplateView):
context["current_week"] = current_week
context["current_day"] = current_day
# Resolve date parameters
if date_param:
parts = date_param.split("-")
year = int(parts[0])
week = None
month = None
day = None
if len(parts) >= 2 and parts[1].startswith("W"):
week = int(parts[1].lstrip("W"))
elif len(parts) >= 2 and parts[1]:
try:
month = int(parts[1])
except ValueError:
pass
if len(parts) >= 3:
if parts[2].startswith("W"):
week = int(parts[2].lstrip("W"))
elif not parts[2].startswith("W"):
day = int(parts[2])
context["period"] = "historical"
context["year"] = year
context["month"] = month
context["month_name"] = calendar.month_name[month] if month else None
context["week"] = week
context["day"] = day
period_str = str(year)
if month:
period_str = f"{calendar.month_name[month]} {period_str}"
if week:
period_str = f"Week {week}, {period_str}"
if day:
period_str = f"{calendar.month_name[month]} {day}, {year}"
context["period_str"] = period_str
else:
year = current_year
month = current_month
week = current_week
day = current_day
context["period"] = "current"
context["year"] = current_year
context["month"] = current_month
context["month_name"] = calendar.month_name[current_month]
context["week"] = current_week
context["day"] = current_day
context["chart_keys"] = {
"today": "Today",
"week": "This Week",
@ -126,295 +171,120 @@ class ChartRecordView(TemplateView):
"artist": {
"today": list(
self.get_charts_for_period(
user,
"artist",
year=current_year,
month=current_month,
day=current_day,
user, "artist", year=year, month=month, day=day,
)
),
"week": list(
self.get_charts_for_period(
user,
"artist",
year=current_year,
week=current_week,
user, "artist", year=year, week=week,
)
),
"month": list(
self.get_charts_for_period(
user,
"artist",
year=current_year,
month=current_month,
user, "artist", year=year, month=month,
)
),
"year": list(
self.get_charts_for_period(user, "artist", year=current_year)
self.get_charts_for_period(user, "artist", year=year)
),
"all": list(self.get_charts_for_period(user, "artist")),
},
"album": {
"today": list(
self.get_charts_for_period(
user,
"album",
year=current_year,
month=current_month,
day=current_day,
user, "album", year=year, month=month, day=day,
)
),
"week": list(
self.get_charts_for_period(
user, "album", year=current_year, week=current_week
user, "album", year=year, week=week,
)
),
"month": list(
self.get_charts_for_period(
user,
"album",
year=current_year,
month=current_month,
user, "album", year=year, month=month,
)
),
"year": list(
self.get_charts_for_period(user, "album", year=current_year)
self.get_charts_for_period(user, "album", year=year)
),
"all": list(self.get_charts_for_period(user, "album")),
},
"tv_series": {
"today": list(
self.get_charts_for_period(
user,
"tv_series",
year=current_year,
month=current_month,
day=current_day,
user, "tv_series", year=year, month=month, day=day,
)
),
"week": list(
self.get_charts_for_period(
user,
"tv_series",
year=current_year,
week=current_week,
user, "tv_series", year=year, week=week,
)
),
"month": list(
self.get_charts_for_period(
user,
"tv_series",
year=current_year,
month=current_month,
user, "tv_series", year=year, month=month,
)
),
"year": list(
self.get_charts_for_period(user, "tv_series", year=current_year)
self.get_charts_for_period(user, "tv_series", year=year)
),
"all": list(self.get_charts_for_period(user, "tv_series")),
},
}
if not date_param:
context["period"] = "current"
context["year"] = current_year
context["month"] = current_month
context["month_name"] = calendar.month_name[current_month]
context["week"] = current_week
context["day"] = current_day
context["charts"] = {
"artist": list(
self.get_charts_for_period(
user, "artist", year=current_year, limit=20
)
),
"album": list(
self.get_charts_for_period(
user, "album", year=current_year, limit=20
)
),
"track": list(
self.get_charts_for_period(
user, "track", year=current_year, limit=20
)
),
"tv_series": list(
self.get_charts_for_period(
user, "tv_series", year=current_year, limit=20
)
),
"video": list(
self.get_charts_for_period(
user, "video", year=current_year, limit=20
)
),
"board_game": list(
self.get_charts_for_period(
user, "board_game", year=current_year, limit=20
)
),
"book": list(
self.get_charts_for_period(
user, "book", year=current_year, limit=20
)
),
"food": list(
self.get_charts_for_period(
user, "food", year=current_year, limit=20
)
),
"podcast": list(
self.get_charts_for_period(
user, "podcast", year=current_year, limit=20
)
),
"trail": list(
self.get_charts_for_period(
user, "trail", year=current_year, limit=20
)
),
}
else:
parts = date_param.split("-")
year = int(parts[0])
week = None
month = None
day = None
if len(parts) >= 2 and parts[1].startswith("W"):
week = int(parts[1].lstrip("W"))
elif len(parts) >= 2 and parts[1]:
try:
month = int(parts[1])
except ValueError:
pass
if len(parts) >= 3:
if parts[2].startswith("W"):
week = int(parts[2].lstrip("W"))
elif not parts[2].startswith("W"):
day = int(parts[2])
context["period"] = "historical"
context["year"] = year
context["month"] = month
context["month_name"] = calendar.month_name[month] if month else None
context["week"] = week
context["day"] = day
period_str = str(year)
if month:
period_str = f"{calendar.month_name[month]} {period_str}"
if week:
period_str = f"Week {week}, {period_str}"
if day:
period_str = f"{calendar.month_name[month]} {day}, {year}"
context["period_str"] = period_str
context["charts"] = {
"artist": list(
self.get_charts_for_period(
user,
"artist",
year=year,
month=month,
week=week,
day=day,
)
),
"album": list(
self.get_charts_for_period(
user,
"album",
year=year,
month=month,
week=week,
day=day,
)
),
"track": list(
self.get_charts_for_period(
user,
"track",
year=year,
month=month,
week=week,
day=day,
)
),
"tv_series": list(
self.get_charts_for_period(
user,
"tv_series",
year=year,
month=month,
week=week,
day=day,
)
),
"video": list(
self.get_charts_for_period(
user,
"video",
year=year,
month=month,
week=week,
day=day,
)
),
"board_game": list(
self.get_charts_for_period(
user,
"board_game",
year=year,
month=month,
week=week,
day=day,
)
),
"book": list(
self.get_charts_for_period(
user,
"book",
year=year,
month=month,
week=week,
day=day,
)
),
"food": list(
self.get_charts_for_period(
user,
"food",
year=year,
month=month,
week=week,
day=day,
)
),
"podcast": list(
self.get_charts_for_period(
user,
"podcast",
year=year,
month=month,
week=week,
day=day,
)
),
"trail": list(
self.get_charts_for_period(
user,
"trail",
year=year,
month=month,
week=week,
day=day,
)
),
}
context["charts"] = {
"artist": list(
self.get_charts_for_period(
user, "artist", year=year, month=month, week=week, day=day, limit=20
)
),
"album": list(
self.get_charts_for_period(
user, "album", year=year, month=month, week=week, day=day, limit=20
)
),
"track": list(
self.get_charts_for_period(
user, "track", year=year, month=month, week=week, day=day, limit=20
)
),
"tv_series": list(
self.get_charts_for_period(
user, "tv_series", year=year, month=month, week=week, day=day, limit=20
)
),
"video": list(
self.get_charts_for_period(
user, "video", year=year, month=month, week=week, day=day, limit=20
)
),
"board_game": list(
self.get_charts_for_period(
user, "board_game", year=year, month=month, week=week, day=day, limit=20
)
),
"book": list(
self.get_charts_for_period(
user, "book", year=year, month=month, week=week, day=day, limit=20
)
),
"food": list(
self.get_charts_for_period(
user, "food", year=year, month=month, week=week, day=day, limit=20
)
),
"podcast": list(
self.get_charts_for_period(
user, "podcast", year=year, month=month, week=week, day=day, limit=20
)
),
"trail": list(
self.get_charts_for_period(
user, "trail", year=year, month=month, week=week, day=day, limit=20
)
),
}
bird_data = self.get_bird_chart_data(
user,

View File

@ -42,6 +42,7 @@ class UserProfileForm(forms.ModelForm):
"home_scrobble_limit",
"live_now_playing",
"weigh_in_units",
"trends_disabled",
]
widgets = {
"lastfm_password": forms.PasswordInput(render_value=True),

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.29 on 2026-06-25 20:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("profiles", "0039_userprofile_live_now_playing"),
]
operations = [
migrations.AddField(
model_name="userprofile",
name="disabled_trends",
field=models.JSONField(
blank=True,
default=list,
help_text="List of trend slugs the user has disabled",
),
),
migrations.AddField(
model_name="userprofile",
name="trends_disabled",
field=models.BooleanField(default=False),
),
]

View File

@ -106,6 +106,14 @@ class UserProfile(TimeStampedModel):
default=WeighUnit.METRIC,
)
trends_disabled = models.BooleanField(default=False)
disabled_trends = models.JSONField(
default=list,
blank=True,
help_text="List of trend slugs the user has disabled",
)
def __str__(self):
return f"User profile for {self.user}"

View File

@ -38,15 +38,36 @@ class Command(BaseCommand):
overall_start = timezone.now()
ok_count = 0
fail_count = 0
skipped_count = 0
for user in users:
total_trends = len(TREND_REGISTRY)
self.stdout.write(f" {user} ({user.id}): {total_trends} trends...")
try:
profile = user.profile
if profile.trends_disabled:
self.stdout.write(
self.style.WARNING(
f" {user} ({user.id}): trends disabled globally, skipping"
)
)
skipped_count += len(TREND_REGISTRY)
continue
disabled_trends = set(profile.disabled_trends or [])
except Exception:
disabled_trends = set()
active_slugs = [
s for s in TREND_REGISTRY if s not in disabled_trends
]
total_trends = len(active_slugs)
self.stdout.write(
f" {user} ({user.id}): {total_trends} trends ("
f"{len(disabled_trends & set(TREND_REGISTRY.keys()))} disabled)..."
)
user_start = timezone.now()
user_ok = 0
user_fail = 0
for idx, (slug, _) in enumerate(TREND_REGISTRY.items(), start=1):
for idx, slug in enumerate(active_slugs, start=1):
periods = get_supported_periods(slug)
self.stdout.write(f" [{idx}/{total_trends}] {slug}...\n")
for period in periods:
@ -76,7 +97,7 @@ class Command(BaseCommand):
overall_elapsed = (timezone.now() - overall_start).total_seconds()
self.stdout.write(
self.style.SUCCESS(
f"Done! {ok_count} OK, {fail_count} failed "
f"Done! {ok_count} OK, {fail_count} failed, {skipped_count} skipped "
f"({overall_elapsed:.1f}s across {total_users} user(s))"
)
)

View File

@ -26,7 +26,17 @@ def compute_user_trends(user_id):
logger.warning("User %s not found, skipping trends", user_id)
return
total = len(TREND_REGISTRY)
try:
profile = user.profile
if profile.trends_disabled:
logger.info("User %s (%d) has trends disabled, skipping", user, user_id)
return
disabled = set(profile.disabled_trends or [])
except Exception:
disabled = set()
active_slugs = [s for s in TREND_REGISTRY if s not in disabled]
total = len(active_slugs)
logger.info(
"Computing %d trends for user %s (%d)",
total,
@ -34,7 +44,7 @@ def compute_user_trends(user_id):
user_id,
)
for idx, (slug, _) in enumerate(TREND_REGISTRY.items(), start=1):
for idx, slug in enumerate(active_slugs, start=1):
compute_single_trend.delay(user_id, slug)
logger.info("Dispatched all %d trends for user %s (%d)", total, user, user_id)
@ -52,6 +62,21 @@ def compute_single_trend(user_id, slug):
logger.warning("Unknown trend slug '%s' for user %d", slug, user_id)
return
try:
profile = user.profile
if profile.trends_disabled:
logger.info(
"User %d has trends disabled, skipping '%s'", user_id, slug
)
return
if slug in (profile.disabled_trends or []):
logger.info(
"User %d has trend '%s' disabled, skipping", user_id, slug
)
return
except Exception:
pass
periods = get_supported_periods(slug)
for period in periods:

View File

@ -3,6 +3,10 @@
{% block title %}{{ trend.title }}{% endblock %}
{% block lists %}
{% if trend_not_found %}
<div class="alert alert-warning">Trend not found.</div>
{% else %}
<div class="row mb-3">
<div class="col-12">
<a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary mb-2">&larr; All Trends</a>
@ -35,13 +39,21 @@
{% if computed_at %}
<small class="text-muted">Last computed: {{ computed_at|date:"F j, Y H:i" }}</small>
{% endif %}
<div class="mt-2">
<form method="post" action="{% url 'trends:trend-toggle-disabled' trend.slug %}" class="d-inline">
{% csrf_token %}
{% if trend_disabled %}
<button type="submit" class="btn btn-sm btn-outline-success">Enable this trend</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-danger">Disable this trend</button>
{% endif %}
</form>
</div>
</div>
</div>
{% if trend_not_found %}
<div class="alert alert-warning">Trend not found.</div>
{% elif data is None %}
{% if data is None %}
<div class="alert alert-info">
No data computed yet for this period. Trends are updated once daily, check back later.
</div>
@ -85,5 +97,6 @@
{% elif trend.slug == "mood-weather" %}
{% include "trends/_mood_weather.html" %}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -25,11 +25,23 @@
</a>
</h5>
<p class="card-text text-muted">{{ trend.description }}</p>
{% if trend.computed_at %}
<small class="text-muted">Last computed: {{ trend.computed_at|date:"M j, Y H:i" }}</small>
{% else %}
<span class="badge bg-warning text-dark">Pending</span>
{% endif %}
<div class="d-flex justify-content-between align-items-center">
<div>
{% if trend.computed_at %}
<small class="text-muted">Last computed: {{ trend.computed_at|date:"M j, Y H:i" }}</small>
{% else %}
<span class="badge bg-warning text-dark">Pending</span>
{% endif %}
</div>
<form method="post" action="{% url 'trends:trend-toggle-disabled' trend.slug %}" class="m-0">
{% csrf_token %}
{% if trend.disabled %}
<button type="submit" class="btn btn-sm btn-outline-success">Enable</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-danger">Disable</button>
{% endif %}
</form>
</div>
</div>
</div>
</div>

View File

@ -1,9 +1,18 @@
from django.urls import path
from trends.views import TrendDetailView, TrendListView
from trends.views import (
ToggleTrendDisabledView,
TrendDetailView,
TrendListView,
)
app_name = "trends"
urlpatterns = [
path("trends/", TrendListView.as_view(), name="trends-home"),
path("trends/<slug:trend_slug>/", TrendDetailView.as_view(), name="trend-detail"),
path(
"trends/<slug:trend_slug>/toggle/",
ToggleTrendDisabledView.as_view(),
name="trend-toggle-disabled",
),
]

View File

@ -3,6 +3,7 @@ from datetime import timedelta
from django.utils import timezone
from trends.models import PERIOD_CHOICES, TrendResult
from trends.trends import TREND_REGISTRY
logger = logging.getLogger(__name__)
@ -35,6 +36,13 @@ TREND_PERIOD_OVERRIDES = {
}
def get_disabled_trends(user):
profile = user.profile
if profile.trends_disabled:
return set(TREND_REGISTRY.keys())
return set(profile.disabled_trends or [])
def get_supported_periods(trend_slug):
if trend_slug in TREND_PERIOD_OVERRIDES:
slugs = TREND_PERIOD_OVERRIDES[trend_slug]

View File

@ -1,8 +1,11 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import TemplateView, View
from trends.models import TrendResult
from trends.trends import TREND_REGISTRY
from trends.utils import get_period_nav, get_supported_periods
from trends.utils import get_disabled_trends, get_period_nav, get_supported_periods
TREND_METADATA = {
"activity-distribution": {
@ -78,6 +81,8 @@ class TrendListView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
disabled = get_disabled_trends(self.request.user)
results = TrendResult.objects.filter(
user=self.request.user,
).order_by("trend_slug", "-computed_at")
@ -89,8 +94,11 @@ class TrendListView(LoginRequiredMixin, TemplateView):
trends = []
for slug in TREND_REGISTRY:
if slug in disabled:
continue
meta = TREND_METADATA.get(slug, {})
result = latest_by_slug.get(slug)
is_disabled = slug in (self.request.user.profile.disabled_trends or [])
trends.append(
{
"slug": slug,
@ -99,6 +107,7 @@ class TrendListView(LoginRequiredMixin, TemplateView):
"icon": meta.get("icon", ""),
"computed_at": result.computed_at if result else None,
"has_data": result is not None,
"disabled": is_disabled,
}
)
ctx["trends"] = trends
@ -148,4 +157,25 @@ class TrendDetailView(LoginRequiredMixin, TemplateView):
ctx["computed_at"] = None
ctx["data"] = None
disabled = get_disabled_trends(self.request.user)
ctx["trend_disabled"] = slug in disabled
ctx["globally_disabled"] = self.request.user.profile.trends_disabled
return ctx
class ToggleTrendDisabledView(LoginRequiredMixin, View):
def post(self, request, trend_slug):
profile = request.user.profile
disabled = set(profile.disabled_trends or [])
if trend_slug in disabled:
disabled.discard(trend_slug)
messages.success(request, f"Trend re-enabled.")
else:
disabled.add(trend_slug)
messages.success(request, f"Trend disabled.")
profile.disabled_trends = list(disabled)
profile.save(update_fields=["disabled_trends"])
return HttpResponseRedirect(
request.META.get("HTTP_REFERER", reverse("trends:trends-home"))
)

View File

@ -122,7 +122,61 @@
{% include "scrobbles/_top_charts.html" %}
<div class="row">
<div class="row mt-4">
{% if charts.artist %}
<div class="col-md-6 col-lg-4 chart-section">
<h3>🎤 Top Artists</h3>
<ul class="list-group">
{% for chart in charts.artist|slice:":20" %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-2"><strong>#{{chart.rank}}</strong></span>
<a href="{{chart.artist.get_absolute_url}}">{{chart.artist.name}}</a>
<span class="badge bg-primary rounded-pill">{{chart.count}}</span>
</li>
{% endfor %}
<li class="list-group-item">
<a href="{% url 'charts:chart-detail' 'artist' %}">View all &raquo;</a>
</li>
</ul>
</div>
{% endif %}
{% if charts.album %}
<div class="col-md-6 col-lg-4 chart-section">
<h3>💿 Top Albums</h3>
<ul class="list-group">
{% for chart in charts.album|slice:":20" %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-2"><strong>#{{chart.rank}}</strong></span>
<a href="{{chart.album.get_absolute_url}}">{{chart.album.name}}</a>
<span class="badge bg-primary rounded-pill">{{chart.count}}</span>
</li>
{% endfor %}
<li class="list-group-item">
<a href="{% url 'charts:chart-detail' 'album' %}">View all &raquo;</a>
</li>
</ul>
</div>
{% endif %}
{% if charts.tv_series %}
<div class="col-md-6 col-lg-4 chart-section">
<h3>📺 Top TV Series</h3>
<ul class="list-group">
{% for chart in charts.tv_series|slice:":20" %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-2"><strong>#{{chart.rank}}</strong></span>
<a href="{{chart.tv_series.get_absolute_url}}">{{chart.tv_series.name}}</a>
<span class="badge bg-primary rounded-pill">{{chart.count}}</span>
</li>
{% endfor %}
<li class="list-group-item">
<a href="{% url 'charts:chart-detail' 'tv_series' %}">View all &raquo;</a>
</li>
</ul>
</div>
{% endif %}
{% if charts.track %}
<div class="col-md-6 col-lg-4 chart-section">
<h3>🎵 Top Tracks</h3>

View File

@ -130,6 +130,11 @@
</ul>
</div>
{% endif %}
{% elif field.name == "trends_disabled" %}
<p class="checkbox-row">
{{ field }}
{{ field.label_tag }}
</p>
{% elif field.name == "home_scrobble_limit" %}
<p>
{{ field.label_tag }}

View File

@ -49,11 +49,12 @@
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
{% for i in "67891011121314" %}
{% with artists|get_item:forloop.counter|add:5 as artist %}
{% for i in "123456789" %}
{% with forloop.counter|add:4 as idx %}
{% with artists|get_item:idx as artist %}
{% if artist %}
<div class="image-wrapper" style="width:33%">
<div class="caption-small">#{{forloop.counter|add:6}} {{artist.artist.name}}</div>
<div class="caption-small">#{{forloop.counter|add:5}} {{artist.artist.name}}</div>
{% if artist.artist.thumbnail %}
<a href="{{artist.artist.get_absolute_url}}"><img src="{{artist.artist.thumbnail_medium.url}}" width="100px"></a>
{% else %}
@ -62,6 +63,7 @@
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
</div>
</div>
@ -123,11 +125,12 @@
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
{% for i in "67891011121314" %}
{% with albums|get_item:forloop.counter|add:5 as album %}
{% for i in "123456789" %}
{% with forloop.counter|add:4 as idx %}
{% with albums|get_item:idx as album %}
{% if album %}
<div class="image-wrapper" style="width:33%">
<div class="caption-small">#{{forloop.counter|add:6}} {{album.album.title}}</div>
<div class="caption-small">#{{forloop.counter|add:5}} {{album.album.title}}</div>
{% if album.album.cover_image %}
<a href="{{album.album.get_absolute_url}}"><img src="{{album.album.cover_image_medium.url}}" width="100px"></a>
{% else %}
@ -136,6 +139,7 @@
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
</div>
</div>
@ -197,11 +201,12 @@
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
{% for i in "67891011121314" %}
{% with shows|get_item:forloop.counter|add:5 as show %}
{% for i in "123456789" %}
{% with forloop.counter|add:4 as idx %}
{% with shows|get_item:idx as show %}
{% if show %}
<div class="image-wrapper" style="width:33%">
<div class="caption-small">#{{forloop.counter|add:6}} {{show.tv_series.name}}</div>
<div class="caption-small">#{{forloop.counter|add:5}} {{show.tv_series.name}}</div>
{% if show.tv_series.cover_image %}
<a href="{{show.tv_series.get_absolute_url}}"><img src="{{show.tv_series.cover_small.url}}" width="100px" height="100px" style="object-fit: cover"></a>
{% else %}
@ -210,6 +215,7 @@
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
</div>
</div>

View File

@ -76,9 +76,11 @@
<div class="btn-group me-2">
<a href="{% url 'scrobbles:scrobble-list' %}" class="btn btn-sm btn-outline-secondary">Live</a>
</div>
{% if not user.profile.trends_disabled %}
<div class="btn-group me-2">
<a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary">Trends</a>
</div>
{% endif %}
<div class="btn-group me-2">
{% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}
<form action="{% url 'scrobbles:lastfm-import' %}" method="get">