- Investigate how historice lastfm imports work - Fix LookupError on scrobble start/long-plays views - Add Flexify workout importer - Add workouts app with exercise catalog and workout routines - When sharing a scrobble, share the log data too
560 lines
20 KiB
Org Mode
560 lines
20 KiB
Org Mode
#+title: Vrobbler Project
|
|
|
|
We should convert this PROJECT file to put tickets in a subdirectory, tickets, with each ticket having it's own shortid_title.org
|
|
|
|
* Overview
|
|
|
|
Vrobbler began humbly enough as a way to use Jellyfin's webhook to keep track of
|
|
the shows and movies I was watching. More specifically, I broke my ankle a few
|
|
days after Christmas in 2022 and spent the next four months very slowly
|
|
recovering after surgical repair. So once I had the webhook working, and
|
|
scrobbling videos, it was only a matter of time till I expaned it to mopidy to
|
|
replicate LastFM. Then I added board games, books via KoReader, sports events,
|
|
podcasts ... it just keeps going. Vrobbler is now a sort of Frankenstein's
|
|
monster of scrobbling an entire life.
|
|
|
|
I am still unconvinced I can keep this going, but being able to scrobble org
|
|
tasks, Todoist tasks, web pages I've read and trails I've hiked has turned out
|
|
to be sometimes cathartic and sometimes functional as I try to remember when I
|
|
did a thing.
|
|
|
|
* Backlog [0/24] :vrobbler:project:personal:
|
|
** TODO [#C] After transition to linux add curl_cffi as webpage scrapper again :webpages:metadata:
|
|
:PROPERTIES:
|
|
:ID: d3cce1a7-d540-4d66-bf66-e75378e4eac7
|
|
:END:
|
|
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :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
|
|
:END:
|
|
*** Example payloads from mopidy-webhooks
|
|
**** Podcast playback ended
|
|
#+begin_src json
|
|
{
|
|
"type": "event",
|
|
"event": "track_playback_ended",
|
|
"data": {
|
|
"tl_track": {
|
|
"__model__": "TlTrack",
|
|
"tlid": 13,
|
|
"track": {
|
|
"__model__": "Track",
|
|
"uri": "file:///var/lib/mopidy/media/podcasts/The%20Prince/2022-09-28-Wolf-warriors.mp3",
|
|
"name": "Wolf warriors",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"name": "The Economist"
|
|
}
|
|
],
|
|
"album": {
|
|
"__model__": "Album",
|
|
"name": "The Prince",
|
|
"date": "2022"
|
|
},
|
|
"genre": "Blues",
|
|
"date": "2022",
|
|
"length": 2437778,
|
|
"bitrate": 127988
|
|
}
|
|
},
|
|
"time_position": 3290
|
|
}
|
|
}
|
|
#+end_src
|
|
**** Podcast playback state changes
|
|
#+begin_src json
|
|
{
|
|
"type": "event",
|
|
"event": "playback_state_changed",
|
|
"data": {
|
|
"old_state": "paused",
|
|
"new_state": "playing"
|
|
}
|
|
}
|
|
#+end_src
|
|
|
|
#+begin_src json
|
|
{
|
|
"type": "event",
|
|
"event": "playback_state_changed",
|
|
"data": {
|
|
"old_state": "stopped",
|
|
"new_state": "playing"
|
|
}
|
|
}
|
|
#+end_src
|
|
**** Podcast playback started
|
|
#+begin_src json
|
|
{
|
|
"type": "event",
|
|
"event": "track_playback_started",
|
|
"data": {
|
|
"tl_track": {
|
|
"__model__": "TlTrack",
|
|
"tlid": 13,
|
|
"track": {
|
|
"__model__": "Track",
|
|
"uri": "file:///var/lib/mopidy/media/podcasts/The%20Prince/2022-09-28-Wolf-warriors.mp3",
|
|
"name": "Wolf warriors",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"name": "The Economist"
|
|
}
|
|
],
|
|
"album": {
|
|
"__model__": "Album",
|
|
"name": "The Prince",
|
|
"date": "2022"
|
|
},
|
|
"genre": "Blues",
|
|
"date": "2022",
|
|
"length": 2437778,
|
|
"bitrate": 127988
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#+end_src
|
|
**** Podcast playback paused
|
|
#+begin_src json
|
|
{
|
|
"type": "status",
|
|
"data": {
|
|
"state": "paused",
|
|
"current_track": {
|
|
"__model__": "Track",
|
|
"uri": "file:///var/lib/mopidy/media/podcasts/The%20Prince/2022-09-28-Wolf-warriors.mp3",
|
|
"name": "Wolf warriors",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"name": "The Economist"
|
|
}
|
|
],
|
|
"album": {
|
|
"__model__": "Album",
|
|
"name": "The Prince",
|
|
"date": "2022"
|
|
},
|
|
"genre": "Blues",
|
|
"date": "2022",
|
|
"length": 2437778,
|
|
"bitrate": 127988
|
|
},
|
|
"time_position": 2350
|
|
}
|
|
}
|
|
|
|
#+end_src
|
|
**** Track playback started
|
|
#+begin_src json
|
|
{
|
|
"type": "event",
|
|
"event": "track_playback_started",
|
|
"data": {
|
|
"tl_track": {
|
|
"__model__": "TlTrack",
|
|
"tlid": 14,
|
|
"track": {
|
|
"__model__": "Track",
|
|
"uri": "local:track:Various%20Artists%20-%202008%20-%20Twilight%20OST/01-muse-supermassive_black_hole.mp3",
|
|
"name": "Supermassive Black Hole",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:250dd6551b66a58a6b4897aa697f200c",
|
|
"name": "Muse",
|
|
"musicbrainz_id": "9c9f1380-2516-4fc9-a3e6-f9f61941d090"
|
|
}
|
|
],
|
|
"album": {
|
|
"__model__": "Album",
|
|
"uri": "local:album:md5:455343d54cdd89cb5a3b5ad537ea99d0",
|
|
"name": "Twilight: Original Motion Picture Soundtrack",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:54e4db2d5624f80b0cc290346e696756",
|
|
"name": "Various Artists",
|
|
"musicbrainz_id": "89ad4ac3-39f7-470e-963a-56509c546377"
|
|
}
|
|
],
|
|
"num_tracks": 12,
|
|
"num_discs": 1,
|
|
"date": "2008-11-04",
|
|
"musicbrainz_id": "b4889eaf-d9f4-434c-a68d-69227b12b6a4"
|
|
},
|
|
"composers": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:4d49cbca0b347e0a89047bb019d2779d",
|
|
"name": "Matt Bellamy"
|
|
}
|
|
],
|
|
"genre": "Rock",
|
|
"track_no": 1,
|
|
"disc_no": 1,
|
|
"date": "2008-11-04",
|
|
"length": 211121,
|
|
"musicbrainz_id": "ff1e3e1a-f6e8-4692-b426-355880383bb6",
|
|
"last_modified": 1672712949510
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#+end_src
|
|
**** Track playback in progress
|
|
#+begin_src json
|
|
{
|
|
"type": "status",
|
|
"data": {
|
|
"state": "playing",
|
|
"current_track": {
|
|
"__model__": "Track",
|
|
"uri": "local:track:Various%20Artists%20-%202008%20-%20Twilight%20OST/01-muse-supermassive_black_hole.mp3",
|
|
"name": "Supermassive Black Hole",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:250dd6551b66a58a6b4897aa697f200c",
|
|
"name": "Muse",
|
|
"musicbrainz_id": "9c9f1380-2516-4fc9-a3e6-f9f61941d090"
|
|
}
|
|
],
|
|
"album": {
|
|
"__model__": "Album",
|
|
"uri": "local:album:md5:455343d54cdd89cb5a3b5ad537ea99d0",
|
|
"name": "Twilight: Original Motion Picture Soundtrack",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:54e4db2d5624f80b0cc290346e696756",
|
|
"name": "Various Artists",
|
|
"musicbrainz_id": "89ad4ac3-39f7-470e-963a-56509c546377"
|
|
}
|
|
],
|
|
"num_tracks": 12,
|
|
"num_discs": 1,
|
|
"date": "2008-11-04",
|
|
"musicbrainz_id": "b4889eaf-d9f4-434c-a68d-69227b12b6a4"
|
|
},
|
|
"composers": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:4d49cbca0b347e0a89047bb019d2779d",
|
|
"name": "Matt Bellamy"
|
|
}
|
|
],
|
|
"genre": "Rock",
|
|
"track_no": 1,
|
|
"disc_no": 1,
|
|
"date": "2008-11-04",
|
|
"length": 211121,
|
|
"musicbrainz_id": "ff1e3e1a-f6e8-4692-b426-355880383bb6",
|
|
"last_modified": 1672712949510
|
|
},
|
|
"time_position": 17031
|
|
}
|
|
}
|
|
#+end_src
|
|
**** Track event playback paused
|
|
#+begin_src json
|
|
{
|
|
"type": "event",
|
|
"event": "track_playback_paused",
|
|
"data": {
|
|
"tl_track": {
|
|
"__model__": "TlTrack",
|
|
"tlid": 14,
|
|
"track": {
|
|
"__model__": "Track",
|
|
"uri": "local:track:Various%20Artists%20-%202008%20-%20Twilight%20OST/01-muse-supermassive_black_hole.mp3",
|
|
"name": "Supermassive Black Hole",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:250dd6551b66a58a6b4897aa697f200c",
|
|
"name": "Muse",
|
|
"musicbrainz_id": "9c9f1380-2516-4fc9-a3e6-f9f61941d090"
|
|
}
|
|
],
|
|
"album": {
|
|
"__model__": "Album",
|
|
"uri": "local:album:md5:455343d54cdd89cb5a3b5ad537ea99d0",
|
|
"name": "Twilight: Original Motion Picture Soundtrack",
|
|
"artists": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:54e4db2d5624f80b0cc290346e696756",
|
|
"name": "Various Artists",
|
|
"musicbrainz_id": "89ad4ac3-39f7-470e-963a-56509c546377"
|
|
}
|
|
],
|
|
"num_tracks": 12,
|
|
"num_discs": 1,
|
|
"date": "2008-11-04",
|
|
"musicbrainz_id": "b4889eaf-d9f4-434c-a68d-69227b12b6a4"
|
|
},
|
|
"composers": [
|
|
{
|
|
"__model__": "Artist",
|
|
"uri": "local:artist:md5:4d49cbca0b347e0a89047bb019d2779d",
|
|
"name": "Matt Bellamy"
|
|
}
|
|
],
|
|
"genre": "Rock",
|
|
"track_no": 1,
|
|
"disc_no": 1,
|
|
"date": "2008-11-04",
|
|
"length": 211121,
|
|
"musicbrainz_id": "ff1e3e1a-f6e8-4692-b426-355880383bb6",
|
|
"last_modified": 1672712949510
|
|
}
|
|
},
|
|
"time_position": 67578
|
|
}
|
|
}
|
|
#+end_src
|
|
** TODO [#C] Allow auto trail tracking via email with Garmin LiveTrack URLs :trails:feature:
|
|
:PROPERTIES:
|
|
:ID: 133bcf71-078f-4efa-a029-1eae4b4d146d
|
|
:END:
|
|
** TODO [#C] Fix exporting so it works reliably :exporting:feature:
|
|
|
|
*** Description
|
|
|
|
The existing export function is very naieve. It runs in the web process, takes
|
|
too long and just dumps tracks. We should make it more robust by creating one
|
|
CSV file per scrobble media type and writing them into a zip file that gets
|
|
placed in the media directory:
|
|
|
|
`/media/exports/user_<user_id>/<timestamp>-export.zip`
|
|
|
|
And this should all be done in a celery task that is just kicked off by the
|
|
"Export" button on the frontend
|
|
|
|
** TODO [#B] Add AllTrails as a source for Trail data :trails:feature:
|
|
: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 TrailLogData class :trails:feature:
|
|
: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] Fix how we show notes and descriptions from scrobbles to users :metadata:notes:tasks:
|
|
:PROPERTIES:
|
|
:ID: adf4c513-a417-4ec9-8831-f01ffcf63276
|
|
:END:
|
|
*** Description
|
|
|
|
Currently the display of notes leaves something to be desired. The biggest issue
|
|
is that they don't look good on mobile and are probably trying to be too cute.
|
|
Rather than post-it note style, we should just put notes in a list under the
|
|
description, above the Edit Log toggle, with timestamps for when they were
|
|
added.
|
|
|
|
They should also probably support markdown formatting and that should be
|
|
displayed in the template.
|
|
|
|
** TODO [#B] Add CSV endpoint for book scrobbles that LibraryThing can ingest :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] Make IMAP and WebDAV configurable :webdav:feature:imap:importers:
|
|
:PROPERTIES:
|
|
:ID: b1426d92-2feb-4d15-9738-d5b7b0594f96
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
Currently we have webdav able to import post types of file-based incoming data,
|
|
usually in the form of CSVs but also gpx files, bgstats json files, and
|
|
audioscrobbler TSV files.
|
|
|
|
What if the user could specify via their profile (settings) which imports they
|
|
wanted to use IMAP for and which ones they wanted to use WebDAV for.
|
|
|
|
Then we'd have two celery tasks that would be kicked off periodically via
|
|
celerybeat, one for IMAP imports every 12 minutes and one for WebDAV every 3
|
|
minutes. Both would be responsible for checking if a user has an configured
|
|
imports of their type, check if an import needs to run, and dispatch the
|
|
needed import celery task. This is how the WebDAV celery task currently works.
|
|
|
|
This would also be an opporunity to clean up the code around WebDAV imports
|
|
and make them more re-usable for other import services.
|
|
|
|
** TODO [#A] Add an exception list of artists as a constant that are exempted from splitting :music:artists:metadata:
|
|
:PROPERTIES:
|
|
:ID: fd86a11a-73ec-470d-b5e3-2d90ba9137c8
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
Certain artists like "Simon & Garfunkel" are actually one artist. While we don't want to mess with splitting up
|
|
tracks into featured artists, we should have a "LITERAL_ARTIST_TITLES" constant that can have exceptions like
|
|
this put into it and then we stop trying to pull the artist apart when we run into it.
|
|
|
|
** TODO [#A] Before enriching anything, trust the POST data :feature:scrobbles:metadata:
|
|
:PROPERTIES:
|
|
:ID: db6b05f8-09f4-49f5-9838-fbacc9fe9cd0
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
Both Jellyfin and Mopidy provide a decent amount of metadata when they POST to our webhooks.
|
|
|
|
In most cases, we should be able to trust this data to created music tracks or videos rather
|
|
than going to third-party services to enrich. Thus, for tracks and videos we should search in
|
|
the local database for imdb_id or musicbrainz_id for the specific content and, if found, not
|
|
enrich further.
|
|
|
|
If not found, tracks and videos from mopidy and jellyfin should be created as completely as
|
|
possible using only the POST data from the webhooks, tagged the scrobble with "webhook-metadata-only"
|
|
and start the scrobble. A separate celery task should be kicked off to enrich the track or video
|
|
async with the POST data stored in the log["raw_data"] and used by the celery enrichment task
|
|
to go try to enrich the media instance. Should this enrichment fail, tag the scrobble as "enrichment-failed"
|
|
log a warning and move on.
|
|
|
|
** TODO [#B] Allow browing a user's favorited media :favorites:feature:
|
|
:PROPERTIES:
|
|
:ID: 5c2cf004-d01f-4576-9bbb-974235e7408a
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
We should have a global view `/favorites/` that shows the logged in users's
|
|
favorited media objects.
|
|
|
|
** TODO [#B] Find page numbers for comic books from ComicVine :feature:books:
|
|
:PROPERTIES:
|
|
:ID: 79f867c3-1288-4143-b6bf-2a452983ee9f
|
|
:END:
|
|
** TODO [#C] Implement loguru into project :feature:loguru:logging:
|
|
:PROPERTIES:
|
|
:ID: efcd0c0a-db81-4518-9c23-5505d59e8ef5
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
Would be great to formalize how we log so we can search for errors and such more
|
|
easily. And our exposure to PII is really low at this point in the project,
|
|
so we can probably use backtrace=True and diagnose=True to help us root cause
|
|
bugs faster.
|
|
|
|
** TODO [#B] Scrape ComicBookRoundUp ratings for comic book metadata :books:feature:comicbook:
|
|
:PROPERTIES:
|
|
:ID: b3cc57ca-3d2c-468d-ab7c-c47f1120309b
|
|
:END:
|
|
|
|
*** Description
|
|
As an example https://comicbookroundup.com/comic-books/reviews/humanoids-publishing/the-history-of-science-fiction
|
|
|
|
** TODO [#C] Make podcast date format configurable in settings :podcasts:configuration:
|
|
:PROPERTIES:
|
|
:ID: b01a94f8-328f-41ed-a62e-8b99c755b82d
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
=PODCAST_DATE_FORMAT= is hardcoded to ="YYYY-MM-DD"=. Should be in Django settings or environment variables for deploy-specific configuration.
|
|
|
|
File: ~vrobbler/apps/podcasts/utils.py~ (line 13)
|
|
|
|
** TODO [#C] Extract zombie scrobble query into custom manager :refactoring:manager:
|
|
:PROPERTIES:
|
|
:ID: 79c874e1-ca6f-4bce-9259-e3eebdca8a41
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
The zombie scrobble cleanup query lives in a utility function. Should be a
|
|
custom model manager method (e.g. =Scrobble.objects.zombies()=).
|
|
|
|
File: ~vrobbler/apps/scrobbles/utils.py~ (line 204)
|
|
|
|
** TODO [#C] Allow profile to set start of week :profiles:configuration:
|
|
:PROPERTIES:
|
|
:ID: 0449279a-9550-430e-be0c-816df7273080
|
|
:END:
|
|
|
|
*** Description
|
|
=start_of_week()= and =end_of_week()= use Monday as default. Should be a user
|
|
profile setting for different cultural week start conventions.
|
|
|
|
File: ~vrobbler/apps/profiles/utils.py~ (lines 39, 44)
|
|
|
|
** TODO [#C] Add constants for data dictionary keys (multiple files) :refactoring:constants:
|
|
:PROPERTIES:
|
|
:ID: d4415f9b-620a-4be7-925d-fa71c02ba1d1
|
|
:END:
|
|
|
|
*** Description
|
|
Multiple files use magic string literals for dict keys. Should be extracted to
|
|
named constants for maintainability.
|
|
|
|
- Files:
|
|
- ~vrobbler/apps/locations/models.py~ (line 63) -- ="lat"=, ="lon"= etc.
|
|
- ~vrobbler/apps/webpages/models.py~ (line 290) -- ="url"=
|
|
- ~vrobbler/apps/scrobbles/importers/tsv.py~ (line 55) -- ="S"= completion status
|
|
|
|
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
|
|
** TODO [#A] Update how board game scrobbles work :boardgames:
|
|
|
|
*** Description
|
|
|
|
When we scrobble a board game from a BGG URL, instead of going to the media
|
|
detail page, we should go to the scrobble detail page, with the Edit Log form
|
|
expanded by default.
|
|
|
|
The Edit log form should have from top to bottom:
|
|
|
|
- Board/Variant (one or many BoardGameVariant in a multi-select widget)
|
|
- People (which should be similar to the Bird widget on BirdLocation and allow setting per user score, win true/false, rank, new true/false, seat_ordrer)
|
|
- Expansion ids (which should a multi-select widget of expansions for this game)
|
|
- Location (which should be a drop down of BoardGameLocations for this user)
|
|
|
|
** TODO [#B] Auto sync board game scrobbles to BGG :boardgames:
|
|
|
|
*** Description
|
|
|
|
** TODO [#A] Add trends tests for concurrent trends :trends:tests:concurrent:
|
|
:PROPERTIES:
|
|
:ID: ca613753-d203-4429-b062-36e3312bca82
|
|
:END:
|
|
|
|
*** Description
|
|
|
|
Added a `workouts` Django app so we can scrobble gym sessions.
|
|
|
|
- New `Exercise` model with the wrkout/exercises.json catalog (873 exercises),
|
|
imported via `python manage.py import_wrkout_exercises`. Image files come from
|
|
inside the tarball and are stored via ImageKit specs.
|
|
- New `WorkoutRoutine` model (ScrobblableMixin, media_type_label "Workout"),
|
|
scrobbled through the existing Scrobble system with a `workout_routine` FK and
|
|
`WorkoutLogData` storing sets/reps/weight per exercise plus duration, bodyweight
|
|
and RPE.
|
|
- Weights are stored canonically in kg; the profile's `weigh_in_units` setting
|
|
drives form input and display units. A generic
|
|
`apply_media_unit_conversions` helper in scrobbles/utils.py handles both
|
|
drinks (`size_ml`) and workout weights, fixing the old GET-only drinks
|
|
conversion so imperial POSTs store kg.
|
|
- Templates, admin, DRF viewsets (`exercises`, `workout-routines`), MCP tools,
|
|
and `tests/workouts_tests/` (unit conversion, form round-trips, importer,
|
|
imperial POST) included.
|
|
|