[charts] Fix chart building
This commit is contained in:
@ -3,7 +3,11 @@ from django.utils import timezone
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from boardgames.models import BoardGame, BoardGameDesigner, BoardGamePublisher
|
from boardgames.models import BoardGame, BoardGameDesigner, BoardGamePublisher
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
from scrobbles.views import (
|
||||||
|
ChartContextMixin,
|
||||||
|
ScrobbleableListView,
|
||||||
|
ScrobbleableDetailView,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BoardGameListView(ScrobbleableListView):
|
class BoardGameListView(ScrobbleableListView):
|
||||||
@ -64,7 +68,7 @@ class BoardGameListView(ScrobbleableListView):
|
|||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
|
|
||||||
class BoardGameDetailView(ScrobbleableDetailView):
|
class BoardGameDetailView(ScrobbleableDetailView, ChartContextMixin):
|
||||||
model = BoardGame
|
model = BoardGame
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -22,12 +22,6 @@ class ChartRecordAdmin(admin.ModelAdmin):
|
|||||||
"month",
|
"month",
|
||||||
"week",
|
"week",
|
||||||
"day",
|
"day",
|
||||||
"tv_series",
|
|
||||||
"podcast",
|
|
||||||
"board_game",
|
|
||||||
"book",
|
|
||||||
"food",
|
|
||||||
"trail",
|
|
||||||
)
|
)
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
search_fields = (
|
search_fields = (
|
||||||
|
|||||||
@ -1,11 +1,58 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from charts.utils import build_all_historical_charts, rebuild_all_charts
|
from charts.utils import (
|
||||||
|
build_all_historical_charts,
|
||||||
|
build_charts_since,
|
||||||
|
rebuild_all_charts,
|
||||||
|
)
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_chart_counts():
|
||||||
|
from charts.models import ChartRecord
|
||||||
|
|
||||||
|
daily = ChartRecord.objects.filter(day__isnull=False).count()
|
||||||
|
weekly = ChartRecord.objects.filter(week__isnull=False, day__isnull=True).count()
|
||||||
|
monthly = ChartRecord.objects.filter(
|
||||||
|
month__isnull=False, week__isnull=True, day__isnull=True
|
||||||
|
).count()
|
||||||
|
yearly = ChartRecord.objects.filter(
|
||||||
|
month__isnull=True, week__isnull=True, day__isnull=True
|
||||||
|
).count()
|
||||||
|
total = ChartRecord.objects.count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"daily": daily,
|
||||||
|
"weekly": weekly,
|
||||||
|
"monthly": monthly,
|
||||||
|
"yearly": yearly,
|
||||||
|
"total": total,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_chart_summary(before=None):
|
||||||
|
counts = get_chart_counts()
|
||||||
|
if before:
|
||||||
|
diff = {k: counts[k] - before[k] for k in counts}
|
||||||
|
print(
|
||||||
|
f" Daily: {counts['daily']} ({diff['daily']:+d}), "
|
||||||
|
f"Weekly: {counts['weekly']} ({diff['weekly']:+d}), "
|
||||||
|
f"Monthly: {counts['monthly']} ({diff['monthly']:+d}), "
|
||||||
|
f"Yearly: {counts['yearly']} ({diff['yearly']:+d}), "
|
||||||
|
f"Total: {counts['total']} ({diff['total']:+d})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f" Daily: {counts['daily']}, "
|
||||||
|
f"Weekly: {counts['weekly']}, "
|
||||||
|
f"Monthly: {counts['monthly']}, "
|
||||||
|
f"Yearly: {counts['yearly']}, "
|
||||||
|
f"Total: {counts['total']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Build missing charts for all users"
|
help = "Build missing charts for all users"
|
||||||
|
|
||||||
@ -38,22 +85,29 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
users = [user]
|
users = [user]
|
||||||
action = "Rebuilding" if full_rebuild else "Building charts for"
|
action = "Full rebuild for" if full_rebuild else "Progressive build for"
|
||||||
self.stdout.write(f"{action} user: {user}")
|
self.stdout.write(f"{action} user: {user}")
|
||||||
else:
|
else:
|
||||||
users = User.objects.all()
|
users = User.objects.all()
|
||||||
action = "Rebuilding" if full_rebuild else "Building charts for"
|
action = "Full rebuild for" if full_rebuild else "Progressive build for"
|
||||||
self.stdout.write(f"{action} charts for {users.count()} users...")
|
self.stdout.write(f"{action} charts for {users.count()} users...")
|
||||||
|
|
||||||
|
before_counts = get_chart_counts()
|
||||||
|
self.stdout.write("Chart counts before:")
|
||||||
|
print_chart_summary()
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Building all historical charts for {user}")
|
logger.info(f"Building charts for {user}")
|
||||||
if full_rebuild:
|
if full_rebuild:
|
||||||
rebuild_all_charts(user)
|
rebuild_all_charts(user)
|
||||||
else:
|
else:
|
||||||
build_all_historical_charts(user)
|
build_charts_since(user)
|
||||||
self.stdout.write(self.style.SUCCESS(f" OK {user}"))
|
self.stdout.write(self.style.SUCCESS(f" OK {user}"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.stderr.write(f" FAILED {user}: {e}")
|
self.stderr.write(f" FAILED {user}: {e}")
|
||||||
|
|
||||||
|
self.stdout.write("Chart counts after (diff):")
|
||||||
|
print_chart_summary(before_counts)
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS("Done!"))
|
self.stdout.write(self.style.SUCCESS("Done!"))
|
||||||
|
|||||||
@ -136,3 +136,13 @@ class ChartRecord(TimeStampedModel):
|
|||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("charts:charts-home")
|
return reverse("charts:charts-home")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chart_url(self) -> str:
|
||||||
|
url = reverse("charts:charts-home")
|
||||||
|
date_str = str(self.year)
|
||||||
|
if self.week:
|
||||||
|
date_str = f"{self.year}-W{self.week}"
|
||||||
|
elif self.month:
|
||||||
|
date_str = f"{self.year}-{self.month:02d}"
|
||||||
|
return f"{url}?date={date_str}"
|
||||||
|
|||||||
@ -29,12 +29,8 @@ def get_time_filter(
|
|||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
|
||||||
if day and month:
|
if day and month:
|
||||||
start = timezone.make_aware(
|
start = timezone.make_aware(datetime.datetime(year, month, day, 0, 0, 0))
|
||||||
datetime.datetime(year, month, day, 0, 0, 0)
|
end = timezone.make_aware(datetime.datetime(year, month, day, 23, 59, 59))
|
||||||
)
|
|
||||||
end = timezone.make_aware(
|
|
||||||
datetime.datetime(year, month, day, 23, 59, 59)
|
|
||||||
)
|
|
||||||
elif week:
|
elif week:
|
||||||
period = datetime.date.fromisocalendar(year, week, 1)
|
period = datetime.date.fromisocalendar(year, week, 1)
|
||||||
start = timezone.make_aware(
|
start = timezone.make_aware(
|
||||||
@ -44,9 +40,7 @@ def get_time_filter(
|
|||||||
elif month:
|
elif month:
|
||||||
last_day = calendar.monthrange(year, month)[1]
|
last_day = calendar.monthrange(year, month)[1]
|
||||||
start = timezone.make_aware(datetime.datetime(year, month, 1))
|
start = timezone.make_aware(datetime.datetime(year, month, 1))
|
||||||
end = timezone.make_aware(
|
end = timezone.make_aware(datetime.datetime(year, month, last_day, 23, 59, 59))
|
||||||
datetime.datetime(year, month, last_day, 23, 59, 59)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
start = timezone.make_aware(datetime.datetime(year, 1, 1))
|
start = timezone.make_aware(datetime.datetime(year, 1, 1))
|
||||||
end = timezone.make_aware(datetime.datetime(year, 12, 31, 23, 59, 59))
|
end = timezone.make_aware(datetime.datetime(year, 12, 31, 23, 59, 59))
|
||||||
@ -77,18 +71,23 @@ def build_charts(
|
|||||||
"podcast_episode",
|
"podcast_episode",
|
||||||
"board_game",
|
"board_game",
|
||||||
"trail",
|
"trail",
|
||||||
"geo_location",
|
|
||||||
"food",
|
"food",
|
||||||
"book",
|
"book",
|
||||||
]
|
]
|
||||||
|
|
||||||
period_filter = Q(year=year)
|
period_filter = Q(year=year)
|
||||||
if month:
|
|
||||||
period_filter &= Q(month=month)
|
|
||||||
if week:
|
|
||||||
period_filter &= Q(week=week)
|
|
||||||
if day:
|
if day:
|
||||||
period_filter &= Q(day=day)
|
period_filter = (
|
||||||
|
period_filter & Q(day=day) & Q(week__isnull=True) & Q(month__isnull=True)
|
||||||
|
)
|
||||||
|
elif week:
|
||||||
|
period_filter = (
|
||||||
|
period_filter & Q(week=week) & Q(day__isnull=True) & Q(month__isnull=True)
|
||||||
|
)
|
||||||
|
elif month:
|
||||||
|
period_filter = (
|
||||||
|
period_filter & Q(month=month) & Q(week__isnull=True) & Q(day__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
ChartRecord.objects.filter(period_filter, user=user).delete()
|
ChartRecord.objects.filter(period_filter, user=user).delete()
|
||||||
|
|
||||||
@ -115,8 +114,7 @@ def build_charts(
|
|||||||
"annotate": Count("id", distinct=True),
|
"annotate": Count("id", distinct=True),
|
||||||
},
|
},
|
||||||
"tv_series": {
|
"tv_series": {
|
||||||
"filter": Q(video__isnull=False)
|
"filter": Q(video__isnull=False) & Q(video__tv_series__isnull=False),
|
||||||
& Q(video__tv_series__isnull=False),
|
|
||||||
"values": "video__tv_series",
|
"values": "video__tv_series",
|
||||||
"annotate": Count("id", distinct=True),
|
"annotate": Count("id", distinct=True),
|
||||||
},
|
},
|
||||||
@ -181,12 +179,8 @@ def build_charts(
|
|||||||
if not results:
|
if not results:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
unique_counts = sorted(
|
unique_counts = sorted(set(r["scrobble_count"] for r in results), reverse=True)
|
||||||
set(r["scrobble_count"] for r in results), reverse=True
|
ranks = {count: rank for rank, count in enumerate(unique_counts, start=1)}
|
||||||
)
|
|
||||||
ranks = {
|
|
||||||
count: rank for rank, count in enumerate(unique_counts, start=1)
|
|
||||||
}
|
|
||||||
|
|
||||||
chart_records = []
|
chart_records = []
|
||||||
total_created = 0
|
total_created = 0
|
||||||
@ -204,16 +198,12 @@ def build_charts(
|
|||||||
chart_records.append(ChartRecord(**chart_record_data))
|
chart_records.append(ChartRecord(**chart_record_data))
|
||||||
|
|
||||||
if len(chart_records) >= BATCH_SIZE:
|
if len(chart_records) >= BATCH_SIZE:
|
||||||
ChartRecord.objects.bulk_create(
|
ChartRecord.objects.bulk_create(chart_records, batch_size=BATCH_SIZE)
|
||||||
chart_records, batch_size=BATCH_SIZE
|
|
||||||
)
|
|
||||||
total_created += len(chart_records)
|
total_created += len(chart_records)
|
||||||
chart_records = []
|
chart_records = []
|
||||||
|
|
||||||
if chart_records:
|
if chart_records:
|
||||||
ChartRecord.objects.bulk_create(
|
ChartRecord.objects.bulk_create(chart_records, batch_size=BATCH_SIZE)
|
||||||
chart_records, batch_size=BATCH_SIZE
|
|
||||||
)
|
|
||||||
total_created += len(chart_records)
|
total_created += len(chart_records)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -266,9 +256,7 @@ def build_monthly_charts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_yearly_charts(
|
def build_yearly_charts(user, year: int, media_types: Optional[list] = None) -> None:
|
||||||
user, year: int, media_types: Optional[list] = None
|
|
||||||
) -> None:
|
|
||||||
"""Build yearly charts for a specific year."""
|
"""Build yearly charts for a specific year."""
|
||||||
build_charts(
|
build_charts(
|
||||||
user=user,
|
user=user,
|
||||||
@ -277,9 +265,7 @@ def build_yearly_charts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_all_historical_charts(
|
def build_all_historical_charts(user, media_types: Optional[list] = None) -> None:
|
||||||
user, media_types: Optional[list] = None
|
|
||||||
) -> None:
|
|
||||||
"""Build all historical charts for a user."""
|
"""Build all historical charts for a user."""
|
||||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
|
|
||||||
@ -325,12 +311,75 @@ def build_daily_charts(
|
|||||||
build_charts(
|
build_charts(
|
||||||
user=user,
|
user=user,
|
||||||
year=year,
|
year=year,
|
||||||
month=month,
|
|
||||||
day=day,
|
day=day,
|
||||||
media_types=media_types,
|
media_types=media_types,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_charts_since(user, media_types: Optional[list] = None) -> None:
|
||||||
|
"""Build/update charts starting from the last known ChartRecord date."""
|
||||||
|
ChartRecord = apps.get_model("charts", "ChartRecord")
|
||||||
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
|
|
||||||
|
latest_daily = (
|
||||||
|
ChartRecord.objects.filter(user=user, day__isnull=False)
|
||||||
|
.order_by("-year", "-month", "-day")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if latest_daily:
|
||||||
|
start_date = datetime.date(
|
||||||
|
latest_daily.year, latest_daily.month, latest_daily.day
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Building charts since {start_date} for {user} (last known record)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
first_scrobble = (
|
||||||
|
Scrobble.objects.filter(user=user, played_to_completion=True)
|
||||||
|
.order_by("timestamp")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not first_scrobble:
|
||||||
|
logger.info(f"No scrobbles found for {user}")
|
||||||
|
return
|
||||||
|
start_date = first_scrobble.timestamp.date()
|
||||||
|
logger.info(
|
||||||
|
f"No existing charts found for {user}, building from "
|
||||||
|
f"first scrobble: {start_date}"
|
||||||
|
)
|
||||||
|
|
||||||
|
start_date -= datetime.timedelta(days=1)
|
||||||
|
last_week = start_date.isocalendar()[1]
|
||||||
|
last_month = start_date.month
|
||||||
|
last_year = start_date.year
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
current = start_date + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
while current <= now.date():
|
||||||
|
year = current.year
|
||||||
|
month = current.month
|
||||||
|
day = current.day
|
||||||
|
week = current.isocalendar()[1]
|
||||||
|
|
||||||
|
if last_month != month:
|
||||||
|
build_monthly_charts(user, year, month, media_types)
|
||||||
|
last_month = month
|
||||||
|
|
||||||
|
build_daily_charts(user, year, month, day, media_types)
|
||||||
|
|
||||||
|
if last_week != week:
|
||||||
|
build_weekly_charts(user, year, week, media_types)
|
||||||
|
last_week = week
|
||||||
|
|
||||||
|
if last_year != year:
|
||||||
|
build_yearly_charts(user, year, media_types)
|
||||||
|
last_year = year
|
||||||
|
|
||||||
|
current += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
def rebuild_all_charts(user, media_types: Optional[list] = None) -> None:
|
def rebuild_all_charts(user, media_types: Optional[list] = None) -> None:
|
||||||
"""Delete all existing chart records for a user and rebuild them."""
|
"""Delete all existing chart records for a user and rebuild them."""
|
||||||
ChartRecord = apps.get_model("charts", "ChartRecord")
|
ChartRecord = apps.get_model("charts", "ChartRecord")
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
from foods.models import Food
|
from foods.models import Food
|
||||||
|
|
||||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
from scrobbles.views import (
|
||||||
|
ScrobbleableListView,
|
||||||
|
ScrobbleableDetailView,
|
||||||
|
ChartContextMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FoodListView(ScrobbleableListView):
|
class FoodListView(ScrobbleableListView):
|
||||||
model = Food
|
model = Food
|
||||||
|
|
||||||
|
|
||||||
class FoodDetailView(ScrobbleableDetailView):
|
class FoodDetailView(ScrobbleableDetailView, ChartContextMixin):
|
||||||
model = Food
|
model = Food
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from django.db.models import Count
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from locations.models import GeoLocation
|
from locations.models import GeoLocation
|
||||||
from scrobbles.stats import get_scrobble_count_qs
|
from scrobbles.stats import get_scrobble_count_qs
|
||||||
|
from scrobbles.views import ChartContextMixin
|
||||||
|
|
||||||
|
|
||||||
class GeoLocationListView(generic.ListView):
|
class GeoLocationListView(generic.ListView):
|
||||||
@ -22,6 +23,6 @@ class GeoLocationListView(generic.ListView):
|
|||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
|
|
||||||
class GeoLocationDetailView(generic.DetailView):
|
class GeoLocationDetailView(ChartContextMixin, generic.DetailView):
|
||||||
model = GeoLocation
|
model = GeoLocation
|
||||||
slug_field = "uuid"
|
slug_field = "uuid"
|
||||||
|
|||||||
@ -47,19 +47,28 @@ 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"]
|
artist = context_data["object"]
|
||||||
rank = 1
|
|
||||||
tracks_ranked = []
|
ranked_tracks = sorted(
|
||||||
scrobbles = artist.tracks.first().scrobble_count
|
artist.tracks.all(), key=lambda t: t.scrobble_count, reverse=True
|
||||||
for track in artist.tracks:
|
)[:10]
|
||||||
if scrobbles > track.scrobble_count:
|
|
||||||
rank += 1
|
tracks_ranked = [
|
||||||
tracks_ranked.append((rank, track))
|
(rank, track) for rank, track in enumerate(ranked_tracks, start=1)
|
||||||
scrobbles = track.scrobble_count
|
]
|
||||||
|
|
||||||
context_data["tracks_ranked"] = tracks_ranked
|
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]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
context_data["recent_scrobbles"] = (
|
||||||
|
Scrobble.objects.filter(track__artist=artist)
|
||||||
|
.select_related("track", "track__album")
|
||||||
|
.order_by("-timestamp")[:100]
|
||||||
|
)
|
||||||
|
|
||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,11 @@ from django.views import generic
|
|||||||
from podcasts.models import Podcast, PodcastEpisode
|
from podcasts.models import Podcast, PodcastEpisode
|
||||||
|
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
from scrobbles.views import (
|
||||||
|
ScrobbleableListView,
|
||||||
|
ScrobbleableDetailView,
|
||||||
|
ChartContextMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PodcastListView(ScrobbleableListView):
|
class PodcastListView(ScrobbleableListView):
|
||||||
@ -61,7 +65,7 @@ class PodcastListView(ScrobbleableListView):
|
|||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
|
|
||||||
class PodcastDetailView(generic.DetailView):
|
class PodcastDetailView(ChartContextMixin, generic.DetailView):
|
||||||
model = Podcast
|
model = Podcast
|
||||||
slug_field = "uuid"
|
slug_field = "uuid"
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from charts.utils import build_all_historical_charts, build_yesterdays_charts
|
from charts.utils import build_charts_since
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
@ -53,27 +53,17 @@ def process_koreader_import(import_id):
|
|||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def create_yesterdays_charts():
|
def create_yesterdays_charts():
|
||||||
|
"""Build/update charts for all users starting from last known record."""
|
||||||
for user in User.objects.all():
|
for user in User.objects.all():
|
||||||
build_yesterdays_charts(user)
|
build_charts_since(user)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def build_missing_charts_for_all_users():
|
|
||||||
"""Build missing charts for all users."""
|
|
||||||
for user in User.objects.all():
|
|
||||||
logger.info(f"Building all historical charts for {user}")
|
|
||||||
try:
|
|
||||||
build_all_historical_charts(user)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to build charts for {user}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def build_charts_for_user(user_id):
|
def build_charts_for_user(user_id):
|
||||||
"""Build all historical charts for a specific user."""
|
"""Build charts for a specific user starting from last known record."""
|
||||||
user = User.objects.filter(id=user_id).first()
|
user = User.objects.filter(id=user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
logger.error(f"User with id {user_id} not found")
|
logger.error(f"User with id {user_id} not found")
|
||||||
return
|
return
|
||||||
logger.info(f"Building all historical charts for {user}")
|
logger.info(f"Building charts for {user}")
|
||||||
build_all_historical_charts(user)
|
build_charts_since(user)
|
||||||
|
|||||||
@ -105,7 +105,40 @@ class ScrobbleableListView(ListView):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleableDetailView(DetailView):
|
class ChartContextMixin:
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
from charts.models import ChartRecord
|
||||||
|
|
||||||
|
obj = context.get("object")
|
||||||
|
if not obj:
|
||||||
|
return context
|
||||||
|
|
||||||
|
media_type_map = {
|
||||||
|
"music.Artist": "artist",
|
||||||
|
"music.Album": "album",
|
||||||
|
"music.Track": "track",
|
||||||
|
"videos.video": "video",
|
||||||
|
"videos.series": "tv_series",
|
||||||
|
"podcasts.podcast": "podcast",
|
||||||
|
"boardgames.boardgame": "board_game",
|
||||||
|
"trails.trail": "trail",
|
||||||
|
"foods.food": "food",
|
||||||
|
"books.book": "book",
|
||||||
|
}
|
||||||
|
|
||||||
|
model_label = f"{obj._meta.app_label}.{obj._meta.model_name}"
|
||||||
|
media_type = media_type_map.get(model_label)
|
||||||
|
|
||||||
|
if media_type:
|
||||||
|
context["charts"] = ChartRecord.objects.filter(
|
||||||
|
**{media_type: obj}, rank__in=[1, 2, 3]
|
||||||
|
)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobbleableDetailView(ChartContextMixin, DetailView):
|
||||||
model = None
|
model = None
|
||||||
slug_field = "uuid"
|
slug_field = "uuid"
|
||||||
|
|
||||||
@ -734,7 +767,6 @@ class ScrobbleStatusView(LoginRequiredMixin, TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp")
|
user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp")
|
||||||
progress_plays = user_scrobble_qs.filter(in_progress=True, is_paused=False)
|
|
||||||
|
|
||||||
data["listening"] = progress_plays.filter(
|
data["listening"] = progress_plays.filter(
|
||||||
Q(track__isnull=False) | Q(podcast_episode__isnull=False)
|
Q(track__isnull=False) | Q(podcast_episode__isnull=False)
|
||||||
|
|||||||
@ -4,7 +4,11 @@ from django.utils import timezone
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from videos.models import Channel, Series, Video
|
from videos.models import Channel, Series, Video
|
||||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
from scrobbles.views import (
|
||||||
|
ScrobbleableListView,
|
||||||
|
ScrobbleableDetailView,
|
||||||
|
ChartContextMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MovieListView(LoginRequiredMixin, generic.ListView):
|
class MovieListView(LoginRequiredMixin, generic.ListView):
|
||||||
@ -19,7 +23,7 @@ class SeriesListView(LoginRequiredMixin, generic.ListView):
|
|||||||
model = Series
|
model = Series
|
||||||
|
|
||||||
|
|
||||||
class SeriesDetailView(LoginRequiredMixin, generic.DetailView):
|
class SeriesDetailView(LoginRequiredMixin, ChartContextMixin, generic.DetailView):
|
||||||
model = Series
|
model = Series
|
||||||
slug_field = "uuid"
|
slug_field = "uuid"
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,13 @@
|
|||||||
<a href="{{object.start_url}}">Play again</a>
|
<a href="{{object.start_url}}">Play again</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if charts %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -41,6 +41,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if charts %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -9,6 +9,14 @@
|
|||||||
<p>{{object.description}}</p>
|
<p>{{object.description}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{charts}}
|
||||||
|
{% if charts %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<p>{{object.scrobbles.count}} scrobbles</p>
|
<p>{{object.scrobbles.count}} scrobbles</p>
|
||||||
{% if charts %}
|
{% if charts %}
|
||||||
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Top tracks</h3>
|
<h3>Top tracks</h3>
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<p>{{artist.scrobbles.count}} scrobbles</p>
|
<p>{{artist.scrobbles.count}} scrobbles</p>
|
||||||
{% if charts %}
|
{% if charts %}
|
||||||
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Top tracks</h3>
|
<h3>Top tracks</h3>
|
||||||
@ -81,7 +81,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for scrobble in object.scrobbles %}
|
{% for scrobble in recent_scrobbles %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href={{scrobble.get_absolute_url}}>{{scrobble.local_timestamp}}</a></td>
|
<td><a href={{scrobble.get_absolute_url}}>{{scrobble.local_timestamp}}</a></td>
|
||||||
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
|
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<p>{{scrobbles.count}} scrobbles</p>
|
<p>{{scrobbles.count}} scrobbles</p>
|
||||||
{% if charts %}
|
{% if charts %}
|
||||||
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -31,6 +31,13 @@
|
|||||||
<div class="deets">
|
<div class="deets">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if charts %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -44,6 +44,13 @@
|
|||||||
<a href="{{object.start_url}}">Play again</a>
|
<a href="{{object.start_url}}">Play again</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if charts %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -44,6 +44,13 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if charts %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
|
|||||||
@ -83,18 +83,25 @@ dd {
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
|
{% if charts %}
|
||||||
|
<p>{% for chart in charts %}<em><a href="{{chart.chart_url}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
|
||||||
|
{% endif %}
|
||||||
<h3>Last scrobbles</h3>
|
<h3>Last scrobbles</h3>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Date</th>
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">With</th>
|
||||||
|
<th scope="col">Rated</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for scrobble in scrobbles.all %}
|
{% for scrobble in scrobbles.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href={{scrobble.get_absolute_url}}>{{scrobble.local_timestamp}}</a></td>
|
<td><a href={{scrobble.get_absolute_url}}>{{scrobble.local_timestamp}}</a></td>
|
||||||
|
<td>{% if scrobble.logdata.with_people %}{{scrobble.logdata.with_people|join:", " }}{% else %}Solo{% endif %}</td>
|
||||||
|
<td>{% if scrobble.logdata.rating %}{{scrobble.logdata.rating }}{% else %}Unrated{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user