Fix chart rank and periods
This commit is contained in:
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user