Bladeren bron

Fix chart rank and periods

Colin Powell 2 jaren geleden
bovenliggende
commit
6e17e4ce0d

+ 23 - 12
vrobbler/apps/music/views.py

@@ -1,5 +1,4 @@
-from datetime import timedelta
-from django.utils import timezone
+from django.db.models import Count
 from django.views import generic
 from music.models import Album, Artist, Track
 from scrobbles.models import ChartRecord
@@ -22,7 +21,6 @@ class TrackDetailView(generic.DetailView):
 
     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]
         )
@@ -34,7 +32,12 @@ class ArtistListView(generic.ListView):
     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(
@@ -50,6 +53,17 @@ class ArtistDetailView(generic.DetailView):
 
     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]
         )
@@ -58,17 +72,14 @@ class ArtistDetailView(generic.DetailView):
 
 class AlbumListView(generic.ListView):
     model = Album
-    paginate_by = 50
 
     def get_queryset(self):
-        return super().get_queryset().order_by("name")
-
-    def get_context_data(self, *, object_list=None, **kwargs):
-        context_data = super().get_context_data(
-            object_list=object_list, **kwargs
+        return (
+            super()
+            .get_queryset()
+            .annotate(scrobble_count=Count('track__scrobble'))
+            .order_by("-scrobble_count")
         )
-        context_data['view'] = self.request.GET.get('view')
-        return context_data
 
 
 class AlbumDetailView(generic.DetailView):

+ 41 - 16
vrobbler/apps/profiles/utils.py

@@ -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)

+ 23 - 0
vrobbler/apps/scrobbles/migrations/0024_chartrecord_period_end_chartrecord_period_start.py

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

+ 30 - 0
vrobbler/apps/scrobbles/models.py

@@ -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):

+ 96 - 2
vrobbler/apps/scrobbles/stats.py

@@ -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,
+        )

+ 11 - 1
vrobbler/apps/scrobbles/tasks.py

@@ -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)

+ 3 - 3
vrobbler/templates/music/album_list.html

@@ -46,17 +46,17 @@
             <table class="table table-striped table-sm">
             <thead>
                 <tr>
+                    <th scope="col">Scrobbles</th>
                     <th scope="col">Album</th>
                     <th scope="col">Artist</th>
-                    <th scope="col">Scrobbles</th>
                 </tr>
             </thead>
             <tbody>
                 {% for album in object_list %}
                 <tr>
-                    <td><a href="{{album.get_absolute_url}}">{{album}}</a></td>
-                    <td><a href="{{album.artist.get_absolute_url}}">{{album.artist}}</a></td>
                     <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>

+ 6 - 6
vrobbler/templates/music/artist_detail.html

@@ -44,15 +44,15 @@
                 </tr>
             </thead>
             <tbody>
-                {% for track in object.tracks %}
+                {% for track in tracks_ranked %}
                 <tr>
-                    <td>{{rank}}#1</td>
-                    <td><a href="{{track.get_absolute_url}}">{{track.title}}</a></td>
-                    <td><a href="{{track.album.get_absolute_url}}">{{track.album}}</a></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>

+ 2 - 4
vrobbler/templates/music/artist_list.html

@@ -45,17 +45,15 @@
             <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>

+ 2 - 2
vrobbler/templates/music/track_list.html

@@ -24,17 +24,17 @@
             <table class="table table-striped table-sm">
             <thead>
                 <tr>
+                    <th scope="col">Scrobbles</th>
                     <th scope="col">Track</th>
                     <th scope="col">Artist</th>
-                    <th scope="col">Scrobbles</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>
-                    <td>{{track.scrobble_set.count}}</td>
                 </tr>
                 {% endfor %}
             </tbody>