diff --git a/PROJECT.org b/PROJECT.org index 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.html new file mode 100644 index 0000000..a80a5de --- /dev/null +++ b/vrobbler/apps/trends/templates/trends/_time_of_day_categories.html @@ -0,0 +1,65 @@ +
+
+ {% if data.total and data.total > 0 %} +
Overall
+
+ + + + + + + + + + {% for slug, info in data.categories.items %} + + + + + + {% endfor %} + + + + + + +
CategoryScrobbles%
{{ info.label }}{{ info.count }}{{ info.pct }}%
Total{{ data.total }}
+
+ +
By Media Type
+ {% for mt, mt_data in data.by_media_type.items %} +
{{ mt }}
+
+ + + + + + + + + + {% for slug, info in mt_data.categories.items %} + + + + + + {% endfor %} + + + + + + +
CategoryScrobbles%
{{ info.label }}{{ info.count }}{{ info.pct }}%
Total{{ mt_data.total }}
+
+ {% endfor %} + + {% else %} +

No data found for Books, Trails, Birding Locations, or Board Games in this period.

+ {% endif %} +
+
diff --git a/vrobbler/apps/trends/templates/trends/trend_detail.html b/vrobbler/apps/trends/templates/trends/trend_detail.html index 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__.py index 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.py new file mode 100644 index 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.py index 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.py index 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?",