Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec4c190e6c | |||
| 58126928c7 | |||
| c0d2881585 | |||
| 41a68291a4 | |||
| 0a411bedf4 |
18
PROJECT.org
18
PROJECT.org
@ -88,7 +88,7 @@ fetching and simple saving.
|
|||||||
*** Metadata sources
|
*** Metadata sources
|
||||||
**** Scraper
|
**** 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] 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:
|
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
||||||
:PROPERTIES:
|
: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:
|
** 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]
|
* Version 58.3 [1/1]
|
||||||
** DONE [#A] Remove curl-cffi as it doesn't work on FreeBSD :webpages:deps:
|
** DONE [#A] Remove curl-cffi as it doesn't work on FreeBSD :webpages:deps:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "58.3"
|
version = "58.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -120,7 +120,12 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
|
|||||||
def player_log(self) -> str:
|
def player_log(self) -> str:
|
||||||
if self.players:
|
if self.players:
|
||||||
return ", ".join(
|
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 ""
|
return ""
|
||||||
|
|
||||||
@ -136,7 +141,9 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
|
|||||||
if self.players:
|
if self.players:
|
||||||
players_html = []
|
players_html = []
|
||||||
for player_data in self.players:
|
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
|
player_info = player.name
|
||||||
if player.score:
|
if player.score:
|
||||||
player_info += f" ({player.score})"
|
player_info += f" ({player.score})"
|
||||||
|
|||||||
@ -114,6 +114,51 @@ class ChartRecordView(TemplateView):
|
|||||||
context["current_week"] = current_week
|
context["current_week"] = current_week
|
||||||
context["current_day"] = current_day
|
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"] = {
|
context["chart_keys"] = {
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"week": "This Week",
|
"week": "This Week",
|
||||||
@ -126,295 +171,120 @@ class ChartRecordView(TemplateView):
|
|||||||
"artist": {
|
"artist": {
|
||||||
"today": list(
|
"today": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "artist", year=year, month=month, day=day,
|
||||||
"artist",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
day=current_day,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"week": list(
|
"week": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "artist", year=year, week=week,
|
||||||
"artist",
|
|
||||||
year=current_year,
|
|
||||||
week=current_week,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"month": list(
|
"month": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "artist", year=year, month=month,
|
||||||
"artist",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"year": list(
|
"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")),
|
"all": list(self.get_charts_for_period(user, "artist")),
|
||||||
},
|
},
|
||||||
"album": {
|
"album": {
|
||||||
"today": list(
|
"today": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "album", year=year, month=month, day=day,
|
||||||
"album",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
day=current_day,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"week": list(
|
"week": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user, "album", year=current_year, week=current_week
|
user, "album", year=year, week=week,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"month": list(
|
"month": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "album", year=year, month=month,
|
||||||
"album",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"year": list(
|
"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")),
|
"all": list(self.get_charts_for_period(user, "album")),
|
||||||
},
|
},
|
||||||
"tv_series": {
|
"tv_series": {
|
||||||
"today": list(
|
"today": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "tv_series", year=year, month=month, day=day,
|
||||||
"tv_series",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
day=current_day,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"week": list(
|
"week": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "tv_series", year=year, week=week,
|
||||||
"tv_series",
|
|
||||||
year=current_year,
|
|
||||||
week=current_week,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"month": list(
|
"month": list(
|
||||||
self.get_charts_for_period(
|
self.get_charts_for_period(
|
||||||
user,
|
user, "tv_series", year=year, month=month,
|
||||||
"tv_series",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"year": list(
|
"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")),
|
"all": list(self.get_charts_for_period(user, "tv_series")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if not date_param:
|
context["charts"] = {
|
||||||
context["period"] = "current"
|
"artist": list(
|
||||||
context["year"] = current_year
|
self.get_charts_for_period(
|
||||||
context["month"] = current_month
|
user, "artist", year=year, month=month, week=week, day=day, limit=20
|
||||||
context["month_name"] = calendar.month_name[current_month]
|
)
|
||||||
context["week"] = current_week
|
),
|
||||||
context["day"] = current_day
|
"album": list(
|
||||||
|
self.get_charts_for_period(
|
||||||
context["charts"] = {
|
user, "album", year=year, month=month, week=week, day=day, limit=20
|
||||||
"artist": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "artist", year=current_year, limit=20
|
"track": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "track", year=year, month=month, week=week, day=day, limit=20
|
||||||
"album": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "album", year=current_year, limit=20
|
"tv_series": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "tv_series", year=year, month=month, week=week, day=day, limit=20
|
||||||
"track": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "track", year=current_year, limit=20
|
"video": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "video", year=year, month=month, week=week, day=day, limit=20
|
||||||
"tv_series": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "tv_series", year=current_year, limit=20
|
"board_game": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "board_game", year=year, month=month, week=week, day=day, limit=20
|
||||||
"video": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "video", year=current_year, limit=20
|
"book": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "book", year=year, month=month, week=week, day=day, limit=20
|
||||||
"board_game": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "board_game", year=current_year, limit=20
|
"food": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "food", year=year, month=month, week=week, day=day, limit=20
|
||||||
"book": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "book", year=current_year, limit=20
|
"podcast": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "podcast", year=year, month=month, week=week, day=day, limit=20
|
||||||
"food": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user, "food", year=current_year, limit=20
|
"trail": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "trail", year=year, month=month, week=week, day=day, 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,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
bird_data = self.get_bird_chart_data(
|
bird_data = self.get_bird_chart_data(
|
||||||
user,
|
user,
|
||||||
|
|||||||
@ -42,6 +42,7 @@ class UserProfileForm(forms.ModelForm):
|
|||||||
"home_scrobble_limit",
|
"home_scrobble_limit",
|
||||||
"live_now_playing",
|
"live_now_playing",
|
||||||
"weigh_in_units",
|
"weigh_in_units",
|
||||||
|
"trends_disabled",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"lastfm_password": forms.PasswordInput(render_value=True),
|
"lastfm_password": forms.PasswordInput(render_value=True),
|
||||||
|
|||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -106,6 +106,14 @@ class UserProfile(TimeStampedModel):
|
|||||||
default=WeighUnit.METRIC,
|
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):
|
def __str__(self):
|
||||||
return f"User profile for {self.user}"
|
return f"User profile for {self.user}"
|
||||||
|
|
||||||
|
|||||||
@ -38,15 +38,36 @@ class Command(BaseCommand):
|
|||||||
overall_start = timezone.now()
|
overall_start = timezone.now()
|
||||||
ok_count = 0
|
ok_count = 0
|
||||||
fail_count = 0
|
fail_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
total_trends = len(TREND_REGISTRY)
|
try:
|
||||||
self.stdout.write(f" {user} ({user.id}): {total_trends} trends...")
|
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_start = timezone.now()
|
||||||
user_ok = 0
|
user_ok = 0
|
||||||
user_fail = 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)
|
periods = get_supported_periods(slug)
|
||||||
self.stdout.write(f" [{idx}/{total_trends}] {slug}...\n")
|
self.stdout.write(f" [{idx}/{total_trends}] {slug}...\n")
|
||||||
for period in periods:
|
for period in periods:
|
||||||
@ -76,7 +97,7 @@ class Command(BaseCommand):
|
|||||||
overall_elapsed = (timezone.now() - overall_start).total_seconds()
|
overall_elapsed = (timezone.now() - overall_start).total_seconds()
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS(
|
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))"
|
f"({overall_elapsed:.1f}s across {total_users} user(s))"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -26,7 +26,17 @@ def compute_user_trends(user_id):
|
|||||||
logger.warning("User %s not found, skipping trends", user_id)
|
logger.warning("User %s not found, skipping trends", user_id)
|
||||||
return
|
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(
|
logger.info(
|
||||||
"Computing %d trends for user %s (%d)",
|
"Computing %d trends for user %s (%d)",
|
||||||
total,
|
total,
|
||||||
@ -34,7 +44,7 @@ def compute_user_trends(user_id):
|
|||||||
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)
|
compute_single_trend.delay(user_id, slug)
|
||||||
|
|
||||||
logger.info("Dispatched all %d trends for user %s (%d)", total, user, user_id)
|
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)
|
logger.warning("Unknown trend slug '%s' for user %d", slug, user_id)
|
||||||
return
|
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)
|
periods = get_supported_periods(slug)
|
||||||
|
|
||||||
for period in periods:
|
for period in periods:
|
||||||
|
|||||||
@ -3,6 +3,10 @@
|
|||||||
{% block title %}{{ trend.title }}{% endblock %}
|
{% block title %}{{ trend.title }}{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
|
{% if trend_not_found %}
|
||||||
|
<div class="alert alert-warning">Trend not found.</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary mb-2">← All Trends</a>
|
<a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary mb-2">← All Trends</a>
|
||||||
@ -35,13 +39,21 @@
|
|||||||
{% if computed_at %}
|
{% if computed_at %}
|
||||||
<small class="text-muted">Last computed: {{ computed_at|date:"F j, Y H:i" }}</small>
|
<small class="text-muted">Last computed: {{ computed_at|date:"F j, Y H:i" }}</small>
|
||||||
{% endif %}
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if trend_not_found %}
|
{% if data is None %}
|
||||||
<div class="alert alert-warning">Trend not found.</div>
|
|
||||||
|
|
||||||
{% elif data is None %}
|
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
No data computed yet for this period. Trends are updated once daily, check back later.
|
No data computed yet for this period. Trends are updated once daily, check back later.
|
||||||
</div>
|
</div>
|
||||||
@ -85,5 +97,6 @@
|
|||||||
{% elif trend.slug == "mood-weather" %}
|
{% elif trend.slug == "mood-weather" %}
|
||||||
{% include "trends/_mood_weather.html" %}
|
{% include "trends/_mood_weather.html" %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -25,11 +25,23 @@
|
|||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="card-text text-muted">{{ trend.description }}</p>
|
<p class="card-text text-muted">{{ trend.description }}</p>
|
||||||
{% if trend.computed_at %}
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<small class="text-muted">Last computed: {{ trend.computed_at|date:"M j, Y H:i" }}</small>
|
<div>
|
||||||
{% else %}
|
{% if trend.computed_at %}
|
||||||
<span class="badge bg-warning text-dark">Pending</span>
|
<small class="text-muted">Last computed: {{ trend.computed_at|date:"M j, Y H:i" }}</small>
|
||||||
{% endif %}
|
{% 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from trends.views import TrendDetailView, TrendListView
|
from trends.views import (
|
||||||
|
ToggleTrendDisabledView,
|
||||||
|
TrendDetailView,
|
||||||
|
TrendListView,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = "trends"
|
app_name = "trends"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("trends/", TrendListView.as_view(), name="trends-home"),
|
path("trends/", TrendListView.as_view(), name="trends-home"),
|
||||||
path("trends/<slug:trend_slug>/", TrendDetailView.as_view(), name="trend-detail"),
|
path("trends/<slug:trend_slug>/", TrendDetailView.as_view(), name="trend-detail"),
|
||||||
|
path(
|
||||||
|
"trends/<slug:trend_slug>/toggle/",
|
||||||
|
ToggleTrendDisabledView.as_view(),
|
||||||
|
name="trend-toggle-disabled",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from trends.models import PERIOD_CHOICES, TrendResult
|
from trends.models import PERIOD_CHOICES, TrendResult
|
||||||
|
from trends.trends import TREND_REGISTRY
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
def get_supported_periods(trend_slug):
|
||||||
if trend_slug in TREND_PERIOD_OVERRIDES:
|
if trend_slug in TREND_PERIOD_OVERRIDES:
|
||||||
slugs = TREND_PERIOD_OVERRIDES[trend_slug]
|
slugs = TREND_PERIOD_OVERRIDES[trend_slug]
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
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.models import TrendResult
|
||||||
from trends.trends import TREND_REGISTRY
|
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 = {
|
TREND_METADATA = {
|
||||||
"activity-distribution": {
|
"activity-distribution": {
|
||||||
@ -78,6 +81,8 @@ class TrendListView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
disabled = get_disabled_trends(self.request.user)
|
||||||
|
|
||||||
results = TrendResult.objects.filter(
|
results = TrendResult.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
).order_by("trend_slug", "-computed_at")
|
).order_by("trend_slug", "-computed_at")
|
||||||
@ -89,8 +94,11 @@ class TrendListView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
trends = []
|
trends = []
|
||||||
for slug in TREND_REGISTRY:
|
for slug in TREND_REGISTRY:
|
||||||
|
if slug in disabled:
|
||||||
|
continue
|
||||||
meta = TREND_METADATA.get(slug, {})
|
meta = TREND_METADATA.get(slug, {})
|
||||||
result = latest_by_slug.get(slug)
|
result = latest_by_slug.get(slug)
|
||||||
|
is_disabled = slug in (self.request.user.profile.disabled_trends or [])
|
||||||
trends.append(
|
trends.append(
|
||||||
{
|
{
|
||||||
"slug": slug,
|
"slug": slug,
|
||||||
@ -99,6 +107,7 @@ class TrendListView(LoginRequiredMixin, TemplateView):
|
|||||||
"icon": meta.get("icon", ""),
|
"icon": meta.get("icon", ""),
|
||||||
"computed_at": result.computed_at if result else None,
|
"computed_at": result.computed_at if result else None,
|
||||||
"has_data": result is not None,
|
"has_data": result is not None,
|
||||||
|
"disabled": is_disabled,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ctx["trends"] = trends
|
ctx["trends"] = trends
|
||||||
@ -148,4 +157,25 @@ class TrendDetailView(LoginRequiredMixin, TemplateView):
|
|||||||
ctx["computed_at"] = None
|
ctx["computed_at"] = None
|
||||||
ctx["data"] = 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
|
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"))
|
||||||
|
)
|
||||||
|
|||||||
@ -122,7 +122,61 @@
|
|||||||
|
|
||||||
{% include "scrobbles/_top_charts.html" %}
|
{% 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 »</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 »</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 »</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if charts.track %}
|
{% if charts.track %}
|
||||||
<div class="col-md-6 col-lg-4 chart-section">
|
<div class="col-md-6 col-lg-4 chart-section">
|
||||||
<h3>🎵 Top Tracks</h3>
|
<h3>🎵 Top Tracks</h3>
|
||||||
|
|||||||
@ -130,6 +130,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% elif field.name == "trends_disabled" %}
|
||||||
|
<p class="checkbox-row">
|
||||||
|
{{ field }}
|
||||||
|
{{ field.label_tag }}
|
||||||
|
</p>
|
||||||
{% elif field.name == "home_scrobble_limit" %}
|
{% elif field.name == "home_scrobble_limit" %}
|
||||||
<p>
|
<p>
|
||||||
{{ field.label_tag }}
|
{{ field.label_tag }}
|
||||||
|
|||||||
@ -49,11 +49,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="float:left; width:300px;">
|
<div style="float:left; width:300px;">
|
||||||
<div style="display:flex; flex-wrap: wrap;">
|
<div style="display:flex; flex-wrap: wrap;">
|
||||||
{% for i in "67891011121314" %}
|
{% for i in "123456789" %}
|
||||||
{% with artists|get_item:forloop.counter|add:5 as artist %}
|
{% with forloop.counter|add:4 as idx %}
|
||||||
|
{% with artists|get_item:idx as artist %}
|
||||||
{% if artist %}
|
{% if artist %}
|
||||||
<div class="image-wrapper" style="width:33%">
|
<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 %}
|
{% if artist.artist.thumbnail %}
|
||||||
<a href="{{artist.artist.get_absolute_url}}"><img src="{{artist.artist.thumbnail_medium.url}}" width="100px"></a>
|
<a href="{{artist.artist.get_absolute_url}}"><img src="{{artist.artist.thumbnail_medium.url}}" width="100px"></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -62,6 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -123,11 +125,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="float:left; width:300px;">
|
<div style="float:left; width:300px;">
|
||||||
<div style="display:flex; flex-wrap: wrap;">
|
<div style="display:flex; flex-wrap: wrap;">
|
||||||
{% for i in "67891011121314" %}
|
{% for i in "123456789" %}
|
||||||
{% with albums|get_item:forloop.counter|add:5 as album %}
|
{% with forloop.counter|add:4 as idx %}
|
||||||
|
{% with albums|get_item:idx as album %}
|
||||||
{% if album %}
|
{% if album %}
|
||||||
<div class="image-wrapper" style="width:33%">
|
<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 %}
|
{% if album.album.cover_image %}
|
||||||
<a href="{{album.album.get_absolute_url}}"><img src="{{album.album.cover_image_medium.url}}" width="100px"></a>
|
<a href="{{album.album.get_absolute_url}}"><img src="{{album.album.cover_image_medium.url}}" width="100px"></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -136,6 +139,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -197,11 +201,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="float:left; width:300px;">
|
<div style="float:left; width:300px;">
|
||||||
<div style="display:flex; flex-wrap: wrap;">
|
<div style="display:flex; flex-wrap: wrap;">
|
||||||
{% for i in "67891011121314" %}
|
{% for i in "123456789" %}
|
||||||
{% with shows|get_item:forloop.counter|add:5 as show %}
|
{% with forloop.counter|add:4 as idx %}
|
||||||
|
{% with shows|get_item:idx as show %}
|
||||||
{% if show %}
|
{% if show %}
|
||||||
<div class="image-wrapper" style="width:33%">
|
<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 %}
|
{% 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>
|
<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 %}
|
{% else %}
|
||||||
@ -210,6 +215,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -76,9 +76,11 @@
|
|||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<a href="{% url 'scrobbles:scrobble-list' %}" class="btn btn-sm btn-outline-secondary">Live</a>
|
<a href="{% url 'scrobbles:scrobble-list' %}" class="btn btn-sm btn-outline-secondary">Live</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% if not user.profile.trends_disabled %}
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary">Trends</a>
|
<a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary">Trends</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
{% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}
|
{% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}
|
||||||
<form action="{% url 'scrobbles:lastfm-import' %}" method="get">
|
<form action="{% url 'scrobbles:lastfm-import' %}" method="get">
|
||||||
|
|||||||
Reference in New Issue
Block a user