#+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/34] :vrobbler:project:personal:
** TODO [#C] Configure IMAP folder/start in user profile :imap:settings:
*** Description

Refactor the IMAP import flow as something that can be configured in a tab of the user profile's settings.

*** Implementation

Mostly I used webdav now, but we should maintain IMAP importing as an option in user settings.

- File: ~vrobbler/apps/scrobbles/importers/imap.py~ (line 30)
- Mailbox folder is hardcoded to ="INBOX"=. Should be configurable in the
    user's IMAP profile settings, especially for users with complex folder structures.

** TODO [#C] Verify Last.fm timestamp timezone handling :lastfm:timestamps:
*** Description

Last.FM timezone handling may not work 100% correctly. And maybe that's okay.

*** Implementation

- File: ~vrobbler/apps/scrobbles/importers/lastfm.py~ (line 148)
- A TODO questions whether the current =utcfromtimestamp()= + =.replace()=
    pattern is correct. The commented-out =datetime.fromtimestamp(..., UTC)=
    may be the right approach. Needs review.

** 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 [#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 [#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 [#C] Add track number and rating to CSV export :export:metadata:

*** Description

Track number and rating are missing in the CSV export of scrobbles.

*** Implementation
Also need to generally clean up how exporting works. 

- File: ~vrobbler/apps/scrobbles/export.py~ (lines 51-52)
- Track number is hardcoded to =0= and rating is hardcoded to ="S"=.
  The data model may need extension to support these fields.

** TODO [#C] Simplify Track creation API in scrobblers :refactoring:api:
    - File: ~vrobbler/apps/scrobbles/scrobblers.py~ (line 79)
    - =Track.find_or_create()= requires individual kwargs extracted from =post_data=.
      Should accept a dict or have a cleaner unified interface.

** TODO [#C] Enrich books/authors after creation (recurring pattern) :books:enrichment:

*** Description

Generally move enrichment of media to follow-up tasks, so long as we have media length and
title from the input source.

*** Implementation
    - Files:
      - ~vrobbler/apps/books/models.py~ (lines 239, 320, 548)
      - ~vrobbler/apps/books/koreader.py~ (lines 79, 119)
    - Multiple places where Authors and Books are created without follow-up enrichment.
      Should fire async enrichment tasks (Celery) after creation.

** TODO [#C] Enrich TMDB resultw with TMDB URL :videos:enrichment:
    - File: ~vrobbler/apps/videos/sources/tmdb.py~ (lines 46, 56)
    - Cover URLs are constructed but the actual TMDB page URL for the movie/show
      is not stored. Minor enrichment.

** TODO [#C] Enrich bricksets from website on creation :bricksets:enrichment:
*** Implementation
- File: ~vrobbler/apps/bricksets/models.py~ (line 75)
- =BrickSet.find_or_create()= only stores the set number. Should fetch additional
  data (name, piece count, image) from brickset.com or similar.

** TODO [#B] Make WebDAV paths configurable per-user :webdav:settings:
*** Description

This may be a no-op now. A lot of webdav code has changed.

*** Implementation

- File: ~vrobbler/apps/scrobbles/importers/webdav.py~ (lines 125, 410)
- Both the GPX path and scale path are hardcoded via =DEFAULT_GPX_PATH= and
    =DEFAULT_SCALE_PATH=. Should be configurable in user profile settings.
** TODO [#B] Add auto-population of serial scrobble IDs for bricksets :bricksets:serials:
:PROPERTIES:
:ID:       69dcb387-c7f0-40d0-895f-60d79c664b10
:END:

*** Implementation

When scrobbling bricksets (LEGO), should check for past scrobbles and auto-populate a serial scrobble ID for tracking multiple builds of the same set.

File: ~vrobbler/apps/scrobbles/scrobblers.py~ (line 1245)
** TODO [#B] Raise exception on WebDAV fetch failure :webdav:error-handling:
*** Implementation

- File: ~vrobbler/apps/books/koreader.py~ (line 418)
- =fetch_file_from_webdav()= returns an empty string on failure, which could
    silently propagate. Should raise an exception for proper error handling.

** TODO [#B] Make book matching use Google Books ID or author name :books:matching:
*** Description

This may be a no-op situation as we've moved away from Google Books. But maybe we haven't.

*** Implementation

- File: ~vrobbler/apps/books/models.py~ (line 270)
- =Book.find_or_create()= only matches by =original_title=, which is fragile.
    Should also match by Google Books ID or author name (like Track does).

** TODO [#B] TODOIST_* variable names flagged as TODOs (false positives) :tasks:metadata:cleanup:
*** Description

Need to clean up how we store task data in logdata.

*** Implementation

Should clean up the task logdata dict so that we have fewer source-specific names, try to generalize task detail implementation from the input source, while keeping the full task payload for future reference.

- Files:
  - ~vrobbler/settings.py~ (lines 72-73) -- =TODOIST_CLIENT_ID=, =TODOIST_CLIENT_SECRET=
  - ~vrobbler/apps/scrobbles/constants.py~ (line 39) -- =TODOIST_TASK_URL=
  - ~vrobbler/apps/tasks/models.py~ (line 13) -- =TODOIST_TASK_URL=
- These are variable/constant names for the Todoist integration, not actual
  TODOs. No action needed, but worth noting they show up in searches.
** 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 [#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 [#B] Is there way to create unique slugs for media instances :media_types:
** TODO [#B] Add async media enrichment after scrobbling (recurring pattern) :async:enrichment:
- Files: ~vrobbler/apps/scrobbles/scrobblers.py~ (lines 1179, 1211, 1244)
- Three separate TODOs for beer, puzzle, and brickset scrobbles all request
    firing an async process to enrich media after creation. This is the same
    pattern repeated across media types -- should be solved generically.

** TODO [#B] Auto sync board game scrobbles to BGG :boardgames:

*** Description

** 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 [#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)
         
