sessions / 327ba94c630a431ca2355e5f121e16498b6af2e0

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

ignore whitespace

commit 327ba94c630a431ca2355e5f121e16498b6af2e0Author: Colin Powell <colin@unbl.ink>Date:   Tue Jun 23 22:21:18 2026 -0400    [trends] Add new time of day trenddiff --git a/PROJECT.org b/PROJECT.orgindex 5422ab5..054cdff 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -604,6 +604,17 @@ independent of the email flow it was originally creatdd for  ** TODO [#B] Is there way to create unique slugs for media instances :media_types: +** DONE [#B] Time of Day Categories trend :trends:+:PROPERTIES:+:ID:       6598074f-2290-46db-967b-29f45d30be29+:END:++*** Description++Added a "Time of Day Categories" trend that groups scrobbles for Books, Trails,+Birding Locations, and Board Games into Early Bird (5-10:59am), Day Jay (11am-6:59pm),+and Night Owl (7pm-4:59am) buckets. Shows both overall and per-media-type breakdowns.+ * Version 58.1 [1/1] ** DONE [#B] Add auto genre tagging for papers :books:papers:metadata: :PROPERTIES:diff --git a/vrobbler/apps/trends/templates/trends/_time_of_day_categories.html b/vrobbler/apps/trends/templates/trends/_time_of_day_categories.htmlnew file mode 100644index 0000000..a80a5de--- /dev/null+++ b/vrobbler/apps/trends/templates/trends/_time_of_day_categories.html@@ -0,0 +1,65 @@+<div class="row">+  <div class="col-12">+    {% if data.total and data.total > 0 %}+    <h5>Overall</h5>+    <div class="table-responsive mb-4">+      <table class="table table-striped table-sm">+        <thead>+          <tr>+            <th>Category</th>+            <th class="text-end">Scrobbles</th>+            <th class="text-end">%</th>+          </tr>+        </thead>+        <tbody>+          {% for slug, info in data.categories.items %}+          <tr>+            <td>{{ info.label }}</td>+            <td class="text-end">{{ info.count }}</td>+            <td class="text-end">{{ info.pct }}%</td>+          </tr>+          {% endfor %}+          <tr class="table-secondary">+            <td><strong>Total</strong></td>+            <td class="text-end"><strong>{{ data.total }}</strong></td>+            <td class="text-end"></td>+          </tr>+        </tbody>+      </table>+    </div>++    <h5>By Media Type</h5>+    {% for mt, mt_data in data.by_media_type.items %}+    <h6 class="mt-3">{{ mt }}</h6>+    <div class="table-responsive">+      <table class="table table-striped table-sm">+        <thead>+          <tr>+            <th>Category</th>+            <th class="text-end">Scrobbles</th>+            <th class="text-end">%</th>+          </tr>+        </thead>+        <tbody>+          {% for slug, info in mt_data.categories.items %}+          <tr>+            <td>{{ info.label }}</td>+            <td class="text-end">{{ info.count }}</td>+            <td class="text-end">{{ info.pct }}%</td>+          </tr>+          {% endfor %}+          <tr class="table-secondary">+            <td><strong>Total</strong></td>+            <td class="text-end"><strong>{{ mt_data.total }}</strong></td>+            <td class="text-end"></td>+          </tr>+        </tbody>+      </table>+    </div>+    {% endfor %}++    {% else %}+    <p class="text-muted">No data found for Books, Trails, Birding Locations, or Board Games in this period.</p>+    {% endif %}+  </div>+</div>diff --git a/vrobbler/apps/trends/templates/trends/trend_detail.html b/vrobbler/apps/trends/templates/trends/trend_detail.htmlindex 8cfb46b..3884243 100644--- a/vrobbler/apps/trends/templates/trends/trend_detail.html+++ b/vrobbler/apps/trends/templates/trends/trend_detail.html@@ -55,6 +55,9 @@ {% elif trend.slug == "reading-pace-vs-activity" %}   {% include "trends/_reading_pace.html" %} +{% elif trend.slug == "time-of-day-categories" %}+  {% include "trends/_time_of_day_categories.html" %}+ {% elif trend.slug == "trending-up" %}   {% include "trends/_trending_up.html" %} diff --git a/vrobbler/apps/trends/trends/__init__.py b/vrobbler/apps/trends/trends/__init__.pyindex c365ae4..2149e33 100644--- a/vrobbler/apps/trends/trends/__init__.py+++ b/vrobbler/apps/trends/trends/__init__.py@@ -15,6 +15,7 @@ from trends.trends.mood import (     compute_mood_weather, ) from trends.trends.reading import compute_reading_pace_vs_activity+from trends.trends.time_of_day import compute_time_of_day_categories from trends.trends.trending import compute_trending_up  TREND_REGISTRY = {}@@ -44,5 +45,8 @@ compute_peak_hours = register("peak-hours")(compute_peak_hours) compute_reading_pace_vs_activity = register("reading-pace-vs-activity")(     compute_reading_pace_vs_activity )+compute_time_of_day_categories = register("time-of-day-categories")(+    compute_time_of_day_categories+) compute_trending_up = register("trending-up")(compute_trending_up) compute_weekly_rhythm = register("weekly-rhythm")(compute_weekly_rhythm)diff --git a/vrobbler/apps/trends/trends/time_of_day.py b/vrobbler/apps/trends/trends/time_of_day.pynew file mode 100644index 0000000..9ac4b21--- /dev/null+++ b/vrobbler/apps/trends/trends/time_of_day.py@@ -0,0 +1,89 @@+from collections import OrderedDict++from django.db.models import Count, Q+from django.db.models.functions import Extract+from scrobbles.models import Scrobble+++TARGET_MEDIA_TYPES = ["Book", "Trail", "BirdingLocation", "BoardGame"]++CATEGORIES = OrderedDict(+    [+        ("early_bird", {"label": "Early Bird", "hours": {5, 6, 7, 8, 9, 10}}),+        ("day_jay", {"label": "Day Jay", "hours": {11, 12, 13, 14, 15, 16, 17, 18}}),+        ("night_owl", {"label": "Night Owl", "hours": {19, 20, 21, 22, 23, 0, 1, 2, 3, 4}}),+    ]+)+++def _categorize_hour(hour):+    for slug, cat in CATEGORIES.items():+        if hour in cat["hours"]:+            return slug+    return None+++def compute_time_of_day_categories(user, period="last_30"):+    from trends.utils import get_date_range++    start, end = get_date_range(period)+    filters = Q(user=user, media_type__in=TARGET_MEDIA_TYPES, timestamp__isnull=False)+    if start:+        filters &= Q(timestamp__gte=start)+    if end:+        filters &= Q(timestamp__lte=end)++    qs = (+        Scrobble.objects.filter(filters)+        .annotate(hour=Extract("timestamp", "hour"))+        .values("media_type", "hour")+        .annotate(count=Count("id"))+        .order_by("media_type", "hour")+    )++    raw = {}+    for row in qs:+        mt = row["media_type"]+        raw.setdefault(mt, {})[row["hour"]] = row["count"]++    by_media_type = {}+    grand_totals = {"early_bird": 0, "day_jay": 0, "night_owl": 0}+    grand_total = 0++    for mt in TARGET_MEDIA_TYPES:+        mt_data = raw.get(mt, {})+        cat_counts = {"early_bird": 0, "day_jay": 0, "night_owl": 0}+        mt_total = 0+        for hour, count in mt_data.items():+            slug = _categorize_hour(hour)+            if slug:+                cat_counts[slug] += count+                mt_total += count+        by_media_type[mt] = {+            "total": mt_total,+            "categories": {},+        }+        for slug in CATEGORIES:+            c = cat_counts[slug]+            by_media_type[mt]["categories"][slug] = {+                "count": c,+                "pct": round((c / mt_total * 100), 1) if mt_total else 0,+                "label": CATEGORIES[slug]["label"],+            }+            grand_totals[slug] += c+        grand_total += mt_total++    categories = {}+    for slug in CATEGORIES:+        c = grand_totals[slug]+        categories[slug] = {+            "count": c,+            "pct": round((c / grand_total * 100), 1) if grand_total else 0,+            "label": CATEGORIES[slug]["label"],+        }++    return {+        "categories": categories,+        "total": grand_total,+        "by_media_type": by_media_type,+    }diff --git a/vrobbler/apps/trends/utils.py b/vrobbler/apps/trends/utils.pyindex b189ceb..7f739cd 100644--- a/vrobbler/apps/trends/utils.py+++ b/vrobbler/apps/trends/utils.py@@ -25,6 +25,7 @@ TIME_BOUND_TRENDS = {     "mood-weather",     "peak-hours",     "reading-pace-vs-activity",+    "time-of-day-categories",     "trending-up",     "weekly-rhythm", }diff --git a/vrobbler/apps/trends/views.py b/vrobbler/apps/trends/views.pyindex aef5dfe..8ad2e21 100644--- a/vrobbler/apps/trends/views.py+++ b/vrobbler/apps/trends/views.py@@ -55,6 +55,11 @@ TREND_METADATA = {         "description": "Compare how long you read per session with and without concurrent music.",         "icon": "๐Ÿ“Š",     },+    "time-of-day-categories": {+        "title": "Time of Day Categories",+        "description": "Are you an early bird, day jay, or night owl? Categorized by Books, Trails, Birding Locations, and Board Games.",+        "icon": "๐Ÿฆ‰",+    },     "trending-up": {         "title": "Trending Media Types",         "description": "Which media types have you been consuming more or less of recently?",