diff --git a/vrobbler/apps/music/aggregators.py b/vrobbler/apps/music/aggregators.py
index 440c768..c6d1903 100644
--- a/vrobbler/apps/music/aggregators.py
+++ b/vrobbler/apps/music/aggregators.py
@@ -55,7 +55,9 @@ def scrobble_counts(user=None):
return data
-def week_of_scrobbles(user=None, media: str = 'tracks') -> dict[str, int]:
+def week_of_scrobbles(
+ user=None, start=None, media: str = 'tracks'
+) -> dict[str, int]:
now = timezone.now()
user_filter = Q()
@@ -63,9 +65,8 @@ def week_of_scrobbles(user=None, media: str = 'tracks') -> dict[str, int]:
now = now_user_timezone(user.profile)
user_filter = Q(user=user)
- start_of_today = datetime.combine(
- now.date(), datetime.min.time(), now.tzinfo
- )
+ if not start:
+ start = datetime.combine(now.date(), datetime.min.time(), now.tzinfo)
scrobble_day_dict = {}
base_qs = Scrobble.objects.filter(user_filter, played_to_completion=True)
@@ -77,7 +78,7 @@ def week_of_scrobbles(user=None, media: str = 'tracks') -> dict[str, int]:
media_filter = Q(video__video_type=Video.VideoType.TV_EPISODE)
for day in range(6, -1, -1):
- start = start_of_today - timedelta(days=day)
+ start = start - timedelta(days=day)
end = datetime.combine(start, datetime.max.time(), now.tzinfo)
day_of_week = start.strftime('%A')
diff --git a/vrobbler/apps/music/views.py b/vrobbler/apps/music/views.py
index aae396b..f2f7b60 100644
--- a/vrobbler/apps/music/views.py
+++ b/vrobbler/apps/music/views.py
@@ -1,3 +1,4 @@
+from django.db.models import Count
from django.views import generic
from music.models import Track, Artist, Album
from scrobbles.stats import get_scrobble_count_qs
diff --git a/vrobbler/apps/scrobbles/migrations/0023_alter_audioscrobblertsvimport_options_and_more.py b/vrobbler/apps/scrobbles/migrations/0023_alter_audioscrobblertsvimport_options_and_more.py
new file mode 100644
index 0000000..075f608
--- /dev/null
+++ b/vrobbler/apps/scrobbles/migrations/0023_alter_audioscrobblertsvimport_options_and_more.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.1.5 on 2023-02-25 00:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('scrobbles', '0022_scrobble_book_pages_read'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='audioscrobblertsvimport',
+ options={'verbose_name': 'AudioScrobbler TSV Import'},
+ ),
+ migrations.AlterModelOptions(
+ name='koreaderimport',
+ options={'verbose_name': 'KOReader Import'},
+ ),
+ migrations.AlterModelOptions(
+ name='lastfmimport',
+ options={'verbose_name': 'Last.FM Import'},
+ ),
+ migrations.AddField(
+ model_name='chartrecord',
+ name='count',
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py
index 075c3a1..dd5f991 100644
--- a/vrobbler/apps/scrobbles/models.py
+++ b/vrobbler/apps/scrobbles/models.py
@@ -14,6 +14,7 @@ from scrobbles.lastfm import LastFM
from scrobbles.utils import check_scrobble_for_finish
from sports.models import SportEvent
from videos.models import Series, Video
+from vrobbler.apps.scrobbles.stats import build_charts
logger = logging.getLogger(__name__)
User = get_user_model()
@@ -203,6 +204,7 @@ class ChartRecord(TimeStampedModel):
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, **BNULL)
rank = models.IntegerField()
+ count = models.IntegerField(default=0)
year = models.IntegerField(default=timezone.now().year)
month = models.IntegerField(**BNULL)
week = models.IntegerField(**BNULL)
@@ -219,6 +221,8 @@ class ChartRecord(TimeStampedModel):
media_obj = self.video
if self.track:
media_obj = self.track
+ if self.artist:
+ media_obj = self.artist
return media_obj
@property
@@ -267,6 +271,26 @@ class ChartRecord(TimeStampedModel):
def __str__(self):
return f"#{self.rank} in {self.period} - {self.media_obj}"
+ @classmethod
+ def build(cls, user, **kwargs):
+ build_charts(user=user, **kwargs)
+
+ @classmethod
+ def for_year(cls, user, year):
+ return cls.objects.filter(year=year, user=user)
+
+ @classmethod
+ def for_month(cls, user, year, month):
+ return cls.objects.filter(year=year, month=month, user=user)
+
+ @classmethod
+ def for_day(cls, user, year, day, month):
+ return cls.objects.filter(year=year, month=month, day=day, user=user)
+
+ @classmethod
+ def for_week(cls, user, year, week):
+ return cls.objects.filter(year=year, week=week, user=user)
+
class Scrobble(TimeStampedModel):
"""A scrobble tracks played media items by a user."""
diff --git a/vrobbler/apps/scrobbles/stats.py b/vrobbler/apps/scrobbles/stats.py
index 1640e9a..16fc579 100644
--- a/vrobbler/apps/scrobbles/stats.py
+++ b/vrobbler/apps/scrobbles/stats.py
@@ -6,7 +6,7 @@ from typing import Optional
import pytz
from django.apps import apps
from django.conf import settings
-from django.db.models import Count, Q
+from django.db.models import Count, Q, ExpressionWrapper, OuterRef, Subquery
logger = logging.getLogger(__name__)
@@ -30,12 +30,14 @@ def get_scrobble_count_qs(
user=None,
model_str="Track",
) -> dict[str, int]:
-
tz = settings.TIME_ZONE
if user and user.is_authenticated:
tz = pytz.timezone(user.profile.timezone)
+ tz = pytz.utc
data_model = apps.get_model(app_label='music', model_name='Track')
+ if model_str == "Artist":
+ data_model = apps.get_model(app_label='music', model_name='Artist')
if model_str == "Video":
data_model = apps.get_model(app_label='videos', model_name='Video')
if model_str == "SportEvent":
@@ -43,10 +45,16 @@ def get_scrobble_count_qs(
app_label='sports', model_name='SportEvent'
)
- base_qs = data_model.objects.filter(
- scrobble__user=user,
- scrobble__played_to_completion=True,
- )
+ if model_str == "Artist":
+ base_qs = data_model.objects.filter(
+ track__scrobble__user=user,
+ track__scrobble__played_to_completion=True,
+ )
+ else:
+ 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:
@@ -56,29 +64,41 @@ def get_scrobble_count_qs(
start = datetime(year, 1, 1, tzinfo=tz)
end = datetime(year, 12, 31, tzinfo=tz)
- if month:
+
+ if year and day and month:
+ logger.debug('Filtering by year, month and day')
+ start = datetime(year, month, day, 0, 0, tzinfo=tz)
+ end = datetime(year, month, day, 23, 59, tzinfo=tz)
+ elif year and week:
+ logger.debug('Filtering by year and week')
+ start, end = get_start_end_dates_by_week(year, week, tz)
+ elif month:
+ logger.debug('Filtering by 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))
+ if model_str == "Artist":
+ scrobble_date_filter = Q(
+ track__scrobble__timestamp__gte=start,
+ track__scrobble__timestamp__lte=end,
)
- .filter(date_filter)
- .order_by("-scrobble_count")
- )
+ qs = (
+ base_qs.filter(scrobble_date_filter)
+ .annotate(scrobble_count=Count("track__scrobble", distinct=True))
+ .order_by("-scrobble_count")
+ )
+ else:
+ scrobble_date_filter = Q(
+ scrobble__timestamp__gte=start, scrobble__timestamp__lte=end
+ )
+ qs = (
+ base_qs.filter(scrobble_date_filter)
+ .annotate(scrobble_count=Count("scrobble", distinct=True))
+ .order_by("-scrobble_count")
+ )
+
+ return qs
def build_charts(
@@ -109,7 +129,12 @@ def build_charts(
'user': user,
}
chart_record['rank'] = ranks[result.scrobble_count]
+ chart_record['count'] = result.scrobble_count
if model_str == 'Track':
chart_record['track'] = result
+ if model_str == 'Video':
+ chart_record['video'] = result
+ if model_str == 'Artist':
+ chart_record['artist'] = result
chart_records.append(ChartRecord(**chart_record))
ChartRecord.objects.bulk_create(chart_records, ignore_conflicts=True)
diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py
index 870fdcd..2b23b76 100644
--- a/vrobbler/apps/scrobbles/urls.py
+++ b/vrobbler/apps/scrobbles/urls.py
@@ -42,4 +42,9 @@ urlpatterns = [
name='mopidy-webhook',
),
path('export/', views.export, name='export'),
+ path(
+ 'charts/',
+ views.ChartRecordView.as_view(),
+ name='charts-home',
+ ),
]
diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py
index 78b59d4..2ef91ef 100644
--- a/vrobbler/apps/scrobbles/views.py
+++ b/vrobbler/apps/scrobbles/views.py
@@ -1,3 +1,4 @@
+import calendar
import json
import logging
from datetime import datetime
@@ -6,12 +7,13 @@ import pytz
from django.conf import settings
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.http import FileResponse, HttpResponseRedirect, JsonResponse
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
-from django.views.generic import FormView
+from django.views.generic import FormView, TemplateView
from django.views.generic.edit import CreateView
from django.views.generic.list import ListView
from music.aggregators import (
@@ -39,6 +41,7 @@ from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
from scrobbles.imdb import lookup_video_from_imdb
from scrobbles.models import (
AudioScrobblerTSVImport,
+ ChartRecord,
KoReaderImport,
LastFmImport,
Scrobble,
@@ -358,3 +361,66 @@ def export(request):
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
+
+
+class ChartRecordView(TemplateView):
+ template_name = 'scrobbles/chart_index.html'
+
+ 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')
+ params = {}
+
+ if not media_type:
+ media_type = 'Track'
+ context_data['media_type'] = media_type
+ media_filter = Q(track__isnull=False)
+ if media_type == 'Video':
+ media_filter = Q(video__isnull=False)
+ if media_type == 'Artist':
+ media_filter = Q(artist__isnull=False)
+
+ year = timezone.now().year
+ params = {'year': year}
+ name = f"Chart for {year}"
+
+ date_params = date.split('-')
+ year = int(date_params[0])
+ if len(date_params) == 2:
+ if 'W' in date_params[1]:
+ week = int(date_params[1].strip('W"'))
+ params['week'] = week
+ r = datetime.strptime(date + '-1', "%Y-W%W-%w").strftime(
+ 'Week of %B %d, %Y'
+ )
+ name = f"Chart for {r}"
+ else:
+ month = int(date_params[1])
+ params['month'] = month
+ month_str = calendar.month_name[month]
+ name = f"Chart for {month_str} {year}"
+ if len(date_params) == 3:
+ month = int(date_params[1])
+ day = int(date_params[2])
+ params['month'] = month
+ params['day'] = day
+ month_str = calendar.month_name[month]
+ name = f"Chart for {month_str} {day}, {year}"
+
+ charts = ChartRecord.objects.filter(
+ media_filter, user=self.request.user, **params
+ ).order_by("rank")
+
+ if charts.count() == 0:
+ ChartRecord.build(
+ user=self.request.user, model_str=media_type, **params
+ )
+ charts = ChartRecord.objects.filter(
+ media_filter, user=self.request.user, **params
+ ).order_by("rank")
+
+ context_data['object_list'] = charts
+ context_data['name'] = name
+ return context_data
diff --git a/vrobbler/apps/videos/views.py b/vrobbler/apps/videos/views.py
index b80ea23..5b29d77 100644
--- a/vrobbler/apps/videos/views.py
+++ b/vrobbler/apps/videos/views.py
@@ -6,6 +6,7 @@ from videos.models import Series, Video
class MovieListView(generic.ListView):
model = Video
+ template_name = "videos/movie_list.html"
def get_queryset(self):
return Video.objects.filter(video_type=Video.VideoType.MOVIE)
diff --git a/vrobbler/templates/base.html b/vrobbler/templates/base.html
index 0ff5364..7d1a615 100644
--- a/vrobbler/templates/base.html
+++ b/vrobbler/templates/base.html
@@ -251,7 +251,7 @@
- Series
+ TV Shows
{% if user.is_authenticated %}
diff --git a/vrobbler/templates/base_list.html b/vrobbler/templates/base_list.html
index f0d989a..0784d60 100644
--- a/vrobbler/templates/base_list.html
+++ b/vrobbler/templates/base_list.html
@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% block content %}
-
-
+
+
{% block title %}{% endblock %}