From 93c4dd3d3b813e4e67dc6fe60a3c742040a35b11 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sun, 26 Feb 2023 01:56:11 -0500 Subject: [PATCH] Fix aggregation --- vrobbler/apps/music/aggregators.py | 11 +-- vrobbler/apps/music/views.py | 1 + ...udioscrobblertsvimport_options_and_more.py | 30 ++++++++ vrobbler/apps/scrobbles/models.py | 24 ++++++ vrobbler/apps/scrobbles/stats.py | 73 +++++++++++++------ vrobbler/apps/scrobbles/urls.py | 5 ++ vrobbler/apps/scrobbles/views.py | 68 ++++++++++++++++- vrobbler/apps/videos/views.py | 1 + vrobbler/templates/base.html | 2 +- vrobbler/templates/base_list.html | 7 +- vrobbler/templates/scrobbles/chart_index.html | 30 ++++++++ vrobbler/templates/videos/movie_list.html | 50 ++++++------- vrobbler/templates/videos/series_list.html | 48 +++++++----- vrobbler/templates/videos/video_detail.html | 45 +++++++++--- vrobbler/templates/videos/video_list.html | 13 ---- 15 files changed, 306 insertions(+), 102 deletions(-) create mode 100644 vrobbler/apps/scrobbles/migrations/0023_alter_audioscrobblertsvimport_options_and_more.py create mode 100644 vrobbler/templates/scrobbles/chart_index.html delete mode 100644 vrobbler/templates/videos/video_list.html 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 @@ {% 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 %}

@@ -15,4 +16,4 @@ {% block lists %}{% endblock %}
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/vrobbler/templates/scrobbles/chart_index.html b/vrobbler/templates/scrobbles/chart_index.html new file mode 100644 index 0000000..b4f3e1b --- /dev/null +++ b/vrobbler/templates/scrobbles/chart_index.html @@ -0,0 +1,30 @@ +{% extends "base_list.html" %} + +{% block lists %} +

{{name}}

+
+
+
+ + + + + + + + + + {% for chartrecord in object_list %} + + + + + + + {% endfor %} + +
Rank{{media_type}}Scrobbles
{{chartrecord.rank}}{{chartrecord.media_obj}}{{chartrecord.count}}
+
+
+
+{% endblock %} diff --git a/vrobbler/templates/videos/movie_list.html b/vrobbler/templates/videos/movie_list.html index 016c3e0..8acde3b 100644 --- a/vrobbler/templates/videos/movie_list.html +++ b/vrobbler/templates/videos/movie_list.html @@ -1,28 +1,28 @@ -{% extends "base.html" %} +{% extends "base_list.html" %} -{% block content %} -
-
-

Movies

-
-
- - -
- +{% block lists %} +
+
+
+ + + + + + + + + + {% for obj in object_list %} + + + + + + {% endfor %} + +
TitleScrobblesAll time
{{obj}}{{obj.scrobble_set.count}}
- -
- -
    - {% for movie in object_list %} -
  • {{movie}}
  • - {% endfor %} -
-
-
-{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/vrobbler/templates/videos/series_list.html b/vrobbler/templates/videos/series_list.html index 205e506..a05127f 100644 --- a/vrobbler/templates/videos/series_list.html +++ b/vrobbler/templates/videos/series_list.html @@ -1,22 +1,32 @@ -{% extends "base.html" %} +{% extends "base_list.html" %} -{% block content %} -
-
-

Series

-
-
- -
+{% block lists %} +
+
+
+ + + + + + + + + + + {% for obj in object_list %} + {% for video in obj.video_set.all %} + + + + + + + {% endfor %} + {% endfor %} + +
SeriesEpisodeScrobblesAll time
{{video}}{{obj}}{{video.scrobble_set.count}}
- -
-
    - {% for movie in object_list %} -
  • {{movie}}
  • - {% endfor %} -
-
-
-{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/vrobbler/templates/videos/video_detail.html b/vrobbler/templates/videos/video_detail.html index 95b8ab3..3c8f3be 100644 --- a/vrobbler/templates/videos/video_detail.html +++ b/vrobbler/templates/videos/video_detail.html @@ -1,14 +1,37 @@ -{% extends "base.html" %} +{% extends "base_detail.html" %} -{% block title %}Videos{% endblock %} +{% block title %}{{object.name}}{% endblock %} -{% block content %} - {{object}} +{% block details %} - {% for scrobble in object.scrobble_set.all %} - - - {% endfor %} -{% endblock %} +
+

{{object.tv_series}}

+
+

Last scrobbles

+
+ + + + + + + + + + + + {% for scrobble in object.scrobble_set.all %} + + + + + + + + {% endfor %} + +
DateTitleSeriesSeasonEpisode
{{scrobble.timestamp}}{{scrobble.video.title}}{{scrobble.video.tv_series}}{{scrobble.video.season_number}}{{scrobble.video.episode_number}}
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/vrobbler/templates/videos/video_list.html b/vrobbler/templates/videos/video_list.html deleted file mode 100644 index ebb4c57..0000000 --- a/vrobbler/templates/videos/video_list.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Movies{% endblock %} -{% block content %} - {% for movie in object_list %} -
-
{{movie}}
- {% for scrobble in movie.scrobble_set.all %} -
{{scrobble}}
- {% endfor %} -
- {% endfor %} -{% endblock %}