Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1695f7393e | |||
| 4468e68110 | |||
| da08eca4ab | |||
| 08752e30a4 | |||
| 619718c045 | |||
| cb23d5a5be | |||
| ec4c190e6c | |||
| 58126928c7 |
@ -1,68 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["**"]
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
VROBBLER_DATABASE_URL: sqlite:///test.db
|
|
||||||
VROBBLER_USDA_API_KEY: ${{ vars.VROBBLER_USDA_API_KEY }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
|
|
||||||
- name: Cache pip/poetry
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/pip
|
|
||||||
~/.cache/pypoetry
|
|
||||||
key: ${{ runner.os }}-py311-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-py311-
|
|
||||||
|
|
||||||
- name: Install Poetry
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install poetry
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
run: |
|
|
||||||
cp vrobbler.conf.test vrobbler.conf
|
|
||||||
poetry install --with test
|
|
||||||
|
|
||||||
- name: Pytest with coverage
|
|
||||||
run: |
|
|
||||||
poetry run pytest -n 5 --cov-report term:skip-covered --cov=vrobbler tests
|
|
||||||
|
|
||||||
- name: Notify success (ntfy)
|
|
||||||
if: success()
|
|
||||||
run: |
|
|
||||||
curl -fsS \
|
|
||||||
-H "Title: vrobbler CI success" \
|
|
||||||
-H "Priority: low" \
|
|
||||||
-H "Tags: success,vrobbler" \
|
|
||||||
-H "Actions: view, Changes, ${{ gitea.server_url }}/${{ gitea.repository }}/commit/${{ gitea.sha }}; view, Build, ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
|
|
||||||
-d "✅ Build succeeded: ${{ gitea.repository }} @ ${{ gitea.sha }}" \
|
|
||||||
https://ntfy.unbl.ink/drone
|
|
||||||
|
|
||||||
- name: Notify failure (ntfy)
|
|
||||||
if: failure()
|
|
||||||
run: |
|
|
||||||
curl -fsS \
|
|
||||||
-H "Title: vrobbler CI failure" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-H "Tags: failure,vrobbler" \
|
|
||||||
-H "Actions: view, Changes, ${{ gitea.server_url }}/${{ gitea.repository }}/commit/${{ gitea.sha }}; view, Build, ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" \
|
|
||||||
-d "❌ Build failed: ${{ gitea.repository }} @ ${{ gitea.sha }}" \
|
|
||||||
https://ntfy.unbl.ink/drone
|
|
||||||
@ -1,8 +1,14 @@
|
|||||||
name: deploy
|
name: ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: ["**"]
|
||||||
tags: ["*"]
|
tags: ["*"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ gitea.workflow }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@ -68,6 +74,7 @@ jobs:
|
|||||||
|
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
needs: [test]
|
needs: [test]
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
22
PROJECT.org
22
PROJECT.org
@ -605,6 +605,28 @@ 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.7 [2/2]
|
||||||
|
** DONE [#B] Split up chart page between tables and maloja :charts:templates:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 103ab084-2016-cfa4-c677-3c5fdc54cce0
|
||||||
|
:END:
|
||||||
|
** DONE [#A] Fix CI so we don't double run deploys and builds :ci:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 1a93e7cb-b883-aae5-2bd5-fcdd6e16f8ab
|
||||||
|
:END:
|
||||||
|
|
||||||
|
* Version 58.6 [1/1]
|
||||||
|
** DONE [#B] Cleanup commands should check for broken images :metadata:cleanup:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: bacce321-73c7-ae1f-bfa7-c3ee517b5441
|
||||||
|
:END:
|
||||||
|
|
||||||
|
* 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]
|
* Version 58.4 [2/2]
|
||||||
** DONE [#B] Allow people all trends or individual trends :trends:profiles:
|
** DONE [#B] Allow people all trends or individual trends :trends:profiles:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "58.4"
|
version = "58.7"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,17 @@ MISSING_ALL = [
|
|||||||
"publish_year",
|
"publish_year",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _cover_missing_or_broken(book) -> bool:
|
||||||
|
if not bool(book.cover):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
return not book.cover.storage.exists(book.cover.name)
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
MISSING_GROUPS = {
|
MISSING_GROUPS = {
|
||||||
"cover": lambda b: not bool(b.cover),
|
"cover": _cover_missing_or_broken,
|
||||||
"summary": lambda b: not b.summary,
|
"summary": lambda b: not b.summary,
|
||||||
"isbn": lambda b: not b.isbn_13 and not b.isbn_10,
|
"isbn": lambda b: not b.isbn_13 and not b.isbn_10,
|
||||||
"pages": lambda b: b.pages is None,
|
"pages": lambda b: b.pages is None,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from charts.views import (
|
|||||||
BirdsChartView,
|
BirdsChartView,
|
||||||
ChartDetailView,
|
ChartDetailView,
|
||||||
ChartRecordView,
|
ChartRecordView,
|
||||||
|
MalojaChartsView,
|
||||||
SpotifyTracksView,
|
SpotifyTracksView,
|
||||||
)
|
)
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
@ -11,6 +12,7 @@ app_name = "charts"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("charts/", ChartRecordView.as_view(), name="charts-home"),
|
path("charts/", ChartRecordView.as_view(), name="charts-home"),
|
||||||
|
path("charts/maloja/", MalojaChartsView.as_view(), name="maloja-charts"),
|
||||||
path("charts/spotify/", SpotifyTracksView.as_view(), name="spotify-tracks"),
|
path("charts/spotify/", SpotifyTracksView.as_view(), name="spotify-tracks"),
|
||||||
path("charts/bandcamp/", BandcampTracksView.as_view(), name="bandcamp-tracks"),
|
path("charts/bandcamp/", BandcampTracksView.as_view(), name="bandcamp-tracks"),
|
||||||
path("charts/birds/", BirdsChartView.as_view(), name="birds-chart"),
|
path("charts/birds/", BirdsChartView.as_view(), name="birds-chart"),
|
||||||
|
|||||||
@ -114,175 +114,13 @@ class ChartRecordView(TemplateView):
|
|||||||
context["current_week"] = current_week
|
context["current_week"] = current_week
|
||||||
context["current_day"] = current_day
|
context["current_day"] = current_day
|
||||||
|
|
||||||
context["chart_keys"] = {
|
# Resolve date parameters
|
||||||
"today": "Today",
|
if date_param:
|
||||||
"week": "This Week",
|
|
||||||
"month": "This Month",
|
|
||||||
"year": "This Year",
|
|
||||||
"all": "All Time",
|
|
||||||
}
|
|
||||||
|
|
||||||
context["maloja_charts"] = {
|
|
||||||
"artist": {
|
|
||||||
"today": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user,
|
|
||||||
"artist",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
day=current_day,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"week": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user,
|
|
||||||
"artist",
|
|
||||||
year=current_year,
|
|
||||||
week=current_week,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"month": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user,
|
|
||||||
"artist",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"year": list(
|
|
||||||
self.get_charts_for_period(user, "artist", year=current_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,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"week": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user, "album", year=current_year, week=current_week
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"month": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user,
|
|
||||||
"album",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"year": list(
|
|
||||||
self.get_charts_for_period(user, "album", year=current_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,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"week": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user,
|
|
||||||
"tv_series",
|
|
||||||
year=current_year,
|
|
||||||
week=current_week,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"month": list(
|
|
||||||
self.get_charts_for_period(
|
|
||||||
user,
|
|
||||||
"tv_series",
|
|
||||||
year=current_year,
|
|
||||||
month=current_month,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"year": list(
|
|
||||||
self.get_charts_for_period(user, "tv_series", year=current_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("-")
|
parts = date_param.split("-")
|
||||||
year = int(parts[0])
|
year = int(parts[0])
|
||||||
|
|
||||||
week = None
|
week = None
|
||||||
month = None
|
month = None
|
||||||
day = None
|
day = None
|
||||||
|
|
||||||
if len(parts) >= 2 and parts[1].startswith("W"):
|
if len(parts) >= 2 and parts[1].startswith("W"):
|
||||||
week = int(parts[1].lstrip("W"))
|
week = int(parts[1].lstrip("W"))
|
||||||
elif len(parts) >= 2 and parts[1]:
|
elif len(parts) >= 2 and parts[1]:
|
||||||
@ -290,20 +128,17 @@ class ChartRecordView(TemplateView):
|
|||||||
month = int(parts[1])
|
month = int(parts[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if len(parts) >= 3:
|
if len(parts) >= 3:
|
||||||
if parts[2].startswith("W"):
|
if parts[2].startswith("W"):
|
||||||
week = int(parts[2].lstrip("W"))
|
week = int(parts[2].lstrip("W"))
|
||||||
elif not parts[2].startswith("W"):
|
elif not parts[2].startswith("W"):
|
||||||
day = int(parts[2])
|
day = int(parts[2])
|
||||||
|
|
||||||
context["period"] = "historical"
|
context["period"] = "historical"
|
||||||
context["year"] = year
|
context["year"] = year
|
||||||
context["month"] = month
|
context["month"] = month
|
||||||
context["month_name"] = calendar.month_name[month] if month else None
|
context["month_name"] = calendar.month_name[month] if month else None
|
||||||
context["week"] = week
|
context["week"] = week
|
||||||
context["day"] = day
|
context["day"] = day
|
||||||
|
|
||||||
period_str = str(year)
|
period_str = str(year)
|
||||||
if month:
|
if month:
|
||||||
period_str = f"{calendar.month_name[month]} {period_str}"
|
period_str = f"{calendar.month_name[month]} {period_str}"
|
||||||
@ -312,109 +147,82 @@ class ChartRecordView(TemplateView):
|
|||||||
if day:
|
if day:
|
||||||
period_str = f"{calendar.month_name[month]} {day}, {year}"
|
period_str = f"{calendar.month_name[month]} {day}, {year}"
|
||||||
context["period_str"] = period_str
|
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["charts"] = {
|
# List-group tables default to week-level when no date param (matches active tab)
|
||||||
"artist": list(
|
if not date_param:
|
||||||
self.get_charts_for_period(
|
list_year = current_year
|
||||||
user,
|
list_month = None
|
||||||
"artist",
|
list_week = current_week
|
||||||
year=year,
|
list_day = None
|
||||||
month=month,
|
else:
|
||||||
week=week,
|
list_year = year
|
||||||
day=day,
|
list_month = month
|
||||||
)
|
list_week = week
|
||||||
),
|
list_day = day
|
||||||
"album": list(
|
|
||||||
self.get_charts_for_period(
|
context["charts"] = {
|
||||||
user,
|
"artist": list(
|
||||||
"album",
|
self.get_charts_for_period(
|
||||||
year=year,
|
user, "artist", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
month=month,
|
)
|
||||||
week=week,
|
),
|
||||||
day=day,
|
"album": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "album", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
"track": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user,
|
"track": list(
|
||||||
"track",
|
self.get_charts_for_period(
|
||||||
year=year,
|
user, "track", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
month=month,
|
)
|
||||||
week=week,
|
),
|
||||||
day=day,
|
"tv_series": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "tv_series", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
"tv_series": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user,
|
"video": list(
|
||||||
"tv_series",
|
self.get_charts_for_period(
|
||||||
year=year,
|
user, "video", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
month=month,
|
)
|
||||||
week=week,
|
),
|
||||||
day=day,
|
"board_game": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "board_game", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
"video": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user,
|
"book": list(
|
||||||
"video",
|
self.get_charts_for_period(
|
||||||
year=year,
|
user, "book", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
month=month,
|
)
|
||||||
week=week,
|
),
|
||||||
day=day,
|
"food": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "food", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
"board_game": list(
|
)
|
||||||
self.get_charts_for_period(
|
),
|
||||||
user,
|
"podcast": list(
|
||||||
"board_game",
|
self.get_charts_for_period(
|
||||||
year=year,
|
user, "podcast", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
month=month,
|
)
|
||||||
week=week,
|
),
|
||||||
day=day,
|
"trail": list(
|
||||||
)
|
self.get_charts_for_period(
|
||||||
),
|
user, "trail", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
||||||
"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,
|
||||||
@ -628,6 +436,53 @@ class ChartRecordView(TemplateView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MalojaChartsView(ChartRecordView):
|
||||||
|
"""Three maloja-themed image grid widgets (artists, albums, TV series)
|
||||||
|
with Today/Week/Month/Year/All tabs. Each tab computes its own period
|
||||||
|
from the current date — no query param needed."""
|
||||||
|
|
||||||
|
template_name = "charts/maloja_charts.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(ChartRecordView, self).get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
today = now.date()
|
||||||
|
|
||||||
|
context["chart_keys"] = {
|
||||||
|
"today": "Today",
|
||||||
|
"week": "This Week",
|
||||||
|
"month": "This Month",
|
||||||
|
"year": "This Year",
|
||||||
|
"all": "All Time",
|
||||||
|
}
|
||||||
|
|
||||||
|
tab_params = {
|
||||||
|
"today": {"year": today.year, "month": today.month, "day": today.day},
|
||||||
|
"week": {"year": today.year, "week": today.isocalendar()[1]},
|
||||||
|
"month": {"year": today.year, "month": today.month},
|
||||||
|
"year": {"year": today.year},
|
||||||
|
}
|
||||||
|
|
||||||
|
maloja_charts = {}
|
||||||
|
for media_type in ("artist", "album", "tv_series"):
|
||||||
|
tabs = {}
|
||||||
|
for key in ("today", "week", "month", "year"):
|
||||||
|
tabs[key] = list(
|
||||||
|
self.get_charts_for_period(user, media_type, **tab_params[key])
|
||||||
|
)
|
||||||
|
tabs["all"] = list(
|
||||||
|
self.get_charts_for_period(user, media_type)
|
||||||
|
)
|
||||||
|
maloja_charts[media_type] = tabs
|
||||||
|
|
||||||
|
context["maloja_charts"] = maloja_charts
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
MEDIA_TYPE_LABELS = {
|
MEDIA_TYPE_LABELS = {
|
||||||
"artist": ("🎤", "Top Artists"),
|
"artist": ("🎤", "Top Artists"),
|
||||||
"album": ("💿", "Top Albums"),
|
"album": ("💿", "Top Albums"),
|
||||||
|
|||||||
@ -0,0 +1,202 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import models, transaction
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Enrich artist and album metadata (covers, thumbnails) from MusicBrainz and TheAudioDB"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Overwrite existing cover image and metadata",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Show what would be done without making changes",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--artists",
|
||||||
|
action="store_true",
|
||||||
|
help="Only process artists",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--albums",
|
||||||
|
action="store_true",
|
||||||
|
help="Only process albums",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--needs-metadata",
|
||||||
|
action="store_true",
|
||||||
|
help="Only process items missing metadata or with broken images",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _has_broken_image(self, obj, field_name: str) -> bool:
|
||||||
|
field = getattr(obj, field_name, None)
|
||||||
|
if not field or not field.name:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return not field.storage.exists(field.name)
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
from music.models import Album, Artist
|
||||||
|
|
||||||
|
force = options["force"]
|
||||||
|
dry_run = options["dry_run"]
|
||||||
|
only_artists = options["artists"]
|
||||||
|
only_albums = options["albums"]
|
||||||
|
needs_metadata = options["needs_metadata"]
|
||||||
|
|
||||||
|
if not only_artists and not only_albums:
|
||||||
|
only_artists = only_albums = True
|
||||||
|
|
||||||
|
updated_total = 0
|
||||||
|
errors_total = 0
|
||||||
|
|
||||||
|
if only_artists:
|
||||||
|
updated_total += self._process_artists(force, dry_run, needs_metadata)
|
||||||
|
errors_total = 0 # reset per section
|
||||||
|
|
||||||
|
if only_albums:
|
||||||
|
updated_total += self._process_albums(force, dry_run, needs_metadata)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"\nDone! {updated_total} items processed")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_artists(self, needs_metadata: bool):
|
||||||
|
from music.models import Artist
|
||||||
|
|
||||||
|
qs = Artist.objects.all()
|
||||||
|
if needs_metadata:
|
||||||
|
qs = qs.filter(
|
||||||
|
models.Q(theaudiodb_id__isnull=True)
|
||||||
|
| models.Q(theaudiodb_id="")
|
||||||
|
| models.Q(thumbnail__isnull=True)
|
||||||
|
| models.Q(thumbnail="")
|
||||||
|
)
|
||||||
|
|
||||||
|
broken = []
|
||||||
|
if needs_metadata:
|
||||||
|
broken_qs = Artist.objects.exclude(
|
||||||
|
models.Q(thumbnail__isnull=True) | models.Q(thumbnail=""),
|
||||||
|
)
|
||||||
|
for artist in broken_qs.iterator():
|
||||||
|
if self._has_broken_image(artist, "thumbnail"):
|
||||||
|
broken.append(artist)
|
||||||
|
|
||||||
|
return list(qs) + broken
|
||||||
|
|
||||||
|
def _get_albums(self, needs_metadata: bool):
|
||||||
|
from music.models import Album
|
||||||
|
|
||||||
|
qs = Album.objects.all()
|
||||||
|
if needs_metadata:
|
||||||
|
qs = qs.filter(
|
||||||
|
models.Q(cover_image__isnull=True)
|
||||||
|
| models.Q(cover_image="")
|
||||||
|
| models.Q(cover_image="default-image-replace-me")
|
||||||
|
)
|
||||||
|
|
||||||
|
broken = []
|
||||||
|
if needs_metadata:
|
||||||
|
broken_qs = Album.objects.exclude(
|
||||||
|
models.Q(cover_image__isnull=True) | models.Q(cover_image=""),
|
||||||
|
)
|
||||||
|
for album in broken_qs.iterator():
|
||||||
|
if self._has_broken_image(album, "cover_image"):
|
||||||
|
broken.append(album)
|
||||||
|
|
||||||
|
return list(qs) + broken
|
||||||
|
|
||||||
|
def _process_artists(self, force, dry_run, needs_metadata):
|
||||||
|
from music.models import Artist
|
||||||
|
|
||||||
|
artists = self._get_artists(needs_metadata) if needs_metadata else list(Artist.objects.all())
|
||||||
|
total = len(artists)
|
||||||
|
self.stdout.write(f"Processing {total} artists")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
for artist in artists:
|
||||||
|
has_tadb = bool(artist.theaudiodb_id)
|
||||||
|
has_thumb = bool(artist.thumbnail)
|
||||||
|
thumb_broken = self._has_broken_image(artist, "thumbnail")
|
||||||
|
status = f"theaudiodb_id={'✓' if has_tadb else '✗'}"
|
||||||
|
if thumb_broken:
|
||||||
|
status += ", thumbnail=BROKEN"
|
||||||
|
elif has_thumb:
|
||||||
|
status += ", thumbnail=✓"
|
||||||
|
else:
|
||||||
|
status += ", thumbnail=✗"
|
||||||
|
self.stdout.write(f" [DRY RUN] Would fix {artist.name} ({status})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
errors = 0
|
||||||
|
for artist in artists:
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
artist.fix_metadata(
|
||||||
|
force_update=force or self._has_broken_image(artist, "thumbnail")
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
self.stdout.write(f" [ARTIST {updated}/{total}] {artist.name}")
|
||||||
|
except Exception as e:
|
||||||
|
errors += 1
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f" Error updating artist {artist.name}: {e}")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"\nArtists done! {updated} updated, {errors} errors")
|
||||||
|
)
|
||||||
|
return updated
|
||||||
|
|
||||||
|
def _process_albums(self, force, dry_run, needs_metadata):
|
||||||
|
from music.models import Album
|
||||||
|
|
||||||
|
albums = self._get_albums(needs_metadata) if needs_metadata else list(Album.objects.all())
|
||||||
|
total = len(albums)
|
||||||
|
self.stdout.write(f"Processing {total} albums")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
for album in albums:
|
||||||
|
has_cover = bool(album.cover_image)
|
||||||
|
cover_broken = self._has_broken_image(album, "cover_image")
|
||||||
|
if cover_broken:
|
||||||
|
status = "cover=BROKEN"
|
||||||
|
elif has_cover:
|
||||||
|
status = "cover=✓"
|
||||||
|
else:
|
||||||
|
status = "cover=✗"
|
||||||
|
self.stdout.write(f" [DRY RUN] Would fix {album.name} ({status})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
errors = 0
|
||||||
|
for album in albums:
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
if self._has_broken_image(album, "cover_image") or force:
|
||||||
|
album.fetch_artwork(force=True)
|
||||||
|
else:
|
||||||
|
album.fix_metadata()
|
||||||
|
updated += 1
|
||||||
|
self.stdout.write(f" [ALBUM {updated}/{total}] {album.name}")
|
||||||
|
except Exception as e:
|
||||||
|
errors += 1
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f" Error updating album {album.name}: {e}")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f"\nAlbums done! {updated} updated, {errors} errors")
|
||||||
|
)
|
||||||
|
return updated
|
||||||
@ -17,8 +17,8 @@ MISSING_ALL = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
MISSING_GROUPS = {
|
MISSING_GROUPS = {
|
||||||
"cover": lambda g: not bool(g.cover),
|
"cover": lambda g: _image_missing_or_broken(g, "cover"),
|
||||||
"screenshot": lambda g: not bool(g.screenshot),
|
"screenshot": lambda g: _image_missing_or_broken(g, "screenshot"),
|
||||||
"summary": lambda g: not g.summary,
|
"summary": lambda g: not g.summary,
|
||||||
"rating": lambda g: g.rating is None,
|
"rating": lambda g: g.rating is None,
|
||||||
"release_date": lambda g: g.release_date is None,
|
"release_date": lambda g: g.release_date is None,
|
||||||
@ -28,6 +28,16 @@ MISSING_GROUPS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _image_missing_or_broken(game, field_name) -> bool:
|
||||||
|
field = getattr(game, field_name)
|
||||||
|
if not bool(field):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
return not field.storage.exists(field.name)
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _game_matches(game, flags):
|
def _game_matches(game, flags):
|
||||||
if not flags:
|
if not flags:
|
||||||
return False
|
return False
|
||||||
@ -103,6 +113,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
if all_missing:
|
if all_missing:
|
||||||
flags = MISSING_ALL
|
flags = MISSING_ALL
|
||||||
|
fix_broken_images = True
|
||||||
|
|
||||||
if not flags and not game_id and not force and not fix_broken_images:
|
if not flags and not game_id and not force and not fix_broken_images:
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
|
|||||||
@ -30,6 +30,19 @@ class Command(BaseCommand):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Only process channels with a twitch_id",
|
help="Only process channels with a twitch_id",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--needs-metadata",
|
||||||
|
action="store_true",
|
||||||
|
help="Only process channels missing youtube_id, twitch_id, cover image, or with broken cover image",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _has_broken_image(self, channel) -> bool:
|
||||||
|
if not channel.cover_image or not channel.cover_image.name:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return not channel.cover_image.storage.exists(channel.cover_image.name)
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
from videos.models import Channel
|
from videos.models import Channel
|
||||||
@ -38,6 +51,7 @@ class Command(BaseCommand):
|
|||||||
dry_run = options["dry_run"]
|
dry_run = options["dry_run"]
|
||||||
youtube_only = options["youtube_only"]
|
youtube_only = options["youtube_only"]
|
||||||
twitch_only = options["twitch_only"]
|
twitch_only = options["twitch_only"]
|
||||||
|
needs_metadata = options["needs_metadata"]
|
||||||
|
|
||||||
qs = Channel.objects.all()
|
qs = Channel.objects.all()
|
||||||
|
|
||||||
@ -45,29 +59,61 @@ class Command(BaseCommand):
|
|||||||
qs = qs.exclude(youtube_id__isnull=True).exclude(youtube_id="")
|
qs = qs.exclude(youtube_id__isnull=True).exclude(youtube_id="")
|
||||||
elif twitch_only:
|
elif twitch_only:
|
||||||
qs = qs.exclude(twitch_id__isnull=True).exclude(twitch_id="")
|
qs = qs.exclude(twitch_id__isnull=True).exclude(twitch_id="")
|
||||||
|
elif needs_metadata:
|
||||||
|
no_id = models.Q(youtube_id__isnull=True) & models.Q(twitch_id__isnull=True)
|
||||||
|
no_id |= models.Q(youtube_id="") & models.Q(twitch_id="")
|
||||||
|
no_id |= models.Q(youtube_id__isnull=True) & models.Q(twitch_id="")
|
||||||
|
no_id |= models.Q(youtube_id="") & models.Q(twitch_id__isnull=True)
|
||||||
|
qs = qs.filter(
|
||||||
|
no_id
|
||||||
|
| models.Q(cover_image__isnull=True)
|
||||||
|
| models.Q(cover_image="")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
models.Q(youtube_id__isnull=False) | models.Q(twitch_id__isnull=False)
|
models.Q(youtube_id__isnull=False) | models.Q(twitch_id__isnull=False)
|
||||||
).exclude(youtube_id="", twitch_id="")
|
).exclude(youtube_id="", twitch_id="")
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
self.stdout.write(f"Processing {total} channels")
|
self.stdout.write(f"Processing {total} channels from DB filter")
|
||||||
|
|
||||||
|
broken_channels = []
|
||||||
|
if needs_metadata:
|
||||||
|
broken_qs = Channel.objects.filter(
|
||||||
|
cover_image__isnull=False,
|
||||||
|
).exclude(
|
||||||
|
cover_image="",
|
||||||
|
)
|
||||||
|
if youtube_only:
|
||||||
|
broken_qs = broken_qs.exclude(youtube_id__isnull=True).exclude(youtube_id="")
|
||||||
|
elif twitch_only:
|
||||||
|
broken_qs = broken_qs.exclude(twitch_id__isnull=True).exclude(twitch_id="")
|
||||||
|
for channel in broken_qs.iterator():
|
||||||
|
if self._has_broken_image(channel):
|
||||||
|
broken_channels.append(channel)
|
||||||
|
|
||||||
|
all_channels = list(qs) + broken_channels
|
||||||
|
total = len(all_channels)
|
||||||
|
self.stdout.write(f"Total channels to process: {total}")
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
for channel in qs.iterator():
|
for channel in all_channels:
|
||||||
source = "youtube" if channel.youtube_id else "twitch"
|
source = "youtube" if channel.youtube_id else "twitch"
|
||||||
identifier = channel.youtube_id or channel.twitch_id
|
identifier = channel.youtube_id or channel.twitch_id
|
||||||
|
status = f"({source}: {identifier})"
|
||||||
|
if self._has_broken_image(channel):
|
||||||
|
status += " [image BROKEN]"
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f" [DRY RUN] Would fix {channel.name} ({source}: {identifier})"
|
f" [DRY RUN] Would fix {channel.name} {status}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
updated = 0
|
updated = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
for channel in qs.iterator():
|
for channel in all_channels:
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
channel.fix_metadata(force=force)
|
channel.fix_metadata(force=force or self._has_broken_image(channel))
|
||||||
updated += 1
|
updated += 1
|
||||||
source = "youtube" if channel.youtube_id else "twitch"
|
source = "youtube" if channel.youtube_id else "twitch"
|
||||||
self.stdout.write(f" [{updated}/{total}] {channel.name} ({source})")
|
self.stdout.write(f" [{updated}/{total}] {channel.name} ({source})")
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
@ -28,9 +29,18 @@ class Command(BaseCommand):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--needs-metadata",
|
"--needs-metadata",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Only process series missing imdb_id or cover image",
|
help="Only process series missing imdb_id or with broken cover image",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _has_broken_image(self, series) -> bool:
|
||||||
|
"""Check if a series has a cover_image set but the file is missing."""
|
||||||
|
if not series.cover_image:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return not os.path.exists(series.cover_image.path)
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
from videos.models import Series
|
from videos.models import Series
|
||||||
|
|
||||||
@ -51,25 +61,50 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
self.stdout.write(f"Processing {total} series")
|
self.stdout.write(f"Processing {total} series from DB filter")
|
||||||
|
|
||||||
|
# Also find series with broken cover images
|
||||||
|
broken_image_series = []
|
||||||
|
if needs_metadata:
|
||||||
|
broken_qs = Series.objects.filter(
|
||||||
|
cover_image__isnull=False,
|
||||||
|
).exclude(
|
||||||
|
models.Q(imdb_id__isnull=True)
|
||||||
|
| models.Q(imdb_id="")
|
||||||
|
| models.Q(cover_image__isnull=True)
|
||||||
|
| models.Q(cover_image=""),
|
||||||
|
)
|
||||||
|
if imdb_id:
|
||||||
|
broken_qs = broken_qs.filter(imdb_id=imdb_id)
|
||||||
|
for series in broken_qs.iterator():
|
||||||
|
if self._has_broken_image(series):
|
||||||
|
broken_image_series.append(series)
|
||||||
|
|
||||||
|
all_series = list(qs) + broken_image_series
|
||||||
|
total = len(all_series)
|
||||||
|
self.stdout.write(f"Total series to process: {total}")
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
for series in qs.iterator():
|
for series in all_series:
|
||||||
has_imdb = bool(series.imdb_id)
|
has_imdb = bool(series.imdb_id)
|
||||||
has_image = bool(series.cover_image)
|
has_image = bool(series.cover_image)
|
||||||
self.stdout.write(
|
image_broken = self._has_broken_image(series)
|
||||||
f" [DRY RUN] Would fix {series.name}"
|
status = f"imdb_id={'✓' if has_imdb else '✗'}"
|
||||||
f" (imdb_id={'✓' if has_imdb else '✗'}"
|
if image_broken:
|
||||||
f", image={'✓' if has_image else '✗'})"
|
status += ", image=BROKEN"
|
||||||
)
|
elif has_image:
|
||||||
|
status += ", image=✓"
|
||||||
|
else:
|
||||||
|
status += ", image=✗"
|
||||||
|
self.stdout.write(f" [DRY RUN] Would fix {series.name} ({status})")
|
||||||
return
|
return
|
||||||
|
|
||||||
updated = 0
|
updated = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
for series in qs.iterator():
|
for series in all_series:
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
series.fix_metadata(force_update=force)
|
series.fix_metadata(force_update=force or self._has_broken_image(series))
|
||||||
updated += 1
|
updated += 1
|
||||||
self.stdout.write(f" [{updated}/{total}] {series.name}")
|
self.stdout.write(f" [{updated}/{total}] {series.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -120,9 +120,67 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include "scrobbles/_top_charts.html" %}
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="{% url 'charts:maloja-charts' %}" class="btn btn-sm btn-outline-secondary">🎨 Maloja Widgets</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
{% 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>
|
||||||
|
|||||||
45
vrobbler/templates/charts/maloja_charts.html
Normal file
45
vrobbler/templates/charts/maloja_charts.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base_list.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Maloja Widgets{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<style>
|
||||||
|
.container { margin-bottom: 100px; }
|
||||||
|
h2 { padding-top: 20px; }
|
||||||
|
.nav-tabs { cursor: pointer; }
|
||||||
|
.image-wrapper { contain: content; }
|
||||||
|
.image-wrapper :hover { background: rgba(0,0,0,0.3); }
|
||||||
|
.caption {
|
||||||
|
position: fixed; top: 5px; left: 5px;
|
||||||
|
padding: 3px; font-size: 90%;
|
||||||
|
color: white; background: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.caption-medium {
|
||||||
|
position: fixed; top: 5px; left: 5px;
|
||||||
|
padding: 3px; font-size: 75%;
|
||||||
|
color: white; background: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.caption-small {
|
||||||
|
position: fixed; top: 5px; left: 5px;
|
||||||
|
padding: 3px; font-size: 60%;
|
||||||
|
color: white; background: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block lists %}
|
||||||
|
|
||||||
|
{% block grid_view_button %}{% endblock %}
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="{% url 'charts:charts-home' %}" class="btn btn-sm btn-outline-secondary">← Full Charts</a>
|
||||||
|
<a href="{% url 'charts:spotify-tracks' %}" class="btn btn-sm btn-outline-secondary">🎵 Spotify Tracks</a>
|
||||||
|
<a href="{% url 'charts:bandcamp-tracks' %}" class="btn btn-sm btn-outline-secondary">🎵 Bandcamp Tracks</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "scrobbles/_top_charts.html" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@ -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>
|
||||||
@ -95,7 +97,7 @@
|
|||||||
<div style="display:block">
|
<div style="display:block">
|
||||||
<div style="float:left;">
|
<div style="float:left;">
|
||||||
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
|
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
|
||||||
<div class="caption">#1 {{albums.0.album.title}}</div>
|
<div class="caption">#1 {{albums.0.album.name}}</div>
|
||||||
{% if albums.0.album.cover_image %}
|
{% if albums.0.album.cover_image %}
|
||||||
<a href="{{albums.0.album.get_absolute_url}}"><img src="{{albums.0.album.cover_image_medium.url}}" width="300px"></a>
|
<a href="{{albums.0.album.get_absolute_url}}"><img src="{{albums.0.album.cover_image_medium.url}}" width="300px"></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -109,7 +111,7 @@
|
|||||||
{% with albums|get_item:forloop.counter as album %}
|
{% with albums|get_item:forloop.counter as album %}
|
||||||
{% if album %}
|
{% if album %}
|
||||||
<div class="image-wrapper" style="width:50%">
|
<div class="image-wrapper" style="width:50%">
|
||||||
<div class="caption-medium">#{{forloop.counter|add:1}} {{album.album.title}}</div>
|
<div class="caption-medium">#{{forloop.counter|add:1}} {{album.album.name}}</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="150px"></a>
|
<a href="{{album.album.get_absolute_url}}"><img src="{{album.album.cover_image_medium.url}}" width="150px"></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -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.name}}</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user