Przeglądaj źródła

Fix aggregation

Colin Powell 2 lat temu
rodzic
commit
93c4dd3d3b

+ 6 - 5
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')
 

+ 1 - 0
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

+ 30 - 0
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),
+        ),
+    ]

+ 24 - 0
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."""

+ 49 - 24
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)

+ 5 - 0
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',
+    ),
 ]

+ 67 - 1
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

+ 1 - 0
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)

+ 1 - 1
vrobbler/templates/base.html

@@ -251,7 +251,7 @@
                             <li class="nav-item">
                                 <a class="nav-link" href="/series/">
                                 <span data-feather="tv"></span>
-                                Series
+                                TV Shows
                                 </a>
                             </li>
                             {% if user.is_authenticated %}

+ 4 - 3
vrobbler/templates/base_list.html

@@ -1,8 +1,9 @@
 {% extends "base.html" %}
 
 {% block content %}
-<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
-    <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+<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">{% block title %}{% endblock %} </h1>
         <div class="btn-toolbar mb-2 mb-md-0">
             <div class="btn-group me-2">
@@ -15,4 +16,4 @@
         {% block lists %}{% endblock %}
     </div>
 </main>
-{% endblock %}
+{% endblock %}

+ 30 - 0
vrobbler/templates/scrobbles/chart_index.html

@@ -0,0 +1,30 @@
+{% extends "base_list.html" %}
+
+{% block lists %}
+<h2>{{name}}</h2>
+<div class="row">
+    <div class="col-md">
+        <div class="table-responsive">
+            <table class="table table-striped table-sm">
+                <thead>
+                    <tr>
+                        <th scope="col">Rank</th>
+                        <th scope="col">{{media_type}}</th>
+                        <th scope="col">Scrobbles</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for chartrecord in object_list %}
+                    <tr>
+                        <td>{{chartrecord.rank}}</td>
+                        <td><a href="{{chartrecord.media_obj.get_absolute_url}}">{{chartrecord.media_obj}}</a></td>
+                        <td>{{chartrecord.count}}</td>
+                        <td></td>
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 25 - 25
vrobbler/templates/videos/movie_list.html

@@ -1,28 +1,28 @@
-{% extends "base.html" %}
+{% extends "base_list.html" %}
 
-{% block content %}
-<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
-    <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
-        <h1 class="h2">Movies</h1>
-        <div class="btn-toolbar mb-2 mb-md-0">
-        <div class="btn-group me-2">
-            <button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
-            <button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
+{% 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">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}}</a></td>
+                        <td>{{obj.scrobble_set.count}}</td>
+                        <td></td>
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
         </div>
-        <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle">
-            <span data-feather="calendar"></span>
-            This week
-        </button>
-        </div>
-    </div>
-
-    <div class="container">
-
-    <ul>
-        {% for movie in object_list %}
-        <li>{{movie}}</li>
-        {% endfor %}
-    </ul>
     </div>
-</main>
-{% endblock %}
+</div>
+{% endblock %}

+ 29 - 19
vrobbler/templates/videos/series_list.html

@@ -1,22 +1,32 @@
-{% extends "base.html" %}
+{% extends "base_list.html" %}
 
-{% block content %}
-<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
-    <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
-        <h1 class="h2">Series</h1>
-        <div class="btn-toolbar mb-2 mb-md-0">
-            <div class="btn-group me-2">
-                <button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
-            </div>
+{% 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">Series</th>
+                        <th scope="col">Episode</th>
+                        <th scope="col">Scrobbles</th>
+                        <th scope="col">All time</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for obj in object_list %}
+                    {% for video in obj.video_set.all %}
+                    <tr>
+                        <td><a href="{{video.get_absolute_url}}">{{video}}</a></td>
+                        <td><a href="{{obj.get_absolute_url}}">{{obj}}</a></td>
+                        <td>{{video.scrobble_set.count}}</td>
+                        <td></td>
+                    </tr>
+                    {% endfor %}
+                    {% endfor %}
+                </tbody>
+            </table>
         </div>
     </div>
-
-    <div class="container">
-        <ul>
-            {% for movie in object_list %}
-            <li>{{movie}}</li>
-            {% endfor %}
-        </ul>
-    </div>
-</main>
-{% endblock %}
+</div>
+{% endblock %}

+ 34 - 11
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 %}
-    <ul>
-        <li>{{scrobble}}</li>
-    </ul>
-
-    {% endfor %}
-{% endblock %}
+<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">Title</th>
+                        <th scope="col">Series</th>
+                        <th scope="col">Season</th>
+                        <th scope="col">Episode</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for scrobble in object.scrobble_set.all %}
+                    <tr>
+                        <td>{{scrobble.timestamp}}</td>
+                        <td>{{scrobble.video.title}}</td>
+                        <td>{{scrobble.video.tv_series}}</td>
+                        <td>{{scrobble.video.season_number}}</td>
+                        <td>{{scrobble.video.episode_number}}</td>
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 0 - 13
vrobbler/templates/videos/video_list.html

@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Movies{% endblock %}
-{% block content %}
-    {% for movie in object_list %}
-    <dl>
-        <dt>{{movie}}</dt>
-        {% for scrobble in movie.scrobble_set.all %}
-        <dd>{{scrobble}}</dd>
-        {% endfor %}
-    </dl>
-    {% endfor %}
-{% endblock %}