diff --git a/PROJECT.org b/PROJECT.org index 409424f..4004bd8 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [0/20] :vrobbler:project:personal: +* Backlog [1/21] :vrobbler:project:personal: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles: :PROPERTIES: :ID: 702462cf-d54b-48c6-8a7c-78b8de751deb @@ -590,6 +590,18 @@ We should rename `email_scrobble_board_game` to reflect the fact that it's just a helper method to create board game scrobbles given a json blob. It's independent of the email flow it was originally creatdd for +** DONE [#B] Trends dont seem to look very far back :trends: +:PROPERTIES: +:ID: ffcfba3f-5a93-9ee0-9680-666e6eccd684 +:END: + +*** Description + +Specificially, looking at reading-pace when run on prod, it claims that I've +only had one reading session without music. Which may be true, but perhaps we +need to indicate what the time frame we're looking at is (month, week, year) +and provide a way to jump back and forward through time, same as charts. + * Version 54.1 [1/1] ** DONE [#A] Concurrent listening trend is inefficient and should be disabled :trends:scrobbles: :PROPERTIES: diff --git a/vrobbler/apps/trends/admin.py b/vrobbler/apps/trends/admin.py index 50a65ab..943072e 100644 --- a/vrobbler/apps/trends/admin.py +++ b/vrobbler/apps/trends/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin - from trends.models import TrendResult diff --git a/vrobbler/apps/trends/management/commands/compute_trends.py b/vrobbler/apps/trends/management/commands/compute_trends.py index 51f7f67..76e2027 100644 --- a/vrobbler/apps/trends/management/commands/compute_trends.py +++ b/vrobbler/apps/trends/management/commands/compute_trends.py @@ -3,9 +3,8 @@ import logging from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand from django.utils import timezone - -from trends.tasks import _compute_and_save_trend from trends.trends import TREND_REGISTRY +from trends.utils import compute_and_save_trend, get_supported_periods logger = logging.getLogger(__name__) User = get_user_model() @@ -48,24 +47,21 @@ class Command(BaseCommand): user_fail = 0 for idx, (slug, _) in enumerate(TREND_REGISTRY.items(), start=1): - trend_start = timezone.now() - self.stdout.write( - f" [{idx}/{total_trends}] {slug}... ", ending="" - ) - try: - elapsed = _compute_and_save_trend(user, slug) - self.stdout.write( - self.style.SUCCESS(f"OK ({elapsed:.1f}s)") - ) - user_ok += 1 - except Exception as e: - elapsed = (timezone.now() - trend_start).total_seconds() - self.stdout.write( - self.style.ERROR( - f"FAILED after {elapsed:.1f}s: {e}" + periods = get_supported_periods(slug) + self.stdout.write(f" [{idx}/{total_trends}] {slug}...\n") + for period in periods: + trend_start = timezone.now() + self.stdout.write(f" {period}... ", ending="") + try: + elapsed = compute_and_save_trend(user, slug, period) + self.stdout.write(self.style.SUCCESS(f"OK ({elapsed:.1f}s)")) + user_ok += 1 + except Exception as e: + elapsed = (timezone.now() - trend_start).total_seconds() + self.stdout.write( + self.style.ERROR(f"FAILED after {elapsed:.1f}s: {e}") ) - ) - user_fail += 1 + user_fail += 1 user_elapsed = (timezone.now() - user_start).total_seconds() self.stdout.write( diff --git a/vrobbler/apps/trends/migrations/0001_initial.py b/vrobbler/apps/trends/migrations/0001_initial.py index b30032a..307e1bb 100644 --- a/vrobbler/apps/trends/migrations/0001_initial.py +++ b/vrobbler/apps/trends/migrations/0001_initial.py @@ -1,9 +1,9 @@ # Generated by Django 4.2.29 on 2026-06-16 14:52 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import django_extensions.db.fields +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/vrobbler/apps/trends/migrations/0002_alter_trendresult_unique_together_trendresult_period_and_more.py b/vrobbler/apps/trends/migrations/0002_alter_trendresult_unique_together_trendresult_period_and_more.py new file mode 100644 index 0000000..7292bf5 --- /dev/null +++ b/vrobbler/apps/trends/migrations/0002_alter_trendresult_unique_together_trendresult_period_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.29 on 2026-06-17 14:32 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("trends", "0001_initial"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="trendresult", + unique_together=set(), + ), + migrations.AddField( + model_name="trendresult", + name="period", + field=models.CharField( + choices=[ + ("last_30", "Last 30 days"), + ("last_90", "Last 90 days"), + ("last_year", "Last year"), + ("all_time", "All time"), + ], + default="all_time", + max_length=20, + ), + ), + migrations.AlterUniqueTogether( + name="trendresult", + unique_together={("user", "trend_slug", "period")}, + ), + ] diff --git a/vrobbler/apps/trends/models.py b/vrobbler/apps/trends/models.py index 0998562..6b8f835 100644 --- a/vrobbler/apps/trends/models.py +++ b/vrobbler/apps/trends/models.py @@ -4,15 +4,27 @@ from django_extensions.db.models import TimeStampedModel User = get_user_model() +PERIOD_CHOICES = [ + ("last_30", "Last 30 days"), + ("last_90", "Last 90 days"), + ("last_year", "Last year"), + ("all_time", "All time"), +] + class TrendResult(TimeStampedModel): user = models.ForeignKey(User, on_delete=models.CASCADE) trend_slug = models.CharField(max_length=100, db_index=True) + period = models.CharField( + max_length=20, + choices=PERIOD_CHOICES, + default="all_time", + ) computed_at = models.DateTimeField(auto_now_add=True) data = models.JSONField(default=dict) class Meta: - unique_together = ["user", "trend_slug"] + unique_together = ["user", "trend_slug", "period"] def __str__(self): - return f"{self.user} - {self.trend_slug} ({self.computed_at})" + return f"{self.user} - {self.trend_slug} ({self.period})" diff --git a/vrobbler/apps/trends/tasks.py b/vrobbler/apps/trends/tasks.py index 39e9500..d7e3fb7 100644 --- a/vrobbler/apps/trends/tasks.py +++ b/vrobbler/apps/trends/tasks.py @@ -3,35 +3,16 @@ import logging from celery import shared_task from django.contrib.auth import get_user_model from django.utils import timezone - -from trends.models import TrendResult from trends.trends import TREND_REGISTRY +from trends.utils import compute_and_save_trend, get_supported_periods logger = logging.getLogger(__name__) User = get_user_model() -def _compute_and_save_trend(user, slug): - """Compute a single trend and persist the result. - - Returns elapsed seconds on success, raises on failure. - """ - fn = TREND_REGISTRY[slug] - start = timezone.now() - data = fn(user) - TrendResult.objects.update_or_create( - user=user, - trend_slug=slug, - defaults={"data": data, "computed_at": timezone.now()}, - ) - return (timezone.now() - start).total_seconds() - - @shared_task def compute_all_trends(): - user_ids = list( - User.objects.filter(is_active=True).values_list("id", flat=True) - ) + user_ids = list(User.objects.filter(is_active=True).values_list("id", flat=True)) logger.info("Dispatching trend computation for %d users", len(user_ids)) for uid in user_ids: compute_user_trends.delay(uid) @@ -48,7 +29,9 @@ def compute_user_trends(user_id): total = len(TREND_REGISTRY) logger.info( "Computing %d trends for user %s (%d)", - total, user, user_id, + total, + user, + user_id, ) for idx, (slug, _) in enumerate(TREND_REGISTRY.items(), start=1): @@ -62,21 +45,25 @@ def compute_single_trend(user_id, slug): try: user = User.objects.get(id=user_id) except User.DoesNotExist: - logger.warning( - "User %d not found for trend '%s', skipping", user_id, slug - ) + logger.warning("User %d not found for trend '%s', skipping", user_id, slug) return if slug not in TREND_REGISTRY: logger.warning("Unknown trend slug '%s' for user %d", slug, user_id) return - logger.info("[%s] Computing for user %d...", slug, user_id) - try: - elapsed = _compute_and_save_trend(user, slug) - logger.info( - "[%s] Completed for user %d in %.1fs", - slug, user_id, elapsed, - ) - except Exception: - logger.exception("[%s] Failed for user %d", slug, user_id) + periods = get_supported_periods(slug) + + for period in periods: + logger.info("[%s/%s] Computing for user %d...", slug, period, user_id) + try: + elapsed = compute_and_save_trend(user, slug, period) + logger.info( + "[%s/%s] Completed for user %d in %.1fs", + slug, + period, + user_id, + elapsed, + ) + except Exception: + logger.exception("[%s/%s] Failed for user %d", slug, period, user_id) diff --git a/vrobbler/apps/trends/templates/trends/_activity_distribution.html b/vrobbler/apps/trends/templates/trends/_activity_distribution.html index e48545c..aa480d3 100644 --- a/vrobbler/apps/trends/templates/trends/_activity_distribution.html +++ b/vrobbler/apps/trends/templates/trends/_activity_distribution.html @@ -2,7 +2,7 @@
- Total scrobbles: {{ data.total_count }} + Total scrobbles{% if current_period_label %} ({{ current_period_label }}){% endif %}: {{ data.total_count }}
| Media Type | -Recent (30 days) | -Previous (30 days) | +Recent ({{ current_period_label }}) | +Previous ({{ current_period_label }}) | Change |
|---|