Compare commits

..

30 Commits

Author SHA1 Message Date
6d21bb2e85 Bump version to 0.11.9 2023-03-03 02:41:47 -05:00
7df3fedc64 Fix bad image templates 2023-03-03 02:41:27 -05:00
b4e83b184e Bump version to 0.11.8 2023-03-03 02:32:55 -05:00
6e885df1dd Small fix to remove unused templatetag 2023-03-03 02:32:32 -05:00
f153f831b3 Bump version to 0.11.7 2023-03-03 02:29:56 -05:00
66a90c87f1 Add demo of maloja style 2023-03-03 02:29:32 -05:00
6e17e4ce0d Fix chart rank and periods 2023-03-03 02:29:15 -05:00
3c3e567573 Bump version to 0.11.6 2023-03-02 16:11:48 -05:00
2775851474 Clean up the video detail page 2023-03-02 16:11:19 -05:00
654a64e82d Stub in sports urls 2023-03-02 16:04:44 -05:00
7dd7f369d8 Fix bug in audiodb scrape path 2023-03-02 15:45:15 -05:00
fb6110c71d Bump version to 0.11.5
Version 1.0 approaches!
2023-03-02 15:11:40 -05:00
93299a1abd Hide album urls that don't exist 2023-03-02 15:08:34 -05:00
a58ddebd23 Fix typo in audiodb lookup 2023-03-02 15:08:22 -05:00
41cdb96e94 Clean up album admin with new data 2023-03-02 15:08:11 -05:00
5a8e828b81 Add rudimentary album metadata from TADB 2023-03-02 14:48:33 -05:00
c84a3072be Dont show None if bio is missing 2023-03-02 11:26:31 -05:00
0bd7ed4463 Clean up music detail views 2023-03-02 11:03:26 -05:00
ee232aa103 Fix Le Static Files 2023-03-01 19:11:16 -05:00
7151646600 Bump version to 0.11.4 2023-03-01 00:15:29 -05:00
1d7cf965ef Clean up album and artist views 2023-03-01 00:13:31 -05:00
0a9279dbd4 Super hack for chart pages 2023-02-28 22:11:37 -05:00
bf3479dbc7 Skip IMDB tests that aren't used 2023-02-28 21:33:46 -05:00
a99dca246b Fix chart display 2023-02-28 01:58:22 -05:00
f76aaf6a9c Condense live charts to one function 2023-02-28 01:57:46 -05:00
ce1541bb2d Add sports API 2023-02-27 13:07:18 -05:00
d34e56aa89 Bump version to 0.11.3 2023-02-27 11:13:45 -05:00
6316d4bead Fix chart templates 2023-02-27 11:13:13 -05:00
56e5728245 Bump version to 0.11.2 2023-02-27 10:30:44 -05:00
6ff170e169 Quite a few bugs 2023-02-27 10:30:20 -05:00
43 changed files with 1767 additions and 378 deletions

View File

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

View File

