Fix chart rank and periods

This commit is contained in:
2023-03-03 02:29:15 -05:00
parent 3c3e567573
commit 6e17e4ce0d
10 changed files with 237 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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