[charts] Fix chart building
This commit is contained in:
@ -22,12 +22,6 @@ class ChartRecordAdmin(admin.ModelAdmin):
|
||||
"month",
|
||||
"week",
|
||||
"day",
|
||||
"tv_series",
|
||||
"podcast",
|
||||
"board_game",
|
||||
"book",
|
||||
"food",
|
||||
"trail",
|
||||
)
|
||||
ordering = ("-created",)
|
||||
search_fields = (
|
||||
|
||||
@ -1,11 +1,58 @@
|
||||
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
|
||||
|
||||
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):
|
||||
help = "Build missing charts for all users"
|
||||
|
||||
@ -38,22 +85,29 @@ class Command(BaseCommand):
|
||||
)
|
||||
return
|
||||
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}")
|
||||
else:
|
||||
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...")
|
||||
|
||||
before_counts = get_chart_counts()
|
||||
self.stdout.write("Chart counts before:")
|
||||
print_chart_summary()
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
logger.info(f"Building all historical charts for {user}")
|
||||
logger.info(f"Building charts for {user}")
|
||||
if full_rebuild:
|
||||
rebuild_all_charts(user)
|
||||
else:
|
||||
build_all_historical_charts(user)
|
||||
build_charts_since(user)
|
||||
self.stdout.write(self.style.SUCCESS(f" OK {user}"))
|
||||
except Exception as 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!"))
|
||||
|
||||
@ -136,3 +136,13 @@ class ChartRecord(TimeStampedModel):
|
||||
|
||||
def get_absolute_url(self):
|
||||
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)
|
||||
|
||||
if day and month:
|
||||
start = timezone.make_aware(
|
||||
datetime.datetime(year, month, day, 0, 0, 0)
|
||||
)
|
||||
end = timezone.make_aware(
|
||||
datetime.datetime(year, month, day, 23, 59, 59)
|
||||
)
|
||||
start = timezone.make_aware(datetime.datetime(year, month, day, 0, 0, 0))
|
||||
end = timezone.make_aware(datetime.datetime(year, month, day, 23, 59, 59))
|
||||
elif week:
|
||||
period = datetime.date.fromisocalendar(year, week, 1)
|
||||
start = timezone.make_aware(
|
||||
@ -44,9 +40,7 @@ def get_time_filter(
|
||||
elif month:
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
start = timezone.make_aware(datetime.datetime(year, month, 1))
|
||||
end = timezone.make_aware(
|
||||
datetime.datetime(year, month, last_day, 23, 59, 59)
|
||||
)
|
||||
end = timezone.make_aware(datetime.datetime(year, month, last_day, 23, 59, 59))
|
||||
else:
|
||||
start = timezone.make_aware(datetime.datetime(year, 1, 1))
|
||||
end = timezone.make_aware(datetime.datetime(year, 12, 31, 23, 59, 59))
|
||||
@ -77,18 +71,23 @@ def build_charts(
|
||||
"podcast_episode",
|
||||
"board_game",
|
||||
"trail",
|
||||
"geo_location",
|
||||
"food",
|
||||
"book",
|
||||
]
|
||||
|
||||
period_filter = Q(year=year)
|
||||
if month:
|
||||
period_filter &= Q(month=month)
|
||||
if week:
|
||||
period_filter &= Q(week=week)
|
||||
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()
|
||||
|
||||
@ -115,8 +114,7 @@ def build_charts(
|
||||
"annotate": Count("id", distinct=True),
|
||||
},
|
||||
"tv_series": {
|
||||
"filter": Q(video__isnull=False)
|
||||
& Q(video__tv_series__isnull=False),
|
||||
"filter": Q(video__isnull=False) & Q(video__tv_series__isnull=False),
|
||||
"values": "video__tv_series",
|
||||
"annotate": Count("id", distinct=True),
|
||||
},
|
||||
@ -181,12 +179,8 @@ def build_charts(
|
||||
if not results:
|
||||
continue
|
||||
|
||||
unique_counts = sorted(
|
||||
set(r["scrobble_count"] for r in results), reverse=True
|
||||
)
|
||||
ranks = {
|
||||
count: rank for rank, count in enumerate(unique_counts, start=1)
|
||||
}
|
||||
unique_counts = sorted(set(r["scrobble_count"] for r in results), reverse=True)
|
||||
ranks = {count: rank for rank, count in enumerate(unique_counts, start=1)}
|
||||
|
||||
chart_records = []
|
||||
total_created = 0
|
||||
@ -204,16 +198,12 @@ def build_charts(
|
||||
chart_records.append(ChartRecord(**chart_record_data))
|
||||
|
||||
if len(chart_records) >= BATCH_SIZE:
|
||||
ChartRecord.objects.bulk_create(
|
||||
chart_records, batch_size=BATCH_SIZE
|
||||
)
|
||||
ChartRecord.objects.bulk_create(chart_records, batch_size=BATCH_SIZE)
|
||||
total_created += len(chart_records)
|
||||
chart_records = []
|
||||
|
||||
if chart_records:
|
||||
ChartRecord.objects.bulk_create(
|
||||
chart_records, batch_size=BATCH_SIZE
|
||||
)
|
||||
ChartRecord.objects.bulk_create(chart_records, batch_size=BATCH_SIZE)
|
||||
total_created += len(chart_records)
|
||||
|
||||
logger.info(
|
||||
@ -266,9 +256,7 @@ def build_monthly_charts(
|
||||
)
|
||||
|
||||
|
||||
def build_yearly_charts(
|
||||
user, year: int, media_types: Optional[list] = None
|
||||
) -> None:
|
||||
def build_yearly_charts(user, year: int, media_types: Optional[list] = None) -> None:
|
||||
"""Build yearly charts for a specific year."""
|
||||
build_charts(
|
||||
user=user,
|
||||
@ -277,9 +265,7 @@ def build_yearly_charts(
|
||||
)
|
||||
|
||||
|
||||
def build_all_historical_charts(
|
||||
user, media_types: Optional[list] = None
|
||||
) -> None:
|
||||
def build_all_historical_charts(user, media_types: Optional[list] = None) -> None:
|
||||
"""Build all historical charts for a user."""
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
|
||||
@ -325,12 +311,75 @@ def build_daily_charts(
|
||||
build_charts(
|
||||
user=user,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
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:
|
||||
"""Delete all existing chart records for a user and rebuild them."""
|
||||
ChartRecord = apps.get_model("charts", "ChartRecord")
|
||||
|
||||
Reference in New Issue
Block a user