Add the beginnings of charts
This commit adds a lot of files, but most of them have no impact on any other code. The thrust here is to start creating chart pages showing which tracks and artists were most played for various time periods. Lots still not working, but we're getting there.
This commit is contained in:
@ -3,10 +3,10 @@ from typing import Dict, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import musicbrainzngs
|
||||
from django.apps.config import cached_property
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from scrobbles.mixins import ScrobblableMixin
|
||||
@ -30,6 +30,29 @@ class Artist(TimeStampedModel):
|
||||
def mb_link(self):
|
||||
return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('music:artist_detail', kwargs={'slug': self.uuid})
|
||||
|
||||
def scrobbles(self):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
return Scrobble.objects.filter(
|
||||
track__in=self.track_set.all()
|
||||
).order_by('-timestamp')
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
return (
|
||||
self.track_set.all()
|
||||
.annotate(scrobble_count=models.Count('scrobble'))
|
||||
.order_by('-scrobble_count')
|
||||
)
|
||||
|
||||
def charts(self):
|
||||
from scrobbles.models import ChartRecord
|
||||
|
||||
return ChartRecord.objects.filter(track__artist=self).order_by('-year')
|
||||
|
||||
|
||||
class Album(TimeStampedModel):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
@ -135,14 +158,13 @@ class Track(ScrobblableMixin):
|
||||
def __str__(self):
|
||||
return f"{self.title} by {self.artist}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('music:track_detail', kwargs={'slug': self.uuid})
|
||||
|
||||
@property
|
||||
def mb_link(self):
|
||||
return f"https://musicbrainz.org/recording/{self.musicbrainz_id}"
|
||||
|
||||
@cached_property
|
||||
def scrobble_count(self):
|
||||
return self.scrobble_set.filter(in_progress=False).count()
|
||||
|
||||
@classmethod
|
||||
def find_or_create(
|
||||
cls, artist_dict: Dict, album_dict: Dict, track_dict: Dict
|
||||
|
||||
21
vrobbler/apps/music/urls.py
Normal file
21
vrobbler/apps/music/urls.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.urls import path
|
||||
from music import views
|
||||
|
||||
app_name = 'music'
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('albums/', views.AlbumListView.as_view(), name='albums_list'),
|
||||
path("tracks/", views.TrackListView.as_view(), name='tracks_list'),
|
||||
path(
|
||||
'tracks/<slug:slug>/',
|
||||
views.TrackDetailView.as_view(),
|
||||
name='track_detail',
|
||||
),
|
||||
path('artists/', views.ArtistListView.as_view(), name='artist_list'),
|
||||
path(
|
||||
'artists/<slug:slug>/',
|
||||
views.ArtistDetailView.as_view(),
|
||||
name='artist_detail',
|
||||
),
|
||||
]
|
||||
33
vrobbler/apps/music/views.py
Normal file
33
vrobbler/apps/music/views.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.views import generic
|
||||
from music.models import Track, Artist, Album
|
||||
from scrobbles.stats import get_scrobble_count_qs
|
||||
|
||||
|
||||
class TrackListView(generic.ListView):
|
||||
model = Track
|
||||
|
||||
def get_queryset(self):
|
||||
return get_scrobble_count_qs(user=self.request.user).order_by(
|
||||
"-scrobble_count"
|
||||
)
|
||||
|
||||
|
||||
class TrackDetailView(generic.DetailView):
|
||||
model = Track
|
||||
slug_field = 'uuid'
|
||||
|
||||
|
||||
class ArtistListView(generic.ListView):
|
||||
model = Artist
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().order_by("name")
|
||||
|
||||
|
||||
class ArtistDetailView(generic.DetailView):
|
||||
model = Artist
|
||||
slug_field = 'uuid'
|
||||
|
||||
|
||||
class AlbumListView(generic.ListView):
|
||||
model = Album
|
||||
88
vrobbler/apps/scrobbles/migrations/0010_chartrecord.py
Normal file
88
vrobbler/apps/scrobbles/migrations/0010_chartrecord.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-30 17:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0009_alter_track_musicbrainz_id_and_more'),
|
||||
('videos', '0006_alter_video_year'),
|
||||
('scrobbles', '0009_scrobble_uuid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChartRecord',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('rank', models.IntegerField()),
|
||||
('year', models.IntegerField(default=2023)),
|
||||
('month', models.IntegerField(blank=True, null=True)),
|
||||
('week', models.IntegerField(blank=True, null=True)),
|
||||
('day', models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
'artist',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='music.artist',
|
||||
),
|
||||
),
|
||||
(
|
||||
'series',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='videos.series',
|
||||
),
|
||||
),
|
||||
(
|
||||
'track',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='music.track',
|
||||
),
|
||||
),
|
||||
(
|
||||
'video',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='videos.video',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
26
vrobbler/apps/scrobbles/migrations/0011_chartrecord_user.py
Normal file
26
vrobbler/apps/scrobbles/migrations/0011_chartrecord_user.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-30 17:44
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('scrobbles', '0010_chartrecord'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chartrecord',
|
||||
name='user',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,3 +1,4 @@
|
||||
import calendar
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from uuid import uuid4
|
||||
@ -6,17 +7,93 @@ from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from music.models import Track
|
||||
from music.models import Artist, Track
|
||||
from podcasts.models import Episode
|
||||
from scrobbles.utils import check_scrobble_for_finish
|
||||
from sports.models import SportEvent
|
||||
from videos.models import Video
|
||||
from videos.models import Series, Video
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
|
||||
class ChartRecord(TimeStampedModel):
|
||||
"""Sort of like a materialized view for what we could dynamically generate,
|
||||
but would kill the DB as it gets larger. Collects time-based records
|
||||
generated by a cron-like archival job
|
||||
|
||||
1972 by Josh Rouse - #3 in 2023, January
|
||||
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, **BNULL)
|
||||
rank = models.IntegerField()
|
||||
year = models.IntegerField(default=timezone.now().year)
|
||||
month = models.IntegerField(**BNULL)
|
||||
week = models.IntegerField(**BNULL)
|
||||
day = models.IntegerField(**BNULL)
|
||||
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
||||
series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
|
||||
artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING, **BNULL)
|
||||
track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
@property
|
||||
def media_obj(self):
|
||||
media_obj = None
|
||||
if self.video:
|
||||
media_obj = self.video
|
||||
if self.track:
|
||||
media_obj = self.track
|
||||
return media_obj
|
||||
|
||||
@property
|
||||
def month_str(self) -> str:
|
||||
month_str = ""
|
||||
if self.month:
|
||||
month_str = calendar.month_name[self.month]
|
||||
return month_str
|
||||
|
||||
@property
|
||||
def day_str(self) -> str:
|
||||
day_str = ""
|
||||
if self.day:
|
||||
day_str = str(self.day)
|
||||
return day_str
|
||||
|
||||
@property
|
||||
def week_str(self) -> str:
|
||||
week_str = ""
|
||||
if self.week:
|
||||
week_str = str(self.week)
|
||||
return "Week " + week_str
|
||||
|
||||
@property
|
||||
def period(self) -> str:
|
||||
period = str(self.year)
|
||||
if self.month:
|
||||
period = " ".join([self.month_str, period])
|
||||
if self.week:
|
||||
period = " ".join([self.week_str, period])
|
||||
if self.day:
|
||||
period = " ".join([self.day_str, period])
|
||||
return period
|
||||
|
||||
@property
|
||||
def period_type(self) -> str:
|
||||
period = 'year'
|
||||
if self.month:
|
||||
period = 'month'
|
||||
if self.week:
|
||||
period = 'week'
|
||||
if self.day:
|
||||
period = 'day'
|
||||
return period
|
||||
|
||||
def __str__(self):
|
||||
return f"#{self.rank} in {self.period} - {self.media_obj}"
|
||||
|
||||
|
||||
class Scrobble(TimeStampedModel):
|
||||
uuid = models.UUIDField(editable=False, **BNULL)
|
||||
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
115
vrobbler/apps/scrobbles/stats.py
Normal file
115
vrobbler/apps/scrobbles/stats.py
Normal file
@ -0,0 +1,115 @@
|
||||
import calendar
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import pytz
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Q
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_start_end_dates_by_week(year, week, tz):
|
||||
d = datetime(year, 1, 1, tzinfo=tz)
|
||||
if d.weekday() <= 3:
|
||||
d = d - timedelta(d.weekday())
|
||||
else:
|
||||
d = d + timedelta(7 - d.weekday())
|
||||
dlt = timedelta(days=(week - 1) * 7)
|
||||
return d + dlt, d + dlt + timedelta(days=6)
|
||||
|
||||
|
||||
def get_scrobble_count_qs(
|
||||
year: Optional[int] = None,
|
||||
month: Optional[int] = None,
|
||||
week: Optional[int] = None,
|
||||
day: Optional[int] = None,
|
||||
user=None,
|
||||
model_str="Track",
|
||||
) -> dict[str, int]:
|
||||
|
||||
tz = settings.TIME_ZONE
|
||||
if user and user.is_authenticated:
|
||||
tz = pytz.timezone(user.profile.timezone)
|
||||
|
||||
data_model = apps.get_model(app_label='music', model_name='Track')
|
||||
if model_str == "Video":
|
||||
data_model = apps.get_model(app_label='videos', model_name='Video')
|
||||
if model_str == "SportEvent":
|
||||
data_model = apps.get_model(
|
||||
app_label='sports', model_name='SportEvent'
|
||||
)
|
||||
|
||||
base_qs = data_model.objects.filter(
|
||||
scrobble__user=user,
|
||||
scrobble__played_to_completion=True,
|
||||
)
|
||||
|
||||
# Returna all media items with scrobble count annotated
|
||||
if not year:
|
||||
return base_qs.annotate(scrobble_count=Count("scrobble")).order_by(
|
||||
"-scrobble_count"
|
||||
)
|
||||
|
||||
start = datetime(year, 1, 1, tzinfo=tz)
|
||||
end = datetime(year, 12, 31, tzinfo=tz)
|
||||
if month:
|
||||
end_day = calendar.monthrange(year, month)[1]
|
||||
start = datetime(year, month, 1, tzinfo=tz)
|
||||
end = datetime(year, month, end_day, tzinfo=tz)
|
||||
elif week:
|
||||
start, end = get_start_end_dates_by_week(year, week, tz)
|
||||
elif day and month:
|
||||
start = datetime(year, month, day, 0, 0, tzinfo=tz)
|
||||
end = datetime(year, month, day, 23, 59, tzinfo=tz)
|
||||
elif day and not month:
|
||||
logger.warning('Day provided with month, defaulting ot all-time')
|
||||
|
||||
date_filter = Q(
|
||||
scrobble__timestamp__gte=start, scrobble__timestamp__lte=end
|
||||
)
|
||||
|
||||
return (
|
||||
base_qs.annotate(
|
||||
scrobble_count=Count("scrobble", filter=Q(date_filter))
|
||||
)
|
||||
.filter(date_filter)
|
||||
.order_by("-scrobble_count")
|
||||
)
|
||||
|
||||
|
||||
def build_charts(
|
||||
year: Optional[int] = None,
|
||||
month: Optional[int] = None,
|
||||
week: Optional[int] = None,
|
||||
day: Optional[int] = None,
|
||||
user=None,
|
||||
model_str="Track",
|
||||
):
|
||||
ChartRecord = apps.get_model(
|
||||
app_label='scrobbles', model_name='ChartRecord'
|
||||
)
|
||||
results = get_scrobble_count_qs(year, month, week, day, user, model_str)
|
||||
unique_counts = list(set([result.scrobble_count for result in results]))
|
||||
unique_counts.sort(reverse=True)
|
||||
ranks = {}
|
||||
for rank, count in enumerate(unique_counts, start=1):
|
||||
ranks[count] = rank
|
||||
|
||||
chart_records = []
|
||||
for result in results:
|
||||
chart_record = {
|
||||
'year': year,
|
||||
'week': week,
|
||||
'month': month,
|
||||
'day': day,
|
||||
'user': user,
|
||||
}
|
||||
chart_record['rank'] = ranks[result.scrobble_count]
|
||||
if model_str == 'Track':
|
||||
chart_record['track'] = result
|
||||
chart_records.append(ChartRecord(**chart_record))
|
||||
ChartRecord.objects.bulk_create(chart_records, ignore_conflicts=True)
|
||||
@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
from videos import views
|
||||
|
||||
app_name = 'scrobbles'
|
||||
app_name = 'videos'
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@ -83,6 +83,7 @@ INSTALLED_APPS = [
|
||||
"music",
|
||||
"podcasts",
|
||||
"sports",
|
||||
"mathfilters",
|
||||
"rest_framework",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
|
||||
18
vrobbler/templates/base_detail.html
Normal file
18
vrobbler/templates/base_detail.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{% block title %}{% endblock %} </h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% block details %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
18
vrobbler/templates/base_list.html
Normal file
18
vrobbler/templates/base_list.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{% block title %}{% endblock %} </h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% block lists %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
11
vrobbler/templates/music/album_list.html
Normal file
11
vrobbler/templates/music/album_list.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}Albums{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
{% for album in object_list %}
|
||||
<dl style="width: 130px; float: left; margin-right:10px;">
|
||||
<dd><img src="{{album.cover_image.url}}" width=120 height=120 /></dd>
|
||||
</dl>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
72
vrobbler/templates/music/artist_detail.html
Normal file
72
vrobbler/templates/music/artist_detail.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% extends "base_detail.html" %}
|
||||
{% load mathfilters %}
|
||||
|
||||
{% block title %}{{object.name}}{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div class="row">
|
||||
{% for album in artist.album_set.all %}
|
||||
{% if album.cover_image %}
|
||||
<p style="width:150px; float:left;"><img src="{{album.cover_image.url}}" width=150 height=150 /></p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<p>{{artist.scrobbles.count}} scrobbles</p>
|
||||
<div class="col-md">
|
||||
<h3>Top tracks</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Count</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in object.tracks %}
|
||||
<tr>
|
||||
<td>{{rank}}#1</td>
|
||||
<td>{{track.title}}</td>
|
||||
<td>{{track.scrobble_count}}</td>
|
||||
<td>
|
||||
<div class="progress-bar" style="margin-right:5px;">
|
||||
<span class="progress-bar-fill" style="width: {{track.scrobble_count|mul:10}}%;"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h3>Last scrobbles</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Track</th>
|
||||
<th scope="col">Album</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scrobble in object.scrobbles %}
|
||||
<tr>
|
||||
<td>{{scrobble.timestamp}}</td>
|
||||
<td>{{scrobble.track.title}}</td>
|
||||
<td>{{scrobble.track.album.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
vrobbler/templates/music/artist_list.html
Normal file
30
vrobbler/templates/music/artist_list.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}Artists{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Scrobbles</th>
|
||||
<th scope="col">All time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for artist in object_list %}
|
||||
<tr>
|
||||
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
|
||||
<td>{{artist.scrobbles.count}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
13
vrobbler/templates/music/track_detail.html
Normal file
13
vrobbler/templates/music/track_detail.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base_detail.html" %}
|
||||
|
||||
{% block title %}{{object.title}}{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<h2>Last scrobbles</h2>
|
||||
{% for scrobble in object.scrobble_set.all %}
|
||||
<ul>
|
||||
<li>{{scrobble.timestamp|date:"d M Y h:m"}} - <img src="{{object.album.cover_image.url}}" width=25 height=25 /> - {{object}}</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
12
vrobbler/templates/music/track_list.html
Normal file
12
vrobbler/templates/music/track_list.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "base_list.html" %}
|
||||
|
||||
{% block title %}Tracks{% endblock %}
|
||||
|
||||
{% block lists %}
|
||||
<h2>All time</h2>
|
||||
{% for track in object_list %}
|
||||
<ul>
|
||||
<li><a href="{{track.get_absolute_url}}">{{track}}</a></li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@ -3,8 +3,8 @@ from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
from scrobbles import urls as scrobble_urls
|
||||
from music import urls as music_urls
|
||||
from videos import urls as video_urls
|
||||
|
||||
urlpatterns = [
|
||||
@ -19,6 +19,7 @@ urlpatterns = [
|
||||
scrobbles_views.ManualScrobbleView.as_view(),
|
||||
name='imdb-manual-scrobble',
|
||||
),
|
||||
path("", include(music_urls, namespace="music")),
|
||||
path("", include(video_urls, namespace="videos")),
|
||||
path("", scrobbles_views.RecentScrobbleList.as_view(), name="home"),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user