@ -5,12 +5,7 @@ import time_machine
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from music.aggregators import (
scrobble_counts,
top_artists,
top_tracks,
week_of_scrobbles,
)
from music.aggregators import live_charts, scrobble_counts, week_of_scrobbles
from profiles.models import UserProfile
from scrobbles.models import Scrobble
@ -55,7 +50,7 @@ def test_week_of_scrobbles_data(client, mopidy_track_request_data):
def test_top_tracks_by_day(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_tracks(user)
tops = live_charts(user)
assert tops[0].title == "Same in the End"
@ -63,7 +58,7 @@ def test_top_tracks_by_day(client, mopidy_track_request_data):
def test_top_tracks_by_week(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_tracks(user, filter='week')
tops = live_charts(user, chart_period='week')
assert tops[0].title == "Same in the End"
@ -71,7 +66,7 @@ def test_top_tracks_by_week(client, mopidy_track_request_data):
def test_top_tracks_by_month(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_tracks(user, filter='month')
tops = live_charts(user, chart_period='month')
assert tops[0].title == "Same in the End"
@ -79,7 +74,7 @@ def test_top_tracks_by_month(client, mopidy_track_request_data):
def test_top_tracks_by_year(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_tracks(user, filter='year')
tops = live_charts(user, chart_period='year')
assert tops[0].title == "Same in the End"
@ -87,7 +82,7 @@ def test_top_tracks_by_year(client, mopidy_track_request_data):
def test_top__artists_by_week(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_artists(user, filter='week')
tops = live_charts(user, chart_period='week', media_type="Artist")
assert tops[0].name == "Sublime"
@ -95,7 +90,7 @@ def test_top__artists_by_week(client, mopidy_track_request_data):
def test_top__artists_by_month(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_artists(user, filter='month')
tops = live_charts(user, chart_period='month', media_type="Artist")
assert tops[0].name == "Sublime"
@ -103,5 +98,5 @@ def test_top__artists_by_month(client, mopidy_track_request_data):
def test_top__artists_by_year(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first()
tops = top_artists(user, filter='year')
tops = live_charts(user, chart_period='year', media_type="Artist")
assert tops[0].name == "Sublime"

View File

@ -1,10 +1,9 @@
import pytest
import imdb
from mock import patch
from vrobbler.apps.scrobbles.imdb import lookup_video_from_imdb
@pytest.mark.skip(reason="Need to sort out third party API testing")
def test_lookup_imdb_bad_id(caplog):
data = lookup_video_from_imdb('3409324')
assert data is None

View File

@ -8,8 +8,18 @@ from scrobbles.admin import ScrobbleInline
@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = ("name", "year", "musicbrainz_id")
list_filter = ("year",)
list_display = (
"name",
"year",
"primary_artist",
"theaudiodb_genre",
"theaudiodb_mood",
"musicbrainz_id",
)
list_filter = (
"theaudiodb_score",
"theaudiodb_genre",
)
ordering = ("name",)
filter_horizontal = [
'artists',

View File

@ -1,22 +1,14 @@
from datetime import datetime, timedelta
from typing import List, Optional
from typing import List
import pytz
from django.db.models import Count, Q, Sum
from django.apps import apps
from django.db.models import Count, Q, QuerySet
from django.utils import timezone
from music.models import Artist, Track
from scrobbles.models import Scrobble
from videos.models import Video
from vrobbler.apps.profiles.utils import now_user_timezone
NOW = timezone.now()
START_OF_TODAY = datetime.combine(NOW.date(), datetime.min.time(), NOW.tzinfo)
STARTING_DAY_OF_CURRENT_WEEK = NOW.date() - timedelta(
days=NOW.today().isoweekday() % 7
)
STARTING_DAY_OF_CURRENT_MONTH = NOW.date().replace(day=1)
STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
def scrobble_counts(user=None):
@ -92,39 +84,61 @@ def week_of_scrobbles(
return scrobble_day_dict
def top_tracks(
user: "User", filter: str = "today", limit: int = 30
) -> List["Track"]:
def live_charts(
user: "User",
media_type: str = "Track",
chart_period: str = "all",
limit: int = 15,
) -> QuerySet:
now = timezone.now()
tzinfo = now.tzinfo
now = now.date()
if user.is_authenticated:
now = now_user_timezone(user.profile)
tzinfo = now.tzinfo
start_of_today = datetime.combine(
now.date(), datetime.min.time(), now.tzinfo
)
starting_day_of_current_week = now.date() - timedelta(
days=now.today().isoweekday() % 7
)
starting_day_of_current_month = now.date().replace(day=1)
starting_day_of_current_year = now.date().replace(month=1, day=1)
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
start_day_of_week = now - timedelta(days=now.today().isoweekday() % 7)
start_day_of_month = now.replace(day=1)
start_day_of_year = now.replace(month=1, day=1)
media_model = apps.get_model(app_label='music', model_name=media_type)
period_queries = {
'today': {'scrobble__timestamp__gte': start_of_today},
'week': {'scrobble__timestamp__gte': start_day_of_week},
'month': {'scrobble__timestamp__gte': start_day_of_month},
'year': {'scrobble__timestamp__gte': start_day_of_year},
'all': {},
}
time_filter = Q()
if filter == "today":
time_filter = Q(scrobble__timestamp__gte=start_of_today)
if filter == "week":
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_week)
if filter == "month":
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_month)
if filter == "year":
time_filter = Q(scrobble__timestamp__gte=starting_day_of_current_year)
completion_filter = Q(
scrobble__user=user, scrobble__played_to_completion=True
)
user_filter = Q(scrobble__user=user)
count_field = "scrobble"
if media_type == "Artist":
for period, query_dict in period_queries.items():
period_queries[period] = {
"track__" + k: v for k, v in query_dict.items()
}
completion_filter = Q(
track__scrobble__user=user,
track__scrobble__played_to_completion=True,
)
count_field = "track__scrobble"
user_filter = Q(track__scrobble__user=user)
time_filter = Q(**period_queries[chart_period])
return (
Track.objects.filter(time_filter)
media_model.objects.filter(user_filter, time_filter)
.annotate(
num_scrobbles=Count(
"scrobble",
filter=Q(scrobble__played_to_completion=True),
count_field,
filter=completion_filter,
distinct=True,
)
)
@ -132,35 +146,5 @@ def top_tracks(
)
def top_artists(
user: "User", filter: str = "today", limit: int = 15
) -> List["Artist"]:
time_filter = Q(track__scrobble__timestamp__gte=START_OF_TODAY)
if filter == "week":
time_filter = Q(
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_WEEK
)
if filter == "month":
time_filter = Q(
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_MONTH
)
if filter == "year":
time_filter = Q(
track__scrobble__timestamp__gte=STARTING_DAY_OF_CURRENT_YEAR
)
return (
Artist.objects.filter(time_filter)
.annotate(
num_scrobbles=Count(
"track__scrobble",
filter=Q(track__scrobble__played_to_completion=True),
distinct=True,
)
)
.order_by("-num_scrobbles")
)
def artist_scrobble_count(artist_id: int, filter: str = "today") -> int:
return Scrobble.objects.filter(track__artist=artist_id).count()

View File

@ -0,0 +1,85 @@
# Generated by Django 4.1.5 on 2023-03-02 19:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0011_artist_thumbnail'),
]
operations = [
migrations.AddField(
model_name='album',
name='allmusic_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='discogs_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='rateyourmusic_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_description',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_genre',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_id',
field=models.CharField(
blank=True, max_length=255, null=True, unique=True
),
),
migrations.AddField(
model_name='album',
name='theaudiodb_mood',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_score',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_score_votes',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_speed',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_style',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='theaudiodb_theme',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='wikidata_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='album',
name='wikipedia_slug',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-03-02 19:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0012_album_allmusic_id_album_discogs_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='album',
name='theaudiodb_score',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-03-02 19:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0013_alter_album_theaudiodb_score'),
]
operations = [
migrations.AddField(
model_name='album',
name='theaudiodb_year_released',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -6,13 +6,14 @@ from uuid import uuid4
import musicbrainzngs
from django.conf import settings
from django.core.files.base import File
from django.core.files.base import ContentFile, File
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
from scrobbles.theaudiodb import lookup_artist_from_tadb
from vrobbler.apps.scrobbles.theaudiodb import lookup_album_from_tadb
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
@ -86,14 +87,58 @@ class Album(TimeStampedModel):
musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
cover_image = models.ImageField(upload_to="albums/", **BNULL)
theaudiodb_id = models.CharField(max_length=255, unique=True, **BNULL)
theaudiodb_description = models.TextField(**BNULL)
theaudiodb_year_released = models.IntegerField(**BNULL)
theaudiodb_score = models.FloatField(**BNULL)
theaudiodb_score_votes = models.IntegerField(**BNULL)
theaudiodb_genre = models.CharField(max_length=255, **BNULL)
theaudiodb_style = models.CharField(max_length=255, **BNULL)
theaudiodb_mood = models.CharField(max_length=255, **BNULL)
theaudiodb_speed = models.CharField(max_length=255, **BNULL)
theaudiodb_theme = models.CharField(max_length=255, **BNULL)
allmusic_id = models.CharField(max_length=255, **BNULL)
rateyourmusic_id = models.CharField(max_length=255, **BNULL)
wikipedia_slug = models.CharField(max_length=255, **BNULL)
discogs_id = models.CharField(max_length=255, **BNULL)
wikidata_id = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("music:album_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')
)
@property
def primary_artist(self):
return self.artists.first()
def scrape_theaudiodb(self) -> None:
artist = "Various Artists"
if self.primary_artist:
artist = self.primary_artist.name
album_data = lookup_album_from_tadb(self.name, artist)
if not album_data.get('theaudiodb_id'):
logger.info(f"No data for {self} found in TheAudioDB")
return
Album.objects.filter(pk=self.pk).update(**album_data)
def fix_metadata(self):
if (
not self.musicbrainz_albumartist_id
@ -141,6 +186,7 @@ class Album(TimeStampedModel):
or self.cover_image == 'default-image-replace-me'
):
self.fetch_artwork()
self.scrape_theaudiodb()
def fetch_artwork(self, force=False):
if not self.cover_image and not force:
@ -179,9 +225,27 @@ class Album(TimeStampedModel):
self.save()
@property
def mb_link(self):
def mb_link(self) -> str:
return f"https://musicbrainz.org/release/{self.musicbrainz_id}"
@property
def allmusic_link(self) -> str:
if self.allmusic_id:
return f"https://www.allmusic.com/artist/{self.allmusic_id}"
return ""
@property
def wikipedia_link(self):
if self.wikipedia_slug:
return f"https://www.wikipedia.org/en/{self.wikipedia_slug}"
return ""
@property
def tadb_link(self):
if self.theaudiodb_id:
return f"https://www.theaudiodb.com/album/{self.theaudiodb_id}"
return ""
class Track(ScrobblableMixin):
COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)

View File

@ -6,6 +6,11 @@ app_name = 'music'
urlpatterns = [
path('albums/', views.AlbumListView.as_view(), name='albums_list'),
path(
'album/<slug:slug>/',
views.AlbumDetailView.as_view(),
name='album_detail',
),
path("tracks/", views.TrackListView.as_view(), name='tracks_list'),
path(
'tracks/<slug:slug>/',

View File

@ -1,11 +1,13 @@
from django.db.models import Count
from django.views import generic
from music.models import Track, Artist, Album
from music.models import Album, Artist, Track
from scrobbles.models import ChartRecord
from scrobbles.stats import get_scrobble_count_qs
class TrackListView(generic.ListView):
model = Track
paginate_by = 200
def get_queryset(self):
return get_scrobble_count_qs(user=self.request.user).order_by(
@ -17,18 +19,76 @@ class TrackDetailView(generic.DetailView):
model = Track
slug_field = 'uuid'
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
context_data['charts'] = ChartRecord.objects.filter(
track=self.object, rank__in=[1, 2, 3]
)
return context_data
class ArtistListView(generic.ListView):
model = Artist
paginate_by = 100
def get_queryset(self):
return super().get_queryset().order_by("name")
return (
super()
.get_queryset()
.annotate(scrobble_count=Count('track__scrobble'))
.order_by("-scrobble_count")
)
def get_context_data(self, *, object_list=None, **kwargs):
context_data = super().get_context_data(
object_list=object_list, **kwargs
)
context_data['view'] = self.request.GET.get('view')
return context_data
class ArtistDetailView(generic.DetailView):
model = Artist
slug_field = 'uuid'
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
artist = context_data['object']
rank = 1
tracks_ranked = []
scrobbles = artist.tracks.first().scrobble_count
for track in artist.tracks:
if scrobbles > track.scrobble_count:
rank += 1
tracks_ranked.append((rank, track))
scrobbles = track.scrobble_count
context_data['tracks_ranked'] = tracks_ranked
context_data['charts'] = ChartRecord.objects.filter(
artist=self.object, rank__in=[1, 2, 3]
)
return context_data
class AlbumListView(generic.ListView):
model = Album
def get_queryset(self):
return (
super()
.get_queryset()
.annotate(scrobble_count=Count('track__scrobble'))
.order_by("-scrobble_count")
)
class AlbumDetailView(generic.DetailView):
model = Album
slug_field = 'uuid'
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
# context_data['charts'] = ChartRecord.objects.filter(
# track__album=self.object, rank__in=[1, 2, 3]
# )
return context_data

View File

@ -1,23 +1,18 @@
import datetime
import pytz
from django.conf import settings
from django.utils import timezone
import calendar
from datetime import datetime, timedelta
# need to translate to a non-naive timezone, even if timezone == settings.TIME_ZONE, so we can compare two dates
def to_user_timezone(date, profile):
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
return date.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)).astimezone(
pytz.timezone(timezone)
)
return date.astimezone(pytz.timezone(timezone))
def to_system_timezone(date, profile):
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
return date.replace(tzinfo=pytz.timezone(timezone)).astimezone(
pytz.timezone(settings.TIME_ZONE)
)
def to_system_timezone(date):
return date.astimezone(pytz.timezone(settings.TIME_ZONE))
def now_user_timezone(profile):
@ -25,9 +20,39 @@ def now_user_timezone(profile):
return timezone.localtime(timezone.now())
def now_system_timezone():
return (
datetime.datetime.now()
.replace(tzinfo=pytz.timezone(settings.TIME_ZONE))
.astimezone(pytz.timezone(settings.TIME_ZONE))
)
def start_of_day(dt, profile) -> datetime:
"""Get the start of the day in the profile's timezone"""
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
tzinfo = pytz.timezone(timezone)
return datetime.combine(dt, datetime.min.time(), tzinfo)
def end_of_day(dt, profile) -> datetime:
"""Get the start of the day in the profile's timezone"""
timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
tzinfo = pytz.timezone(timezone)
return datetime.combine(dt, datetime.max.time(), tzinfo)
def start_of_week(dt, profile) -> datetime:
# TODO allow profile to set start of week
return start_of_day(dt, profile) - timedelta(dt.weekday())
def end_of_week(dt, profile) -> datetime:
# TODO allow profile to set start of week
return start_of_week(dt, profile) + timedelta(days=6)
def start_of_month(dt, profile) -> datetime:
return start_of_day(dt, profile).replace(day=1)
def end_of_month(dt, profile) -> datetime:
next_month = end_of_day(dt, profile).replace(day=28) + timedelta(days=4)
# subtracting the number of the current day brings us back one month
return next_month - timedelta(days=next_month.day)
def start_of_year(dt, profile) -> datetime:
return start_of_day(dt, profile).replace(month=1, day=1)

View File

@ -6,14 +6,12 @@ from scrobbles.models import Scrobble
def now_playing(request):
user = request.user
now = timezone.now()
if user.is_authenticated:
if user.profile:
timezone.activate(pytz.timezone(user.profile.timezone))
now = timezone.localtime(timezone.now())
return {
'now_playing_list': Scrobble.objects.filter(
in_progress=True,
is_paused=False,
user=user,
)
}
if not user.is_authenticated:
return {}
return {
'now_playing_list': Scrobble.objects.filter(
in_progress=True,
is_paused=False,
user=user,
)
}

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.5 on 2023-03-03 00:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scrobbles', '0023_alter_audioscrobblertsvimport_options_and_more'),
]
operations = [
migrations.AddField(
model_name='chartrecord',
name='period_end',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='chartrecord',
name='period_start',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -1,4 +1,5 @@
import calendar
import datetime
import logging
from uuid import uuid4
@ -15,6 +16,14 @@ from scrobbles.utils import check_scrobble_for_finish
from sports.models import SportEvent
from videos.models import Series, Video
from vrobbler.apps.profiles.utils import (
end_of_day,
end_of_month,
end_of_week,
start_of_day,
start_of_month,
start_of_week,
)
from vrobbler.apps.scrobbles.stats import build_charts
logger = logging.getLogger(__name__)
@ -62,6 +71,7 @@ class BaseFileImportMixin(TimeStampedModel):
from scrobbles.models import Scrobble
if not self.process_log:
logger.warning("No lines in process log found to undo")
return
@ -256,6 +266,26 @@ class ChartRecord(TimeStampedModel):
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)
period_start = models.DateTimeField(**BNULL)
period_end = models.DateTimeField(**BNULL)
def save(self, *args, **kwargs):
profile = self.user.profile
if self.week:
# set start and end to start and end of week
period = datetime.date.fromisocalendar(self.year, self.week, 1)
self.period_start = start_of_week(period, profile)
self.period_start = end_of_week(period, profile)
if self.day:
period = datetime.datetime(self.year, self.month, self.day)
self.period_start = start_of_day(period, profile)
self.period_end = end_of_day(period, profile)
if self.month and not self.day:
period = datetime.datetime(self.year, self.month, 1)
self.period_start = start_of_month(period, profile)
self.period_end = end_of_month(period, profile)
super(ChartRecord, self).save(*args, **kwargs)
@property
def media_obj(self):
@ -312,7 +342,22 @@ class ChartRecord(TimeStampedModel):
return period
def __str__(self):
return f"#{self.rank} in {self.period} - {self.media_obj}"
title = f"#{self.rank} in {self.period}"
if self.day or self.week:
title = f"#{self.rank} on {self.period}"
return title
def link(self):
get_params = f"?date={self.year}"
if self.week:
get_params = get_params = get_params + f"-W{self.week}"
if self.month:
get_params = get_params = get_params + f"-{self.month}"
if self.day:
get_params = get_params = get_params + f"-{self.day}"
if self.artist:
get_params = get_params + "&media=Artist"
return reverse('scrobbles:charts-home') + get_params
@classmethod
def build(cls, user, **kwargs):

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -6,8 +6,11 @@ from typing import Optional
import pytz
from django.apps import apps
from django.conf import settings
from django.db.models import Count, Q, ExpressionWrapper, OuterRef, Subquery
from django.contrib.auth import get_user_model
from django.db.models import Count, Q
from django.utils import timezone
User = get_user_model()
logger = logging.getLogger(__name__)
@ -102,11 +105,11 @@ def get_scrobble_count_qs(
def build_charts(
user: "User",
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(
@ -140,3 +143,94 @@ def build_charts(
ChartRecord.objects.bulk_create(
chart_records, ignore_conflicts=True, batch_size=500
)
def build_yesterdays_charts_for_user(user: "User", model_str="Track") -> None:
"""Given a user calculate needed charts."""
ChartRecord = apps.get_model(
app_label='scrobbles', model_name='ChartRecord'
)
tz = pytz.timezone(settings.TIME_ZONE)
if user and user.is_authenticated:
tz = pytz.timezone(user.profile.timezone)
now = timezone.now().astimezone(tz)
yesterday = now - timedelta(days=1)
logger.info(
f"Generating charts for yesterday ({yesterday.date()}) for {user}"
)
# Always build yesterday's chart
ChartRecord.build(
user,
year=yesterday.year,
month=yesterday.month,
day=yesterday.day,
model_str=model_str,
)
now_week = now.isocalendar()[1]
yesterday_week = now.isocalendar()[1]
if now_week != yesterday_week:
logger.info(
f"New weekly charts for {yesterday.year}-{yesterday_week} for {user}"
)
ChartRecord.build(
user,
year=yesterday.year,
month=yesterday_week,
model_str=model_str,
)
# If the month has changed, build charts
if now.month != yesterday.month:
logger.info(
f"New monthly charts for {yesterday.year}-{yesterday.month} for {user}"
)
ChartRecord.build(
user,
year=yesterday.year,
month=yesterday.month,
model_str=model_str,
)
# If the year has changed, build charts
if now.year != yesterday.year:
logger.info(f"New annual charts for {yesterday.year} for {user}")
ChartRecord.build(user, year=yesterday.year, model_str=model_str)
def build_missing_charts_for_user(user: "User", model_str="Track") -> None:
""""""
ChartRecord = apps.get_model(
app_label='scrobbles', model_name='ChartRecord'
)
Scrobble = apps.get_model(app_label='scrobbles', model_name='Scrobble')
logger.info(f"Generating historical charts for {user}")
tz = pytz.timezone(settings.TIME_ZONE)
if user and user.is_authenticated:
tz = pytz.timezone(user.profile.timezone)
now = timezone.now().astimezone(tz)
first_scrobble = (
Scrobble.objects.filter(user=user, played_to_completion=True)
.order_by('created')
.first()
)
start_date = first_scrobble.timestamp
days_since = (now - start_date).days
for day_num in range(0, days_since):
build_date = start_date + timedelta(days=day_num)
logger.info(f"Generating chart batch for {build_date}")
ChartRecord.build(user=user, year=build_date.year)
ChartRecord.build(
user=user, year=build_date.year, week=build_date.isocalendar()[1]
)
ChartRecord.build(
user=user, year=build_date.year, month=build_date.month
)
ChartRecord.build(
user=user,
year=build_date.year,
month=build_date.month,
day=build_date.day,
)

View File

@ -1,13 +1,17 @@
import logging
from celery import shared_task
from celery import shared_task
from django.contrib.auth import get_user_model
from scrobbles.models import (
AudioScrobblerTSVImport,
KoReaderImport,
LastFmImport,
)
from vrobbler.apps.scrobbles.stats import build_yesterdays_charts_for_user
logger = logging.getLogger(__name__)
User = get_user_model()
@shared_task
@ -35,3 +39,9 @@ def process_koreader_import(import_id):
logger.warn(f"KOReaderImport not found with id {import_id}")
koreader_import.process()
@shared_task
def create_yesterdays_charts():
for user in User.objects.all():
build_yesterdays_charts_for_user(user)

View File

@ -0,0 +1,10 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def urlreplace(context, **kwargs):
query = context['request'].GET.copy()
query.update(kwargs)
return query.urlencode()

View File

@ -5,14 +5,15 @@ import requests
from django.conf import settings
THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY")
SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
ARTIST_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
ALBUM_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/searchalbum.php?s="
logger = logging.getLogger(__name__)
def lookup_artist_from_tadb(name: str) -> dict:
artist_info = {}
response = requests.get(SEARCH_URL + name)
response = requests.get(ARTIST_SEARCH_URL + name)
if response.status_code != 200:
logger.warn(f"Bad response from TADB: {response.status_code}")
@ -23,11 +24,55 @@ def lookup_artist_from_tadb(name: str) -> dict:
return {}
results = json.loads(response.content)
artist = results['artists'][0]
if results['artists']:
artist = results['artists'][0]
artist_info['biography'] = artist['strBiographyEN']
artist_info['genre'] = artist['strGenre']
artist_info['mood'] = artist['strMood']
artist_info['thumb_url'] = artist['strArtistThumb']
artist_info['biography'] = artist.get('strBiographyEN')
artist_info['genre'] = artist.get('strGenre')
artist_info['mood'] = artist.get('strMood')
artist_info['thumb_url'] = artist.get('strArtistThumb')
return artist_info
def lookup_album_from_tadb(name: str, artist: str) -> dict:
album_info = {}
response = requests.get(''.join([ALBUM_SEARCH_URL, artist, "&a=", name]))
if response.status_code != 200:
logger.warn(f"Bad response from TADB: {response.status_code}")
return {}
if not response.content:
logger.warn(f"Bad content from TADB: {response.content}")
return {}
results = json.loads(response.content)
if results['album']:
album = results['album'][0]
album_info['theaudiodb_id'] = album.get('idAlbum')
album_info['theaudiodb_description'] = album.get('strDescriptionEN')
album_info['theaudiodb_genre'] = album.get('strGenre')
album_info['theaudiodb_style'] = album.get('strStyle')
album_info['theaudiodb_mood'] = album.get('strMood')
album_info['theaudiodb_speed'] = album.get('strSpeed')
album_info['theaudiodb_theme'] = album.get('strTheme')
album_info['allmusic_id'] = album.get('strAllMusicID')
album_info['wikipedia_slug'] = album.get('strWikipediaID')
album_info['discogs_id'] = album.get('strDiscogsID')
album_info['wikidata_id'] = album.get('strWikidataID')
album_info['rateyourmusic_id'] = album.get('strRateYourMusicID')
if album.get('intYearReleased'):
album_info['theaudiodb_year_released'] = float(
album.get('intYearReleased')
)
if album.get('intScore'):
album_info['theaudiodb_score'] = float(album.get('intScore'))
if album.get('intScoreVotes'):
album_info['theaudiodb_score_votes'] = int(
album.get('intScoreVotes')
)
return album_info

View File

@ -2,7 +2,6 @@ import calendar
import json
import logging
from datetime import datetime, timedelta
from django.db.models.query import QuerySet
import pytz
from django.conf import settings
@ -10,6 +9,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.db.models.fields import timezone
from django.db.models.query import QuerySet
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
from django.urls import reverse, reverse_lazy
from django.utils import timezone
@ -17,12 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic import DetailView, FormView, TemplateView
from django.views.generic.edit import CreateView
from django.views.generic.list import ListView
from music.aggregators import (
scrobble_counts,
top_artists,
top_tracks,
week_of_scrobbles,
)
from music.aggregators import scrobble_counts, week_of_scrobbles
from rest_framework import status
from rest_framework.decorators import (
api_view,
@ -62,6 +57,8 @@ from scrobbles.tasks import (
)
from scrobbles.thesportsdb import lookup_event_from_thesportsdb
from vrobbler.apps.music.aggregators import live_charts
logger = logging.getLogger(__name__)
@ -70,34 +67,67 @@ class RecentScrobbleList(ListView):
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
view = self.request.GET.get('view', 'vrobbler')
user = self.request.user
if user.is_authenticated:
if view == 'vrobbler':
completed_for_user = Scrobble.objects.filter(
played_to_completion=True, user=user
)
data['video_scrobble_list'] = completed_for_user.filter(
video__isnull=False
).order_by('-timestamp')[:15]
completed_for_user = Scrobble.objects.filter(
played_to_completion=True, user=user
)
data['video_scrobble_list'] = completed_for_user.filter(
video__isnull=False
).order_by('-timestamp')[:15]
data['podcast_scrobble_list'] = completed_for_user.filter(
podcast_episode__isnull=False
).order_by('-timestamp')[:15]
data['podcast_scrobble_list'] = completed_for_user.filter(
podcast_episode__isnull=False
).order_by('-timestamp')[:15]
data['sport_scrobble_list'] = completed_for_user.filter(
sport_event__isnull=False
).order_by('-timestamp')[:15]
data['sport_scrobble_list'] = completed_for_user.filter(
sport_event__isnull=False
).order_by('-timestamp')[:15]
data[
'active_imports'
] = AudioScrobblerTSVImport.objects.filter(
processing_started__isnull=False,
processed_finished__isnull=True,
user=self.request.user,
)
if view == "maloja":
artist_params = {'user': user, 'media_type': 'Artist'}
data['current_artist_charts'] = {
"today": live_charts(
**artist_params, chart_period="today", limit=14
),
"week": live_charts(
**artist_params, chart_period="week", limit=14
),
"month": live_charts(
**artist_params, chart_period="month", limit=14
),
"all": live_charts(**artist_params, limit=14),
}
track_params = {'user': user, 'media_type': 'Track'}
data['current_track_charts'] = {
"today": live_charts(
**track_params, chart_period="today", limit=14
),
"week": live_charts(
**track_params, chart_period="week", limit=14
),
"month": live_charts(
**track_params, chart_period="month", limit=14
),
"all": live_charts(**track_params, limit=14),
}
data["view"] = view
data["weekly_data"] = week_of_scrobbles(user=user)
data['counts'] = scrobble_counts(user)
data['imdb_form'] = ScrobbleForm
data['export_form'] = ExportScrobbleForm
data['active_imports'] = AudioScrobblerTSVImport.objects.filter(
processing_started__isnull=False,
processed_finished__isnull=True,
user=self.request.user,
)
return data
def get_queryset(self):
@ -406,19 +436,17 @@ class ChartRecordView(TemplateView):
template_name = 'scrobbles/chart_index.html'
@staticmethod
def get_media_filter(media_type: str = "Track"):
media_filter = Q()
if media_type == 'Track':
media_filter = Q(track__isnull=False)
if media_type == 'Artist':
media_filter = Q(artist__isnull=False)
if media_type == 'Series':
media_filter = Q(series__isnull=False)
if media_type == 'Video':
media_filter = Q(video__isnull=False)
return media_filter
def get_media_filter(media_type: str = "") -> Q:
filters = {
"Track": Q(track__isnull=False),
"Artist": Q(artist__isnull=False),
"Series": Q(series__isnull=False),
"Video": Q(video__isnull=False),
"": Q(),
}
return filters[media_type]
def get_chart_records(self, media_type: str = "Track", **kwargs):
def get_chart_records(self, media_type: str = "", **kwargs):
media_filter = self.get_media_filter(media_type)
charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **kwargs
@ -434,62 +462,52 @@ class ChartRecordView(TemplateView):
return charts
def get_chart(
self, period: str = "all_time", limit=15, media: str = "Track"
self, period: str = "all_time", limit=15, media: str = ""
) -> QuerySet:
chart = QuerySet()
now = timezone.now()
if period == "all_time":
chart = self.get_chart_records(media_type=media)
params = {}
params['media_type'] = media
if period == "today":
chart = self.get_chart_records(
media_type=media,
day=now.day,
month=now.month,
year=now.year,
)
params['day'] = now.day
params['month'] = now.month
params['year'] = now.year
if period == "week":
chart = self.get_chart_records(
media_type=media,
year=now.year,
week=now.isocalendar()[1],
)
params['week'] = now.ioscalendar()[1]
params['year'] = now.year
if period == "month":
chart = self.get_chart_records(
media_type=media,
year=now.year,
month=now.month,
)
params['month'] = now.month
params['year'] = now.year
if period == "year":
chart = self.get_chart_records(
media_type=media,
year=now.year,
)
return chart[:limit]
params['year'] = now.year
return self.get_chart_records(**params)[:limit]
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
date = self.request.GET.get('date')
media_type = self.request.GET.get('media')
date = self.request.GET.get("date")
media_type = self.request.GET.get("media", "Track")
user = self.request.user
params = {}
context_data['artist_charts'] = {}
context_data["artist_charts"] = {}
if not date:
context_data['artist_charts'] = {
"today": top_artists(user, filter="today")[:30],
"week": top_artists(user, filter="week")[:30],
"month": top_artists(user, filter="month")[:30],
"all": top_artists(user),
artist_params = {'user': user, 'media_type': 'Artist'}
context_data['current_artist_charts'] = {
"today": live_charts(**artist_params, chart_period="today"),
"week": live_charts(**artist_params, chart_period="week"),
"month": live_charts(**artist_params, chart_period="month"),
"all": live_charts(**artist_params),
}
context_data['track_charts'] = {
"today": top_tracks(user, filter="today")[:30],
"week": top_tracks(user, filter="week")[:30],
"month": top_tracks(user, filter="month")[:30],
"all": top_tracks(user),
track_params = {'user': user, 'media_type': 'Track'}
context_data['current_track_charts'] = {
"today": live_charts(**track_params, chart_period="today"),
"week": live_charts(**track_params, chart_period="week"),
"month": live_charts(**track_params, chart_period="month"),
"all": live_charts(**track_params),
}
return context_data
# Date provided, lookup past charts, returning nothing if it's now or in the future.
now = timezone.now()
year = now.year
params = {'year': year}
@ -526,23 +544,35 @@ class ChartRecordView(TemplateView):
now.month == month and now.year == year and now.day == day
)
media_filter = self.get_media_filter(media_type)
charts = ChartRecord.objects.filter(
media_filter = self.get_media_filter("Track")
track_charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **params
).order_by("rank")
media_filter = self.get_media_filter("Artist")
artist_charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **params
).order_by("rank")
if charts.count() == 0:
if track_charts.count() == 0 and not in_progress:
ChartRecord.build(
user=self.request.user, model_str=media_type, **params
user=self.request.user, model_str="Track", **params
)
charts = ChartRecord.objects.filter(
media_filter = self.get_media_filter("Track")
track_charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **params
).order_by("rank")
if artist_charts.count() == 0 and not in_progress:
ChartRecord.build(
user=self.request.user, model_str="Artist", **params
)
media_filter = self.get_media_filter("Artist")
artist_charts = ChartRecord.objects.filter(
media_filter, user=self.request.user, **params
).order_by("rank")
if in_progress:
# TODO recalculate
...
context_data['name'] = name
context_data['media_type'] = media_type
context_data['track_charts'] = track_charts
context_data['artist_charts'] = artist_charts
context_data['name'] = " ".join(["Top", media_type, "for", name])
context_data['in_progress'] = in_progress
return context_data

View File

View File

@ -0,0 +1,52 @@
from rest_framework import serializers
from sports.models import (
League,
SportEvent,
Round,
Player,
Team,
Season,
Sport,
)
class SportEventSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SportEvent
fields = "__all__"
class LeagueSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = League
fields = "__all__"
class RoundSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Round
fields = "__all__"
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Player
fields = "__all__"
class TeamSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Team
fields = "__all__"
class SeasonSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Season
fields = "__all__"
class SportSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Sport
fields = "__all__"

View File

@ -0,0 +1,61 @@
from rest_framework import permissions, viewsets
from sports.api.serializers import (
LeagueSerializer,
PlayerSerializer,
RoundSerializer,
SeasonSerializer,
SportEventSerializer,
SportSerializer,
TeamSerializer,
)
from sports.models import (
League,
Player,
Round,
Season,
Sport,
SportEvent,
Team,
)
class SportEventViewSet(viewsets.ModelViewSet):
queryset = SportEvent.objects.all().order_by('-created')
serializer_class = SportEventSerializer
permission_classes = [permissions.IsAuthenticated]
class LeagueViewSet(viewsets.ModelViewSet):
queryset = League.objects.all().order_by('-created')
serializer_class = LeagueSerializer
permission_classes = [permissions.IsAuthenticated]
class RoundViewSet(viewsets.ModelViewSet):
queryset = Round.objects.all().order_by('-created')
serializer_class = RoundSerializer
permission_classes = [permissions.IsAuthenticated]
class SportViewSet(viewsets.ModelViewSet):
queryset = Sport.objects.all().order_by('-created')
serializer_class = SportSerializer
permission_classes = [permissions.IsAuthenticated]
class PlayerViewSet(viewsets.ModelViewSet):
queryset = Player.objects.all().order_by('-created')
serializer_class = PlayerSerializer
permission_classes = [permissions.IsAuthenticated]
class TeamViewSet(viewsets.ModelViewSet):
queryset = Team.objects.all().order_by('-created')
serializer_class = TeamSerializer
permission_classes = [permissions.IsAuthenticated]
class SeasonViewSet(viewsets.ModelViewSet):
queryset = Season.objects.all().order_by('-created')
serializer_class = SeasonSerializer
permission_classes = [permissions.IsAuthenticated]

View File

@ -0,0 +1,18 @@
from django.urls import path
from sports import views
app_name = 'sports'
urlpatterns = [
path(
'sport-events/',
views.SportEventListView.as_view(),
name='event_list',
),
path(
'sport-events/<slug:slug>/',
views.SportEventDetailView.as_view(),
name='event_detail',
),
]

View File

@ -0,0 +1,12 @@
from django.views import generic
from sports.models import SportEvent
class SportEventListView(generic.ListView):
model = SportEvent
paginate_by = 50
class SportEventDetailView(generic.DetailView):
model = SportEvent
slug_field = 'uuid'

View File

@ -231,11 +231,10 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "static/"
STATIC_URL = "/static/"
STATIC_ROOT = os.getenv(
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
)
MEDIA_URL = "/media/"
MEDIA_ROOT = os.getenv(
"VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media")

View File

@ -10,6 +10,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
@ -211,6 +212,7 @@
Dashboard
</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/charts/">
<span data-feather="bar-chart"></span>
@ -229,6 +231,12 @@
Artists
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/albums/">
<span data-feather="music"></span>
Albums
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/movies/">
<span data-feather="film"></span>
@ -241,7 +249,6 @@
TV Shows
</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="/admin/">
<span data-feather="key"></span>
@ -260,13 +267,13 @@
{% for scrobble in now_playing_list %}
<div>
{% if scrobble.media_obj.album.cover_image %}
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120
style="border:1px solid black; " /></td><br/>
<td><img src="{{scrobble.track.album.cover_image.url}}" width=120 height=120 style="border:1px solid black; " /></td><br/>
{% endif %}
{{scrobble.media_obj.title}}<br/>
{% if scrobble.media_obj.subtitle %}<em>{{scrobble.media_obj.subtitle}}</em><br/>{% endif %}
<small>{{scrobble.timestamp|naturaltime}}<br/>
from {{scrobble.source}}</small>
<a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a><br/>
{% if scrobble.media_obj.subtitle %}
<em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em><br/>
{% endif %}
<small>{{scrobble.timestamp|naturaltime}}<br/> from {{scrobble.source}}</small>
<div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
@ -295,54 +302,6 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script><!-- comment ------------------------------------------------->
/* globals Chart:false, feather:false */
(function () {
'use strict'
feather.replace({ 'aria-hidden': 'true' })
// Graphs
var ctx = document.getElementById('myChart')
// eslint-disable-next-line no-unused-vars
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: [
{% for day in weekly_data.keys %}
"{{day}}"{% if not forloop.last %},{% endif %}
{% endfor %}
],
datasets: [{
data: [
{% for count in weekly_data.values %}
{{count}}{% if not forloop.last %},{% endif %}
{% endfor %}
],
lineTension: 0,
backgroundColor: 'transparent',
borderColor: '#007bf0',
borderWidth: 4,
pointBackgroundColor: '#007bff'
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: false
}
}]
},
legend: {
display: false
}
}
})
})()
</script>
{% block extra_js %}
{% endblock %}
</body>

View File

@ -0,0 +1,85 @@
{% extends "base_list.html" %}
{% load mathfilters %}
{% block title %}{{object.name}}{% endblock %}
{% block lists %}
<div class="row">
{% if object.cover_image %}
<p style="float:left; width:302px; padding:0; border: 1px solid #ccc">
<img src="{{object.cover_image.url}}" width=300 height=300 />
</p>
{% endif %}
<div style="float:left; width:600px; margin-left:10px; ">
{% if object.theaudiodb_description %}
<p>{{object.theaudiodb_description|safe|linebreaks|truncatewords:160}}</p>
<hr/>
{% endif %}
<p><a href="{{album.mb_link}}">Musicbrainz</a> {% if album.tadb_link %}| <a href="{{album.tadb_link}}">TheAudioDB</a>{% endif %} {% if album.allmusic_link %}| <a href="{{album.allmusic_link}}">AllMusic</a>{% endif %}</p>
</div>
</div>
<div class="row">
<p>{{object.scrobbles.count}} scrobbles</p>
{% if charts %}
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
{% endif %}
<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">Artist</th>
<th scope="col">Count</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for track in object.tracks %}
<tr>
<td>{{rank}}#1</td>
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></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">Artist</th>
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobbles %}
<tr>
<td>{{scrobble.timestamp}}</td>
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
<td><a href="{{scrobble.track.artist.get_absolute_url}}">{{scrobble.track.artist.name}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,11 +1,82 @@
{% extends "base_list.html" %}
{% load urlreplace %}
{% 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 %}
<div class="row">
<p class="view">
<span class="view-links">
{% if view == 'grid' %}
View as <a href="?{% urlreplace view='list' %}">List</a>
{% else %}
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
{% endif %}
</span>
</p>
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</p>
<hr />
{% if view == 'grid' %}
<div>
{% for album in object_list %}
{% if album.cover_image %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{album.cover_image.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Scrobbles</th>
<th scope="col">Album</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for album in object_list %}
<tr>
<td>{{album.scrobbles.count}}</td>
<td><a href="{{album.get_absolute_url}}">{{album}}</a></td>
<td><a href="{{album.primary_artist.get_absolute_url}}">{{album.primary_artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -1,19 +1,35 @@
{% extends "base_detail.html" %}
{% extends "base_list.html" %}
{% load mathfilters %}
{% block title %}{{object.name}}{% endblock %}
{% block details %}
{% block lists %}
<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>
{% if object.thumbnail %}
<p style="float:left; width:300px; margin-right:10px;">
<img style="border:1px solid #ccc;" src="{{artist.thumbnail.url}}" width=300 height=300 />
</p>
{% else %}
{% if object.album_set.first.cover_image %}
<p style="float:left; width:302px; padding:0; border: 1px solid #ccc">
<img src="{{object.album_set.first.cover_image.url}}" width=300 height=300 />
</p>
{% endif %}
{% endfor %}
{% endif %}
<div style="float:left; width:600px; margin-left:10px; ">
{% if artist.biography %}
<p>{{artist.biography|safe|linebreaks|truncatewords:160}}</p>
<hr/>
{% endif %}
<p><a href="{{artist.mb_link}}">Musicbrainz</a></p>
</div>
</div>
<div class="row">
<p>{{artist.scrobbles.count}} scrobbles</p>
{% if charts %}
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
{% endif %}
<div class="col-md">
<h3>Top tracks</h3>
<div class="table-responsive">
@ -22,19 +38,21 @@
<tr>
<th scope="col">Rank</th>
<th scope="col">Track</th>
<th scope="col">Album</th>
<th scope="col">Count</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for track in object.tracks %}
{% for track in tracks_ranked %}
<tr>
<td>{{rank}}#1</td>
<td>{{track.title}}</td>
<td>{{track.scrobble_count}}</td>
<td>#{{track.0}}</td>
<td><a href="{{track.1.get_absolute_url}}">{{track.1.title}}</a></td>
<td><a href="{{track.1.album.get_absolute_url}}">{{track.1.album}}</a></td>
<td>{{track.1.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>
<span class="progress-bar-fill" style="width: {{track.1.scrobble_count|mul:10}}%;"></span>
</div>
</td>
</tr>
@ -60,8 +78,8 @@
{% for scrobble in object.scrobbles %}
<tr>
<td>{{scrobble.timestamp}}</td>
<td>{{scrobble.track.title}}</td>
<td>{{scrobble.track.album.name}}</td>
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
</tr>
{% endfor %}
</tbody>

View File

@ -1,30 +1,79 @@
{% extends "base_list.html" %}
{% load urlreplace %}
{% block title %}Artists{% endblock %}
{% block lists %}
<div class="row">
<p class="view">
<span class="view-links">
{% if view == 'grid' %}
View as <a href="?{% urlreplace view='list' %}">List</a>
{% else %}
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
{% endif %}
</span>
</p>
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</p>
<hr />
{% if view == 'grid' %}
<div>
{% for artist in object_list %}
{% if artist.thumbnail %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{artist.thumbnail.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<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>
<th scope="col">Artist</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>
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -1,13 +1,43 @@
{% extends "base_detail.html" %}
{% extends "base_list.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 %}
{% block lists %}
<div class="row">
{% if track.album.cover_image %}
<p style="width:150px; float:left;"><img src="{{track.album.cover_image.url}}" width=150 height=150 /></p>
{% endif %}
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
{% if charts %}
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
{% endif %}
<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>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
<tr>
<td>{{scrobble.timestamp}}</td>
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album}}</a></td>
<td><a href="{{scrobble.track.artist.get_absolute_url}}">{{scrobble.track.artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,10 +3,57 @@
{% 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 %}
<div class="row">
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</p>
<hr />
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Scrobbles</th>
<th scope="col">Track</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for track in object_list %}
<tr>
<td>{{track.scrobble_set.count}}</td>
<td><a href="{{track.get_absolute_url}}">{{track}}</a></td>
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -4,23 +4,9 @@
{% block lists %}
<div class="row">
<h2>Top Artists</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for chart_name in artist_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="artist-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="artistTabContent">
{% for chart_name, artists in artist_charts.items %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}" id="artist-{{chart_name}}" role="tabpanel"
aria-labelledby="artist-{[chart}}-tab">
{% if artist_charts %}
<div class="col-md">
<div class="tab-content" id="artistTabContent">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
@ -31,64 +17,134 @@
</tr>
</thead>
<tbody>
{% for artist in artists %}
{% for chart in artist_charts %}
<tr>
<td>{{artist.rank}}</td>
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
<td>{{artist.num_scrobbles}}</td>
<td>{{chart.rank}}</td>
<td><a href="{{chart.media_obj.get_absolute_url}}">{{chart.media_obj}}</a></td>
<td>{{chart.count}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="row">
<h2>Top Tracks</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for chart_name in track_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="track-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#track-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="trackTabContent">
{% for chart_name, tracks in track_charts.items %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}" id="track-{{chart_name}}" role="tabpanel"
aria-labelledby="track-{[chart_name}}-tab">
{% if track_charts %}
<div class="col-md">
<div class="tab-content" id="artistTabContent">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Rank</th>
<th scope="col">Artist</th>
<th scope="col">Track</th>
<th scope="col">Scrobbles</th>
</tr>
</thead>
<tbody>
{% for track in tracks %}
{% for chart in track_charts %}
<tr>
<td>{{track.rank}}</td>
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
<td>{{track.num_scrobbles}}</td>
<td>{{chart.rank}}</td>
<td><a href="{{chart.media_obj.get_absolute_url}}">{{chart.media_obj.title}}</a></td>
<td>{{chart.count}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if current_artist_charts %}
<div class="col-md">
<h2>Top Artists</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for chart_name in current_artist_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="artist-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="artistTabContent">
{% for chart_name, artists in current_artist_charts.items %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}" id="artist-{{chart_name}}" role="tabpanel"
aria-labelledby="artist-{[chart}}-tab">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Artist</th>
<th scope="col">Scrobbles</th>
</tr>
</thead>
<tbody>
{% for artist in artists %}
<tr>
<td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
<td>{{artist.num_scrobbles}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% if current_track_charts %}
<div class="col-md">
<h2>Top Tracks</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for chart_name in current_track_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="track-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#track-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="trackTabContent">
{% for chart_name, tracks in current_track_charts.items %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}" id="track-{{chart_name}}" role="tabpanel"
aria-labelledby="track-{[chart_name}}-tab">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Track</th>
<th scope="col">Artist</th>
<th scope="col">Scrobbles</th>
</tr>
</thead>
<tbody>
{% for track in tracks %}
<tr>
<td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
<td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
<td>{{track.num_scrobbles}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,12 +1,51 @@
{% extends "base.html" %}
{% load humanize %}
{% block head_extra %}
<style>
h2 { padding-top:20px; }
.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%;
background:rgba(0,0,0,0.3);
}
.caption-medium {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 75%;
background:rgba(0,0,0,0.3);
}
.caption-small {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 60%;
background:rgba(0,0,0,0.3);
}
.maloja-chart img { border: 1px solid #ccc; }
</style>
{% endblock %}
{% block content %}
<main class="col-md-9 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">Dashboard</h1>
<h1 class="h2">Dashboard</h1><span><a href="/?view=maloja">like maloja</a></span>
<div class="btn-toolbar mb-2 mb-md-0">
{% if user.is_authenticated %}
<div class="btn-group me-2">
@ -25,7 +64,7 @@
<div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span data-feather="calendar"></span>
<div data-feather="calendar"></div>
This week
</button>
<div class="dropdown-menu" data-bs-toggle="#graphDataChange" aria-labelledby="graphDateButton">
@ -36,14 +75,253 @@
</div>
</div>
{% if view == "vrobbler" %}
<canvas class="my-4 w-100" id="myChart" width="900" height="220"></canvas>
{% endif %}
<div class="container">
{% if user.is_authenticated %}
{% if view == "maloja" %}
<div class="row">
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> |
This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
<h2>Top Artist</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for chart_name in current_artist_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}"
id="artist-{{chart_name}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{chart_name}}"
type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="artistTabContent" class="maloja-chart">
{% for chart_name, artists in current_artist_charts.items %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}" id="artist-{{chart_name}}" role="tabpanel" aria-labelledby="artist-{{chart_name}}-tab">
<div style="display:block">
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">{{artists.0.name}}</div>
{% if artists.0.thumbnail %}
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{{artists.0.thumbnail.url}}" width="300px"></a>
{% endif %}
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.1.name}}</div>
{% if artists.1.thumbnail %}
<a href="{{artists.1.get_absolute_url}}"><img lt="{{artists.1.name}}" src="{{artists.1.thumbnail.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.2.name}}</div>
{% if artists.2.thumbnail %}
<a href="{{artists.2.get_absolute_url}}"><img lt="{{artists.2.name}}" src="{{artists.2.thumbnail.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.3.name}}</div>
{% if artists.3.thumbnail %}
<a href="{{artists.3.get_absolute_url}}"><img lt="{{artists.3.name}}" src="{{artists.3.thumbnail.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">{{artists.4.name}}</div>
{% if artists.4.thumbnail %}
<a href="{{artists.4.get_absolute_url}}"><img lt="{{artists.4.name}}" src="{{artists.4.thumbnail.url}}" width="150px"></a>
{% endif %}
</div>
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.5.name}}</div>
{% if artists.5.thumbnail %}
<a href="{{artists.5.get_absolute_url}}"><img lt="{{artists.5.name}}" src="{{artists.5.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.6.name}}</div>
{% if artists.6.thumbnail %}
<a href="{{artists.6.get_absolute_url}}"><img lt="{{artists.6.name}}" src="{{artists.6.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.7.name}}</div>
{% if artists.7.thumbnail %}
<a href="{{artists.7.get_absolute_url}}"><img lt="{{artists.7.name}}" src="{{artists.7.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.8.name}}</div>
{% if artists.8.thumbnail %}
<a href="{{artists.8.get_absolute_url}}"><img lt="{{artists.8.name}}" src="{{artists.8.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.9.name}}</div>
{% if artists.9.thumbnail %}
<a href="{{artists.9.get_absolute_url}}"><img lt="{{artists.9.name}}" src="{{artists.9.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.10.name}}</div>
{% if artists.10.thumbnail %}
<a href="{{artists.10.get_absolute_url}}"><img lt="{{artists.10.name}}" src="{{artists.10.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.11.name}}</div>
{% if artists.11.thumbnail %}
<a href="{{artists.11.get_absolute_url}}"><img lt="{{artists.11.name}}" src="{{artists.11.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.12.name}}</div>
{% if artists.12thumbnail %}
<a href="{{artists.12.get_absolute_url}}"><img lt="{{artists.13.name}}" src="{{artists.12.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{artists.13.name}}</div>
{% if artists.13.thumbnail %}
<a href="{{artists.13.get_absolute_url}}"><img lt="{{artists.13.name}}" src="{{artists.13.thumbnail.url}}" width="100px"></a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row">
<h2>Top Tracks</h2>
<ul class="nav nav-tabs" id="trackTab" role="tablist">
{% for chart_name in current_track_charts.keys %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="track-{{chart_name}}-tab" data-bs-toggle="tab"
data-bs-target="#track-{{chart_name}}" type="button" role="tab" aria-controls="home" aria-selected="true">
{{chart_name}}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="trackTabContent" class="maloja-chart">
{% for chart_name, tracks in current_track_charts.items %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}" id="track-{{chart_name}}" role="tabpanel" aria-labelledby="track-{{chart_name}}-tab">
<div style="display:block">
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">{{tracks.0.title}}</div>
{% if tracks.0.album.cover_image %}
<a href="{{tracks.0.get_absolute_url}}"><img lt="{{tracks.0.title}}" src="{{tracks.0.album.cover_image.url}}" width="300px"></a>
{% endif %}
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.1.title}}</div>
{% if tracks.1.album.cover_image %}
<a href="{{tracks.1.get_absolute_url}}"><img lt="{{tracks.1.title}}" src="{{tracks.1.album.cover_image.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.2.title}}</div>
{% if tracks.2.album.cover_image %}
<a href="{{tracks.2.get_absolute_url}}"><img lt="{{tracks.2.title}}" src="{{tracks.2.album.cover_image.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.3.title}}</div>
{% if tracks.3.album.cover_image %}
<a href="{{tracks.3.get_absolute_url}}"><img lt="{{tracks.3.title}}" src="{{tracks.3.album.cover_image.url}}" width="150px"></a>
{% endif %}
</div>
<div class="image-wrapper" style="width:50%">
<div class="caption-medium">{{tracks.4.title}}</div>
{% if tracks.4.album.cover_image %}
<a href="{{tracks.4.get_absolute_url}}"><img lt="{{tracks.4.title}}" src="{{tracks.4.album.cover_image.urL}}" width="150px"></a>
{% endif %}
</div>
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.5.title}}</div>
{% if tracks.5.album.cover_image %}
<a href="{{tracks.5.get_absolute_url}}"><img lt="{{tracks.5.title}}" src="{{tracks.5.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.6.title}}</div>
{% if tracks.6.album.cover_image %}
<a href="{{tracks.6.get_absolute_url}}"><img lt="{{tracks.6.title}}" src="{{tracks.6.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.7.title}}</div>
{% if tracks.7.album.cover_image %}
<a href="{{track.7.get_absolute_url}}"><img lt="{{tracks.7.title}}" src="{{tracks.7.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.8.title}}</div>
{% if tracks.8.album.cover_image %}
<a href="{{track.8.get_absolute_url}}"><img lt="{{tracks.8.title}}" src="{{tracks.8.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.9.title}}</div>
{% if tracks.9.album.cover_image %}
<a href="{{track.9.get_absolute_url}}"><img lt="{{tracks.9.title}}" src="{{tracks.9.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.10.title}}</div>
{% if tracks.10.album.cover_image %}
<a href="{{tracks.10.get_absolute_url}}"><img lt="{{tracks.10.title}}" src="{{tracks.10.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.11.title}}</div>
{% if tracks.11.album.cover_image %}
<a href="{{track.11.get_absolute_url}}"><img lt="{{tracks.11.title}}" src="{{tracks.11.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.12.title}}</div>
{% if tracks.12.album.cover_image %}
<a href="{{track.12.get_absolute_url}}"><img lt="{{tracks.12.title}}" src="{{tracks.12.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">{{tracks.13.title}}</div>
{% if tracks.13.album.cover_image %}
<a href="{{track.13.get_absolute_url}}"><img lt="{{tracks.13.title}}" src="{{tracks.13.album.cover_image.url}}" width="100px"></a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="row">
<h2>Last Scrobbles</h2>
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> |
This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
</div>
<div class="row">
<ul class="nav nav-tabs" id="myTab" role="tablist">
@ -68,7 +346,6 @@
<div class="tab-content" id="myTabContent2">
<div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
aria-labelledby="latest-listened-tab">
<h2>Latest listened</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
@ -84,13 +361,12 @@
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
{% if scrobble.track.album.cover_image %}
<td><img src="{{scrobble.track.album.cover_image.url}}" width=25 height=25
style="border:1px solid black;" /></td>
<td><a href="{{scrobble.track.album.get_absolute_url}}"><img src="{{scrobble.track.album.cover_image.url}}" width=25 height=25 style="border:1px solid black;" /></aa></td>
{% else %}
<td>{{scrobble.track.album.name}}</td>
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
{% endif %}
<td>{{scrobble.track.title}}</td>
<td>{{scrobble.track.artist.name}}</td>
<td><a href="{{scrobble.track.get_absolute_url }}">{{scrobble.track.title}}</a></td>
<td><a href="{{scrobble.track.artist.get_absolute_url }}">{{scrobble.track.artist.name}}</aa></td>
</tr>
{% endfor %}
</tbody>
@ -184,7 +460,7 @@
<div class="modal-header">
<h5 class="modal-title" id="importModalLabel">Import scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
<div aria-hidden="true">&times;</div>
</button>
</div>
<form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
@ -222,7 +498,7 @@
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
<div aria-hidden="true">&times;</div>
</button>
</div>
<form action="{% url 'scrobbles:export' %}" method="get">
@ -247,4 +523,53 @@
$('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') });
$('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') });
</script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script><!-- comment ------------------------------------------------->
/* globals Chart:false, feather:false */
(function () {
'use strict'
feather.replace({ 'aria-hidden': 'true' })
// Graphs
var ctx = document.getElementById('myChart')
// eslint-disable-next-line no-unused-vars
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: [
{% for day in weekly_data.keys %}
"{{day}}"{% if not forloop.last %},{% endif %}
{% endfor %}
],
datasets: [{
data: [
{% for count in weekly_data.values %}
{{count}}{% if not forloop.last %},{% endif %}
{% endfor %}
],
lineTension: 0,
backgroundColor: 'transparent',
borderColor: '#007bf0',
borderWidth: 4,
pointBackgroundColor: '#007bff'
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: false
}
}]
},
legend: {
display: false
}
}
})
})()
</script>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "base_detail.html" %}
{% block title %}{{object.title}} - {{object.round.season.league}}{% endblock %}
{% block details %}
<div class="row">
<h2>{{object.tv_series}}</h2>
<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">Season</th>
<th scope="col">League</th>
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
<tr>
<td>{{scrobble.timestamp}}</td>
<td>{{scrobble.media_obj.round.season.name}}</td>
<td>{{scrobble.media_obj.round.season.league}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "base_list.html" %}
{% block title %}Sport events{% 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">Title</th>
<th scope="col">Date</th>
<th scope="col">Round</th>
<th scope="col">League</th>
<th scope="col">Scrobbles</th>
<th scope="col">All time</th>
</tr>
</thead>
<tbody>
{% for obj in object_list %}
<tr>
<td><a href="{{obj.get_absolute_url}}">{{obj.title}}</a></td>
<td>{{obj.start}}</td>
<td>{{obj.round.name}}</td>
<td>{{obj.round.league}}</td>
<td>{{obj.scrobble_set.count}}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,11 +1,9 @@
{% extends "base_detail.html" %}
{% block title %}{{object.name}}{% endblock %}
{% block title %}{{object.title}}{% if object.tv_series %} - {{object.tv_series}}{% endif %}{% endblock %}
{% block details %}
<div class="row">
<h2>{{object.tv_series}}</h2>
<div class="col-md">
<h3>Last scrobbles</h3>
<div class="table-responsive">
@ -14,9 +12,11 @@
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
{% if object.tv_series %}
<th scope="col">Series</th>
<th scope="col">Season</th>
<th scope="col">Episode</th>
{% endif %}
</tr>
</thead>
<tbody>
@ -24,9 +24,11 @@
<tr>
<td>{{scrobble.timestamp}}</td>
<td>{{scrobble.video.title}}</td>
{% if object.tv_series %}
<td>{{scrobble.video.tv_series}}</td>
<td>{{scrobble.video.season_number}}</td>
<td>{{scrobble.video.episode_number}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
@ -34,4 +36,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -1,12 +1,11 @@
import scrobbles.views as scrobbles_views
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
import vrobbler.apps.scrobbles.views as scrobbles_views
from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
from vrobbler.apps.music import urls as music_urls
from vrobbler.apps.sports import urls as sports_urls
from vrobbler.apps.music.api.views import (
AlbumViewSet,
ArtistViewSet,
@ -20,6 +19,14 @@ from vrobbler.apps.scrobbles.api.views import (
LastFmImportViewSet,
ScrobbleViewSet,
)
from vrobbler.apps.sports.api.views import (
LeagueViewSet,
PlayerViewSet,
SeasonViewSet,
SportEventViewSet,
SportViewSet,
TeamViewSet,
)
from vrobbler.apps.videos import urls as video_urls
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
@ -35,6 +42,12 @@ router.register(r'series', SeriesViewSet)
router.register(r'videos', VideoViewSet)
router.register(r'authors', AuthorViewSet)
router.register(r'books', BookViewSet)
router.register(r'leagues', LeagueViewSet)
router.register(r'sports', SportViewSet)
router.register(r'seasons', SeasonViewSet)
router.register(r'players', PlayerViewSet)
router.register(r'sport-events', SportEventViewSet)
router.register(r'teams', TeamViewSet)
router.register(r'users', UserViewSet)
router.register(r'user_profiles', UserProfileViewSet)
@ -45,16 +58,9 @@ urlpatterns = [
path("accounts/", include("allauth.urls")),
path("", include(music_urls, namespace="music")),
path("", include(video_urls, namespace="videos")),
path("", include(sports_urls, namespace="sports")),
path("", include(scrobble_urls, namespace="scrobbles")),
path(
"", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"
),
]
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
)
urlpatterns += static(
settings.STATIC_URL, document_root=settings.STATIC_ROOT
)