浏览代码

Fix chart rank and periods

Colin Powell 2 年之前
父节点
当前提交
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 django.views import generic
 from music.models import Album, Artist, Track
 from music.models import Album, Artist, Track
 from scrobbles.models import ChartRecord
 from scrobbles.models import ChartRecord
@@ -22,7 +21,6 @@ class TrackDetailView(generic.DetailView):
 
 
     def get_context_data(self, **kwargs):
     def get_context_data(self, **kwargs):
         context_data = super().get_context_data(**kwargs)
         context_data = super().get_context_data(**kwargs)
-
         context_data['charts'] = ChartRecord.objects.filter(
         context_data['charts'] = ChartRecord.objects.filter(
             track=self.object, rank__in=[1, 2, 3]
             track=self.object, rank__in=[1, 2, 3]
         )
         )
@@ -34,7 +32,12 @@ class ArtistListView(generic.ListView):
     paginate_by = 100
     paginate_by = 100
 
 
     def get_queryset(self):
     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):
     def get_context_data(self, *, object_list=None, **kwargs):
         context_data = super().get_context_data(
         context_data = super().get_context_data(
@@ -50,6 +53,17 @@ class ArtistDetailView(generic.DetailView):
 
 
     def get_context_data(self, **kwargs):
     def get_context_data(self, **kwargs):
         context_data = super().get_context_data(**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(
         context_data['charts'] = ChartRecord.objects.filter(
             artist=self.object, rank__in=[1, 2, 3]
             artist=self.object, rank__in=[1, 2, 3]
         )
         )
@@ -58,17 +72,14 @@ class ArtistDetailView(generic.DetailView):
 
 
 class AlbumListView(generic.ListView):
 class AlbumListView(generic.ListView):
     model = Album
     model = Album
-    paginate_by = 50
 
 
     def get_queryset(self):
     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):
 class AlbumDetailView(generic.DetailView):

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

@@ -1,23 +1,18 @@
-import datetime
-
 import pytz
 import pytz
 from django.conf import settings
 from django.conf import settings
 from django.utils import timezone
 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
 # 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):
 def to_user_timezone(date, profile):
     timezone = profile.timezone if profile.timezone else settings.TIME_ZONE
     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):
 def now_user_timezone(profile):
@@ -25,9 +20,39 @@ def now_user_timezone(profile):
     return timezone.localtime(timezone.now())
     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 calendar
+import datetime
 import logging
 import logging
 from uuid import uuid4
 from uuid import uuid4
 
 
@@ -15,6 +16,14 @@ from scrobbles.utils import check_scrobble_for_finish
 from sports.models import SportEvent
 from sports.models import SportEvent
 from videos.models import Series, Video
 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
 from vrobbler.apps.scrobbles.stats import build_charts
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -62,6 +71,7 @@ class BaseFileImportMixin(TimeStampedModel):
         from scrobbles.models import Scrobble
         from scrobbles.models import Scrobble
 
 
         if not self.process_log:
         if not self.process_log:
+
             logger.warning("No lines in process log found to undo")
             logger.warning("No lines in process log found to undo")
             return
             return
 
 
@@ -256,6 +266,26 @@ class ChartRecord(TimeStampedModel):
     series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
     series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
     artist = models.ForeignKey(Artist, 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)
     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
     @property
     def media_obj(self):
     def media_obj(self):

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

@@ -6,8 +6,11 @@ from typing import Optional
 import pytz
 import pytz
 from django.apps import apps
 from django.apps import apps
 from django.conf import settings
 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__)
 logger = logging.getLogger(__name__)
 
 
