diff --git a/PROJECT.org b/PROJECT.org index 516e79b..34d801b 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -93,9 +93,15 @@ fetching and simple saving. :LOGBOOK: CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 :END: -* Backlog [22/42] -** TODO Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: +* Backlog [26/42] +** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: +:PROPERTIES: +:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85 +:END: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles: +:PROPERTIES: +:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb +:END: ** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement: :PROPERTIES: :ID: ab31fdc3-359c-1b1d-6b9d-546b476021ba @@ -391,16 +397,32 @@ CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 } #+end_src ** TODO [#C] User should be able to enable auto trail tracking via amail reader with Garmin LiveTrack URLs :vrobbler:trails:project:feature:personal: -** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature: -https://codepen.io/oliviale/pen/QYqybo -** TODO [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks: +:PROPERTIES: +:ID: 133bcf71-078f-4efa-a029-1eae4b4d146d +:END: ** TODO [#B] Add importer class for IMAP imports :vrobbler:feature:imap:importers:project:personal: +:PROPERTIES: +:ID: b1426d92-2feb-4d15-9738-d5b7b0594f96 +:END: ** TODO [#B] Add AllTrails as a source for Trail data :vrobbler:trails:feature:personal:project: +:PROPERTIES: +:ID: 39313362-cdfe-46e7-bbd4-9139a65c0b3c +:END: + +*** Description Pretty clear, I would love to make trails more useful. Historically I wasn't hiking a lot, which made the source for this a bit silly. But it's clear that AllTrails is the best source, though having TrailForks is nice to. + ** TODO [#B] Add `garmin_activity_id` to the TrailMetadataLog class :vrobbler:trails:feature:personal:project: -Would be nice to have some loose connection to the actual event in my Garmin profile. +:PROPERTIES: +:ID: 5a4fb0f8-0555-40ec-b06f-93c26bd686f4 +:END: +*** Description + +Would be nice to have some loose connection to the actual event in my Garmin +profile. + ** TODO [#B] Explore a way to add metadata editing to scrobbles after saving :vrobbler:spike:scrobbling:personal:project: Could be as simple as a JSON form on the scrobble detail page (do I have have @@ -409,15 +431,19 @@ one of those yet?). ** TODO [#B] Add webdav syncing to retroarch imports :vrobbler:videogames:webdav:feature:project:personal: ** TODO [#B] Add CSV endpoint for book scrobbles that LibraryThing can ingest :personal:project:books:feature:export: https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-library-thing-can-ingest-6X7QPMRp265xMXqg#comment-6X7QrXq6gJjMP4hg -** TODO [#B] Fix PuzzleLogData has no attribute form :vrobbler:puzzles:personal:project:logdata: ** TODO [#B] Scrape ComicBookRoundUp ratings for comic book metadata :vrobbler:books:feature:comicbook:personal:project: - Note taken on [2025-09-25 Thu 10:51] As an example https://comicbookroundup.com/comic-books/reviews/humanoids-publishing/the-history-of-science-fiction -** TODO [#B] Add PuzzleLogData class with with_people and completed :vrobbler:feature:puzzles:logdata:personal:project: ** TODO [#A] Find page numbers for comic books from ComicVine :vrobbler:feature:books:personal:project: +:PROPERTIES: +:ID: 79f867c3-1288-4143-b6bf-2a452983ee9f +:END: ** TODO [#A] Fix koreader scrobble imports to use DST properly :vrobbler:personal:bug:books:imports: +:PROPERTIES: +:ID: 79758cba-a440-48b6-a637-efb88827acf2 +:END: - Note taken on [2025-09-25 Thu 10:37] \\ @@ -431,11 +457,73 @@ https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-li an hour for part of the year. Also, we'd need to adjust any old scrobbles that took place with DST off to roll them back by an hour. + +*** Description + +This is a long-standing problem when daylight saving time takes effect. Time is +manually set on a KoReader device (or at least, always saved in local time). So +whatever time KoReader reports, we need to know, given the date and the user +profile's historic timezone, how many hours to adjust the KoReader time to get +to GMT to save it in the database. + ** TODO [#A] When creating org-mode tasks, don't copy comments :vrobbler:bug:scrobbles:tasks: -** TODO Fix various artist album problem with Superwolves (track with multiple artists) :vrobbler:project:music:bug:artists: +:PROPERTIES: +:ID: 0c762d09-fc69-4e75-be40-7eaaf04f178e +:END: + +*** Description + +It would be nice to not duplicate comments that exist on a task when it's first scrobbled. + +** TODO [#A] Fix various artist album problem with Superwolves (track with multiple artists) :vrobbler:project:music:bug:artists: :PROPERTIES: :ID: 590bc038-745f-710b-8272-4d8a3d2efa01 :END: + +*** Description +We have an issue with tracks where there are two artists, like `Matt Sweeney & +Bonnie "Prince" Billy` + +I think we need to allow creating a single artist with both names, so the Artist +is the full name. That said, the ampersand is usually used to split feature +artsits (I think) so I'm not sure how this would work reliably. + +Also, it's possible musicbrainz does not have dual artist listings. So it's +possible the longer term solution is to allow multiple artists per track the way +we now allow tracks to be on multiple albums. We could deprecate the Artist FK +or at least make it optional, and then require a M2M between Track and Artist. + +Then this one would be a Track by both `Matt Sweeney` and `Bonnie "Prince" +Billy` + +** DONE [#A] Add CSS Grid calendar view for scrobbles :vrobbler:personal:project:templates:feature: +:PROPERTIES: +:ID: be915acf-d803-466a-8770-823819ebf2a9 +:END: + +*** Description + +Calendar view at /scrobbles/calendar/ showing select media types (Tasks, +Birding, Food, Trails, VideoGames, Books) in a CSS Grid month layout. + +- Emoji badges per scrobble on each day cell, hover reveals title +- Click emoji to navigate to scrobble detail +- Prev/Next month navigation +- Based on the CodePen CSS Grid calendar pattern: + https://codepen.io/oliviale/pen/QYqybo + +** DONE [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks: +:PROPERTIES: +:ID: dd751edf-6192-2ed0-cd85-905f423f82b0 +:END: +** DONE [#B] Fix PuzzleLogData has no attribute form :vrobbler:puzzles:personal:project:logdata: +:PROPERTIES: +:ID: cb8e697a-9fc2-c463-5d4d-fa15d3e833a0 +:END: +** DONE [#B] Add PuzzleLogData class with with_people and completed :vrobbler:feature:puzzles:logdata:personal:project: +:PROPERTIES: +:ID: 1b38c8a8-86d3-b73d-c584-dfb0497970dc +:END: ** DONE Add weather lookup to the mood check-in flow :vrobbler:project:moods:feature:checkin: :PROPERTIES: :ID: 175678e2-bfe1-0833-f2a9-f03b9323db78 diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index 37813e7..eba34fd 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -5,6 +5,7 @@ from tasks.webhooks import EmacsWebhookView, TodoistWebhookView app_name = "scrobbles" urlpatterns = [ + path("calendar/", views.ScrobbleCalendarView.as_view(), name="calendar"), path("search/", views.ScrobbleSearchView.as_view(), name="search"), path("status/", views.ScrobbleStatusView.as_view(), name="status"), path( diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 4950a40..5e38ce5 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -2,6 +2,7 @@ import calendar import json import logging from datetime import datetime, timedelta +from uuid import uuid4 import pendulum import pytz @@ -1281,6 +1282,129 @@ class EmbeddableTopArtistsWidget(TemplateView): return context +class ScrobbleCalendarView(LoginRequiredMixin, TemplateView): + template_name = "scrobbles/calendar.html" + + CALENDAR_MEDIA_TYPES = [ + "Task", + "BirdingLocation", + "Food", + "Trail", + "VideoGame", + "Book", + "Mood", + ] + + MEDIA_EMOJI = { + "Task": "✅", + "BirdingLocation": "🐦", + "Food": "🍽️", + "Trail": "🥾", + "VideoGame": "🎮", + "Book": "📚", + "Mood": "🥰", + } + + MONTH_COLORS = [ + "#db7a7a", # Jan + "#db847a", # Feb + "#b0db7a", # Mar + "#7adb82", # Apr + "#7adbb3", # May + "#7ab6db", # Jun + "#7a8edb", # Jul + "#977adb", # Aug + "#c47adb", # Sep + "#db7ac5", # Oct + "#db7a90", # Nov + "#db7a7a", # Dec + ] + + def _day_color(self, month_index, day_num, total_days): + import colorsys + hue = (month_index - 1) * 30 / 360 + lightness = 0.80 + (day_num / total_days) * 0.15 + r, g, b = colorsys.hls_to_rgb(hue, lightness, 0.5) + return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + today = timezone.localdate() + date_param = self.request.GET.get("date", today.isoformat()[:7]) + try: + year, month = [int(x) for x in date_param.split("-")] + except (ValueError, IndexError): + year, month = today.year, today.month + + _, total_days = calendar.monthrange(year, month) + month_start = datetime(year, month, 1).date() + month_end = datetime(year, month, total_days).date() + first_weekday = month_start.weekday() + + prev_month = month_start - timedelta(days=1) + next_month = month_end + timedelta(days=1) + + scrobbles = ( + Scrobble.objects.filter( + user=self.request.user, + timestamp__date__gte=month_start, + timestamp__date__lte=month_end, + media_type__in=self.CALENDAR_MEDIA_TYPES, + ) + .select_related("task", "birding_location", "food", "trail", "video_game", "book", "mood") + .order_by("timestamp") + ) + + day_map = {d: [] for d in range(1, total_days + 1)} + for scrobble in scrobbles: + day_map[scrobble.timestamp.day].append(scrobble) + + missing_uuids = [s for s in scrobbles if not s.uuid] + if missing_uuids: + for scrobble in missing_uuids: + scrobble.uuid = uuid4() + Scrobble.objects.bulk_update(missing_uuids, ["uuid"]) + + calendar_days = [] + month_color = self.MONTH_COLORS[(month - 1) % 12] + for day_num in range(1, total_days + 1): + day_scrobbles = [] + for scrobble in day_map[day_num]: + day_scrobbles.append( + { + "uuid": scrobble.uuid, + "emoji": self.MEDIA_EMOJI.get(scrobble.media_type, "📌"), + "title": str(scrobble.media_obj) if scrobble.media_obj else scrobble.media_type, + "media_type": scrobble.media_type, + } + ) + calendar_days.append({ + "day": day_num, + "scrobbles": day_scrobbles, + "is_today": year == today.year and month == today.month and day_num == today.day, + "color": self._day_color(month, day_num, total_days), + }) + + ctx.update( + { + "year": year, + "month": month, + "month_name": month_start.strftime("%B"), + "total_days": total_days, + "first_weekday": first_weekday, + "calendar_days": calendar_days, + "prev_month": prev_month.isoformat()[:7], + "next_month": next_month.isoformat()[:7], + "today": today, + "current_month": today.isoformat()[:7], + "day_names": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + "month_color": month_color, + } + ) + return ctx + + class ScrobbleSearchView(LoginRequiredMixin, TemplateView): template_name = "scrobbles/scrobble_search.html" diff --git a/vrobbler/templates/scrobbles/calendar.html b/vrobbler/templates/scrobbles/calendar.html new file mode 100644 index 0000000..9f3dc88 --- /dev/null +++ b/vrobbler/templates/scrobbles/calendar.html @@ -0,0 +1,180 @@ +{% extends "base.html" %} +{% load static %} +{% load humanize %} + +{% block head_extra %} + +{% endblock %} + +{% block content %} +Home +