@@ -102,11 +105,11 @@ def get_scrobble_count_qs(
 
 
 
 
 def build_charts(
 def build_charts(
+    user: "User",
     year: Optional[int] = None,
     year: Optional[int] = None,
     month: Optional[int] = None,
     month: Optional[int] = None,
     week: Optional[int] = None,
     week: Optional[int] = None,
     day: Optional[int] = None,
     day: Optional[int] = None,
-    user=None,
     model_str="Track",
     model_str="Track",
 ):
 ):
     ChartRecord = apps.get_model(
     ChartRecord = apps.get_model(
@@ -140,3 +143,94 @@ def build_charts(
     ChartRecord.objects.bulk_create(
     ChartRecord.objects.bulk_create(
         chart_records, ignore_conflicts=True, batch_size=500
         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
 import logging
-from celery import shared_task
 
 
+from celery import shared_task
+from django.contrib.auth import get_user_model
 from scrobbles.models import (
 from scrobbles.models import (
     AudioScrobblerTSVImport,
     AudioScrobblerTSVImport,
     KoReaderImport,
     KoReaderImport,
     LastFmImport,
     LastFmImport,
 )
 )
 
 
+from vrobbler.apps.scrobbles.stats import build_yesterdays_charts_for_user
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
+User = get_user_model()
 
 
 
 
 @shared_task
 @shared_task
@@ -35,3 +39,9 @@ def process_koreader_import(import_id):
         logger.warn(f"KOReaderImport not found with id {import_id}")
         logger.warn(f"KOReaderImport not found with id {import_id}")
 
 
     koreader_import.process()
     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">
             <table class="table table-striped table-sm">
             <thead>
             <thead>
                 <tr>
                 <tr>
+                    <th scope="col">Scrobbles</th>
                     <th scope="col">Album</th>
                     <th scope="col">Album</th>
                     <th scope="col">Artist</th>
                     <th scope="col">Artist</th>
-                    <th scope="col">Scrobbles</th>
                 </tr>
                 </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
                 {% for album in object_list %}
                 {% for album in object_list %}
                 <tr>
                 <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>{{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>
                 </tr>
                 {% endfor %}
                 {% endfor %}
             </tbody>
             </tbody>

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

@@ -44,15 +44,15 @@
                 </tr>
                 </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
-                {% for track in object.tracks %}
+                {% for track in tracks_ranked %}
                 <tr>
                 <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>
                     <td>
                         <div class="progress-bar" style="margin-right:5px;">
                         <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>
                         </div>
                     </td>
                     </td>
                 </tr>
                 </tr>

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

@@ -45,17 +45,15 @@
             <table class="table table-striped table-sm">
             <table class="table table-striped table-sm">
             <thead>
             <thead>
                 <tr>
                 <tr>
-                    <th scope="col">Artist</th>
                     <th scope="col">Scrobbles</th>
                     <th scope="col">Scrobbles</th>
-                    <th scope="col">All time</th>
+                    <th scope="col">Artist</th>
                 </tr>
                 </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
                 {% for artist in object_list %}
                 {% for artist in object_list %}
                 <tr>
                 <tr>
-                    <td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
                     <td>{{artist.scrobbles.count}}</td>
                     <td>{{artist.scrobbles.count}}</td>
-                    <td></td>
+                    <td><a href="{{artist.get_absolute_url}}">{{artist}}</a></td>
                 </tr>
                 </tr>
                 {% endfor %}
                 {% endfor %}
             </tbody>
             </tbody>

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

@@ -24,17 +24,17 @@
             <table class="table table-striped table-sm">
             <table class="table table-striped table-sm">
             <thead>
             <thead>
                 <tr>
                 <tr>
+                    <th scope="col">Scrobbles</th>
                     <th scope="col">Track</th>
                     <th scope="col">Track</th>
                     <th scope="col">Artist</th>
                     <th scope="col">Artist</th>
-                    <th scope="col">Scrobbles</th>
                 </tr>
                 </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
                 {% for track in object_list %}
                 {% for track in object_list %}
                 <tr>
                 <tr>
+                    <td>{{track.scrobble_set.count}}</td>
                     <td><a href="{{track.get_absolute_url}}">{{track}}</a></td>
                     <td><a href="{{track.get_absolute_url}}">{{track}}</a></td>
                     <td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
                     <td><a href="{{track.artist.get_absolute_url}}">{{track.artist}}</a></td>
-                    <td>{{track.scrobble_set.count}}</td>
                 </tr>
                 </tr>
                 {% endfor %}
                 {% endfor %}
             </tbody>
             </tbody>