Compare commits

...
484 Commits
Author SHA1 Message Date
FoxxMD 869c7dcfe1 fea(ytm): Allow user-defined oauth scopes 2025-09-02 13:07:46 +00:00
FoxxMD 7068da5fc2 test(ytm): Fix failing type assertion 2025-09-02 12:24:33 +00:00
FoxxMD 3704fba1f8 feat(ytm): Add debug logging #345 2025-09-02 12:18:21 +00:00
FoxxMD 6d99790f7d chore: Update ytm #345 2025-09-02 12:15:49 +00:00
Matt Foxx 3e7f1965e7 Merge pull request #341 from FoxxMD/nowPlayingPlayerUpdates
feat: Improve Player behavior for Sources only tracking Now Playing data
2025-08-27 11:23:24 -04:00
FoxxMD 87d05b7c75 fix(listenbrainz): Fix missing album/release_name for listenbrainz endpoint #342 2025-08-27 12:26:46 +00:00
FoxxMD aca23ed90d test: Update lz expected config count 2025-08-26 20:26:32 +00:00
FoxxMD a01c8e3f17 feat(ui): Add Now Playing behavior for ui Player
* show "Now Playing" instead of Unknown/Playing
* Don't show empty listened duration/percent
2025-08-26 20:22:12 +00:00
FoxxMD eba9bc1b6b feat(webscrobbler): Use Now Playing Player 2025-08-26 20:01:03 +00:00
FoxxMD 34be8add14 docs: Remove koito from listenbrainz example
Not really needed since koito has its own docs
2025-08-26 19:45:48 +00:00
FoxxMD bd44fa5609 feat: Change stale/orphan interval to match current play duration for Players that only track Now Playing data
#338
2025-08-26 19:40:18 +00:00
FoxxMD c81bdf608f fix(lastfm): Remove VA from album artist #340 2025-08-25 17:00:19 +00:00
FoxxMD e9e5987330 chore: Bump version for release 2025-08-20 13:23:16 +00:00
FoxxMD ce4b0f7aa7 docs: Add Maloja source to readme 2025-08-20 13:23:00 +00:00
Matt Foxx eb6e469dc9 Merge pull request #336 from FoxxMD/GH-295/malojaSource
feat: Add Maloja Source
2025-08-19 23:19:26 -04:00
FoxxMD 4f6fb4ffea fix(maloja): Remove null duration/length 2025-08-18 14:24:43 +00:00
FoxxMD 24df22781f fix: Don't removed undefined keys from nested objects
Don't want to break 3p libraries (dayjs)
2025-08-18 14:23:35 +00:00
FoxxMD abf532c6bb feat(maloja): Add Maloja Source #295 2025-08-06 16:48:39 +00:00
FoxxMD 976c7c0e82 refactor(maloja)!: Modernize Maloja API usage
* Move API code into own class for reusability with future Source
* Remove Maloja V2 compatibility
* Throw on connection if server version is too old or unknown
2025-08-06 15:44:59 +00:00
FoxxMD 06b23d6287 docs: Update Spotify docs and usage due to Redirect URI changes
As of April 2025 Spotify no longer allows insecure redirect URIs which complicates initial Spotify setup.

* Improve spotify App creation instructions
* Add several approaches to creating a valid redirect URI
* Remove Spotify as the default Source example in docs due to complexity of Redirect URI requirements

Closes #330
2025-08-01 17:28:16 +00:00
FoxxMD 38a15d6426 chore: Bump version for release 2025-08-01 12:50:29 +00:00
FoxxMD c2c86033cb feat(listenbrainz): Add recording_mbid to submit payload for Koito compatibility
Koito should accept track_mbid but it only uses recording_mbid, so include that in payload.

#333
2025-07-31 16:40:21 +00:00
FoxxMD 72c94ca1e3 feat(listenbrainz): Include additional_info from LZ source Play if none parsed for MS play 2025-07-31 16:32:16 +00:00
FoxxMD bf6bd4be55 fix(listenbrainz): Parse MB metadata from LZ Source listens and set in Play
Had not updated LZ source to parse metadata into Play since adding brainz metadata to Play object

#333
2025-07-31 15:53:43 +00:00
FoxxMD 53f78b02fb chore: Bump for release 2025-07-28 13:59:17 +00:00
FoxxMD 6f5a8a7bc7 docs: Improve wording around Now Playing state 2025-07-28 13:46:46 +00:00
FoxxMD 0d5dd34597 docs: Remove koito mentions from LZ docs
Full Koito integration should be used! Removing mentions reduces ambiguity for new users.
2025-07-24 17:51:16 +00:00
FoxxMD a93ec8dd7e docs: Add clarifying callout for multi-use components 2025-07-24 17:51:16 +00:00
Matt Foxx 1e488b576c Merge pull request #317 from FoxxMD/GH-314/plexjsVersion
fix(Plex): Temporary plex validation fix
2025-07-21 08:50:40 -04:00
Matt Foxx d3d8eb40b5 Merge pull request #329 from FoxxMD/repeatPlayPersistence
Repeat play detection improvements
2025-07-21 08:50:17 -04:00
FoxxMD db54761422 fix: Add last played data and check for repeat on new Play #318 2025-07-17 15:45:18 +00:00
FoxxMD aa9f20f544 Fix formatting for hints 2025-07-17 15:44:35 +00:00
FoxxMD 60371f52f8 refactor: Move Play position closeness and duration comparisons to own functions
* Hint for logs returns for all conditions
* Add tests
2025-07-17 15:08:09 +00:00
FoxxMD 87394fbce7 Remove patch
Didn't work anyways
2025-07-17 13:24:30 +00:00
FoxxMD 72341ff1de fix(plex): Temporary fix for library response validation #314
Catches and ignores outdated validation from @lukehagar/plexjs if we can verify raw value contains the required objects

https://github.com/LukeHagar/plexjs/issues/35
2025-07-17 13:24:30 +00:00
FoxxMD 63cebf3784 fix(plex): Ignore library response validation
As temporary fix for #314
2025-07-17 13:24:30 +00:00
FoxxMD 60f3f05f67 fix(Plex): Bump plexjs version to fix response validation
#314
2025-07-17 13:24:30 +00:00
FoxxMD ce39065c80 docs: Fix social card image 2025-07-16 13:33:10 +00:00
FoxxMD 3f39e9efa1 docs: Update docusaurus to 3.8 2025-07-16 13:19:04 +00:00
Matt Foxx f37dbd75c6 Merge pull request #328 from FoxxMD/GH-318/repeatDetection
feat: Track if play is a repeat and use in duplicate scrobble detecti…
2025-07-15 15:40:39 -04:00
FoxxMD 47ad7a3b74 feat: Track if play is a repeat and use in duplicate scrobble detection #318
* Use MS Player to mark Play as a repeat if session repeat is detected
* Remove FUZZY temporal accuracy as acceptable for detecting duplicate if candidate play is marked as repeat
2025-07-15 19:23:49 +00:00
FoxxMD 91747c0682 docs: Add Source client config guidance #325 2025-07-15 15:48:13 +00:00
FoxxMD 80f8495f8e fix(plex): Fix regression with allowing non-music when using librariesAllow 2025-07-14 20:19:13 +00:00
Matt Foxx c6bb258bf4 Merge pull request #324 from FoxxMD/lzAlbumField
Lz album field
2025-07-14 12:48:14 -04:00
FoxxMD e70abc593c chore: Update readme Koito entries 2025-07-14 14:55:29 +00:00
FoxxMD 22d73c6a7f fix(lastfm): Fix lastfm providing empty string for non-defined album #323 2025-07-14 14:48:50 +00:00
FoxxMD 82afe4195c chore: Bump version for release 2025-07-14 14:12:23 +00:00
FoxxMD 5e9409ab66 feat(plex): Add libraries allow/block warning 2025-07-14 13:49:40 +00:00
FoxxMD e64854eb14 fix(plex): Strict comparison of libnrary names #326
* Fixes libraries containing partial allow/disallow names from being able to pass filter
* Fix not checking valid library types
2025-07-14 13:43:22 +00:00
FoxxMD 333f70bd7e docs: hide navbar on scroll 2025-07-11 17:58:50 +00:00
FoxxMD ace7d5efc9 docs: Fix anchor scrolling by specifying logo height 2025-07-11 17:56:13 +00:00
FoxxMD cd14f82753 docs: Move updating instructions to own page 2025-07-11 17:27:22 +00:00
FoxxMD aba718aef1 docs: Move docker install instructions before local 2025-07-11 17:05:24 +00:00
FoxxMD c87497071d docs: Add updating instructions 2025-07-11 17:04:02 +00:00
FoxxMD 2a35e7cf5c fix(listenbrainz): Fix sending undefined release_name in scrobble #323 2025-07-10 19:39:08 +00:00
FoxxMD 3b98036687 docs: Remove old FAQ entries 2025-07-10 16:37:56 +00:00
FoxxMD 3fc935b96d feat(listenbrainz): Add additional player/service meta info for LZ scrobbles #224 2025-07-10 15:52:51 +00:00
FoxxMD 7586f226ba feat(listenbrainz): Add submission_client #224 2025-07-10 14:34:01 +00:00
FoxxMD 1a0e600db5 docs: Sort Source/Clients alphabetically
* Sort on readme/landing page and in configuration docs
* Makes skimming docs easier
2025-07-10 14:22:19 +00:00
FoxxMD 1dba5a6c6f docs: Add Koito to overview summary 2025-07-10 13:15:45 +00:00
FoxxMD 3ec0652fbb docs: Update docker installation wording 2025-07-10 13:11:17 +00:00
Matt Foxx e71d5983a4 Merge pull request #322 from FoxxMD/koito
Implement Koito Client/Source
2025-07-10 07:50:50 -04:00
FoxxMD bb9b5fc5cd fix: superagent type error false positive 2025-07-09 16:30:15 +00:00
FoxxMD 89fde5a6fe chore(listenbrainz): Add guidance in logging to switch to Koito source/client 2025-07-09 16:19:47 +00:00
FoxxMD 24ff8f2b0f docs: Add Koito client/source docs 2025-07-09 16:17:47 +00:00
FoxxMD f103563cb7 feat(koito): Implement Koito Source 2025-07-09 15:40:13 +00:00
FoxxMD 1767268a48 fix: Only return interpolated album value if not undefined 2025-07-09 15:39:22 +00:00
FoxxMD 8ab2d61742 feat(koito): Implement Koito client 2025-07-09 15:04:19 +00:00
FoxxMD 132580af6f chore: Update superagent types 2025-07-09 15:04:01 +00:00
FoxxMD 375c0123c6 feat(koito): Add koito types/config 2025-07-09 13:12:15 +00:00
FoxxMD 769002412d docs: Remove flatpak install guidance 2025-07-08 20:34:16 +00:00
FoxxMD 7fa567c1e4 docs(mpris): Add remote socket guidance 2025-07-08 20:13:10 +00:00
FoxxMD 8dfc9a6d55 refactor(mpris): Remove socat service in favor of simplified dbus connection
Can connect to dbus over TCP socket using ENV which is much simpler.
2025-07-08 19:53:55 +00:00
FoxxMD 0efc85c516 feat(mpris): Better handling of socket error on connection check
Prevents a low-level emitted error from completely crashing MS
2025-07-08 19:51:57 +00:00
FoxxMD dbb8e3c5e0 feat(mpris): Add s6 finish script to prevent restart of socat 2025-07-08 19:36:08 +00:00
FoxxMD f4f1eed645 feat: Add socat service 2025-07-08 14:38:22 -04:00
FoxxMD 0a31f51567 docs: Update MPRIS instructions for docker 2025-07-08 17:25:28 +00:00
FoxxMD c255138a6e docs: Add Koito quickstart instructions 2025-07-08 14:36:10 +00:00
Matt Foxx 856a05a357 Merge pull request #320 from FoxxMD/symfoniumPlex
Fix symfonium reporting issues for plex
2025-07-08 08:27:53 -04:00
FoxxMD 18915167f4 feat: Add more logging to apprise errors #158 2025-07-07 17:44:17 +00:00
FoxxMD 34b0995d6b fix(plex): Increase max drift to 35 seconds to account for symfonium lag #271
Symfonium only reports position every 30 seconds vs. plex/plexamp 15 seconds. Increase max drift so MS doesn't falsely seek and reset play state.
2025-07-07 15:04:43 +00:00
FoxxMD 4e5f38378e feat: Add RT Player metrics to player logging 2025-07-07 15:03:30 +00:00
FoxxMD 5d2c9261ae chore: Bump version for release 2025-07-07 12:22:23 +00:00
FoxxMD e039d4512c ci: Add optional rybbit analytics to docs build 2025-07-07 12:20:16 +00:00
FoxxMD 51509a3531 ci: Add rybbit variables for gh actions 2025-07-03 17:51:38 +00:00
FoxxMD 8f032f2379 feat(listenbrainz): Allow now playing reporting for koito
Doesn't affect scrobbling and means if/when NP is actually supported it'll work automatically
2025-07-03 13:10:52 +00:00
FoxxMD ff11e26219 Clean up logging for Now Playing 2025-07-03 13:10:13 +00:00
FoxxMD f004aa4cfd feat(listenbrainz): Implement Now Playing functionality 2025-07-03 12:50:40 +00:00
FoxxMD 2104f2a99d fix(now playing): Catch update errors but only log
We don't need to stop the scrobbler for now playing issues since its not a critical function. Just log as warning.
2025-07-03 12:19:34 +00:00
Matt Foxx 389129aadf Merge pull request #316 from msparkles/feat/lastfm-nowplaying
feat: lastfm nowplaying
2025-07-02 16:59:21 -04:00
FoxxMD f885726ed1 docs: Add Now Playing and configuration 2025-07-02 20:50:03 +00:00
FoxxMD 3cc452323d feat: Allow Now Playing to be globally toggled by ENV 2025-07-02 20:42:12 +00:00
FoxxMD 780acb32d7 fix(scrobbler): Fix duplicate map keys causing delay in Now Playing update 2025-07-02 17:03:48 +00:00
FoxxMD bf5fdb95f4 test: Add Now Playing updating and scheduler tests 2025-07-02 17:03:26 +00:00
FoxxMD 7d7ccf53bb test: Add Now Playing filter/sorting tests 2025-07-02 16:03:09 +00:00
Matt Foxx 94837b7e03 feat(subsonic): Implement filtering plays by user (#319)
* feat(subsonic): Implement filtering plays by user #301
2025-07-02 08:15:44 -04:00
FoxxMD a9c1c7083d feat: Add config source to startup logging #309 2025-07-01 19:33:10 +00:00
FoxxMD 8dd6fc15cc docs: Improve example config rendering for ambiguous configs
* Filter examples based on client/source configuration
* Add `type` to all configs in AIO examples

Fixes #310
2025-07-01 18:30:24 +00:00
FoxxMD 32ca57fc65 feat: Implement Now Playing queue, scheduler, and source sorting
* Add queue for aggregating Now Playing updates
* Filter play based on 1. source priority and 2. player stickiness or deterministic order
* Use toad scheduler to processing NP queue
2025-07-01 16:18:15 +00:00
FoxxMD 9d748f26c0 feat: Pass source type-name to playingNow 2025-07-01 14:55:38 +00:00
FoxxMD 5af1454f36 feat: Add Now Playing user config
* Enable/disable now playing reporting
* WIP source filters for now playing
2025-06-30 16:12:11 +00:00
FoxxMD bf7a660633 feat: Add Now Playing as class configuration and implement rate limit intervals
* Define if scrobble client can update Now Playing based on class property, use this to filter now playing update
* Use thresholds to determine when/if Now Playing should be updated
* Set LastfmScrobbler as supported for Now Playing
2025-06-30 15:29:51 +00:00
Madeline Sparkles f0723f23e8 feat: lastfm now-playing scrobbling 2025-06-30 00:14:02 +02:00
Madeline Sparkles 36357135cb fix(docs): remove archaic JSDocs on ScrobbleClients.scrobble 2025-06-29 23:17:28 +02:00
FoxxMD 8be08a5c29 chore: Bump for release 2025-06-20 20:09:21 +00:00
Matt Foxx 9fe5b9b671 Merge pull request #312 from FoxxMD/lzEnrich
feat: Enrich Listenbrainz submit payload
2025-06-20 16:07:43 -04:00
FoxxMD 720c07b008 feat(listenbrainz): Include more ids/fields in submit additional_info #311 2025-06-20 16:43:01 +00:00
FoxxMD 7fdf93c5d3 feat(spotify): Add spotify meta ids to play 2025-06-20 16:42:04 +00:00
FoxxMD a0fd466bcf dev: Add ENV only launch config 2025-06-19 18:15:45 +00:00
FoxxMD 35d62c913a fix: Guard against undefined type in config.json #310 2025-06-19 18:12:57 +00:00
FoxxMD ede2d9f71a chore: Bump version for release 2025-06-19 16:22:39 +00:00
Matt Foxx acccc32813 Merge pull request #296 from FoxxMD/deezer-internal
feat: Implement Deezer Internal (ARL) Source
2025-06-17 20:41:08 -04:00
FoxxMD 69f73ff09f docs(deezer): Rewrite Deezer Source with ARL implementation
* Add ARL implementation docs and warnings
* Update json examples to include both types of deezer configs
2025-06-17 15:16:13 +00:00
FoxxMD 305f3c42ae fix(deezer): options are optional 2025-06-17 15:14:01 +00:00
FoxxMD d6ac4c34c5 feat: Implement better during/range temporal comparisons
* Refactor temporal comparisons to provide granular "during" thresholds
* Use duration for ignoring discovery with aggressive mode in Deezer #296
2025-06-17 10:10:34 -04:00
FoxxMD 9c6eeaafdb feat: Fuzzy diff threshold configurable
* Refactor fuzzy diff theshold to be configurable
* Configure aggressive fuzzy discovery for Deezer to be 40 seconds #296
2025-06-17 10:10:34 -04:00
FoxxMD 6bd841fea2 feat(deezer internal): Add fuzzy discovery ignore option
Allows user-configurable decision on whether to ignore fuzzy-matched discovered plays
2025-06-17 10:10:34 -04:00
FoxxMD e05c46e9ce fix(deezer official): Fix missing interval interface property 2025-06-17 10:10:34 -04:00
FoxxMD e94109dc75 feat: Implement Deezer Internal (ARL) Source 2025-06-17 10:10:34 -04:00
FoxxMD a68dea5193 chore: Bump version for release 2025-06-16 19:22:43 +00:00
FoxxMD b5d56b75d7 test(listenbrainz): Fix expected client count 2025-06-16 16:06:57 +00:00
FoxxMD c8daffb935 docs(listenbrainz): Add Koito compatibility and config example #300 2025-06-16 16:02:13 +00:00
FoxxMD 18a7f26c54 feat(listenbrainz): Implement limited support for Koito as Listenbrainz Source/Client #300
* Support Koito server detection via public route
  * Warns users of limited support on detection
* Implement play history retrival for Koito server
2025-06-16 15:49:39 +00:00
FoxxMD e7395e79a5 feat(listenbrainz): Detect /1/ version prefix on base URL
Remove /1/ from base URL to further normalize user URL and make config easier #300
2025-06-16 14:15:48 +00:00
FoxxMD 08ee6d379f fix(listenbrainz): Normalize URL and join parts correctly
* Normalize base URL to make user URL configuration easier/more robust
* Use joinPath for concetenating URL to avoid missing slashes #300
2025-06-16 14:04:15 +00:00
Matt Foxx 9c692539ce Merge pull request #298 from FoxxMD/jellyfin-musicbrainz
feat(jellyfin): Add musicbrainz metadata #297
2025-06-04 14:15:09 -04:00
FoxxMD 5cd5061f20 feat(jellyfin): Add musicbrainz metadata #297 2025-06-04 13:44:15 +00:00
FoxxMD 46bcf6d6a6 chore: Bump version for release 2025-05-29 13:20:34 +00:00
FoxxMD 4da931269c fix(plex): On Plex webhook routes filter plex sources to get webhook source only
#291
2025-05-27 17:31:34 +00:00
FoxxMD d19d40d479 test: Fix real time player not triggered during event loop
Running tests synchronously preventing rt player from advancing which cause each player update to be considered an overdrift
2025-05-27 17:07:30 +00:00
FoxxMD b3e1c7f660 chore: Update devcontainer to node 20 to match docker image 2025-05-27 17:05:50 +00:00
FoxxMD 1242a710df fix: Fix NaN and undefined initial states for rt player #292 2025-05-27 15:29:06 +00:00
FoxxMD b724eca9be docs: Add missing var for docs deploy 2025-04-23 15:04:20 +00:00
FoxxMD 376d4936cc docs: Add google site verification 2025-04-23 15:01:42 +00:00
FoxxMD a5da0573c5 docs: Remove flatpak installation method
See #287
2025-03-31 18:31:46 +00:00
FoxxMD 4694ce45a7 chore: Update flatpak description 2025-03-31 12:12:28 -04:00
FoxxMD 7c7d710cd3 fix(ui): Guard against null listenedDuration
Fixes #285
2025-03-28 08:21:04 -04:00
FoxxMD e2cd7e2991 chore: Bump version for release 2025-03-25 01:15:40 +00:00
FoxxMD 92d8eb8d6f chore: Update static config schema 2025-03-25 01:15:26 +00:00
FoxxMD d14e7d9b2e fix(lastfm): Remove duration for scrobble if value is 0 2025-03-25 01:09:31 +00:00
FoxxMD e4befae55a fix(lastfm): Fix endpoint regex to exclude auth callback url 2025-03-20 19:14:34 +00:00
FoxxMD 0e00b425b4 fix(icecast): rename old scrobble behavior variable 2025-03-20 16:27:30 +00:00
FoxxMD fcf6c8eb4c docs: Add some common source options 2025-03-20 16:23:01 +00:00
FoxxMD 242bacc1be feat(source): Optional position for non-positional sources
* Allow non-positional Sources to still report position (for display only)
* Refactor Azuracast to be non-positional due to reported position not always being accurate (azuracast approximates track length)
2025-03-20 15:40:31 +00:00
FoxxMD cbe28f97a0 feat(source): Improve "Should Scrobble" functionality
* Rename from manualListening to Should Scrobble to better describe functionality
* Move check to Source scrobble function to avoid short circuting Source player for no reason
* Add to Azuracast and make functionality more generic
2025-03-20 15:12:00 +00:00
FoxxMD 336972f586 fix(ui): Update status indicator for ingress sources 2025-03-20 14:13:48 +00:00
Matt Foxx 81edb31e7b Merge pull request #283 from FoxxMD/icecast
Icecast
2025-03-19 12:57:38 -04:00
FoxxMD dbde66ad77 docs: Add Icecast docs 2025-03-19 16:50:57 +00:00
FoxxMD efbf2da816 feat(icecast): Switch to metadata-only parsing 2025-03-19 16:34:19 +00:00
FoxxMD 38d1db2f3f feat: Implement Icecast Source MVP 2025-03-19 15:20:44 +00:00
Matt Foxx 265ae82663 Merge pull request #278 from FoxxMD/GH-263/azuracast
feat: Add Azuracast Source
2025-03-19 09:28:27 -04:00
FoxxMD 2bd2972af6 docs: Fix bad merge of azuracast/musiccast config 2025-03-19 13:22:52 +00:00
Matt Foxx 1bdf083e28 Merge branch 'master' into GH-263/azuracast 2025-03-19 09:17:44 -04:00
Matt Foxx 4c233fdded Merge pull request #279 from FoxxMD/GH-274/musiccast
feat(musiccast): Implement MusicCast Source
2025-03-19 08:42:40 -04:00
FoxxMD e0060fe3e5 feat: Add ENVs for default scrobble threshold for duration/percent
* SOURCE_SCROBBLE_DURATION env sets default duration
* SOURCE_SCROBBLE_PERCENT env sets default percent
2025-03-18 17:28:14 +00:00
FoxxMD 2adb3bd052 docs(musiccast): Add docs for Yahama MusicCast 2025-03-17 21:02:26 +00:00
FoxxMD 0dca49ab43 fix(player): Check for 0 duration before finalizing listen session with duration-based calculation 2025-03-17 20:40:06 +00:00
FoxxMD 6a71d4a495 fix(musiccast): Treat 0 total_total as undefined
Prevents some positional player logic from running on a functionally-useless value
2025-03-17 20:40:06 +00:00
FoxxMD 230ddf4a48 fix(source): Do not calculate polling interval decrease if duration does not exist 2025-03-17 20:40:06 +00:00
FoxxMD e4e1797ac5 fix(musiccast): Remove hardocded port for debugging 2025-03-17 20:40:06 +00:00
FoxxMD 1dd3185715 fix: Better handling of 0 duration tracks
* Check for zero duration before using progress bar in debug logging
* Adds tests for scrobble thresholds to make sure 0 duration is handled
2025-03-17 20:40:06 +00:00
FoxxMD 4fee231a7b test(musiccast): Add tests for basic connectivity 2025-03-17 20:36:15 +00:00
FoxxMD b5ead39792 feat(musiccast): Switch play info fetch order
net/usb is more likely to be used since it has more sources. Likely using it will reduce required fetches in most usage scenarios.
2025-03-17 20:36:15 +00:00
FoxxMD 190431b9d4 feat(musiccast): Implement MusicCast Source 2025-03-17 20:36:15 +00:00
FoxxMD 210fa35cb6 docs: Add Last.fm Endpoint source docs 2025-03-14 16:34:43 +00:00
FoxxMD 479af1cd5e docs: Add Listenbrainz Endpoint source docs 2025-03-14 16:29:48 +00:00
Matt Foxx c72831cd12 Merge pull request #281 from FoxxMD/normalScrobbleEndpoints
feat: Implement dummy endpoint Sources for Listenbrainz and Last.fm
2025-03-14 11:24:08 -04:00
FoxxMD f68a9493ce fix(listenbrainz): add dummy validate-token endpoint 2025-03-14 08:16:56 -04:00
Matt Foxx 78a93d0809 Merge pull request #256 from FoxxMD/configurablePlayerStale
feat(source): Stale/orphan scrobble and timestamp improvments
2025-03-13 15:26:46 -04:00
FoxxMD 3680ac8af1 feat(lastm): Implement MVP Lastfm endpoint source 2025-03-13 18:44:08 +00:00
FoxxMD e74ba52c47 fix(listenbrainz): Improved slug and normal lz endpoint detection 2025-03-13 16:53:25 +00:00
FoxxMD d3cd07c5c3 feat(listenbrainz): Implement MVP LZ endpoint source 2025-03-13 16:20:37 +00:00
FoxxMD 2b890d4704 refactor: Pass reportedTS for testing purposes 2025-03-13 11:06:26 -04:00
FoxxMD 47eead2a18 feat: Use timestamp for position MS change on RT player
Makes testing easier
2025-03-13 11:06:26 -04:00
FoxxMD 38ea3115f6 test: Add tests for scrobble behavior when cleaning up players 2025-03-13 11:06:26 -04:00
FoxxMD f11a2b5442 fix: Use last known position for final range when cleaning up stale RT player
Prevents listen duration from being reported as overdrift RT position due to lack of player updates
2025-03-13 11:06:26 -04:00
FoxxMD 818f67d6da feat(source): Add relative time since last activity to logging
Makes logs easier to read when looking for stale source/player
2025-03-13 11:06:26 -04:00
FoxxMD 84f353220d feat(source): Clear listening range on stale to prevent erroneous scrobble on resume
Clearing pre-stale listening range prevents any future resume (player becomes non-stale) from using old listening range to scrobble. Represents a physical "break" between listening sessions for the same song
2025-03-13 11:06:26 -04:00
FoxxMD 47541a449b feat: Use human-friendly timestamp for stale/ophan interva logging 2025-03-13 11:06:26 -04:00
FoxxMD 1a62edab37 feat(source): Add stale/orphaned intervals as user configurable 2025-03-13 11:06:26 -04:00
FoxxMD 5927450840 fix(ytm): Recover artist/album from YTM when not surfaced by youtube.js
Use flex_columns for parsing raw data when musicshelf when artist/album does not have an endpoint LuanRT/YouTube.js#381

Fixes #280
2025-03-13 10:40:47 -04:00
FoxxMD ec9bea70fd chore: Bump YTM dependency version 2025-03-13 10:37:53 -04:00
FoxxMD 2f9a45b1e4 fix: Filter components by name and check for undefined
Fixes #273
2025-03-12 19:16:32 +00:00
FoxxMD 3c649df2e9 docs(azuracast): Add azuracast Source docs 2025-03-11 16:41:42 +00:00
FoxxMD ab1766ca0a feat(azuracast): Add station name hint and fix labels 2025-03-11 16:10:38 +00:00
FoxxMD a42de78ad3 feat(azuracast): Implement manual listening 2025-03-11 16:06:46 +00:00
FoxxMD 81bde3a771 feat(azuracast): Add MVP azuracast source implementation 2025-03-11 15:21:07 +00:00
FoxxMD 982e6655ef chore: Add more log guidance on bug report 2025-02-14 17:51:17 +00:00
FoxxMD 44ce0cba3c feat(azuracast): Implement config and types 2025-02-03 16:53:39 +00:00
FoxxMD c1e1c69b5e fix: Missing optional period on vs. when parsing artist 2025-01-27 21:13:32 +00:00
FoxxMD 142defdd20 docs: Put delimited artist example in expander 2025-01-27 14:57:15 +00:00
FoxxMD aad9f2b4de docs: Add primary artist extraction examples for scrobble modification 2025-01-27 09:46:14 -05:00
FoxxMD 8e92407182 test: Add modification example for parsing a primary artist from delimited string 2025-01-27 09:36:15 -05:00
FoxxMD 94210e29d1 docs: Fix extra array in title modification example
Fixes #258
2025-01-27 09:16:34 -05:00
FoxxMD d2c6194314 ci: Add comments on PR SHA usage 2025-01-21 14:06:16 -05:00
FoxxMD 11ca7a724d try pull SHA directory for build
So that build summary details used SHA
2025-01-21 13:58:47 -05:00
FoxxMD 3057908074 fix(ci): Add context to build-push so it uses checked out PR ref 2025-01-21 13:28:45 -05:00
FoxxMD 6a9e538845 chore: Bump version for release 2025-01-21 15:04:38 +00:00
Matt Foxx 130538cd9a Merge pull request #255 from FoxxMD/GH-254/playCompletedOnCleanup
fix(spotify): Generate played object from cleanup with correct TS
2025-01-18 12:57:51 -05:00
FoxxMD 1486613744 fix(spotify): Generate played object from player cleanup with correct timestamp
Missing arg to pass to getPlayedObject to ensure the timestamp used for playDate is the same as normally discovered tracks. Without the arg two tracks played with the second being scrobbled on stale would cause the latter to have the same TS as the former

fixes #254
2025-01-17 18:55:35 +00:00
Matt Foxx f2516206c0 ci: Refactor OCI image workflows to use multi-runner for cross-arch builds (#246)
Replaces `build-push-action` buildx QEMU building for multiple `arch` with a multi-runner matrix that builds each image on native runners and combines digests before pushing to registries.

* Use arm public preview runner
* Refactor publish image workflow to use multi-runner
* Refactor PR workflow to use multi-runner
2025-01-17 09:35:11 -05:00
FoxxMD f8902e05a6 ci: Update sanity test to install/build concurrently 2025-01-16 16:15:00 -05:00
Matt Foxx c59b47c801 Merge pull request #252 from FoxxMD/paralellBuild
feat(docker): Paralellize npm install and build processes
2025-01-16 16:06:27 -05:00
FoxxMD 0e4845f2e2 feat(docker): Paralellize npm install and build processes 2025-01-16 20:48:05 +00:00
FoxxMD e2775cd9de chore(docker): Update node version and reduce npm verbosity
* Update to latest node 20.x minor version
* Remove `--verbose` and enable `--no-audit` for npm install to reduce install time
  * These were left over from debugging ARM builds from Feb 2024
2025-01-16 14:43:29 -05:00
Matt Foxx 816bec88c8 Merge pull request #251 from FoxxMD/docusaurus36
docs: Update to Docusaurs 3.6 to improve build times
2025-01-16 14:25:19 -05:00
FoxxMD d724bef4e4 docs: Update to Docusaurs 3.6 to improve build times 2025-01-16 14:07:47 -05:00
FoxxMD cac851f120 chore: Bump version for release 2025-01-15 15:16:26 +00:00
FoxxMD c1df74ee27 docs: Strikeout tautulli in readme 2025-01-15 15:16:01 +00:00
FoxxMD 0c7b51c622 docs: Update bug report template to correct debug mode link 2025-01-15 15:12:26 +00:00
FoxxMD 56ecad24d4 docs: Update kitchensink with newer jellyfin/plex examples 2025-01-15 15:09:38 +00:00
FoxxMD 6bb600648a docs: Add final deprecation notices for Plex/Tautull/Jellyfin legacy sources 2025-01-15 15:00:44 +00:00
FoxxMD fe2610c35a docs: Add debug mode information 2025-01-15 14:36:48 +00:00
FoxxMD 84bcbc9cea docs(ytm): Update docs with more debugging options 2025-01-15 14:02:50 +00:00
FoxxMD d741377e5a feat(ytm): Debug mode enables log diffs 2025-01-15 13:50:47 +00:00
Matt Foxx 1ea106970d Merge pull request #248 from FoxxMD/GH-227/ytmMusicalChairs
fix(ytm): Improve history handling
2025-01-15 08:27:55 -05:00
Matt Foxx a879b7aded Merge pull request #249 from FoxxMD/GH-223/plexPlatformCleanup
fix(plex): Improve handling of orphaned API sessions
2025-01-14 08:19:12 -05:00
FoxxMD 30ee000676 fix(plex): Refactor final range position to be configurable
* Keep default 3 seconds grace period
* Set grace period for plex to API viewoffset period (rtdrift) of 17 seconds #223
2025-01-09 18:42:09 +00:00
FoxxMD 703cf80d5a fix(source): Implement player cleanup with play discovery for stale platforms
* Do play cleanup during polling for new plays so that we can discover tracks from abandoned players
* Fixes missed scrobbles from Plex API when Source stops returning a session due to player queue end #223
2025-01-09 18:01:12 +00:00
FoxxMD 34289f0176 docs: defer script load 2025-01-03 19:24:51 +00:00
FoxxMD 8b8f32de02 feat(ytm): Use durations to generate play dates for interim tracks
#226
2025-01-03 18:28:55 +00:00
FoxxMD 095e5cc0d9 feat(ytm): Conservative skip detection
#226
2025-01-03 17:56:07 +00:00
FoxxMD 0c46c0a079 refactor(ytm): Clean up tests and history parsing functions 2025-01-03 16:25:33 +00:00
FoxxMD dd4b2ebb8c fix(ytm): Compare recent changed history to detect outdated responses
Store responses when history changes and then check if YTM returns an outdated response after previously returning new tracks.

#227
2025-01-02 19:03:38 +00:00
FoxxMD d30ee0c35a feat(source): Ensure delay on close-interval for scrobbling is less than interval 2025-01-02 18:29:34 +00:00
FoxxMD 484fb16bba fix(ytm): Use diff type to restrict valid history changes
* Diff type should always be prepend since we are checking for new tracks
  * Warn and log if another type is detected
* Warn if more than one track diffed for discovery
* Refactor and and make YTM history parsing testable

#227
2025-01-02 18:28:59 +00:00
FoxxMD f320e6cef0 feat: Include comment in play diff when present 2025-01-02 18:25:27 +00:00
FoxxMD ea2421bc6d chore: Bump plexjs version
Includes originalTitle spec addition so we can remove patch
2025-01-02 16:59:36 +00:00
FoxxMD f43c31fb9a docs: Add lnav config 2024-12-27 18:49:29 +00:00
FoxxMD 6825ad55e3 feat(ui): Disable minifiying frontend identifiers
No need to minify to save space since this is not a high-traffic site and preserving identifiers helps with debug issues
2024-12-27 18:49:16 +00:00
FoxxMD 92841f8bb3 fix: Default positional listen range to 0 position if none is provided
* doesn't make sense to use undefined as start. if we are providing a position it should be 0, if unknown

#245
2024-12-27 18:43:51 +00:00
FoxxMD 38e5e4c9a9 feat: Add init log message for debug mode status 2024-12-20 13:47:54 -05:00
FoxxMD 1bcfed05af feat: Log player state when using debug mode 2024-12-20 18:28:21 +00:00
FoxxMD e00ec4098a feat: Reduce polling log noise 2024-12-20 18:10:22 +00:00
FoxxMD 5d44953435 revert ts comment causing failing build 2024-12-20 17:46:04 +00:00
FoxxMD 80ad5a3518 feat: Fully implement use of DEBUG_MODE 2024-12-20 14:02:21 +00:00
FoxxMD b5683ba4b8 fix(jellyfin): Fix missing check for undefined play
#239
2024-12-17 20:53:54 +00:00
FoxxMD 884b3963d3 fix(plex): Use track artist from response data when album artist is present
* Fixes #241 by using the track artist as the primary artist when album artist is present
* Bug is reported in LukeHagar/plexjs#27 but will need be patched at runtime for the foreeable future due to npm packages not being published since September LukeHagar/plexjs#26
2024-12-16 20:11:50 +00:00
Matt Foxx 8de2296aaf Merge pull request #221 from FoxxMD/improveInitUsage
refactor: bubble init usage and errors to simplify recovery
2024-12-16 09:43:53 -05:00
Matt Foxx c1869f469c Merge pull request #240 from Fratt/fix-spotify-album-artists
fix(spotify): Album artists no longer missing
2024-12-16 09:34:29 -05:00
FoxxMD 86196b3122 test(spotify): Test album artist parsing 2024-12-16 14:31:10 +00:00
Fratt fd03e42240 fix(spotify): Album artists no longer missing 2024-12-14 13:08:00 +01:00
FoxxMD 3e230b0d68 fix(ui): Fix card header right-side alignment 2024-12-11 20:41:06 +00:00
FoxxMD 133d7f56e5 fix(ui): Wording for non-polling sources start/restart 2024-12-11 20:35:22 +00:00
FoxxMD 78d048ba62 fix: missing implemented methods for test class 2024-12-11 18:24:42 +00:00
FoxxMD 323004a730 feat: Implement start/restart/force init from UI regardless of status 2024-12-11 18:20:12 +00:00
FoxxMD 626bca9951 feat: Initialize usage refactored everywhere
* try-catch and bubble up depending on usage
* replace poll/monitor code on startup with scheduler task usage
* better notification messages with truncated cause chain
2024-12-11 17:30:08 +00:00
FoxxMD 3ae0d4ff45 feat: Add stage Error classes and cause chain for notification 2024-12-10 21:17:44 +00:00
FoxxMD 9d85df54e1 refactor: Normalize all testAuth implementation to throw on error 2024-12-10 20:56:31 +00:00
FoxxMD 0ff1c95fd1 TODO partial refactor of init usage to throw errors 2024-12-10 20:34:33 +00:00
FoxxMD b93274fd53 Merge remote-tracking branch 'origin/master' 2024-12-10 15:19:55 -05:00
FoxxMD 0f0d1d64f9 feat(jellyfin): More robust connection check and logging 2024-12-10 20:19:30 +00:00
FoxxMD d77e6c6f22 feat(jellyfin): Bump client major version 2024-12-10 20:19:10 +00:00
Matt Foxx 83ff5348c6 Merge pull request #216 from FoxxMD/GH-215/notifiersRelax
feat(notifications): Make init more granular and always complete init
2024-12-10 11:54:59 -05:00
FoxxMD 65012d40ed feat(notifications): Make init more granular and always complete init
* Notifications are not mission critical so its okay if they fail on init because service outage may be transient. Always init notification services and always attempt to push (we catch failures anyway)
* Parse more info from notification URL and test simple reachability of host/port to make troubleshooting easier

Fixes #215
2024-12-10 11:33:33 -05:00
FoxxMD feb02bfa85 docs(ytmusic): Clearer wording for troubleshooting content
Also increase non-erroneous diff logging from DEBUG to VERBOSE to raise signal a bit.
2024-12-10 15:55:20 +00:00
FoxxMD 276e51b8ea docs: Fix broken links 2024-12-10 15:40:15 +00:00
FoxxMD c83d77c02a docs: Fix bad transform links 2024-12-10 15:28:25 +00:00
FoxxMD 0cdb48a71a ci: Generate schema files for docsite in GH docs deploy workflow 2024-12-10 10:13:06 -05:00
FoxxMD 1556d7ac69 chore: Generate schema files during docker build for docsite 2024-12-10 10:08:02 -05:00
Matt Foxx 1e65abd962 Merge pull request #236 from FoxxMD/ytOauthRedux
feat(ytm): Re-implement auth with cookie and oauth
2024-12-10 09:16:14 -05:00
FoxxMD 9f11d799ac feat(source): Add experimental file logging 2024-12-09 18:44:43 +00:00
FoxxMD 5c0f419e7e feat(ytm): Implement ENV config 2024-12-06 19:28:32 +00:00
FoxxMD c33d42808a docs(ytmusic): Add new auth docs and update FAQ 2024-12-06 19:09:32 +00:00
FoxxMD e8716a45b2 feat(ytmusic): Custom redirectUri checks 2024-12-06 15:56:32 +00:00
FoxxMD 570dee30e3 fix(ytmusic): Add missing logging for redirect URI 2024-12-06 15:33:02 +00:00
FoxxMD 97222ed233 refactor(ytmusic): Improve auth handling for reauthentication 2024-12-06 15:18:07 +00:00
FoxxMD e2a7fff6f9 chore: Bump youtubei.js major version
* Fixes parsing warning for history response
2024-12-06 15:17:18 +00:00
FoxxMD efc8246a76 chore: Add docker-compose dev file and vscode task 2024-12-05 09:37:58 -05:00
FoxxMD b4a95e0cf9 fix(ytmusic): query string on redirect uri incorrectly encoded 2024-12-05 09:18:05 -05:00
FoxxMD c66d5d4627 docs: Improve schema generation and compatibility
* Patch generator to generate definition titles until vega/ts-json-schema-generator#2127 is resolved
* Add launch config for debugging schema generation
* Disallow topRef generation so docusarus json viewer works
* Write to docsite static assets on generation
2024-12-04 14:46:40 -05:00
FoxxMD 4de28e0390 feat(ytmusic): Add more innertube options to user configuration 2024-12-04 19:38:54 +00:00
FoxxMD f74721b366 fix(ytmusic): Fix missing api prefix for redirectUri 2024-12-04 15:04:36 +00:00
Matt Foxx 4fb16e7098 Merge pull request #232 from CodeWithMa/remove-deprecated-version
fix: remove deprecated version from docker-compose.yml
2024-12-03 16:35:46 -05:00
Matt Foxx a76c351206 Merge pull request #235 from CodeWithMa/fix_case
fix: fix WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match
2024-12-03 16:35:03 -05:00
FoxxMD 7faf94cfab feat(ytm): Re-implement auth with cookie and oauth
Youtube TV seems no longer have scope for reading history or account details?

Cookie may work and custom oauth seems most stable based on reporting from LuanRT/YouTube.js#803
2024-12-03 15:58:08 -05:00
Ma 3a1e29f682 fix: fix WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match 2024-11-28 15:57:17 +01:00
Ma 88cb287df8 fix: remove deprecated version from docker-compose.yml 2024-11-20 17:16:14 +01:00
dependabot[bot] eb26ee0cd4 Merge pull request #194 from FoxxMD/dependabot/npm_and_yarn/npm_and_yarn-ac4cac0c7a 2024-10-31 12:44:16 +00:00
dependabot[bot] 844833f348 chore(deps): bump the npm_and_yarn group across 2 directories with 3 updates
Bumps the npm_and_yarn group with 1 update in the / directory: [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js).
Bumps the npm_and_yarn group with 2 updates in the /docsite directory: [micromatch](https://github.com/micromatch/micromatch) and [webpack](https://github.com/webpack/webpack).


Updates `xml2js` from 0.6.0 to 0.6.1
- [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/compare/0.6.0...0.6.1)

Updates `micromatch` from 4.0.7 to 4.0.8
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8)

Updates `webpack` from 5.92.1 to 5.95.0
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.92.1...v5.95.0)

---
updated-dependencies:
- dependency-name: xml2js
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: micromatch
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: webpack
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-31 12:40:57 +00:00
FoxxMD 38b02cf771 Update dependencies 2024-10-31 12:38:04 +00:00
Matt Foxx 1706d5cab9 Merge pull request #203 from FoxxMD/GH-195/ytoauth
feat(youtube)!: Use oauth-based api library for better stability
2024-10-31 08:30:48 -04:00
Matt Foxx 640d2ff28b Merge pull request #219 from FoxxMD/GH-218/plexLocal
fix(plex): Handle plex local user
2024-10-30 13:42:55 -04:00
FoxxMD 312b6ce987 fix(plex): Handle plex local user
* Identify when session is for the local user (no user is logged in)
* Add Local User as default allowed when no usersAllow settings
* Allow user config to allow/block local user
* Update plex docs

Fixes #218
2024-10-30 16:44:51 +00:00
FoxxMD 489f70929e docs: Fix spelling mistake 2024-10-29 13:47:38 -04:00
FoxxMD 904ddfa5c6 docs(tautulli): Add deprecation notice and discussion link 2024-10-29 16:57:59 +00:00
FoxxMD 3ed6840607 feat: Improved filtering for platform players with multiple states
* Add session key dervied from source data to Play and PlayerState
* Discriminate states based on a session key, if possible
  * Log which states are being dropped based on session key (plex)
* Add session key to important logging (new player play, in UI for player info)
2024-10-29 14:59:55 +00:00
FoxxMD 93125dd7c8 feat: Reduce drop reason logging noise for jellyfin/plex API sources
Only log the first occurrence of a unique session drop so that subsequent drops don't make logging noisy
2024-10-28 18:33:52 +00:00
FoxxMD 34356c6c56 fix(youtube): Fix signout check 2024-10-28 18:03:10 +00:00
FoxxMD 05466fed2b feat(youtube): Add config option to always log diff 2024-10-28 18:03:10 +00:00
FoxxMD 535b818a22 fix(youtube): Implement bump-only list change detection
Should fix new way YTM orders recent history #195
2024-10-28 18:03:10 +00:00
FoxxMD e62e04dd14 feat(youtube)!: Use oauth-based api library for better stability
* Fixes #195 missing (404) history endpoint which only appear when using cookies
* Might fix #158 due to using a different authentication method
2024-10-28 18:03:10 +00:00
FoxxMD 61cd5fcf7b fix: Bump @foxxmd/logging to fix DST rolling file issue
#214
2024-10-28 18:02:04 +00:00
Matt Foxx f308f71b5d Merge pull request #212 from FoxxMD/playerStateCleanup
feat(player): Real time positional player state
2024-10-25 14:54:23 -04:00
FoxxMD c498bde1fd feat(ui): Show indeterminate state for non-positional players 2024-10-25 15:57:30 +00:00
FoxxMD 0c9b496137 feat(player): Implement real-time positional state tracking
* Introduce positional and non-positional data structures
  * Refactor state player into types of each
  * Refactor listen ranges and progress into types of each
* Implment (internal) real-time player and base positional player seeked/repeat on real-time drift instead of just reported position
* Refactor tests to use (emulated) real-time components
2024-10-24 20:37:41 +00:00
FoxxMD 9682d4f8f9 refactor(player): Cleaner player update signature 2024-10-24 14:53:30 +00:00
Matt Foxx da76945f5e Merge pull request #211 from FoxxMD/plexApi
feat(plex): Initial Plex API Source implementation
2024-10-24 09:45:17 -04:00
FoxxMD 51074a58f4 Add deprecation warning to plex webhook source 2024-10-24 13:41:10 +00:00
FoxxMD 3cc735b9c8 fix(plex): Refactor player state to allow for drift and tracking pause based on Source behavior
Plex only updates player position every 15 seconds (of played track) so player state needs to be adapted to not detect this as a pause or seek.

* Allow per-source drift allowed before triggering seek
* Allow per-source pause detection
2024-10-23 20:30:33 +00:00
FoxxMD b74f31c26d feat(plex): Implement album art for UI 2024-10-23 18:42:55 +00:00
FoxxMD 4a96cbfb0e docs(plex): Clean up api/webhook sections and add migration guide 2024-10-23 15:18:13 +00:00
FoxxMD 77a3c352b1 feat(plex): Implement workaround for getting plex token details
Ignore sdk validation error and get raw value in order to parse authenticated user
2024-10-23 10:59:36 -04:00
FoxxMD 598ae01082 feat(plex): Add track progress tracking to api source 2024-10-23 13:40:50 +00:00
FoxxMD 54e11899a0 Fix missing comma 2024-10-22 18:29:11 +00:00
FoxxMD dc4962ade1 feat(plex): Initial Plex API Source implementation 2024-10-22 18:21:28 +00:00
FoxxMD ccf317c06f docs: Fix Jellyfin API migration version 2024-10-17 10:40:54 -04:00
FoxxMD aff5717f19 fix(ui): Fix album art ordering 2024-10-16 16:26:23 +00:00
FoxxMD fa39209272 feat(ui): Implement album art for Jellyfin and Spotify 2024-10-16 16:09:24 +00:00
FoxxMD d63082a889 fix(ui): Skip lg breakpoint for cleaner interface 2024-10-16 16:09:05 +00:00
FoxxMD a28d46ecbe feat(ui): Add album art support in player 2024-10-16 16:08:41 +00:00
FoxxMD 3bc72ce881 feat(ui): Improve player ui when player is stopping 2024-10-16 13:56:05 +00:00
FoxxMD 314210117b fix(player): Fix missing calculated state on player stop 2024-10-16 13:55:35 +00:00
FoxxMD 57a8e48d3a fix(jellyfin): Fix memory player hanging on end of state
Was not passing stopped state due to undefined play but need to do this to let last play actually be scrobbled.

Refactor jellyfin session validation and types for MemorySource to correctly allow processing player state with no plays
2024-10-16 13:55:05 +00:00
FoxxMD 2d3ab86238 fix: Fix string splitting 2024-10-16 13:03:37 +00:00
FoxxMD b76edbf7de fix: Revert including platform for track dispay by default 2024-10-16 12:47:43 +00:00
Matt Foxx b2efc34b80 Merge pull request #210 from FoxxMD/GH-209/jellyfinCleanup
fix(jellyfin): Enforcing media type and session handling fixes
2024-10-15 14:36:56 -04:00
FoxxMD dee8d6bc61 chore(vscode): Add markdown extension to devcontainer 2024-10-15 17:42:04 +00:00
FoxxMD 3e5576481d feat(jellyfin): Support and enforce library collection type
* Default disallow any activity not from a library of collectionType 'music'
* Allow user config for allow/block by library name
* Allow user config for allowing additional library collection types
* Update docs and example config
2024-10-15 17:41:33 +00:00
FoxxMD ffd74aea6f docs: Enable parsing json5 for AIO examples 2024-10-15 17:39:49 +00:00
FoxxMD 145af33239 feat(jellyfin): More item type validation 2024-10-15 14:25:51 +00:00
FoxxMD 3851b2226a fix(jellyfin): Detect theme song extras play and discard
Fixes #209
2024-10-15 13:13:36 +00:00
FoxxMD bb132101b5 fix(jellyfin): Use correct id for tracking first-time seen media 2024-10-14 17:19:41 -04:00
FoxxMD 1e6251edfb ci: Fix sanity test checkout ref and shorten APP_VERSION sha usage 2024-10-14 13:07:12 -04:00
FoxxMD e251da9b8e test(jellyfin): Update valid activity tests 2024-10-14 15:42:54 +00:00
FoxxMD 335ee0cd60 fix(jellyfin): Set multi-platform in constructor 2024-10-14 11:15:59 -04:00
FoxxMD dc9f6c22ad feat(jellyfin): Add debug options for media data troubleshooting 2024-10-14 11:11:58 -04:00
FoxxMD 5568b54c99 fix(jellyfin): Enforce media type when determining session validity
Fixes #209 for erroneous scrobbling of video
2024-10-14 11:01:48 -04:00
FoxxMD e6e35e052a feat: Implement platform output when building track info 2024-10-14 11:01:11 -04:00
Matt Foxx 9c21db8172 Merge pull request #205 from FoxxMD/GH-200/jfPlayFormat
fix(jellyfin): Gaurd against undefined playback data
2024-10-11 15:57:57 -04:00
FoxxMD 77c9141d92 fix(jellyfin): Gaurd against active session without now playing data 2024-10-11 13:47:39 -04:00
FoxxMD 68e86f4f90 fix(jellyfin): Gaurd against undefined playback data
SDK types say AlbumArtists etc. should be defined but #200 shows that's not always the case, for some reason.

Check for undefined value on playback data that requires additional transformation to prevent errors.

Fixes #200
2024-10-11 13:47:39 -04:00
Matt Foxx 5e95f60461 Merge pull request #208 from FoxxMD/vegaSchema
fix: Replace typescript-json-schema with ts-json-schema-generator
2024-10-11 13:32:52 -04:00
FoxxMD 908d7d4177 fix: Replace typescript-json-schema with ts-json-schema-generator
Fixes a lot of headaches:

* vega is more up to date and extensible
  * no more hacky regex for reference names
  * complete control over parsing and formatting (const as enum)
  * does not duplicate typescript as dependency
  * should properly throw errors on schema generation instead of just logging to console
* move schema static files into programmatic creation instead of package scripts
* fix types being removed in prod which caused schema generation errors
2024-10-11 12:28:55 -04:00
FoxxMD de1a9c35ec fix: Add missing express types for prod dependencies
Required for ts-schema generator to correctly work since we use it in config definitions
2024-10-11 09:28:01 -04:00
FoxxMD f7fa9cd330 fix: Guard against ts-schema generation errors
* Check if generator is null and throw
* Check if schema is null before trying to validate, will make errors less cryptic
2024-10-11 09:27:12 -04:00
FoxxMD ee8b371850 fix: Missing tsconfig and other root files for some reason?? 2024-10-11 09:23:02 -04:00
FoxxMD 601228a369 fix: Fix boolean options in issue template forms 2024-10-10 15:12:24 -04:00
FoxxMD 5dcaefc460 chore: Rename config template 2024-10-10 15:06:21 -04:00
FoxxMD 026e94fc1b chore: Add issue template config 2024-10-10 15:05:24 -04:00
FoxxMD fd7c8467b4 chore: Add templates for feature request and new source/client 2024-10-10 15:03:31 -04:00
FoxxMD 06e651a844 chore: Migrate bug report template to form 2024-10-10 14:41:38 -04:00
FoxxMD 1b0e7a3dfd ci: Make test workflow reusable and add run sanity check
* Add sanity run steps to check for obvious errors when running in production
* Refactor test job to be reusable
2024-10-10 12:05:16 -04:00
FoxxMD abc5d1861a fix: Specify ts-json-schema as NON dev dep
Not enough to include it in both dependencies and devDependencies -- it needs to be NOT in devDependencies to prevent `npm ci --omit=dev` from purging it.
2024-10-10 12:02:06 -04:00
FoxxMD 4b8129fd19 fix: missing schema package in regular deps and freeze ts version 2024-10-10 11:36:23 -04:00
FoxxMD f610f385bd test: Try timeout in test instead of before() 2024-10-09 16:15:21 -04:00
FoxxMD 6994635fe4 test: Try another timeout and destroy scheduler 2024-10-09 16:13:15 -04:00
FoxxMD 61525e1351 test: Add long timeout to first config parse due to slow schema generation
Not a problem on my machine but causing timeout on github actions
2024-10-09 15:55:06 -04:00
FoxxMD a50070e133 ci: Add ACT run for just backend test 2024-10-09 15:51:58 -04:00
Matt Foxx db632424b1 Merge pull request #202 from FoxxMD/validateGranular
feat: Improve client/source schema validation generation and error handling
2024-10-09 12:52:47 -04:00
FoxxMD 6b92920f50 fix: Use silent logger for source config tests 2024-10-09 12:20:10 -04:00
FoxxMD a30d362695 fix: Fix error logging line numbers
Don't use ts-node for json-schema
2024-10-09 12:18:23 -04:00
FoxxMD d105e62f18 feat: Refactor validation errors to emit an event instead of throwing
* Prevents app from crashing due to only one thing going wrong
* Will be used for testing error handling in the future
2024-10-09 12:18:23 -04:00
FoxxMD d5ad796339 fix: Fix example configs so they pass validation tests
* Add missing sources to dual configs
* Fix slug type for webscrobbler example
2024-10-09 12:18:23 -04:00
FoxxMD c0aabefc72 test: Add tests for example configs and test parsing/validation
* Add suite for testing client/source parsing and validation
* Additionally, use example configs for two-birds-one-stone checking they are valid
* Refactor sources init as separate step from component building
2024-10-09 12:18:23 -04:00
FoxxMD 259d321f3d chore: Remove tests folder from tsconfig exclude to get autocomplete 2024-10-09 12:18:23 -04:00
FoxxMD d2375d92e6 test: Force mocha to exit 2024-10-09 12:18:23 -04:00
FoxxMD f8a63c5df1 fix: Add missing glob package 2024-10-09 12:18:23 -04:00
FoxxMD 5004d99473 chore: Add tmp testing folders to ignore files 2024-10-09 12:18:23 -04:00
FoxxMD 5b26cc73e8 feat: Improve client/source schema validation generation and error handling
* Use runtime-generated schemas
  * Ensures validated schemas always match code
  * Can use schemas for individual source/clients without having to generate files
* catch invididual validation errors and log instead of crashing
* refactor validation error output into chained error instead of as individual log statements to improve clarity
2024-10-09 12:18:23 -04:00
FoxxMD 72a4c4542b fix: Fix some source schemas to reflect documentation WRT optional properties 2024-10-09 12:18:23 -04:00
Matt Foxx fdaeb8f6b8 Merge pull request #201 from FoxxMD/GH-199/ytschema
fix(ytmusic): Allow authUser to be number or string
2024-10-09 08:28:25 -04:00
FoxxMD f2f550dae7 Update devcontainer usage with docusarus and docs 2024-10-08 19:53:50 +00:00
FoxxMD 6780d72296 Improve testing, linting, and node usage in devcontainer
* Swtch to devcontainer for node 18.x and remove terminal/launch settings for version (removes nvm dependency in container)
* Update eslint config to use proper config for include/ignore files
* Add spec for mocha and mocha test runner extension + settings
2024-10-08 19:33:47 +00:00
FoxxMD f1784f3c21 fix(ytmusic): Allow authUser to be number or string
Fixes #199
2024-10-07 10:38:55 -04:00
FoxxMD 22d17ba621 docs: Fix link typo for flatpak 2024-10-03 09:18:26 -04:00
FoxxMD 4b4cc1c438 docs: Update/fix docker-compose usage
* Remove bad direct links to non-docsite docker-compose and replace with inline file
* Fix quotes around base url ENV
* Simplify config volume mapping
2024-10-03 09:11:56 -04:00
FoxxMD 7aac664916 docs: Update jellyfin instructions in quickstart to use API Source 2024-10-03 08:55:36 -04:00
FoxxMD 12c4a014f2 fix(jellyfin): Fix tests not exiting 2024-09-30 13:43:15 -04:00
FoxxMD 1eada12714 fix(jellyfin): Fix naming in documentation for allow/disallow options
* Fix documentation and jellyfin example to match code
* Add tests for jellyfin api config parsing and valid activities
* Fix jellyin api ENV naming to match code
* Fix config parsing missing lowercase transform

Fixes #196
2024-09-30 11:07:43 -04:00
FoxxMD 5adb831136 Add multi-runner WIP tests to ACT
Would like to move to this to improve build time on GH Actions but its not working yet. Will also be good when GH Runners release arm nodes publicly.
2024-09-27 16:30:02 -04:00
FoxxMD 5b8a8a7449 ci: Refactor ACT for more thorough usage
* All required files in the same file by using .actrc
* Include example .secrets and .env
* Update README with usage
* Refactor actTest to make docker build steps optional with ENV
2024-09-27 11:41:38 -04:00
FoxxMD 005dae76ab chore: Bump version for release 2024-09-27 09:02:04 -04:00
FoxxMD cb2c7276dd docs: Add scrobble modification logging docs 2024-09-24 11:45:50 -04:00
FoxxMD 54fe61c51c docs: Add when condition docs and examples 2024-09-24 11:43:56 -04:00
FoxxMD bdf27ec421 fix: Freeze typescript to fix weird unused error directive problem 2024-09-23 09:49:31 -04:00
FoxxMD 3addd39fc5 chore(vscode): Add default built task 2024-09-23 09:49:10 -04:00
FoxxMD 19bf456e47 Fix version for yt-music-api 2024-09-23 08:39:46 -04:00
Matt Foxx 66bf889e9f Merge pull request #186 from FoxxMD/GH-185/condition
feat(modifications): Implement "when" conditions
2024-09-23 08:34:53 -04:00
FoxxMD 09ea158734 Update lock file after rebase 2024-09-23 08:34:09 -04:00
FoxxMD 88c1b553e3 feat(modifications): Refactor hooks to accept an array of parts
* Hook can be single object part or array of parts
* All arrays are processed

#185
2024-09-23 08:32:40 -04:00
FoxxMD 1d1fd1b54d feat(modifications): Implement "when" conditions for scrobble modifications
Enable using multiple "when" conditions of regex matching to determine if a hook or individual part-rules should run

#185
2024-09-23 08:32:37 -04:00
FoxxMD 9d3267b1fc Add dockerignore entries for vscode 2024-09-23 08:29:47 -04:00
FoxxMD 22a04adde0 docker: Update dockerfile to ignore devcontainer and vscode settings 2024-09-20 12:33:23 -04:00
FoxxMD 1dba90ceb5 docs: Update development docs for VSCode usage 2024-09-20 12:31:34 -04:00
FoxxMD 4e9794d574 feat: Add VSCode devcontainer 2024-09-20 12:17:37 -04:00
FoxxMD 23a747d3a9 feat: Add VSCode native dev settings
* Add settings.json to ensure terminal runs with correct node version
* Add launch settings for debugging individual files, whole app, and mocha tests
2024-09-20 11:57:37 -04:00
FoxxMD 4678c35453 Merge remote-tracking branch 'origin/master' 2024-09-19 12:56:03 -04:00
FoxxMD c153e53c5b feat: Improve upstream scrobble refresh logic and caching controls
* Add refreshMinInterval to prevent hammering upstream services
* Refactor refresh logic to be simpler and account for backlogged tracks
* (test): Improve play generation utils for testing
* (test): Refactor testing for upstream scrobble refreshing to use more actual scrobbler class behavior
2024-09-19 12:55:50 -04:00
FoxxMD e20ee09bbb chore: Update faker to official release 2024-09-19 12:53:00 -04:00
Matt Foxx 619687472d Merge pull request #192 from FoxxMD/GH-191/plex-formdata-fix
fix(plex): Refactor plex webhook formdata parsing to be more defensive
2024-09-17 08:51:18 -04:00
FoxxMD b74f23f6d1 fix: Don't use es module in vite config because of gross esm/cjs reasons 2024-09-13 17:07:32 -04:00
FoxxMD d073b4c402 fix: Strip wrapping quotes from base url
Compensates for scenarios where a user accidentally encloses value in quotes when it shouldn't be

fixes #187
2024-09-13 16:51:02 -04:00
FoxxMD 258883d88b test: Add missing version prop for test components 2024-09-13 16:49:37 -04:00
FoxxMD 29c8d6236a fix(plex): Refactor plex webhook formdata parsing to be more defensive
* Move file parsing to own function
* Add some typings to file parsing to help with development (typings are bad)
* Defensively check structure of formdata for nested arrays since Plex seems to waffle on how it sends data (fixes #191)
* Add logging for all files found to help with future end-user debugging
2024-09-13 13:54:50 -04:00
FoxxMD a1443594f7 docs: Fix youtube music identifier
Closes #190
2024-09-12 08:46:49 -04:00
Matt Foxx ac2e9570e5 Merge pull request #189 from jackwilsdon/fix-docs-link
docs: fix link
2024-09-11 15:00:04 -04:00
FoxxMD 983b1c67ac fix: Bump @foxxmd/logging version to fix read-only system issues 2024-09-11 14:48:27 -04:00
Jack Wilsdon 764493f5c9 docs: fix link 2024-09-11 19:35:39 +01:00
Matt Foxx d7981e7cc0 Merge pull request #188 from jackwilsdon/fix-local-docs
fix: local documentation
2024-09-11 08:33:03 -04:00
Jack Wilsdon 11fbe97325 fix: local documentation
express-static prefers directories over files when using the extensions
option, meaning an extensionless route will always route to the
directory instead of the file (expressjs/serve-static#138).
2024-09-10 21:09:09 +01:00
Matt Foxx 3f37a63439 Merge pull request #182 from FoxxMD/dependabot/npm_and_yarn/npm_and_yarn-cccb3c3a64
chore(deps): bump the npm_and_yarn group across 2 directories with 7 updates
2024-08-30 11:03:09 -04:00
dependabot[bot] ee78b95617 chore(deps): bump the npm_and_yarn group across 2 directories with 7 updates
Bumps the npm_and_yarn group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [formidable](https://github.com/node-formidable/formidable) | `2.1.2` | `3.5.1` |
| [@types/formidable](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/formidable) | `2.0.6` | `3.4.5` |
| [ntfy](https://github.com/ffflorian/node-packages) | `1.5.4` | `1.6.2` |
| [postcss](https://github.com/postcss/postcss) | `8.4.38` | `8.4.41` |
| [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) | `0.5.0` | `0.6.0` |
| [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) | `7.24.7` | `7.25.6` |

Bumps the npm_and_yarn group with 2 updates in the /docsite directory: [postcss](https://github.com/postcss/postcss) and [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse).


Updates `formidable` from 2.1.2 to 3.5.1
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits/v3.5.1)

Updates `@types/formidable` from 2.0.6 to 3.4.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/formidable)

Updates `ntfy` from 1.5.4 to 1.6.2
- [Commits](https://github.com/ffflorian/node-packages/compare/ntfy@1.5.4...ntfy@1.6.2)

Updates `postcss` from 8.4.38 to 8.4.41
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.38...8.4.41)

Updates `xml2js` from 0.5.0 to 0.6.0
- [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/compare/0.5.0...0.6.0)

Updates `@babel/traverse` from 7.24.7 to 7.25.6
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.6/packages/babel-traverse)

Updates `axios` from 1.7.2 to 1.7.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4)

Updates `postcss` from 8.4.39 to 8.4.41
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.38...8.4.41)

Updates `@babel/traverse` from 7.24.7 to 7.25.6
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.6/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: formidable
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@types/formidable"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: ntfy
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: xml2js
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: "@babel/traverse"
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: axios
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: "@babel/traverse"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-30 14:55:40 +00:00
FoxxMD d7091f8a8e chore(ci): Add dependabot PR test gh action 2024-08-30 10:53:09 -04:00
FoxxMD 4b5cdade77 chore(ci): Disable alpine builds for PR image 2024-08-30 10:37:21 -04:00
FoxxMD cb67d1ae40 chore!: Fix latest/tag for images and disable alpine builds
* Ensure pre-release tags don't also get latest tag
* Disable alpine builds
  * Only increases testing and build complexity and there has been no feedback from users about needing it
2024-08-30 10:26:23 -04:00
FoxxMD 676af4a2bf Merge branch 'develop' 2024-08-30 09:54:24 -04:00
FoxxMD 0feb02babe chore: Refactor git/dev workflow to use master as unstable branch
* Aligns with "normal" default branch usage better, allows dependabot to actually run
* Change image publishing to push tagged images as 'latest' and master images as `edge`
2024-08-30 09:53:52 -04:00
FoxxMD 90c481b12b Merge remote-tracking branch 'origin/develop' into develop 2024-08-30 09:23:02 -04:00
FoxxMD 59fed488ba feat(jellyfin): Update schema 2024-08-30 09:22:52 -04:00
Matt Foxx 8431e7b6b2 Merge pull request #172 from FoxxMD/jellyfinApi
feat: Refactor Jellyfin source to use Jellyfin API
2024-08-30 09:13:56 -04:00
FoxxMD be30cdd540 feat(jellyfin): Implement partial WS client for future use 2024-08-29 15:16:49 -04:00
FoxxMD 54927ed77c fix(jellyfin): Map AlbumArtist Name property 2024-08-29 11:30:59 -04:00
FoxxMD 97f974d931 update vlc source building after rebase 2024-08-29 11:29:41 -04:00
FoxxMD 0de2cc0922 feat(jellyfin)!: Refactor Jellyfin source to use Jellyfin API
* Implement new jellyfin source using JF typescript api client library for communication
  * Use either API Key or username/password
* Implement real-time scrobble monitoring
* Implement feature parity filters from old jellyfin source (users) and add devices filters
* Refactor documentation for new configuration with api and add migration steps for webhook users
* Add deprecation warning to jellyfin webhook source
2024-08-29 11:28:27 -04:00
FoxxMD e80ad81f05 Merge branch 'develop' 2024-08-28 09:35:28 -04:00
FoxxMD a3960421d7 chore: Bump version for release 2024-08-28 09:35:13 -04:00
FoxxMD 2f2eeeb13a docs(youtube): Add FAQ entries for YTM auth and scrobble issues 2024-08-28 09:32:46 -04:00
Matt Foxx eeb026abf0 Merge pull request #163 from FoxxMD/ytMusicDebug
Improve YTM experience
2024-08-28 09:11:38 -04:00
FoxxMD 0fed96f59c Merge branch 'develop' 2024-08-28 09:07:12 -04:00
Matt Foxx 8369939135 Merge pull request #181 from FoxxMD/malojaDuplicateHandling
fix(maloja): Improve handling for Maloja warnings-as-errors
2024-08-27 16:54:11 -04:00
FoxxMD 08056a650f fix: Actually store recent scrobbles
Refresh scrobbles refactor missed actually storing scrobbles on refresh
2024-08-27 09:40:30 -04:00
FoxxMD 5b53b7490c fix(maloja): Improve handling for Maloja warnings-as-errors
* Fixes incorrect handling for duplicate scrobble error from Maloja 3.2.2 #180
* More defensive Maloja scrobble parsing
2024-08-26 11:09:49 -04:00
Matt Foxx ce95c3ae8c Merge pull request #179 from FoxxMD/vlc
Implement VLC as Source
2024-08-26 10:55:01 -04:00
FoxxMD 5e0c4e3e77 docs(vlc): Some fixes for VLC docs
tags <> in code block in details causes parsing error
2024-08-23 09:00:14 -04:00
FoxxMD 10bf48df40 docs(vlc): Add VLC docs 2024-08-22 17:30:37 -04:00
FoxxMD ae935798f3 chore: Update schema 2024-08-22 16:44:15 -04:00
FoxxMD bf71f8cf86 feat(vlc): Use regex from configuration to extract metadata from filenames 2024-08-22 16:44:08 -04:00
FoxxMD 4206696f0d feat(vlc): Better metadata parsing 2024-08-22 15:07:47 -04:00
FoxxMD 2903662f8b feat: Implement POC vlc Source #177 2024-08-22 13:11:12 -04:00
FoxxMD 469847e4b7 feat(youtube): Use just-validated history as recently played on new plays #156
Rather than generating our own just use the known "good" history we just compared against to see if this improves consistency.
2024-08-21 13:14:38 -04:00
FoxxMD d7a86b0401 feat(youtube): Increase level of auth update logging when configured to output changes #158
Increasing to INFO makes the signal better for long-running MS processes instead of requiring logging all of DEBUG
2024-08-21 13:14:38 -04:00
FoxxMD 4fdd346c00 Merge branch 'refs/heads/develop' 2024-08-20 09:18:01 -04:00
FoxxMD 4f26b63ab1 chore: Bump version for release 2024-08-20 09:17:28 -04:00
FoxxMD 0fed91302c chore(ci): Update untagged package schedule to run once a week
Doesn't need to run every day.
2024-08-19 11:55:47 -04:00
FoxxMD 831cefd9a7 docs(deezer): Deprecate due to discontinued API support
Deprecate Source with warning message and update docs. #175
2024-08-19 11:25:11 -04:00
FoxxMD 114a3dda8b feat(ui): Add timestamp to recent pages #167 2024-07-26 10:07:25 -04:00
FoxxMD 55ddd2c5a0 docs: Mention data modification in readme 2024-07-26 09:38:18 -04:00
FoxxMD 1d276186f8 feat: Implement removing field if field is an empty string after transforming 2024-07-26 09:30:58 -04:00
FoxxMD fb3886b981 docs: Modification improvements 2024-07-26 09:19:11 -04:00
FoxxMD 2bed7be654 docs: Fix compare example 2024-07-25 17:47:57 -04:00
FoxxMD 5c460ad747 docs: Add Play Transform docs 2024-07-25 17:28:49 -04:00
FoxxMD 32220d7509 feat: Implement play transforms in source/client logic 2024-07-25 16:16:58 -04:00
FoxxMD b0265001f8 feat: Improve logging for play transforms
* Use a proper child logger and error messages on catch
* Log full play string before => after instead of just parts
2024-07-25 15:12:39 -04:00
FoxxMD 9ede4634cb test: Use faker 9.0RC for more plausible artist/album names in generated data 2024-07-25 15:04:37 -04:00
FoxxMD 0e008ecd5c feat: Add album option to play string building function 2024-07-25 15:03:39 -04:00
FoxxMD 2ae82fdd9e feat: Implement common play transform functionality 2024-07-25 13:43:34 -04:00
Matt Foxx 3b430b95ed Merge pull request #174 from FoxxMD/issue-173-scrobbler-improvement
Enable more agressive scrobble checking behavior
2024-07-25 09:18:07 -04:00
FoxxMD e907892c4a refactor(scrobbler): Change refresh force to user-configurable staleness time
Use refreshStaleAfter to force refresh to user can configure the time.

Allows more nuanced refreshing behavior as well as always refresh (0 seconds)
2024-07-24 12:06:20 -04:00
FoxxMD d9cd3764b9 refactor(scrobbler): Consolidate refresh logic and add more logging
* Move recentScrobbles and generic refresh logic into abstract class
* New abstract function for scrobbler children to implement to get scrobbler-specific play lists on refresh
* Implement shouldRefereshScrobble with logging for better insight into why or why not a scrobbler refreshed upstream scrobbles
2024-07-24 11:20:09 -04:00
FoxxMD 9c2bf05a24 refactor(listenbrainz): Simplify scrobble api calls and logging 2024-07-24 10:31:37 -04:00
FoxxMD 91e52c4c5d feat(musikcube): Provide more error logging and example config
* Update iso-websockets to fix typing exports and get node network error from WS error
* Add url example to config
2024-07-23 12:11:19 -04:00
FoxxMD b41d7f539f feat(scrobble): Enable forcing existing scrobbles refresh on every scrobble #173 2024-07-23 10:48:35 -04:00
FoxxMD 9735b980d7 fix(tests): Do not re-use scrobbler between tests 2024-07-19 14:44:39 -04:00
FoxxMD 706483ed06 feat: On polling error retry check source is initialized and reinitialize if not 2024-07-19 14:36:50 -04:00
FoxxMD 7760776a5f feat(mpd): Use idle event to wake up from polling early 2024-07-19 14:36:27 -04:00
FoxxMD 8b9a7893d3 feat(mpd): Implement MPD Source MVP 2024-07-19 12:10:51 -04:00
FoxxMD 729250a2cc Merge branch 'refs/heads/develop' 2024-07-18 14:18:39 -04:00
FoxxMD f14203f85e docs: More improvements to configuration docs 2024-07-18 11:14:40 -04:00
FoxxMD 60e880f7f2 Merge branch 'refs/heads/develop'
# Conflicts:
#	docsite/docs/configuration/configuration.md
2024-07-17 12:47:23 -04:00
FoxxMD 16cef0ae65 chore: Bump versions for release 2024-07-17 12:46:18 -04:00
FoxxMD 12ff1f421e docs: Use relative paths for volume mounting 2024-07-17 11:46:00 -04:00
FoxxMD 61c33da2b0 docs: Fix quickstart link 2024-07-17 11:39:41 -04:00
FoxxMD 8b0c7205b7 docs: Add quick start guide 2024-07-17 11:33:56 -04:00
FoxxMD b02c96ada8 docs: Parse and render config examples from repository 2024-07-17 09:51:44 -04:00
FoxxMD 52b20e4c8e docs: Update kitchensink code block titles 2024-07-16 15:37:09 -04:00
FoxxMD a94ac8284f docs: refactor installation for docusaurus 2024-07-16 15:28:09 -04:00
FoxxMD ee399136a9 docs: Lean 100% into docusaurus documentation
Since docs are now generated alongside builds and hosted on the app server there is no longer a barrier to using docusaurus docs as the main documentation presentation. Any user looking for docs can use the GH Pages hosted site or a "versioned" docs site hosted on their MS app. Due to this we can fully commit to docusaurus and take advantage of MDX layouts to simplify docs.

* Replace env/file configs headings in configuration with MDX tabs to make reading easier
* Remove superfluous TOCs
* Replace internal links in repo readme with links to docsite
2024-07-16 14:11:14 -04:00
FoxxMD ab54422acf docs: Replace manual notes with admonitions 2024-07-16 11:58:09 -04:00
FoxxMD d56c5720eb docs(flatpak): Fix typo in docs website link 2024-07-16 11:46:13 -04:00
FoxxMD 040de4b000 docs(flatpak): Implement bash setup script and move flatpak instructions to docsite
* setup.sh takes care of building generated sources for project/docsite and building flatpak app
* Modify flatpak manifest to include two generated sources files and build docsite files
* Update flatpak docs with script usage and generated sources changes, move to docusauraus doc site
2024-07-16 11:34:15 -04:00
FoxxMD 815a774e3d docs(fix): Fix broken links/anchors 2024-07-15 14:24:00 -04:00
FoxxMD b2e7f58e14 docs(fix): Add missing docsite build steps to alpine docker variant 2024-07-15 13:50:37 -04:00
FoxxMD 8d124a20ac docs(fix): Update package scripts to use npx to invoke docusarus 2024-07-15 13:44:07 -04:00
FoxxMD d1a4a54d2a docs: Implement self-hosted docs
* Add npm scripts for installing/building docusaurus from main project
* Set base url based on ENV
* Build docsite in docker image
* Add link to doc in dashboard and link to github
* Update nodejs install instructions to include doc build command
* Update GH pages workflow to use correct base url
* Add hint page to main app when docs are not built
2024-07-15 13:26:57 -04:00
FoxxMD 049bb06762 docs: Make docs site more prominent in readme 2024-07-12 16:55:48 -04:00
FoxxMD e5dd473981 Merge branch 'refs/heads/develop' 2024-07-12 14:39:53 -04:00
FoxxMD ca5cc3bb30 docs(feat): Add TOC to dev docs and links in readme 2024-07-12 14:37:52 -04:00
FoxxMD 444d75c803 docs(feat): Add architecture overview, common dev structures, and dev guidance for Sources 2024-07-12 14:31:11 -04:00
FoxxMD ccda3a6098 Merge branch 'refs/heads/develop' 2024-07-09 16:06:05 -04:00
FoxxMD ce1c70a4e1 ci: Ignore flatpak and docsite for image deploy 2024-07-09 16:05:15 -04:00
FoxxMD 5fcfa979c5 docs: Improve TOC for docusaurus and github
* Use lower headers so docusaurs sidebar TOC is more accurate
* Specify header levels for config/FAQ to make sidebar more concise
* Wrap inline TOC in details block -- so docusaurs users don't have to see it twice but its still available to github viewers
2024-07-09 15:56:44 -04:00
FoxxMD ec4d609ba1 docs: Update docusaurs playground schema for MS to latest 2024-07-09 15:55:07 -04:00
FoxxMD b42278a372 docs: Update docusaurus to v3 2024-07-09 15:54:45 -04:00
274 changed files with 42955 additions and 23037 deletions
+38
View File
@@ -0,0 +1,38 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{
"name": "Node.js",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [9078,3000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"hbenl.vscode-mocha-test-adapter",
"dbaeumer.vscode-eslint",
"unifiedjs.vscode-mdx"
]
}
},
"portsAttributes": {
"3000": {
"label": "Docusaurus"
},
"9078": {
"label": "App"
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
+6 -1
View File
@@ -15,7 +15,7 @@ config/*.p8
/docs
/logs
.flatpak-builder
flatpak/generated-sources.json
**/generated-sources.*
flatpak/.flatpak-builder
docsite/build
docsite/node_modules
@@ -23,3 +23,8 @@ docsite/.docusaurus
docsite/.cache-loader
build
*.secrets
.devcontainer
.vscode
**/act/*
tmp-*
.env
+67
View File
@@ -0,0 +1,67 @@
name: Bug Report
description: Report a reproducible problem
title: "bug: "
labels: ["bug"]
body:
- type: checkboxes
attributes:
label: Please check existing knowledge before opening an issue
options:
- label: I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
required: true
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required: true
- type: markdown
attributes:
value: |
If this a how-do-I question, troublehooting, or generally **not** a bug/error with multi-scrobbler please consider [**starting a discussion**](https://github.com/FoxxMD/multi-scrobbler/discussions) instead of opening an issue.
- type: textarea
id: what-happened
attributes:
label: Describe the Bug
description: Describe what you expected to happen and what happened instead. Please provide as much detail as possible.
placeholder: The app should have scrobbled from Spotify but instead it crashed with an error about undefined properties, etc...
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: What platform are you running multi-scrobbler on?
options:
- Docker
- Local (NodeJS)
- Flatpak
default: 0
validations:
required: true
- type: textarea
attributes:
label: Versions
description: What version of multi-scrobbler and issue-related sources/clients are running?
placeholder: |
- multi-scrobbler: 0.8.3 on docker
- maloja 3.1.4
- jellyfin 10.8.9
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: |
Please copy and paste any/all relevant from the DEBUG level log output.
Even if your issue does not seem to be shown in the logs you should still add as much detail as possible, a _lack_ of issues in logs may be a good hint during debugging.
Your logs will be automatically formatted into code, do not need use backticks.
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/docs/configuration#debug-mode).
render: shell
placeholder: |
[2025-02-14 12:47:57.170 -0500] VERBOSE: [Init] Config Dir ENV: undefined -> Resolved: CWD/config
[2025-02-14 12:47:57.236 -0500] INFO : [Init] Debug Mode: NO
[2025-02-14 12:47:57.258 -0500] INFO : [Init] Version: master
[2025-02-14 12:47:57.259 -0500] INFO : [Init] Generating schema definitions...
[2025-02-14 12:47:59.148 -0500] DEBUG : [App] [Scrobblers] [Maloja - default] Found 100 recent scrobbles
- type: textarea
attributes:
label: Additional Context
description: Add any other context about the problem here.
@@ -0,0 +1,31 @@
name: Feature Request
description: Suggest a new feature or functionality
title: "feat: "
labels: ["enhancement"]
body:
- type: checkboxes
attributes:
label: Please check existing knowledge before opening an issue
options:
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
required: true
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required: true
- type: textarea
attributes:
label: Describe the new functionality
description: Please describe the feature or functionality you want multi-scrobbler to have. Use as much detail as possible about the exact behavior you wish to see.
validations:
required: true
- type: textarea
attributes:
label: Configuration Example
description: |
If the functionality involves changes to configuration files please provide an example here
render: json5
- type: checkboxes
attributes:
label: Will you help implement it?
options:
- label: I am willing to contribute code towards completing this feature
- label: I am willing to help test preview builds for this feature
@@ -0,0 +1,75 @@
name: Request a new Source or Client
description: Request a new Source or Client to be implemented
title: "feat: "
labels: ["enhancement"]
body:
- type: checkboxes
attributes:
label: Please check existing knowledge before opening an issue
options:
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
required: true
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required: true
- type: dropdown
attributes:
label: What Type?
description: Is this a Source or a Client?
options:
- Source
- Client
default: 1
validations:
required: true
- type: input
attributes:
label: Name
description: "What is the name of the new service/software?"
placeholder: "My Cool Player"
validations:
required: true
- type: input
attributes:
label: Website
description: "Website for the service/software. Preferably a github repo or other source-code homepage"
validations:
required: true
- type: dropdown
attributes:
label: API Available?
description: Does the service have an API or other protocol that multi-scrobbler can use to communicate? **Without this the likelyhood of implementation is very low.**
options:
- 'Yes'
- 'No'
default: 1
validations:
required: true
- type: dropdown
attributes:
label: Free To Use?
description: Is this service/software freely available or does it require purchasing/subscription?
options:
- 'Yes'
- 'No'
default: 1
validations:
required: true
- type: dropdown
attributes:
label: Willing to finance development?
description: If the software is **non-free** are you will to provide a paid account/subscription to multi-scrobbler developer's in order to facilitate development/testing?
options:
- 'Yes'
- 'No'
validations:
required: false
- type: checkboxes
attributes:
label: Will you help implement it?
options:
- label: I am willing to contribute code towards completing this feature
- label: I am willing to help test preview builds for this feature
- type: textarea
attributes:
label: Additional Context
description: Add any other context about the new Source/Client here
-40
View File
@@ -1,40 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
```
Copy and paste as much log data as possible related to this issue here.
```
**Versions (please complete the following information):**
Provide version information for any related sources/clients.
- multi-scrobbler: [e.g. 0.4.0 on docker]
- maloja [e.g. 3.1.4]
- jellyfin [e.g. 10.8.9]
**Additional context**
Add any other context about the problem here.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+5
View File
@@ -0,0 +1,5 @@
--directory ../../
--env-file .github/act/.env
--secret-file .github/act/.secrets
--artifact-server-path out
#--action-offline-mode
+5
View File
@@ -0,0 +1,5 @@
NO_DOCKER_BUILD=true
#https://github.com/nektos/act/issues/329#issuecomment-1905955589
ACTIONS_RUNTIME_URL=http://YOUR_HOST_IP:8082/
ACTIONS_RUNTIME_TOKEN=foo
ACTIONS_CACHE_URL=http://YOUR_HOST_IP:8082/
+3
View File
@@ -0,0 +1,3 @@
DOCKER_PASSWORD=
DOCKER_USERNAME=
GITHUB_TOKEN=
+41 -2
View File
@@ -1,4 +1,24 @@
Testing GH Actions with [ACT]
Testing GH Actions with ACT
Need to have credentials in a [`.secrets` file](https://nektosact.com/usage/index.html#secrets), copy and rename [`.secrets.example`](./secrets.example) to `.secrets`, then fill out blank fields. Required for docker/metadata-action to read...something. Fails with `Parameter token or opts.auth is required` if they are not supplied.
An ENV file can also be made by copy and renaming [`.env.example`](./env.example). Set `NO_DOCKER_BUILD=true` if you only want to test APP_VERSION and docker tags output.
If running a full docker build for multi-runner workflows you will need to create an [artifact server](https://github.com/nektos/act/issues/329#issuecomment-1905955589) for ACT to work:
```shell
docker pull ghcr.io/jefuller/artifact-server:latest
docker run -d --name artifact-server -p 8082:8080 --add-host host.docker.internal:host-gateway -e AUTH_KEY=foo ghcr.io/jefuller/artifact-server:latest
```
Run the following **from this directory** to make use of `.actrc` and proper working directory.
### Test Branch Test Suite
```shell
act -W '.github/act/testSuite.yml' -e '.github/act/actBranchEvent.json'
```
### Test Branch Push
@@ -7,8 +27,27 @@ act -W '.github/act/actTest.yml' -e '.github/act/actBranchEvent.json'
```
### Test Tag Push
### Test Tag (Release) Push
```shell
act -W '.github/act/actTest.yml' -e '.github/act/actTagEvent.json'
```
### Test Tag (Pre-Release) Push
```shell
act -W '.github/act/actTest.yml' -e '.github/act/actTagPreEvent.json'
```
### Test Multi-Runner Push (WIP)
```shell
act -W '.github/act/multiRunnerTest.yml' -e '.github/act/actBranchEvent.json'
```
### Test Multi-Runner Bake Push (WIP)
```shell
act -W '.github/act/multiRunnerBakeTest.yml' -e '.github/act/actBranchEvent.json'
```
+1 -1
View File
@@ -1,4 +1,4 @@
{
"ref": "refs/heads/master",
"sha": "0a1a94d9ad4efa373f8d34aace5e7f0a3fff42ad"
"sha": "14d5eb3313110f2cfd1aa8fe9c229c21901c1eeb"
}
+164
View File
@@ -0,0 +1,164 @@
{
"action": "synchronize",
"number": 210,
"pull_request": {
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/pulls/210",
"id": 2123337348,
"node_id": "PR_kwDOEpP-x85-j46E",
"number": 210,
"state": "open",
"locked": false,
"merge_commit_sha": "349bea1f4580b9e61aa22446eae73f8b1225bbb7",
"labels": [
{
"id": 2494982061,
"node_id": "MDU6TGFiZWwyNDk0OTgyMDYx",
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/labels/bug",
"name": "bug",
"color": "d73a4a",
"default": true,
"description": "Something isn't working"
},
{
"id": 7131135993,
"node_id": "LA_kwDOEpP-x88AAAABqQx_-Q",
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/labels/safe%20to%20test",
"name": "safe to test",
"color": "D4C5F9",
"default": false,
"description": "trusted to build image"
}
],
"milestone": null,
"draft": false,
"head": {
"label": "FoxxMD:GH-209/jellyfinCleanup",
"ref": "GH-209/jellyfinCleanup",
"sha": "e251da9b8eecfcb3aba8756393e5075d0092794b",
"user": {
"login": "FoxxMD",
"id": 4663766,
"node_id": "MDQ6VXNlcjQ2NjM3NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/4663766?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/FoxxMD",
"html_url": "https://github.com/FoxxMD",
"followers_url": "https://api.github.com/users/FoxxMD/followers",
"following_url": "https://api.github.com/users/FoxxMD/following{/other_user}",
"gists_url": "https://api.github.com/users/FoxxMD/gists{/gist_id}",
"starred_url": "https://api.github.com/users/FoxxMD/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/FoxxMD/subscriptions",
"organizations_url": "https://api.github.com/users/FoxxMD/orgs",
"repos_url": "https://api.github.com/users/FoxxMD/repos",
"events_url": "https://api.github.com/users/FoxxMD/events{/privacy}",
"received_events_url": "https://api.github.com/users/FoxxMD/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 311688903,
"node_id": "MDEwOlJlcG9zaXRvcnkzMTE2ODg5MDM=",
"name": "multi-scrobbler",
"full_name": "FoxxMD/multi-scrobbler",
"private": false,
"owner": {
"login": "FoxxMD",
"id": 4663766,
"node_id": "MDQ6VXNlcjQ2NjM3NjY=",
"avatar_url": "https://avatars.githubusercontent.com/u/4663766?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/FoxxMD",
"html_url": "https://github.com/FoxxMD",
"followers_url": "https://api.github.com/users/FoxxMD/followers",
"following_url": "https://api.github.com/users/FoxxMD/following{/other_user}",
"gists_url": "https://api.github.com/users/FoxxMD/gists{/gist_id}",
"starred_url": "https://api.github.com/users/FoxxMD/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/FoxxMD/subscriptions",
"organizations_url": "https://api.github.com/users/FoxxMD/orgs",
"repos_url": "https://api.github.com/users/FoxxMD/repos",
"events_url": "https://api.github.com/users/FoxxMD/events{/privacy}",
"received_events_url": "https://api.github.com/users/FoxxMD/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/FoxxMD/multi-scrobbler",
"description": "Scrobble plays from multiple sources to multiple clients",
"fork": false,
"created_at": "2020-11-10T14:45:38Z",
"updated_at": "2024-10-14T09:21:37Z",
"pushed_at": "2024-10-14T15:43:00Z",
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"visibility": "public",
"forks": 16,
"open_issues": 19,
"watchers": 369,
"default_branch": "master",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_rebase_merge": true,
"allow_auto_merge": false,
"delete_branch_on_merge": false,
"allow_update_branch": false,
"use_squash_pr_title_as_default": false,
"squash_merge_commit_message": "COMMIT_MESSAGES",
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
"merge_commit_message": "PR_TITLE",
"merge_commit_title": "MERGE_MESSAGE"
}
},
"base": {
"label": "FoxxMD:master",
"ref": "master",
"sha": "9c21db8172b6beff9d321e831e9353bf17e92a37",
"repo": {
"id": 311688903,
"node_id": "MDEwOlJlcG9zaXRvcnkzMTE2ODg5MDM=",
"name": "multi-scrobbler",
"full_name": "FoxxMD/multi-scrobbler",
"private": false,
"fork": false,
"created_at": "2020-11-10T14:45:38Z",
"updated_at": "2024-10-14T09:21:37Z",
"pushed_at": "2024-10-14T15:43:00Z",
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"visibility": "public",
"forks": 16,
"open_issues": 19,
"watchers": 369,
"default_branch": "master",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_rebase_merge": true,
"allow_auto_merge": false,
"delete_branch_on_merge": false,
"allow_update_branch": false,
"use_squash_pr_title_as_default": false,
"squash_merge_commit_message": "COMMIT_MESSAGES",
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
"merge_commit_message": "PR_TITLE",
"merge_commit_title": "MERGE_MESSAGE"
}
},
"author_association": "OWNER",
"auto_merge": null,
"active_lock_reason": null,
"merged": false,
"mergeable": null,
"rebaseable": null,
"mergeable_state": "unknown",
"merged_by": null,
"comments": 0,
"review_comments": 0,
"maintainer_can_modify": false,
"commits": 5,
"additions": 84,
"deletions": 28,
"changed_files": 5
},
"before": "335ee0cd601f5a82e36643c812507b2b53e7daf0",
"after": "e251da9b8eecfcb3aba8756393e5075d0092794b"
}
+6 -3
View File
@@ -1,5 +1,8 @@
{
"ref": "refs/tags/0.8.0",
"ref_name": "0.8.0",
"sha": "0a1a94d9ad4efa373f8d34aace5e7f0a3fff42ad"
"ref": "refs/tags/0.8.4",
"ref_name": "0.8.4",
"before": "0000000000000000000000000000000000000000",
"after": "005dae76ab51799d3d55112738e301cb1af0dafd",
"sha": "005dae76ab51799d3d55112738e301cb1af0dafd",
"base_ref": "refs/heads/master"
}
+8
View File
@@ -0,0 +1,8 @@
{
"ref": "refs/tags/0.8.5-gh-runner",
"ref_name": "0.8.5-gh-runner",
"before": "0000000000000000000000000000000000000000",
"after": "005dae76ab51799d3d55112738e301cb1af0dafd",
"sha": "005dae76ab51799d3d55112738e301cb1af0dafd",
"base_ref": "refs/heads/master"
}
+35 -34
View File
@@ -1,15 +1,17 @@
name: Publish Docker image to Dockerhub
on:
workflow_dispatch:
push:
branches:
- 'master'
- 'develop'
tags:
- '*.*.*'
# don't trigger if just updating docs
paths-ignore:
- '**.md'
- 'README.md'
- '.github/**'
- 'flatpak/**'
# use release instead of tags once version is correctly parsed
# https://github.com/docker/metadata-action/issues/422
# https://github.com/docker/metadata-action/issues/240
@@ -18,16 +20,10 @@ on:
jobs:
test:
push_to_registry:
name: Build and push container images
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
# strategy:
# fail-fast: false
# matrix:
# include:
# - dockerfile: ./Dockerfile
# suffix: ''
# platforms: 'linux/amd64'
steps:
- name: Check out the repo
uses: actions/checkout@v4
@@ -47,11 +43,6 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
# - name: Set push event short ref
# run: |
# shortEventRef=${github.event.push.ref:10}
# echo "SHORT_REF=$shortEventRef" >> $GITHUB_ENV
- name: Check App Version
env:
# use release instead of tags once version is correctly parsed
@@ -63,26 +54,38 @@ jobs:
run: |
echo $APP_VERSION
# - name: Extract metadata (tags, labels) for Docker
# id: meta
# uses: docker/metadata-action@v5
# with:
# # generate Docker tags based on the following events/attributes
# # https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
# tags: |
# type=raw,value=latest,enable={{endsWith(github.ref, 'master')}},suffix=${{ matrix.suffix }}
# type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }},suffix=${{ matrix.suffix }}
# type=semver,pattern={{version}},suffix=${{ matrix.suffix }}
# flavor: |
# latest=false
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
# generate Docker tags based on the following events/attributes
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
tags: |
type=edge
# maybe re-enable branch-named tags in the futures
#type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
type=match,pattern=\d.\d.\d$,priority=901
# tag all semver (include pre-release)
type=semver,pattern={{version}}
# flavor: |
# latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
id: qemu
if: ${{ !env.NO_DOCKER_BUILD }}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
if: ${{ steps.qemu.outcome == 'success' }}
continue-on-error: true
- name: Build and push Docker image
if: ${{ steps.qemu.outcome == 'success' }}
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
@@ -96,9 +99,7 @@ jobs:
# https://github.com/docker/build-push-action/issues/1026#issue-2041857786
build-args: |
APP_BUILD_VERSION=${{env.APP_VERSION}}
file: ${{ matrix.dockerfile }}
push: false
tags: mstest:latest
#tags: ${{ steps.meta.outputs.tags }}
#labels: ${{ steps.meta.outputs.labels }}
#platforms: ${{ matrix.platforms }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
+30
View File
@@ -0,0 +1,30 @@
variable "DEFAULT_TAG" {
default = "multi-scrobbler:local"
}
// Special target: https://github.com/docker/metadata-action#bake-definition
target "docker-metadata-action" {
tags = ["${DEFAULT_TAG}"]
}
// Default target if none specified
group "default" {
targets = ["image-local"]
}
target "image" {
inherits = ["docker-metadata-action"]
}
target "image-local" {
inherits = ["image"]
output = ["type=docker"]
}
target "image-all" {
inherits = ["image"]
platforms = [
"linux/amd64",
"linux/arm64"
]
}
+233
View File
@@ -0,0 +1,233 @@
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#with-bake (https://github.com/docker/build-push-action/issues/671#issuecomment-1609106171)
# with example from here https://github.com/crazy-max/docker-linguist/blob/master/.github/workflows/build.yml that uses caching
#
# may have to use this to enable pushing to multiple registries? https://github.com/docker/build-push-action/discussions/1067#discussioncomment-8677682
name: build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'master'
tags:
- '*.*.*'
paths-ignore:
- 'README.md'
- '.github/**'
- 'flatpak/**'
env:
DOCKERHUB_SLUG: foxxmd/multi-scrobbler
GHCR_SLUG: ghcr.io/foxxmd/multi-scrobbler
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
- name: Set short git commit SHA
id: appvars
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
# short sha available under env.COMMIT_SHORT_SHA
run: |
calculatedSha=$(git rev-parse --short HEAD)
branchName=$(git rev-parse --abbrev-ref HEAD)
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
- name: Get App Version
id: appversion
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
run: |
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
-
name: Create matrix
id: platforms
run: |
echo "matrix=$(docker buildx bake --file ./docker-bake.hcl image-all --print | jq -cr '.target."image-all".platforms')" >>${GITHUB_OUTPUT}
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_SLUG }}
${{ env.GHCR_SLUG }}
tags: |
type=edge
# maybe re-enable branch-named tags in the futures
#type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
type=match,pattern=\d.\d.\d$,priority=901
# tag all semver (include pre-release)
type=semver,pattern={{version}}
labels: |
org.opencontainers.image.title=Multi-Scrobbler
org.opencontainers.image.description=Scrobble from many sources to many clients
org.opencontainers.image.vendor=FoxxMD
-
name: Rename meta bake definition file
run: |
mv "${{ steps.meta.outputs.bake-file }}" "/tmp/bake-meta.json"
-
name: Upload meta bake definition
uses: actions/upload-artifact@v3
with:
name: bake-meta
path: /tmp/bake-meta.json
if-no-files-found: error
retention-days: 1
build:
runs-on: ubuntu-latest
needs:
- prepare
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps:
-
name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
-
name: Checkout
uses: actions/checkout@v4
-
name: Test App Version Transfer
run: |
echo "App Version in Build ${{ needs.prepare.appversion.outputs.appversion }}"
-
name: Download meta bake definition
uses: actions/download-artifact@v3
with:
name: bake-meta
path: /tmp
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: "--debug"
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build
id: bake
uses: docker/bake-action@v5
with:
files: |
./.github/act/docker-bake.hcl
/tmp/bake-meta.json
targets: image
set: |
*.args.APP_BUILD_VERSION=${{ needs.prepare.appversion.outputs.appversion }}
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
*.cache-to=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
*.output=type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
-
name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}"
touch "/tmp/digests/${digest#sha256:}"
-
name: Upload digest
uses: actions/upload-artifact@v3
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
needs:
- build
steps:
-
name: Download meta bake definition
uses: actions/download-artifact@v3
with:
name: bake-meta
path: /tmp
-
name: Download digests
uses: actions/download-artifact@v3
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.DOCKERHUB_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
$(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.GHCR_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
$(printf '${{ env.GHCR_SLUG }}@sha256:%s ' *)
-
name: Inspect image
run: |
tag=$(jq -r '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${tag}
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${tag}
+198
View File
@@ -0,0 +1,198 @@
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
name: build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'master'
tags:
- '*.*.*'
paths-ignore:
- 'README.md'
- '.github/**'
- 'flatpak/**'
env:
DOCKERHUB_SLUG: foxxmd/multi-scrobbler
GHCR_SLUG: ghcr.io/foxxmd/multi-scrobbler
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v5
- name: Set short git commit SHA
id: appvars
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
# short sha available under env.COMMIT_SHORT_SHA
run: |
calculatedSha=$(git rev-parse --short HEAD)
branchName=$(git rev-parse --abbrev-ref HEAD)
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
- name: Get App Version
id: appversion
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
run: |
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
# - name: Extract metadata (tags, labels) for Docker
# id: meta
# uses: docker/metadata-action@v5
# with:
# images: |
# ${{ env.DOCKERHUB_SLUG }}
# ${{ env.GHCR_SLUG }}
# labels: |
# org.opencontainers.image.title=Multi-Scrobbler
# org.opencontainers.image.description=Scrobble from many sources to many clients
# org.opencontainers.image.vendor=FoxxMD
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
# for caching
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
build-args: |
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
#tags: ${{ steps.meta.outputs.tags }}
outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=true
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_SLUG }}
${{ env.GHCR_SLUG }}
# generate Docker tags based on the following events/attributes
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
tags: |
type=edge
# maybe re-enable branch-named tags in the futures
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
type=match,pattern=\d.\d.\d$,priority=901
# tag all semver (include pre-release)
type=semver,pattern={{version}}
# flavor: |
# latest=false
labels: |
org.opencontainers.image.title=Multi-Scrobbler
org.opencontainers.image.description=Scrobble from many sources to many clients
org.opencontainers.image.vendor=FoxxMD
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_SLUG }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
+10
View File
@@ -0,0 +1,10 @@
name: PR Workflow
on:
push:
branches:
- 'master'
jobs:
test:
uses: ./.github/workflows/testAndSanity.yml
-1
View File
@@ -1,6 +1,5 @@
## Checklist before requesting a review
- [ ] **I am opening this PR for the [`develop` branch](https://github.com/FoxxMD/multi-scrobbler/tree/develop) and NOT `master`.**
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
## Type of change
+16
View File
@@ -0,0 +1,16 @@
name: Test Dependabot PR
on:
pull_request_target:
types:
- synchronize
- reopened
- opened
branches:
- 'master'
jobs:
test:
name: Tests / Build / Sanity Run
if: github.actor == 'dependabot[bot]'
uses: ./.github/workflows/testAndSanity.yml
+13 -5
View File
@@ -2,10 +2,8 @@ name: Deploy to GitHub Pages
on:
workflow_dispatch:
push:
branches:
- master
- developDocs
release:
types: [released]
# Review gh actions docs if you want to further define triggers, paths, etc
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
@@ -20,13 +18,23 @@ jobs:
node-version: 20
cache: npm
- name: Install dependencies
- name: Install app dev dependencies
run: npm ci
- name: Generate schema files
run: npm run schema
- name: Install docusaurus dependencies
run: npm ci
working-directory: ./docsite
- name: Build website
env:
ANALYTICS: ${{ vars.ANALYTICS }}
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
RY_ANALYTICS: ${{ vars.RY_ANALYTICS }}
RY_ANALYTICS_SITEID: ${{ vars.RY_ANALYTICS_SITEID }}
RY_ANALYTICS_REPLAY: ${{ vars.RY_ANALYTICS_REPLAY }}
GSITEVERIFICATION: ${{ vars.GSITEVERIFICATION }}
DOCS_BASE: '/multi-scrobbler'
run: npm run build
working-directory: ./docsite
+1 -1
View File
@@ -1,7 +1,7 @@
name: Delete Untagged Packages
on:
schedule:
- cron: '30 1 * * *'
- cron: '30 1 * * 0'
workflow_run:
workflows: ["Publish Docker image to Dockerhub"]
types:
+154 -54
View File
@@ -1,5 +1,9 @@
name: PR Workflow
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha }}
cancel-in-progress: true
on:
pull_request_target:
types:
@@ -8,33 +12,20 @@ on:
- reopened
- opened
branches:
- 'develop'
- 'master'
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'npm'
- name: Install dev dependencies
run: npm ci
- name: Build Backend
run: 'npm run build:backend'
- name: Test Backend
run: npm run test
uses: ./.github/workflows/testAndSanity.yml
with:
ref: ${{ github.event.pull_request.head.sha }}
release-snapshot:
name: Release snapshot
runs-on: ubuntu-latest
build-snapshot:
name: Build OCI snapshot
needs: test
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
runs-on: ${{ matrix.os }}
permissions:
packages: write
contents: read
@@ -42,62 +33,172 @@ jobs:
fail-fast: false
matrix:
include:
- dockerfile: ./Dockerfile
suffix: ''
platforms: 'linux/amd64,linux/arm64'
- dockerfile: ./alpine.Dockerfile
suffix: '-alpine'
platforms: 'linux/amd64,linux/arm64'
- os: ubuntu-latest
arch: amd64
platform: linux/amd64
- os: ubuntu-24.04-arm
arch: arm64
platform: linux/arm64
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
# list all registries to push to and join all non-empty with comma
# https://unix.stackexchange.com/a/693165/116849
# https://stackoverflow.com/a/9429887/1469797
strings=("${{vars.DOCKERHUB_SLUG}}" "${{vars.GHCR_SLUG}}")
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
joined_string=$(IFS=, ; echo "${strings[*]}")
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
- name: Log in to Docker Hub
# do not use here!
# instead we can specify repo#sha in build-push-action so we only deal with foreign SHA for actually building image
# - uses: actions/checkout@v4
# with:
# ref: ${{ github.event.pull_request.head.sha }}
- name: Set short git commit SHA
run: |
short=$(echo "${{ github.event.pull_request.head.sha }}" | head -c7)
echo "COMMIT_SHORT_SHA=$short" >> $GITHUB_ENV
- name: Get App Version
id: appversion
env:
APP_VERSION: ${{ format('pr{0}-{1}', github.event.number, env.COMMIT_SHORT_SHA ) }}
run: |
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
- name: Login to Docker Hub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
# metadata extract for docker labels/image names is done in merge job
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
# for caching
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
# specify foreign repo#sha from PR that is used to build image
context: ${{github.event.pull_request.head.repo.clone_url}}#${{github.event.pull_request.head.sha}}
build-args: |
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
release-snapshot:
name: Merge OCI Images and Push
if: ${{ vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '' }}
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
needs:
- build-snapshot
- test
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: |
foxxmd/multi-scrobbler
ghcr.io/foxxmd/multi-scrobbler
${{ vars.DOCKERHUB_SLUG }}
${{ vars.GHCR_SLUG }}
# generate Docker tags based on the following events/attributes
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
tags: |
type=ref,event=pr,suffix=${{ matrix.suffix }}
type=ref,event=pr
flavor: |
latest=false
labels: |
org.opencontainers.image.title=Multi-Scrobbler
org.opencontainers.image.description=Scrobble from many sources to many clients
org.opencontainers.image.vendor=FoxxMD
- name: Create manifest list and push dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.DOCKERHUB_SLUG }}@sha256:%s ' *)
- name: Create manifest list and push gchr
if: ${{ vars.GHCR_SLUG != '' }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.GHCR_SLUG }}@sha256:%s ' *)
- name: Inspect image dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Inspect image ghcr
if: ${{ vars.GHCR_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
env:
APP_VERSION: ${{ format('pr{0}-{1}', github.event.number, github.event.pull_request.head.sha ) }}
with:
context: .
build-args: |
APP_BUILD_VERSION=${{env.APP_VERSION}}
file: ${{ matrix.dockerfile }}
push: ${{ !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platforms }}
combine-and-comment:
name: Leave comment
@@ -115,7 +216,6 @@ jobs:
To play around with this PR, pull an image:
* `foxxmd/multi-scrobbler:pr-${{ github.event.number }}`
* `foxxmd/multi-scrobbler:pr-${{ github.event.number }}-alpine`.
Images are available for x86_64 and ARM64.
+168 -67
View File
@@ -1,68 +1,77 @@
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
name: Publish Docker image to Dockerhub
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- 'master'
- 'develop'
tags:
- '*.*.*'
# don't trigger if just updating docs
paths-ignore:
- '**.md'
- 'README.md'
- '.github/**'
- 'flatpak/**'
# use release instead of tags once version is correctly parsed
# https://github.com/docker/metadata-action/issues/422
# https://github.com/docker/metadata-action/issues/240
# release:
# types: [ published ]
# define in GH Repository -> Actions -> Variables (or act .variables) to enable pushing to registries
# -- will only push to registries that are defined
# EX
# DOCKERHUB_SLUG=foxxmd/multi-scrobbler
# GHCR_SLUG=ghcr.io/foxxmd/multi-scrobbler
jobs:
test:
name: Run Tests
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'npm'
- name: Install dev dependencies
run: npm ci
- name: Build Backend
run: 'npm run build:backend'
- name: Test Backend
run: npm run test
uses: ./.github/workflows/testAndSanity.yml
push_to_registry:
name: Build and push container images
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
build:
name: Build OCI Images
if: ${{ github.event_name != 'pull_request' && (vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '') }}
needs: test
runs-on: ${{ matrix.os }}
permissions:
packages: write
contents: read
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./Dockerfile
suffix: ''
platforms: 'linux/amd64,linux/arm64'
- dockerfile: ./alpine.Dockerfile
suffix: '-alpine'
platforms: 'linux/amd64,linux/arm64'
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
permissions:
packages: write
contents: read
- os: ubuntu-latest
arch: amd64
platform: linux/amd64
- os: ubuntu-24.04-arm
arch: arm64
platform: linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
# list all registries to push to and join all non-empty with comma
# https://unix.stackexchange.com/a/693165/116849
# https://stackoverflow.com/a/9429887/1469797
strings=("${{vars.DOCKERHUB_SLUG}}" "${{vars.GHCR_SLUG}}")
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
joined_string=$(IFS=, ; echo "${strings[*]}")
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v4
- name: Set short git commit SHA
id: vars
id: appvars
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
# short sha available under env.COMMIT_SHORT_SHA
run: |
@@ -71,57 +80,149 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
- name: Log in to Docker Hub
- name: Get App Version
id: appversion
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
run: |
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
# metadata extract for docker labels/image names is done in merge job
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
# for caching
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
build-args: |
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Merge OCI Images and Push
if: ${{ github.event_name != 'pull_request' && (vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '') }}
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
needs:
- build
- test
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: |
foxxmd/multi-scrobbler
ghcr.io/foxxmd/multi-scrobbler
${{ vars.DOCKERHUB_SLUG }}
${{ vars.GHCR_SLUG }}
# generate Docker tags based on the following events/attributes
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
tags: |
type=raw,value=latest,enable={{is_default_branch}},suffix=${{ matrix.suffix }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }},suffix=${{ matrix.suffix }}
type=semver,pattern={{version}},suffix=${{ matrix.suffix }}
flavor: |
latest=false
type=edge
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# push with branch name as tag if not master/main
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
type=match,pattern=\d.\d.\d$,priority=901
# tag all semver (include pre-release)
type=semver,pattern={{version}}
# flavor: |
# latest=false
labels: |
org.opencontainers.image.title=Multi-Scrobbler
org.opencontainers.image.description=Scrobble from many sources to many clients
org.opencontainers.image.vendor=FoxxMD
- name: Create manifest list and push dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.DOCKERHUB_SLUG }}@sha256:%s ' *)
- name: Create manifest list and push gchr
if: ${{ vars.GHCR_SLUG != '' }}
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ vars.GHCR_SLUG }}@sha256:%s ' *)
- name: Inspect image dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
uses: docker/build-push-action@v5
with:
context: .
# https://github.com/docker/build-push-action/issues/1026#issue-2041857786
build-args: |
APP_BUILD_VERSION=${{env.APP_VERSION}}
file: ${{ matrix.dockerfile }}
push: ${{ !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platforms }}
- name: Inspect image ghcr
if: ${{ vars.GHCR_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
+65
View File
@@ -0,0 +1,65 @@
name: Tests and Sanity Run
on:
workflow_call:
inputs:
node-version:
description: "Node version"
required: false
default: '20.x'
type: string
ref:
description: "Ref to checkout"
required: false
type: string
jobs:
test:
name: Tests / Build / Sanity Run
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install concurrently
run: npm install -g concurrently
- name: Install dev dependencies
run: npm run install:parallel
- name: Test Backend
run: npm run test
- name: Build
run: npm run build:parallel
# remove modules that might include dev stuff
# so that in the next step we are sure that prod-only runs work correctly
- name: Install Prod Deps
run: |
rm -rf node_modules && \
rm -rf docsite/node_modules && \
NODE_ENV=production npm ci --omit=dev
# run app for 10 seconds as sanity check to see if it errors for any reason
# easy testcase for missing packages and init errors
- name: Sanity Run
run: |
set +e
export NODE_ENV=production
timeout --preserve-status 10s node node_modules/.bin/tsx src/backend/index.ts
exitcode="$?"
if [[ "$exitcode" -eq 143 ]] || [[ "$exitcode" -eq 137 ]]; then
echo "App stayed up long enough and exited with expected status"
exit 0
else
echo "App exited with unexpected code $exitcode"
exit "$exitcode"
fi
+4
View File
@@ -118,6 +118,7 @@ dist
.pnp.*
config/*.json
config/yti-*
*.txt
.idea/
@@ -132,3 +133,6 @@ flatpak/generated-sources.json
build
!setupProxy.js
**/generated-sources.*
.github/act/out
tmp-*
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "https://lnav.org/schemas/format-v1.schema.json",
"error_log": {
"title": "Multi-Scrobbler Log",
"description": "Logs from Multi Scrobbler",
"regex": {
"std": {
"pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?)(?:.*?)\\] (?<level>[A-Z]*) : (?<body>.*)"
}
},
"level-field": "level",
"level": {
"info": "^(?i)(?:info)$",
"debug": "^(?i)(?:debug)$",
"error": "^(?i)(error)$",
"warning": "^(?i)(warn)$",
"trace": "^(?i)verbose$",
"critical": "^(?i)(?:crit|Cr)$",
"fatal": "^(?i)(?:alert|fatal|panic|Al|Em)$"
},
"sample": [
{
"line": "[2024-12-26 01:58:48.261 -0500] DEBUG : [App] [Sources] [Jellyfin - unnamed] Last activity was at 01:58:48-05:00 | Next check in 10.00s | No new tracks discovered",
"level": "debug"
}
]
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
{
"reporter": "dot",
"extension": "ts",
"import": "tsx/esm"
"import": "tsx/esm",
"spec": "./src/backend/tests/**/*.test.ts",
"exit": true
}
+123
View File
@@ -0,0 +1,123 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "dev",
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/index.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "dev (ENV Only)",
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/index.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"env": {
"CONFIG_DIR": "./not-real"
},
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "tsx",
"type": "node",
"request": "launch",
// Debug current file in VSCode
"program": "${file}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "schema",
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/utils/SchemaStaticUtil.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
//"${workspaceFolder}/node_modules/**",
],
"resolveSourceMapLocations": [
"${workspaceFolder}/src/**/*",
"${workspaceFolder}/node_modules/**/*"
]
},
{
"args": [
"-u",
"bdd",
"--timeout",
"999999",
"--reporter",
"spec",
"--colors",
"--config",
"${workspaceRoot}/.mocharc.json",
"--recursive",
"${workspaceFolder}/src/backend/tests/**/*.test.ts"
],
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"internalConsoleOptions": "openOnSessionStart",
"name": "Mocha Tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"request": "launch",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
"type": "node"
},
{
"args": [
"-u",
"bdd",
"--timeout",
"999999",
"--reporter",
"spec",
"--colors",
"--config", "${workspaceRoot}/.mocharc.json",
"${file}"
],
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"internalConsoleOptions": "openOnSessionStart",
"name": "Mocha Test on File",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"request": "launch",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
"type": "node"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"mochaExplorer.require": "tsx/esm",
"mochaExplorer.timeout": 1200000,
"mochaExplorer.exit": true
}
+37
View File
@@ -0,0 +1,37 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "src/backend/tsconfig.json",
"problemMatcher": [
"$tsc"
],
"group": {
"kind": "build",
"isDefault": true
},
"label": "tsc: build - src/backend/tsconfig.json"
},
{
"type": "shell",
"label": "compose-up-dev",
"command": "docker-compose -f docker-compose.dev.yml up",
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"type": "shell",
"label": "compose-build-dev",
"command": "docker-compose -f docker-compose.dev.yml build",
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
}
]
}
+1 -2
View File
@@ -2,7 +2,6 @@
Please follow these guidelines when contributing code to this repository:
* The PR **must be for the [`develop` branch.](https://github.com/FoxxMD/multi-scrobbler/tree/develop)** The `master` branch is for releases only.
* Use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format when creating commits.
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `develop`.
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `master`.
* Ensure that if your code is covered by [an existing test](./src/backend/tests) that you have updated the test accordingly
+16 -13
View File
@@ -1,7 +1,7 @@
FROM ghcr.io/linuxserver/baseimage-debian:bookworm as base
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
ENV TZ=Etc/GMT
ENV NODE_VERSION 20.11.1
ENV NODE_VERSION=20.18.1
# borrowing openssl header removal trick from offical docker-node
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
@@ -49,6 +49,8 @@ RUN \
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN npm install -g concurrently
ARG data_dir=/config
VOLUME $data_dir
ENV CONFIG_DIR=$data_dir
@@ -57,10 +59,11 @@ COPY docker/root /
WORKDIR /app
FROM base as build
FROM base AS build
COPY --chown=abc:abc package*.json tsconfig.json ./
COPY --chown=abc:abc patches ./patches
COPY --chown=abc:abc docsite/package*.json tsconfig.json ./docsite/
# for debugging, so the build fails faster when timing out (arm64)
#RUN npm config set fetch-retries 1 && \
@@ -74,24 +77,24 @@ COPY --chown=abc:abc patches ./patches
# This FAILED for node < 20 when building arm64 but not amd64 (and alpine-based Dockerfile has no issues building arm64)
# see https://github.com/FoxxMD/multi-scrobbler/issues/126
RUN npm ci \
--verbose \
# --no-audit \
&& chown -R root:root node_modules
RUN npm run install:parallel \
&& chown -R root:root node_modules \
&& chown -R root:root docsite/node_modules
COPY --chown=abc:abc . /app
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run build && rm -rf node_modules
RUN npm run build:parallel && rm -rf node_modules && rm -rf docsite/node_modules
FROM base as app
FROM base AS app
COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc *.json *.js *.ts index.html ./
COPY --chown=abc:abc patches ./patches
COPY --from=build --chown=abc:abc /app/dist /app/dist
COPY --from=build --chown=abc:abc /app/src /app/src
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
COPY --from=base /usr/bin /usr/bin
COPY --from=base /usr/lib /usr/lib
@@ -102,11 +105,11 @@ ENV IS_DOCKER=true
ARG APP_BUILD_VERSION
ENV APP_VERSION=$APP_BUILD_VERSION
RUN npm ci --omit=dev \
RUN npm ci --omit=dev --no-audit \
&& npm cache clean --force \
&& chown -R abc:abc node_modules \
&& rm -rf node_modules/@types \
/root/.cache
&& rm -rf /root/.cache
ARG webPort=9078
ENV PORT=$webPort
+49 -26
View File
@@ -3,6 +3,8 @@
[![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)](https://github.com/FoxxMD/multi-scrobbler/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)](https://hub.docker.com/r/foxxmd/multi-scrobbler)
[![Docs](https://img.shields.io/badge/Read%20The%20Docs-1082c2)](https://foxxmd.github.io/multi-scrobbler/)
<img src="/assets/icon.png" align="right"
alt="multi-scrobbler logo" width="180" height="180">
@@ -10,32 +12,47 @@ alt="multi-scrobbler logo" width="180" height="180">
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
* Supports scrobbling from many **Sources**
* [Spotify](/docsite/docs/configuration/configuration.md#spotify)
* [Plex](/docsite/docs/configuration/configuration.md#plex) or [Tautulli](/docsite/docs/configuration/configuration.md#tautulli)
* [Subsonic-compatible APIs](/docsite/docs/configuration/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
* [WebScrobbler](/docsite/docs/configuration/configuration.md#webscrobbler)
* [Youtube Music](/docsite/docs/configuration/configuration.md#youtube-music)
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm-source)
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz--source-)
* [Deezer](/docsite/docs/configuration/configuration.md#deezer)
* [MPRIS (Linux Desktop)](/docsite/docs/configuration/configuration.md#mpris)
* [Mopidy](/docsite/docs/configuration/configuration.md#mopidy)
* [JRiver](/docsite/docs/configuration/configuration.md#jriver)
* [Kodi](/docsite/docs/configuration/configuration.md#kodi)
* [Google Cast (Chromecast)](/docsite/docs/configuration/configuration.md#google-cast--chromecast-)
* [Musikcube](/docsite/docs/configuration/configuration.md#muikcube)
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#azuracast)
* [Deezer](https://foxxmd.github.io/multi-scrobbler/docs/configuration#deezer)
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#google-cast-chromecast)
* [Icecast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#icecast)
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jellyfin)
* [JRiver](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jriver)
* [Kodi](https://foxxmd.github.io/multi-scrobbler/docs/configuration#kodi)
* [Koito](https://foxxmd.github.io/multi-scrobbler/docs/configuration#koito-source)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-source)
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-endpoint)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-source)
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-endpoint)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/docs/configuration#maloja-source)
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mopidy)
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpd-music-player-daemon)
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris)
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/docs/configuration#muikcube)
* [Plex](https://foxxmd.github.io/multi-scrobbler/docs/configuration#plex) or [~~Tautulli~~](https://foxxmd.github.io/multi-scrobbler/docs/configuration#tautulli)
* [Spotify](https://foxxmd.github.io/multi-scrobbler/docs/configuration#spotify)
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webscrobbler)
* [VLC](https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc)
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#yamaha-musiccast)
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/docs/configuration#youtube-music)
* Supports scrobbling to many **Clients**
* [Maloja](/docsite/docs/configuration/configuration.md#maloja)
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm)
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
* [Koito](https://foxxmd.github.io/multi-scrobbler/docs/configuration#koito)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/docs/configuration#maloja)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webhook-configurations) or [healthcheck endpoint](https://foxxmd.github.io/multi-scrobbler/docs/configuration#health-endpoint)
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
* Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/docs/configuration/transforms)
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/docs/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/docs/installation#nodejs)
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
<img src="/assets/status-ui.png" width="800">
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -46,9 +63,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
<img src="/assets/status-ui.png" width="800">
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
@@ -73,13 +88,17 @@ Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Quick Start
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
## Installation
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/docs/installation)
## Configuration
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration)
## Usage
@@ -94,7 +113,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
## Help/FAQ
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/docs/development/dev-common)
## License
+2 -1
View File
@@ -40,7 +40,7 @@ COPY --chown=abc:abc . /app
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run build && rm -rf node_modules
RUN npm run docs:install && npm run build && rm -rf node_modules && rm -rf docsite/node_modules
FROM base as app
@@ -49,6 +49,7 @@ COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc patches ./patches
COPY --from=build --chown=abc:abc /app/dist /app/dist
COPY --from=build --chown=abc:abc /app/src /app/src
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
COPY --from=base /usr/local/bin /usr/local/bin
COPY --from=base /usr/local/lib /usr/local/lib
-217
View File
@@ -1,217 +0,0 @@
import {API, Collection, FileInfo, Options} from 'jscodeshift';
import path from 'path';
import fs from 'fs';
export type TransformFrom = 'none' | 'any' | string;
export type TransformTo = 'none' | 'any' | string;
export type ImportTypes = 'any' | 'relative' | 'alias';
const base = process.cwd();
export default function transformer(
file: FileInfo,
{ jscodeshift: j }: API,
options: Options,
) {
const tf = options.transformFrom as string | undefined;
if(tf === undefined) {
throw new Error(`arg '--transformFrom' must be defined.`);
}
const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[];
const tfIsAny = transformFrom.includes('any');
const tt = options.transformTo as string | undefined;
if(tt === undefined) {
throw new Error(`arg '--transformTo' must be defined.`);
}
const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[];
if(transformTo.length > 1 && transformFrom.length !== transformTo.length) {
throw new Error('When more than one Transform To is specified then number of Transform From arguments must match');
}
const itypes = options.importTypes as string | undefined;
if(itypes === undefined) {
throw new Error(`arg '--importTypes' must be defined.`);
}
const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[];
const importsIsAny = importTypes.includes('any');
const fileDir = path.dirname(path.join(base, file.path));
const processingFilePath = file.path;
console.log(`Processing => ${processingFilePath}`);
let source: Collection<any>;
try {
source = j(file.source);
} catch (e) {
console.error(`Failed to parse ${processingFilePath}, will skip`);
console.error(e);
}
const imports = source.find(j.ImportDeclaration)
imports.forEach((x) => {
const importPath = x.value.source.value as string;
const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`;
const pathInfo = path.parse(importPath);
const hasNoExt = pathInfo.ext === '';
let filenameFromDir: string;
if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) {
const pathFull = path.join(fileDir, importPath);
const dir = path.dirname(pathFull);
const normalExt = pathInfo.ext.replace('.', '');
let isRelative: boolean | undefined;
let dirExists: boolean | undefined;
let fileExists: boolean | undefined;
try {
// is dir path real?
fs.realpathSync(dir);
dirExists = true;
} catch (e) {
isRelative = false;
dirExists = false;
}
if(isRelative === undefined) {
// if extension is none we need to check for any file in dir with this name
if(hasNoExt) {
const dirFiles = fs.readdirSync(dir)
filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name);
if(filenameFromDir === undefined) {
fileExists = false;
isRelative = false;
} else {
fileExists = true;
isRelative = true;
}
} else {
try {
// does a file exist?
fs.realpathSync(pathFull);
isRelative = true;
} catch (e) {
isRelative = false;
fileExists = false;
}
}
}
if(!importsIsAny) {
if(!isRelative && !importTypes.includes('alias')) {
console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`);
return;
} else if(isRelative && !importTypes.includes('relative')) {
console.log(`${pathFull} => Import is relative but import types does not specify relative`);
return;
}
}
// determine transformTo
// if there is only one TO then use it
let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined;
if(derivedTT === undefined) {
// otherwise we find the TO by using the same index as the matching FROM extension type
const tfIndex = transformFrom.findIndex((x) => {
if(x === 'any') {
return true;
}
if(x === 'none' && hasNoExt) {
return true;
}
if(x === normalExt) {
return true;
}
});
if(tfIndex === -1) {
console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`);
return;
}
derivedTT = transformTo[tfIndex];
}
let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`;
let transformedImport: string;
switch(derivedTT) {
case 'none':
if(hasNoExt) {
console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`);
return;
}
transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name);
break;
case 'any':
if(hasNoExt && filenameFromDir) {
transformedImport = filenameFromDir;
} else {
const otherFilename = fs.readdirSync(dir).find(x => {
const pinfo = path.parse(x);
return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext;
});
if(otherFilename === undefined) {
console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
return;
}
transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename);
transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})`
}
break;
default:
transformedImport = combineDirFile(pathInfo.dir, `${path.parse(filenameFromDir).name}.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`);
break;
}
console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`);
j(x).replaceWith(
j.importDeclaration(
x.node.specifiers,
j.stringLiteral(transformedImport)
)
);
} else {
console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`);
return;
}
});
/**
* Early exit condition
* -----
* It is often good practice to exit early and return the original source file
* if it does not contain code relevant to the codemod.
* See this page for more information:
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output
*/
// if (/* Some condition here */ true) {
// return file.source;
// }
/**
* Codemod logic goes here 👇
* -----
* This is where the core logic for your codemod will go,
* consider grouping specific actions into 'motions' and running them in sequence
*
* See this page for more information:
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions
*/
//source.findVariableDeclarators('foo').renameTo('bar');
/**
* Return your modified AST here 👇
* -----
* This is where your modified AST will be transformed back into a string
* and written back to the file.
*/
return source.toSource(options.printOptions);
}
const combineDirFile = (dir: string, file: string) => {
if(dir === '') {
return file;
}
return `${dir}${path.sep}${file}`;
}
+1 -1
View File
@@ -8,5 +8,5 @@ These are **NOT** exhaustive examples. You should consult the **configuration**
Documentation at
* [internal docs](../docsite/docs/configuration/configuration.md)
* [internal docs](../docsite/docs/configuration/configuration.mdx)
* External Link: https://foxxmd.github.io/multi-scrobbler/docs/configuration
+13
View File
@@ -0,0 +1,13 @@
[
{
"type": "azuracast",
"enable": true,
"name": "azura",
"data": {
"url": "ws://192.168.0.101",
"station": "my-station-name",
"monitorWhenLive": true,
"monitorWhenListeners": 1
}
}
]
+13
View File
@@ -0,0 +1,13 @@
[
{
"name": "DeezerOfficalDeprecated",
"enable": true,
"clients": [],
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/deezer/callback",
"interval": 60
}
}
]
+2 -5
View File
@@ -1,13 +1,10 @@
[
{
"name": "FoxxMDeezer",
"name": "DeezerARL",
"enable": true,
"clients": [],
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/deezer/callback",
"interval": 60
"arl": "UOsRPjT3U5Dhaaup3xQ30DWFoSgqugdSYzL3QE743waaVy2eur2ckyJcZ4RV123Q3PsbKXkLwA6ahc9XmEfev6BtfSP8LujE3JswWRQogZCDUOwVzWO5sAig",
}
}
]
+9
View File
@@ -0,0 +1,9 @@
[
{
"name": "myLfm",
"enable": true,
"data": {
"slug": "mySlug"
}
}
]
+9
View File
@@ -0,0 +1,9 @@
[
{
"name": "myLz",
"enable": true,
"data": {
"token": "myToken"
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "myIcy",
"enable": true,
"data": {
"url": "http://myIcecast.com/radio"
}
}
]
+15 -4
View File
@@ -4,12 +4,23 @@
"enable": true,
"clients": [],
"data": {
"users": ["FoxxMD"],
"servers": ["myServer","anotherServer"]
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
// everything below is optional
"usersAllow": ["FoxxMD","SomeOtherUser"],
"usersBlock": ["AnotherUser"],
"devicesAllow": ["firefox"],
"devicesBlock": ["google-home"],
"librariesAllow": ["GoodMusic"],
"librariesBlock": ["BadMusic"],
"additionalAllowedLibraryTypes": ["musicvideos"],
"allowUnknown": false,
},
"options": {
"logPayload": false,
"logFilterFailure": "warn"
"logPayload": true,
"logFilterFailure": "debug"
}
}
]
+20
View File
@@ -0,0 +1,20 @@
[
{
"name": "koito-client",
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
},
{
"name": "koito-source",
"configureAs": "source",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
}
]
+11 -1
View File
@@ -1,6 +1,6 @@
[
{
"name": "myLastFm",
"name": "myLastFmClient",
"enable": true,
"configureAs": "client",
"data": {
@@ -8,5 +8,15 @@
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
},
{
"name": "myLastFmSource",
"enable": true,
"configureAs": "source",
"data": {
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
+10 -1
View File
@@ -1,11 +1,20 @@
[
{
"name": "brainz",
"name": "brainzClient",
"enable": true,
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
}
},
{
"name": "brainzSource",
"enable": true,
"configureAs": "source",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
}
}
]
+11 -1
View File
@@ -1,7 +1,17 @@
[
{
"name": "myMaloja",
"name": "myMaloja-client",
"enable": true,
"configureAs": "client",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
}
},
{
"name": "myMaloja-source",
"enable": true,
"configureAs": "source",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
+13
View File
@@ -0,0 +1,13 @@
[
{
"enable": true,
"name": "MyMPD",
"data": {
"url": "192.168.0.100:6600",
"password": "MY_PASSWORD"
},
"options": {
"disableDiscovery": false
}
}
]
+9
View File
@@ -0,0 +1,9 @@
[
{
"name": "myYamaha",
"enable": true,
"data": {
"url": "192.168.0.101"
}
}
]
+1
View File
@@ -4,6 +4,7 @@
"enable": true,
"name": "musikcube",
"data": {
"url": "ws://localhost:7905",
"password": "MY_PASSWORD"
}
}
+13 -7
View File
@@ -1,15 +1,21 @@
[
{
"name": "MyPlex",
"name": "MyPlexApi",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
"token": "1234",
"url": "http://192.168.0.120:32400",
"usersAllow": ["FoxxMD","SomeOtherUser"],
"usersBlock": ["AnotherUser"],
"devicesAllow": ["firefox"],
"devicesBlock": ["google-home"],
"librariesAllow": ["GoodMusic"],
"librariesBlock": ["BadMusic"]
},
"options": {
"logPayload": true,
"logFilterFailure": "debug"
}
}
}
]
+17
View File
@@ -0,0 +1,17 @@
[
// DEPRECATED, use API Source instead
// rename files to plex.json to use
{
"name": "MyPlex",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+2 -1
View File
@@ -5,7 +5,8 @@
"data": {
"url": "http://localhost:4040/airsonic",
"user": "yourUser",
"password": "yourPassword"
"password": "yourPassword",
"usersAllow": ["yourUser"]
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"enable": true,
"name": "MyVlc",
"data": {
"url": "localhost:8080",
"password": "changeme"
}
}
]
+1 -1
View File
@@ -2,7 +2,7 @@
{
"name": "MyWebScrobbler",
"data": {
"slug": null,
"slug": "MyOptionalSlug",
"whitelist": [],
"blacklist": []
}
+10 -2
View File
@@ -4,8 +4,16 @@
"enable": true,
"clients": [],
"data": {
"cookie": "VISITOR_INFO1_LIVE=jMDXz2_L8rY; __Secure-3PAPISID=3AxsXpSXGqOInSDn1jEKn; DEVICE_INFO=ChxOekU0TmTBpjek5EWZ0G; YSC=7gZdl3Zdl3; SID=TwhNsaZRXYTAtXxzGyu6rZdpg2HvGROeW8J4Ym_FhkhoZMUYEQ.; __Secure-1PSID=TwhNOsaZRXYTyRBe4rxAtXRIKsIEtk_Qot2VLBNfHQrQ.; __Secure-3PSID=ZRXYTAtXRIKsIEtk_Qot2yRBerZdpg2HvvZRXYTAtXRIKsIEtk_Qot2yRBerkuZICFQ.; HSID=A1UMmELW79; SSID=AKhomOs; APISID=IlHHmuzkPdQzZZDhHn3; SAPISID=3AxsXpy0u75Qb/n1jEKn; __Secure-1PAPISID=3AxsXpQb/AkSDn1jEKn; LOGIN_INFO=AFmP6vFpyVCZZAIgDwbkhWMBBhluaIWAPP:QUQ314UW5NWMjNmd2ZUJnYnJsakdIMjZoaE5zVVMjNmd2ZZUiHRlb3ZlV3ZIcUVyRVIMjNmdjNmd2ZZUivYlNqX2ZNZUiHdUNFNFdaYmJIW1NkJRX3hqdlU2YnFESkFuSS1uTldnZVRmLXNjWFc5OUJuR3dTd3JsZGZYa2EtZFQ2a0k2Ry1KQQ==; PREF=volume=26; SIDCC=AFvI_94PxXwls-ndqpGfPgFX3FWj80y_94PxXwls-ndqfSh15sP; __Secure-1PSIDCC=AFvIBnUbRr96I96UCIp2U4T8HRVk2B0HfKzhzxwsiP; __Secure-3PSIDCC=AFvIB3bINuUN0ETDR9gO91wpwWIVmpGki3BxT3bINuUN0ETDR9gO91wCH",
"authUser": "0"
"cookie": "__Secure-1PSIDTS=sidts-CjEB3EgAEvCd-......",
// either cookie or id/secret needs to be provided
"clientId": "891098404....apps.googleusercontent.com",
"clientSecret": "GOCS..."
// optional
//"redirectUri": "http://my.custom.tld/api/ytmusic/callback?name=MyYTMusic"
},
"options": {
"logDiff": true
}
}
]
+29
View File
@@ -0,0 +1,29 @@
version: '3.7'
services:
multi-scrobbler:
build:
context: .
env_file:
- path: ./.env
required: false
#environment:
#- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#- SPOTIFY_CLIENT_ID=
#- SPOTIFY_CLIENT_SECRET=
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
#- MALOJA_URL=http://maloja:42010
#- MALOJA_API_KEY=
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
# set if using a source/client with redirect URI that you have not explicitly set and MS is NOT running on the same machine that you will view the dashboard from
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
#- BASE_URL=http://MyHostIP:9078
volumes:
- "./config:/config"
#networks:
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
ports:
- 9078:9078 # first port is the HOST port multi-scrobbler will serve UI and API on
restart: unless-stopped
+4 -6
View File
@@ -1,14 +1,12 @@
version: '3.7'
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#- SPOTIFY_CLIENT_ID=
#- SPOTIFY_CLIENT_SECRET=
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
#- JELLYFIN_APIKEY=
#- JELLYFIN_URL=
#- JELLYFIN_USER=
#- MALOJA_URL=http://maloja:42010
#- MALOJA_API_KEY=
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
@@ -18,7 +16,7 @@ services:
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
#- BASE_URL=http://MyHostIP:9078
volumes:
- /path_on_host/multi-scrobbler-config:/config
- "./config:/config"
#networks:
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
ports:
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
+144 -106
View File
@@ -1,123 +1,53 @@
<!-- TOC -->
* [Connection Issues](#connection-issues)
* [Plex/Tautulli/Jellyfin don't connect](#plextautullijellyfin-dont-connect)
* [Jellyfin has warnings about undefined or missing data](#jellyfin-has-warnings-about-undefined-or-missing-data)
* [Jellyfin has warnings about missing headers](#jellyfin-has-warnings-about-missing-headers)
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
* [Configuration Issues](#configuration-issues)
* [Config could not be parsed](#config-could-not-be-parsed)
* [Scrobbling Issues](#scrobbling-issues)
* [Last.fm does not scrobble tracks with multiple artists correctly](#lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly)
* [Jellyfin does not scrobble tracks with multiple artists correctly](#jellyfin-does-not-scrobble-tracks-with-multiple-artists-correctly)
* [Google Cast track information is missing/incorrect or MS player has weird times](#google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times)
* [Google Cast device does not track media](#google-cast-device-does-not-track-media)
<!-- TOC -->
---
toc_min_heading_level: 2
toc_max_heading_level: 5
---
# Connection Issues
## Connection Issues
## Plex/Tautulli/Jellyfin don't connect
### Webscrobbler/Listenbrainz Endpoint doesn't connect {#ingress-connection}
These three [sources](/#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
These sources are **ingress-based** which means that multi-scrobbler waits for the Webscrobbler extension or app using Listenbrainz Endpoint to contact multi-scrobbler, as opposed to multi-scrobbler contacting them.
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:
```
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] Received request from a new remote address: ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59)
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) Received valid data from server examplePlex for the first time.
2023-02-22T10:55:56-05:00 warn : [Plex Request ] Received valid Plex webhook payload but no Plex sources are configured
[2025-07-10 12:32:21.417 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] Received request from a new remote address: ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0)
[2025-07-10 12:32:21.418 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0) Received data for API URL slug '(none)' for the first time.
```
It also logs if a server tries to connect to a URL that it does not recognize:
```
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.100 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
```
**So, if you do not see either of these in your logs then Plex/Tautulli/Jellyfin is not able to connect to your multi-scrobbler instance at all.**
**So, if you do not see either of these in your logs then the Source is not able to connect to your multi-scrobbler instance at all.**
This is not something multi-scrobbler can fix and means you have an issue in your network.
### Troubleshooting
#### Troubleshooting
Check or try all these steps before submitting an issue:
#### Turn on Debug Logging
##### Turn on Debug Logging
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `LOG_LEVEL=debug`:
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `DEBUG_MODE=true`. Then, check the output for any additional information.
* using node `LOG_LEVEL=debug ... node src/index.js`
* using docker `docker run -e LOG_LEVEL=debug ... foxxmd/multi-scrobbler`
##### Check Host name and URL
Check the output for any additional information.
The URLs examples in the [configuration](configuration/configuration.mdx) documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
#### Check Host name and URL
EX `http://localhost:9078/api/webscrobbler` -> `http://192.168.0.140:9078/api/webscrobbler`
The URLs examples in the [configuration](configuration/configuration.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
#### Check Firewall and Port Forwarding
##### Check Firewall and Port Forwarding
If the machine multi-scrobbler is running on has a firewall ensure that port **9078** is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine.
#### Check Source Service Logs
##### Check Source Service Logs
Plex/Tautulli/Jellyfin all have logs that will log if they cannot connect to multi-scrobbler. Check these for further information.
###### Webscrobbler
##### Plex
See [Debugging the extension](https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension) to get logs which should have information about failed requests.
Settings -> Manage -> Console
##### Tautulli
Check the command-line output of the application or docker logs.
##### Jellyfin
Administration -> Dashboard -> Advanced -> Logs
## Jellyfin has warnings about undefined or missing data
Make sure you have
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
You can verify the payload sent from the webhook by modifying your jellyfin configuration to include `logPayload: true` which will output the raw payload to DEBUG level logging:
```json
[
{
"name": "MyJellyfin",
"clients": [],
"data": {
"users": ["FoxxMD"],
"options": {
"logPayload": true
}
}
}
]
```
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
## Jellyfin has warnings about missing headers
If you experience issues trying to scrobble with Jellyfin and find this in your MS logs
```
[API] Jellyfin is not sending a request with valid headers...
```
A workaround that may fix this:
* In Webhook settings:
* [In the webhook you have already configured...](configuration/configuration.md#jellyfin)
* Add Request Header...
* **Key:** `Content-Type`
* **Value:** `application/json`
* Then Save
## Spotify/Deezer/LastFM won't authenticate
### Spotify/Deezer/LastFM won't authenticate
Ensure any **client id** or **secrets** are correct in your configuration.
@@ -130,9 +60,31 @@ If multi-scrobbler is not running on the same machine your browser is on then th
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
# Configuration Issues
### Youtube Music fails after some time
## Config could not be parsed
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
```
Error: Could not send the specified request to browse. Status code: 401
```
then YTM has invalidated your authentication.
First, ensure you are NOT using [YoutubeTV authentication.](configuration/configuration.mdx?ytmAuth=ytt#youtube-music) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
#### When using OAuth Client Authentication
Refresh your authentication by using the **(Re)authenticate** link from MS's web dashboard.
#### When using Cookies Authentication
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](configuration/configuration.mdx?ytmAuth=cookie#youtube-music) and then restart MS to potentially resolve the problem.
## Configuration Issues
### Config could not be parsed
If you see something like this in your logs:
@@ -144,25 +96,21 @@ If you see something like this in your logs:
It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like [JSONLint](https://jsonlint.com/) to find out where errors you have and fix them.
# Scrobbling Issues
## Scrobbling Issues
## Last.fm does not scrobble tracks with multiple artists correctly
### Last.fm does not scrobble tracks with multiple artists correctly
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
## Jellyfin does not scrobble tracks with multiple artists correctly
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
## Google Cast track information is missing/incorrect or MS player has weird times
### Google Cast track information is missing/incorrect or MS player has weird times
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
To diagnose these issues you [**must enable payload logging**](configuration/configuration.md#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
To diagnose these issues you [**must enable payload logging**](configuration/configuration.mdx#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
## Google Cast device does not track media
### Google Cast device does not track media
It is likely the app playing on the cast device is incorrectly reporting the media type as **not music**.
@@ -172,10 +120,100 @@ MS logs will tell you what type the media is reported as with lines like:
My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track
```
Refer to [Allow Unknown Media Type](configuration/configuration.md#allow-unknown-media-type) section to fix this
Refer to [Allow Unknown Media Type](configuration/configuration.mdx#allow-unknown-media-type) section to fix this
```
My Artist - Example Track has 'movie' media type so will not track
```
Refer to [Force Media Tracking](configuration/configuration.md#forcing-media-tracking) section to fix this
Refer to [Force Media Tracking](configuration/configuration.mdx#forcing-media-tracking) section to fix this
### VLC is not scrobbling fields correctly
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](configuration/configuration.mdx#vlc-information-reporting)
### Youtube Music misses or duplicates scrobbles
<details>
<summary>How MS detects YTM history</summary>
In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs **a source of truth.** For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).
To compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.
Duplicate scrobbles can also occur if the change between two checks is technically consistent. For instance, if you listen to a track twice in some period, separated by other music, YTM will sometimes "remove" the track from the earlier time (further down in your history) and "re-add" it at the top of the history.
</details>
#### Reporting YTM scrobble issues
If you experience scrobble detection issues you can help improve MS's YTM heureistic by providing thorough feedback as [an issue.](https://github.com/FoxxMD/multi-scrobbler/issues/new?assignees=&labels=bug&projects=&template=01-bug-report.yml&title=bug%3A+) **Please do the following to provide the most useful report:**
##### Turn on Change Detection
In your YTM configuration (`ytmusic.json`) add `logDiff` under `options` like this:
```json
{
"type": "ytmusic",
"name": "MyYTM",
"data": { ... },
"options": {
"logDiff": true
}
}
```
or set either ENVs:
* `YTM_LOG_DIFF=true`
* [`DEBUG_MODE=true`](configuration/configuration.mdx?#debug-mode)
This will cause MS to log YTM history changes similar to this:
```
[Ytmusic - MyYTM] Changes from last seen list:
1. (tuhe1CpHRxY) KNOWER - Im The President --- undefined => Moved - Originally at 6
2. (Mtg8V6Xa2nc) Vulfpeck - Romanian Drinking Song --- Schvitz => Moved - Originally at 1
3. (rxbCaiyYSXM) Nightmares On Wax - You Wish --- In A Space Outta Sound => Moved - Originally at 2
4. (tMt_YXr90AM) Gorillaz - O Green World --- undefined => Moved - Originally at 3
...
```
Which are essential to troubleshooting this behavior.
##### Turn on File Logging
Optionally, if your logs are busy due to many other sources, you can log **only** YTM logs to a file. Turn this on with the `logToFile` config option:
```json
{
"type": "ytmusic",
"name": "MyYTM",
"data": { ... },
"options": {
"logDiff": true,
"logToFile": true
}
}
```
This will cause MS to write YTM logs to a file in your `logs` folder named `ytmusic-MyYTM.log` (based on your source name).
##### Provide Detail and Context
Provide a detailed account of how you were using YTM when the issue occurred, including things like:
* the platform listening on (desktop, mobile, 3rd party client, etc...)
* any changes in platform
* > I switched from desktop to listening on my phone...
* how you were listening to music
* > I was playing an album start to finish
* > I listened to two songs in a row, then browsed for a new song in library by artist, then went back to a song in the queue...
Explain the expected behavior (it should have scrobbled songs x, y, then z) and what actually happened (it scrobbled songs x, then y, then x again, then z)
Provide ALL logs from the time when the issue occurred including logs from BEFORE (ideally 2-3 minutes of logs) and AFTER the issue.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Configuration",
"position": 2,
"position": 4,
"link": {
"type": "generated-index",
"description": "Configuring Multi-Scrobbler and examples"
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

+35 -33
View File
@@ -21,9 +21,7 @@ Scenario:
### All-in-one Config
Using just one config file located at `CONFIG_DIR/config.json`:
```json5
```json5 title="CONFIG_DIR/config.json"
{
"sourceDefaults": {
"maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config
@@ -71,7 +69,9 @@ Using just one config file located at `CONFIG_DIR/config.json`:
"name": "fredPlex",
"clients": ["fredMaloja"],
"data": {
"user": ["fred@email.com"]
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["fredUser"]
}
},
{
@@ -79,8 +79,10 @@ Using just one config file located at `CONFIG_DIR/config.json`:
"name": "maryPlex",
"clients": ["maryMaloja"],
"data": {
"user": ["mary@email.com"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
"libraries": ["podcasts"]
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["maryUser"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
"librariesAllow": ["podcasts"]
}
},
{
@@ -88,7 +90,9 @@ Using just one config file located at `CONFIG_DIR/config.json`:
"name": "partyPlex",
// omitting clients (or making it empty) will make this Source scrobble to all Clients
"data": {
"libraries": ["party"],
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"librariesAllow": ["party"]
}
},
{
@@ -96,7 +100,10 @@ Using just one config file located at `CONFIG_DIR/config.json`:
"name": "FredJelly",
// omitting clients (or making it empty) will make this Source scrobble to all Clients
"data": {
"user": ["fred@email.com"]
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
"usersAllow": ["fredUser"]
}
},
{
@@ -115,7 +122,6 @@ Using just one config file located at `CONFIG_DIR/config.json`:
"clients": ["foxxMaloja"],
"data": {
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
"authUser": 1
}
},
],
@@ -160,9 +166,7 @@ Using just one config file located at `CONFIG_DIR/config.json`:
### Separate JSON files
In `CONFIG_DIR/spotify.json`:
```json5
```json5 title="CONFIG_DIR/spotify.json"
[
{
// may omit 'type' property since app knows this is file is for spotify configs
@@ -193,50 +197,53 @@ In `CONFIG_DIR/spotify.json`:
]
```
In `CONFIG_DIR/plex.json`
```json5
```json5 title="CONFIG_DIR/plex.json"
[
{
"name": "fredPlex",
"clients": ["fredMaloja"],
"data": {
"user": ["fred@email.com"]
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["fredUser"]
}
},
{
"name": "maryPlex",
"clients": ["maryMaloja"],
"data": {
"user": ["mary@email.com"],
"libraries": ["podcasts"]
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["maryUser"],
"librariesAllow": ["podcasts"]
}
},
{
"name": "partyPlex",
"data": {
"libraries": ["party"]
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"librariesAllow": ["party"]
}
}
]
```
In `CONFIG_DIR/jellyfin.json`
```json5
```json5 title="CONFIG_DIR/jellyfin.json"
[
{
"name": "FredJelly",
"data": {
"user": ["fred@email.com"]
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
"usersAllow": ["fredUser"]
}
}
]
```
In `CONFIG_DIR/ytmusic.json`
```json5
```json5 title="CONFIG_DIR/ytmusic.json"
[
{
"type": "ytmusic",
@@ -244,15 +251,12 @@ In `CONFIG_DIR/ytmusic.json`
"clients": ["foxxMaloja"],
"data": {
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
"authUser": 1
}
}
]
```
In `CONFIG_DIR/maloja.json`:
```json5
```json5 title="CONFIG_DIR/maloja.json"
[
{
"name": "foxxMaloja",
@@ -278,9 +282,7 @@ In `CONFIG_DIR/maloja.json`:
]
```
In `CONFIG_DIR/lastfm.json`:
```json5
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"name": "maryLFM",
+9 -1
View File
@@ -1,8 +1,16 @@
---
sidebar_position: 2
title: Plex/Tautulli
title: Plex/Tautulli (Deprecated)
---
:::warning[Deprecated]
Multi-scrobbler >= 0.8.8 supports a Plex Source that [directly uses the API](configuration/configuration.mdx?plexType=api#plex) which removes the need to use Tautulli since it does not require Plex Pass.
The Tautulli source **and** Plex Webhook source are deprecated and will be removed in an upcoming release. Existing users should migrate to the API source as soon as possible.
:::
Tracks played on [Plex](https://plex.tv/) can be scrobbled either by:
* A [Tautulli](https://tautulli.com/) notification agent with a webhook.
* Using Plex [Webhooks](https://support.plex.tv/articles/115002267687-webhooks) (restricted to Plex Pass users)
+523
View File
@@ -0,0 +1,523 @@
---
sidebar_position: 4
title: Scrobble Modification
toc_max_heading_level: 4
---
Multi-scrobbler configs support the ability to modify scrobble data in an automated fashion by matching and replacing strings in **title, artists, and album** at many different times in multi-scrobbler's lifecycle.
### Why?
You may need to "clean up" data from a Source or before sending to a scrobble Client due to any number of reasons:
* ID3 tags in your music collection are dirty or have repeating garbage IE `[YourMusicSource.com] My Artist - My Title`
* A Source's service often incorrectly adds data to some field IE `My Artist - My Title (Album Version)` when the title should just be `My Title`
* An Artist you listen to often is spelled different between a Source and a Client which causes duplicate scrobbles
In any scenario where a repeating pattern can be found in the data it would be nice to be able to fix it before the data gets downstream or to help prevent duplicate scrobbling. Multi-scrobbler can help you do this.
## Overview
### Journey of a Scrobble
First, let's recap the lifecycle of a scrobble in multi-scrobbler:
**Sources** are the beginning of the journey for a **Play** (song you've listened to long enough to be scrobblable)
* A Source finds a new valid **Play**
* The Source **compares** this new Play to all the other Plays it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Source **discovers** the Play, adds it to Plays it has seen already, and broadcasts the Play should be scrobbled to all Clients
Scrobble **Clients** listen for discovered Plays from Sources, then...
* A Client receives a **Play** from a Source
* The Client **compares** this Play to all the other scrobbles it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Client **scrobbles** the Play downstream to the scrobble service and adds it as a Scrobble it has seen already
### Lifecyle Hooks
You'll notice there is a pattern above that looks like this:
* **Before** data is compared
* Data is **compared**
* **After** data is compared
These points, during both Source and Client processes, are when you can hook into the scrobble lifecycle and modify it.
#### TLDR
In more concrete terms this is the structure of hooks within a configuration (can be used in any **Source** or **Client**):
```json5 title="lastfm.json" {10-14}
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": {/* ... */},
"compare": {/* ... */},
"postCompare": {/* ... */}
}
}
}
]
```
##### Hook
For **Sources**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if Play was already discovered
* `postCompare` - modify Play data before sending to scrobble **Clients**
For **Clients**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if it was already scrobbled
* `postCompare` - modify Play data before scrobbling it to downstream service and adding to already seen scrobbles
:::tip
Keep in mind that modifying Scrobble/Play data earlier in the lifecycle will affect that data at all times later in the lifecycle.
For example, to modify the track so it's the same anywhere it is processed in multi-scrobbler you only need to modify it in the **Source's** `preCompare` hook because all later processes will receive the data with the modified track.
:::
### Modification Parts
Each [**hook**](#hook) (`preCompare` etc...) is an object that specifies what part of the **Play** to modify:
```json5
{
"title": [/* ... */],
"artists": [/* ... */],
"album": [/* ... */]
}
```
##### Expression
and then a **list** what pattern/replacements (expressions) to use for the modification by using either simple strings or `search-replace` objects:
```json5
[
"badTerm", // remove all instances of 'badTerm'
{
"search": "anotherBadTerm", // and also match all instances of 'anotherBadTerm'
"replace": "goodTerm" // replace with the string 'goodTerm'
}
]
```
Putting it all together:
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": {
"title": [
"badTerm",
{
"search": "badTerm",
"replace": "goodTerm"
}
]
},
}
}
}
]
```
:::note
If the value of the field (title, an artist, album) is an empty string after transforming then the field is **removed.**
:::
:::tip
Modifications can also be applied to **all Sources** or **all Clients** when using the [AIO Config](./configuration.mdx?configType=aio#configuration-types) `config.json` by setting `playTransform` in `sourceDefaults` or `clientDefaults`:
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": { // will apply playTransform to all sources
"playTransform": {
"preCompare": {
"title": [
"(Album Version)"
]
}
}
},
"sources": [/* ... */],
"clients": [/* ... */]
}
```
</details>
:::
#### Compare Hook
The `compare` [hook](#hook) is slightly different than `preCompare` and `postCompare`. It consists of an object where you define which side(s) of the comparison should be modified. It also **does not modify downstream data!** Instead, the modifications are made only for use in the comparison.
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": {
"candidate": {/* ... */}, // modify the "new" Play being compared
"existing": {/* ... */}, // modify all "existing" Play/Scrobbles the new Play is being compared against
},
}
}
}
]
```
#### Regular Expressions
In addition to plain strings [expressions](#expression) that are matched and removed you can also use Regular Expressions. Write your regex like you normally would, but as a string, and it'll automatically be parsed:
```json5
[
"/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)' and removes it
{
"search": "/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)'
"replace": "[MySite.com]" // replace with the string '[MySite.com]'
}
]
```
The `replace` property uses javascript's [`replace()` function and so can use any special string characters.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement)
### Conditional Modification
#### "When" Condition
Top-level hooks **and** individual rules also support a `when` key for testing **if they should be run.**
The `when` key is similar to a normal [modification](#modification-parts) except:
* the keys accept a single string instead of an array
* the `when` key data is an array instead of a single object
All parts of an individual `when` clause must test true to "pass" but if **any** `when` clauses pass the hook/rule is processed. Example `when` data:
```json5
{
"when": [
{
"artist": "Elephant Gym", // both of these must match the Play object (AND)
"album": "Dreams" // both of these must match the Play object (AND)
},
// OR
{
"title": "/(Remastered)$/", // both of these must match the Play object (AND)
"album": "Various Artists" // both of these must match the Play object (AND)
}
]
}
```
More succinctly:
* All parts (`artist` `album` `title`) of a `when` are `AND` conditions
* All part-objects in the `when` array are `OR` conditions
<details>
<summary>Example of top-level hook with when condition</summary>
```json5
{
// IF the artist is Elephant Gym
// THEN Run preCompare hook ELSE skip this hook
//
// Run search-replace on album
// Run regex title remove
"sourceDefaults": {
"playTransform": {
"preCompare": {
"when": [
{
"artist": "/Elephant Gym/"
}
],
"album": [
{
"search": "Dreams",
"replace": "夢境"
}
],
"title": ["/\s\-\s滾石40\s滾石撞樂隊\s40團拚經典(.+)$/i"]
},
}
}
}
```
</details>
<details>
<summary>Example of individual rule with when condition</summary>
```json5
{
// Always run preCompare
//
// On search-replace in title...
// IF artist matches "Elephant Gym"
// THEN Run regex search-replace ELSE skip this rule
//
// Run live|remastered regex remove
"sourceDefaults": {
"playTransform": {
"preCompare": {
"title": [
{
"search": "/\\s\\-\\s滾石40\\s滾石撞樂隊\\s40團拚經典(.+)$/i",
"replace": "",
"when": [
{
"artist": "/Elephant Gym/"
}
]
},
"/(\\s\\-\\s|\\s)(feat\\.(.+)|live|remastered(.+))$/i"
],
}
}
}
}
```
</details>
#### Top-level Hook array
Top-level hooks can also be an array of hooks. This makes creating multiple scenarios for top-level `when`-gated hooks easier. All hooks in the array will be run (assuming their `when`'s pass, if they exist) and their **input will be the Play object output of the previous hook in the array.**
<details>
<summary>Example</summary>
```json5
{
"sourceDefaults": {
"playTransform": {
"preCompare": [
// first lifecycle hook of preCompare to run
{
"title": [
{
"search": "something",
"replace": "else unique"
}
]
},
// second lifecycle hook of preCompare to run
{
"title": [
{
"search": "else unique",
"replace": "very demure"
}
]
},
]
}
}
}
```
</details>
### Logging
MS can log the output of hook transformations if/when they occur. In the `playTransform` object of a Source/Client config use `log`:
* `"log": true` => Output original play + final transformed output of last hook in the array
* `"log": "all"` => Output original play + final transformed output of **each** hook in the array
```json5
{
"name": "myThing",
"data": {/*...*/},
"options": {
"playTransform": {
"preCompare": {/*...*/},
"log": true
}
}
}
```
## Examples
### Remove phrase from Title in all new Plays
Removes the phrase `(Album Version)` from the Title of a Play
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"title": [
"(Album Version)"
]
}
}
}
}
```
</details>
### Remove all parenthesized content from the end of a title
<details>
<summary>Example</summary>
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": {
"candidate": {
"title": [
"/(\(.+\))\s*$/"
]
},
"existing": {
"title": [
"/(\(.+\))\s*$/"
]
},
},
}
}
}
]
```
</details>
### Rename misspelled artist in all new Plays
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"artists": [
{
"search": "Boz Skaggs",
"replace": "Boz Scaggs"
}
]
}
}
}
}
```
</details>
### Remove "Various Artists" albums in all new Plays
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"album": [
{
"search": "Various Artists",
"replace": ""
}
]
}
}
}
}
```
</details>
### Extract primary Artist from delimited, multi-Artist string
<details>
When the Artist string is actually a multi-artist, delimited string, this search-and-replace will replace the string with just the first artist found.
Ex
```
My Artist One / My Artist Two / Another Guy
My Artist One
```
Artists are delimited with a spaced forward slash (`/`) in the regex below. Replace the contents of the `delim` capture group with the delimiter for your use case. Some more common scenarios:
* `(?<delim>\\/)` No spaces between slash IE `My Artist One/My Artist Two/Another Guy`
* `(?<delim>\\s*\\\\\s*)` Backslash instead of forward slash IE `My Artist One \ My Artist Two \ Another Guy`
* `(?<delim>,)` Comma IE `My Artist One, My Artist Two, Another Guy`
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"artists": [
{
"search": "(.*?)(?<delim>\\s*\\/\\s*)(.*$)",
"replace": "$1"
}
]
}
}
}
}
```
</details>
</details>
+8
View File
@@ -0,0 +1,8 @@
{
"label": "Development",
"position": 5,
"link": {
"type": "generated-index",
"description": "Developing for Multi-Scrobbler and tutorials"
}
}
+8
View File
@@ -0,0 +1,8 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 3
title: Client Development/Tutorial
---
To do...
+180
View File
@@ -0,0 +1,180 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 1
title: Common Development
description: Start here for MS development
---
# Development
## Architecture
Multi-scrobbler is written entirely in [Typescript](https://www.typescriptlang.org/). It consists of a backend and frontend. The backend handles all Source/Client logic, mounts web server endpoints that listen for Auth callbacks and Source ingress using [expressjs](https://expressjs.com/), and serves the frontend. The frontend is a standalone [Vitejs](https://vitejs.dev/) app that communicates via API to the backend in order to render the dashboard.
## Project Setup
Development requires [Node v18.19.1](https://nodejs.org/en) or higher is installed on your system.
:::tip
When running locally (not with a devcontainer) you can use [nvm](https://github.com/nvm-sh/nvm) to manage the installed node version.
:::
Clone this repository somewhere and then install from the working directory
```shell
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
nvm use # optional, sets correct node version when running without devcontainer
npm install
npm run start
```
### VSCode
This repository contains [workspace settings](https://github.com/FoxxMD/multi-scrobbler/blob/master/.devcontainer) for development with VSCode. These include:
* Run/Debug [Launch configurations](https://code.visualstudio.com/Docs/editor/debugging#_launch-configurations) for the application and tests
* [Devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) for development with all dependencies already installed
* Useful extensions for linting and running tests
To use the Devcontainer simple open the repository in VSCode and "Use Devcontainer" when the notification is presented. `npm install` will be run when a new container is created.
## Common Development
:::info
In this document, when referring to aspects of Sources and Clients that are shared between both, the Source/Client will be referred to as a **Component.**
:::
A Component is composed of two parts:
* Typescript interfaces describing structure of configuration for that Component
* A concrete class inheriting from a common "startup" abstract class that enforces how the Component is built and operates
In both parts Source/Clients share some common properties/behavior before diverging in how they operate.
### Config
The configuration for a Component should always have this minimum shape, enforced respectively by the interfaces [CommonSourceConfig](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/infrastructure/config/source/index.ts#L105) and [CommonClientConfig](https://github.com/FoxxMD/multi-scrobbler/blob/ce1c70a4e1e87fb5bea7cca960eaafbd15881a1f/src/backend/common/infrastructure/config/client/index.ts#L68):
```ts
interface MyConfig {
name: string
data?: object
options?: object
}
```
* `data` contains data that is required for a Component to operate such as credentials, callback urls, api keys, endpoints, etc...
* `options` are **optional** settings that can be used to fine-tune the usage of the Component but are not required or do not majorly affect behavior. EX additional logging toggles
### Concrete Class
Components inherit from an abstract base class, [`AbstractComponent`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts), that defines different "stages" of how a Component is built and initialized when MS first starts as well as when restarting the Component in the event it stops due to an error/network failure/etc...
#### Stages
Stages below are invoked in the order listed. All stages are asynchronous to allow fetching network requests or reading files.
The stage function (described in each stage below) should return a value or throw:
* return `null` if the stage is not required
* return `true` if the stage succeeded
* return a `string` if the stage succeeded and you wish to append a result to the log output for this stage
* throw an `Exception` if the stage failed for any reason and the Component should not continue to run/start up
##### Stage: Build Data
This stage should be used to validate user configuration, parse any additional data from async sources (file, network), and finalize the shape of any configuration/data needed for the Component to operate.
:::info
Implement [`doBuildInitData`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L71) in your child class to invoke this stage.
::::
<details>
<summary>Examples</summary>
* Parse a full URL like `http://SOME_IP:7000/subfolder/api` from user config containing a base url like `data.baseUrl: 'SOME_IP'` and then store this in the class config
* Validate that config `data` contains required properties `user` `password` `salt`
* Read stored credentials from `${this.configDir}/currentCreds-MySource-${name}.json`;
</details>
##### Stage: Check Connection
This stage is used to validate that MS can communicate with the service the Component is interacting with. This stage is invoked on MS startup as well as any time the Component tries to restart after a failure.
If the Component depends on **ingress** (like Jellyfin/Plex webhook) this stage is not necessary.
:::info
Implement [`doCheckConnection`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L103) in your child class to invoke this stage.
::::
<details>
<summary>Examples</summary>
* Make a [`request`](https://nodejs.org/docs/latest-v18.x/api/http.html#httprequesturl-options-callback) to the service's server to ensure it is accessible
* Open a websocket connection and check for a ping-pong
</details>
##### Stage: Test Auth
MS determines if Auth is required for a Component based on two class properties. You should set these properties during `constructor` initialization for your Component class:
* `requiresAuth` - (default `false`) Set to `true` if MS should check/test Auth for this Component
* `requiresAuthInteraction` - (default `false`) Set to `true` if user interaction is required to complete auth IE user needs to visit a callback URL
If the Component requires authentication in order to communicate with a service then any required data should be built in this stage and a request made to the service to ensure the authentication data is valid.
This stage should return:
* `true` if auth succeeded
* `false` if auth failed without unexpected errors
* IE the authentication data is not valid and requires user interaction to resolve the failure
* throw an exception if network failure or unexpected error occurred
You _should_ attempt to re-authenticate, if possible. Only throw an exception or return `false` if there is no way to recover from an authentication failure.
:::info
Implement [`doAuthentication`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts#L111) in your child class to invoke this stage.
::::
<details>
<summary>Examples</summary>
* Generate a Bearer Token for Basic Auth from user/password given in config and store in class properties
* Make a request to a known endpoint with Authorization token from read credentials file to see if succeeds or returns 403
* Catch a 403 and attempt to reauthenticate at an auth endpoint with user/password given in config
</details>
### Play Object
The **PlayObject** is the standard data structure MS uses to store listen (track) information and data required for scrobbling. It consists of:
* Track Data -- a standard format for storing track, artists, album, track duration, the date the track was played at, etc...
* Listen Metadata -- Optional but useful data related to the specific play or specifics about the Source/Client context for this play such as
* Platform specific ID, web URL to track, device/user ID that played this track, etc...
Both Sources and Clients use the **PlayObject** interface. When a Component receives track info from its corresponding service it must transform this data into a PlayObject before it can be interacted with.
For more refer to the TS documentation for `PlayObject` or [`AmbPlayObject`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/core/Atomic.ts#L141) in your project
## Creating Clients and Sources
* [Source Development and Tutorial](dev-source.md)
* [Client Development and Tutorial](dev-client.md)
+552
View File
@@ -0,0 +1,552 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 2
title: Source Development/Tutorial
---
This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review [Common Development](dev-common.md#common-development).
## Scenario
You are the developer of a fancy, new self-hosted web-based media player called **Cool Player.** Cool Player has a slick interface and many bells and whistles, but most importantly it has an API. The API:
* Has an unauthenticated health endpoint at `/api/health` that returns `200` if the service is running properly
* Has authenticated endpoints that require a user-generated token in the header `Authorization MY_TOKEN`
* Has a `/api/recent` endpoint that lists recently played tracks with a timestamp
* Has a `/api/now-playing` endpoint that returns information about the state of the player like current track, player position in the track, etc...
* Cool Player is by default accessed on port `6969`
* Your personal instance of Cool Player is hosted at `http://192.168.0.100:6969` and the api is accessed at `http://192.168.0.100:6969/api`
Because there is an API that MS can actively read this will be a **polling** Source where MS sends requests to Cool Player to get scrobble information -- as opposed to an **ingress** Source like Jellyfin/Plex that uses webhooks from the service to send data to MS.
## Minimal Implementation
### Define and Implement Config
We will create a new config interface for Cool Player using the [Common Config](dev-common.md#config) and tell MS it is a valid config that can be used.
Create a new file for your config:
```ts title="/src/backend/common/infrastructure/config/source/coolplayer.ts"
import { PollingOptions } from "../common.js";
import { CommonSourceConfig, CommonSourceData } from "./index.js";
// all of the required data for the Build Data and Test Auth stages (from Common Development docs)
// should go here
export interface CoolPlayerSourceData extends CommonSourceData, PollingOptions {
// remember to annotation your properties!
/**
* The user-generated token for Cool Player auth created in Cool Player -> Settings -> User -> Tokens
*
* @example f243331e-cf5b-49d7-846b-0845bdc965b4
* */
token: string
/**
* The host and port where Cool Player is hosted
*
* @example http://192.168.0.100:6969
* */
baseUrl: string
}
export interface CoolPlayerSourceConfig extends CommonSourceConfig {
data: CoolPlayerSourceData
}
export interface CoolPlayerSourceAIOConfig extends CoolPlayerSourceConfig {
// when using the all-in-one 'config.json' this is how users will identify this source
type: 'coolplayer'
}
```
Add the new interface to the list of valid interfaces for sources:
```ts title="src/backend/common/infrastructure/config/source/sources.ts"
import { ChromecastSourceAIOConfig, ChromecastSourceConfig } from "./chromecast.js";
// ...
// highlight-next-line
import { CoolPlayerSourceAIOConfig, CoolPlayerSourceConfig } from "./coolplayer.js";
export type SourceConfig =
SpotifySourceConfig
// ...
// highlight-next-line
| CoolPlayerSourceConfig;
export type SourceAIOConfig =
SpotifySourceAIOConfig
// ...
// highlight-next-line
| CoolPlayerSourceAIOConfig;
```
Finally, add the source type identifier to the list of valid identifiers
```ts title="src/backend/common/infrastructure/Atomic.ts"
export type SourceType =
'spotify'
// ...
// highlight-next-line
| 'coolplayer';
export const sourceTypes: SourceType[] = [
'spotify',
// ...
// highlight-next-line
'coolplayer'
];
```
Now we will create a new Source inheriting from [`AbstractComponent`](dev-common.md#concrete-class) that:
* accepts our config interface
* implements a function to transform CoolPlayer's track data into a [**PlayObject**](dev-common.md#play-object)
* implements required [stages](dev-common.md#stages)
* implements required methods to current player state and/or now playing track
### Create CoolPlayer Source
First we create a new Source called `CoolPlayerSource` and setup our constructor to accept the config and [specify Auth behavior.](dev-common.md#stage-test-auth)
```ts title="src/backend/sources/SpotifySource.ts"
import { CoolPlayerSourceConfig } from "../common/infrastructure/config/source/coolplayer.js";
import MemorySource from "./MemorySource.js";
import {
InternalConfig,
} from "../common/infrastructure/Atomic.js";
// MemorySource is the base class used for polling-type Sources
export default class CoolPlayerSource extends MemorySource {
// type hints for TS to know what the base class config looks like
declare config: CoolPlayerSourceConfig;
constructor(name: any, config: CoolPlayerSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
super('coolplayer', name, config, internal, emitter);
// Cool Player required authentication
this.requiresAuth = true;
// but does not require user interaction for auth to work
this.requiresAuthInteraction = false;
// tells MS this is a Source that can be activity monitored through API
this.canPoll = true;
}
}
```
### Initialize Source from Config
When MS starts it reads all configs and determines which Source to build based on the configs found. We need to tell it to build a `CoolPlayerSource` when a `coolplayer` config type is found.
We modify `ScrobbleSources.ts` to add `CoolPlayerSource` as an option:
```ts title="src/backend/sources/ScrobbleSources.ts"
// ...
import { CoolPlayerSource, CoolPlayerData } from "./CoolPlayerSource.js";
export default class ScrobbleSources {
// ...
buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = []) => {
// ...
// if CoolPlayerSource should be able to be built from ENVs only
// then add it as a case statement here
for (const sourceType of sourceTypes) {
switch (sourceType) {
// ...
case 'musikcube':
// ...
break;
// highlight-start
case 'coolplayer':
const cp = {
baseUrl: process.env.COOL_URL,
token: process.env.COOL_TOKEN
}
if (!Object.values(cp).every(x => x === undefined)) {
configs.push({
type: 'coolplayer',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
data: cp as CoolPlayerData
});
}
break;
// highlight-end
default:
break;
}
}
}
// ...
// (required) create new CoolPlayerSource if source config type is 'coolplayer'
addSource = async (clientConfig: ParsedConfig, defaults: SourceDefaults = {}) => {
// ...
let newSource: AbstractSource;
switch (type) {
// ...
case 'musikcube':
// ...
break;
// highlight-start
case 'coolplayer':
newSource = await new CoolPlayerSource(name, compositeConfig as CoolPlayerSourceConfig, internal, this.emitter);
break;
// highlight-end
default:
break;
}
}
}
```
### Implement Play Object Transform
Now we will create a static function that is used to take the track data returned from Cool Player's API and return a standard [`PlayObject`.](dev-common.md#play-object)
```ts title="src/backend/sources/CoolPlayerSource.ts"
import dayjs from "dayjs";
import {
FormatPlayObjectOptions,
} from "../common/infrastructure/Atomic.js";
import { PlayObject } from "../../core/Atomic.js";
export default class CoolPlayerSource extends MemorySource {
// ...
// 'obj' should ideally be a real TS interface
// if CoolPlayer has a ts/js client we would use that otherwise
// we should build our own interfaces to represent track data from Cool Player's API
static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {
const {
trackName,
artistName,
albumName,
duration,
playedAt,
} = obj;
return {
data: {
artists: [artistName],
album: albumName,
track: trackName,
// assuming seconds
duration,
// assuming playedAt is an ISO8601 timestamp
playDate: dayjs(playedAt)
},
meta: {
source: 'CoolPlayer'
}
}
}
}
```
### Implement Stages
Next we will implement the [Stages](dev-common.md#stages) required to get CoolPlayerSource running.
#### Build Data
First we implement the [Build Data Stage](dev-common.md#stage-build-data). We will check that the `token` and `baseUrl` properties have been provided by the user. Additionally, we will parse the baseUrl and add default ports/prefix.
```ts title="src/backend/sources/CoolPlayerSource.ts"
import { URL } from "url";
// ...
export default class CoolPlayerSource extends MemorySource {
baseUrl!: URL;
// ...
static parseConnectionUrl(val: string) {
const normal = normalizeUrl(val, {removeTrailingSlash: false, normalizeProtocol: true})
const url = new URL(normal);
if (url.port === null || url.port === '') {
url.port = '6969';
}
if (url.pathname === '/') {
url.pathname = '/api/';
}
return url;
}
protected async doBuildInitData(): Promise<true | string | undefined> {
const {
token,
baseUrl
} = this.config;
if (token === null || token === undefined || (typeof token === 'string' && token.trim() === '')) {
throw new Error(`'token' must be defined`);
}
if (baseUrl === null || baseUrl === undefined || (typeof baseUrl === 'string' && baseUrl.trim() === '')) {
throw new Error(`'baseUrl' must be defined`);
}
try {
this.baseUrl = CoolPlayerSource.parseConnectionUrl(baseUrl);
} catch (e) {
throw new Error(`Could not parse baseUrl: ${baseUrl}`, {cause: e});
}
this.logger.verbose(`Config URL: ${baseUrl} => Normalized: '${this.url.toString()}'`);
return true;
}
}
```
#### Check Connection
Second we will implement the [Check Connection Stage](dev-common.md#stage-check-connection):
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import { UpstreamError } from "../common/errors/UpstreamError.js";
// ...
export default class CoolPlayerSource extends MemorySource {
// ...
protected async doCheckConnection(): Promise<true | string | undefined> {
try {
const resp = await request.get(`${this.baseUrl}/health`);
return true;
// if /health returned version info we could instead read response and return a string with version info for visibility to the user
// return `Cool Player Version: ${resp.body.version}`;
} catch (e) {
throw e;
}
}
}
```
#### Test Auth
Finally, we will implement [Auth Test Stage](dev-common.md#stage-test-auth):
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import { UpstreamError } from "../common/errors/UpstreamError.js";
// ...
export default class CoolPlayerSource extends MemorySource {
// ...
doAuthentication = async () => {
try {
const resp = await request
.get(`${this.baseUrl}/recent`)
.set('Authorization', `Token ${this.config.token}`);
return true;
} catch (e) {
// if Cool Player returned an error as json we could parse it from error body and throw new Error with the message
throw e;
}
}
}
```
### Implement Polling
The majority of Sources MS monitors primarily operate as a source of truth for a **music player** rather than a **played music history.** Only Listenbrainz and Last.fm operate as a source of music history.
To this end, MS implements a [state machine](https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/) that emulates the behavior of a music player in order to keep track of when a song you are listening to should be scrobbled. It does this by monitoring the "currently playing" track reported by a Source's service, with varying degrees of accuracy depending on what information is returned from the service. The state machine is implemented in `MemorySource` which our `CoolPlayerSource` inherits from.
For a polling Source to work properly we need to implement a function, [`getRecentlyPlayed`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L92), that returns PlayObjects that are "newly" played. These are then checked against previously "discovered" plays and their timestamp to determine if they should be surfaced to Clients to scrobble.
To take advantage of the `MemorySource` state machine we will additionally use [`processRecentPlays`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/MemorySource.ts#L113) from `MemorySource` inside `getRecentlyPlayed`. We pass track and/or player state returned from the Source service to `processRecentPlayers`. It then takes care of deriving Source player state based on how this data changes over time. The advantage to using `processRecentPlays` is that our Source service does not necessarily need to pass any player information -- as long as the track info has a **duration** we can more-or-less determine if it has been played long enough to scrobble.
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import {
SourceData,
PlayerStateData,
SINGLE_USER_PLATFORM_ID
} from "../common/infrastructure/Atomic.js";
// ...
export default class CoolPlayerSource extends MemorySource {
// ...
protected async getRecentlyPlayed(options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> {
const plays: SourceData[] = [];
try {
// currently playing tracks/player state data
const resp = await request
.get(`${this.baseUrl}/now-playing`)
.set('Authorization', `Token ${this.config.token}`);
const {
body: {
playerState, // 'playing' or 'stopped' or 'paused'...
position, // number of seconds into the track IE at position 48 -> ( 0:48/3:56 in player UI )
play: { /* track data */}
}
} = resp;
// transform into standard player state data
const playerData: PlayerStateData = {
platformId: SINGLE_USER_PLATFORM_ID,
play: CoolPlayerSource.formatPlayObj(play),
position
};
// if Cool Player does return player state we can also push a regular PlayObject to this list
plays.push(playerData);
} catch (e) {
throw e;
}
// process player state through state machine
// if the track changes or player state changes
// and currently played track has been listened to long enough to be scrobbled it will return in newPlays
const newPlays = this.processRecentPlays(plays);
// finally, we return new plays and MS checks to see if they have been previously seen
// before signalling to Clients that they can be scrobbled
return newPlays;
}
}
```
Congratulations! Your `CoolPlayerSource` has been minimally implemented and can now be used in multi-scrobbler.
## Further Implementation
### Backlog
To have your Source try to scrobble "missed" tracks when MS starts up the Source's service must be able to provide:
* track information
* timestamp of when the track was played
In your Source implement [`getBackloggedPlays`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L235) and set setting in constructor indicating it has backlogging capabilities:
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import {
PlayObject,
} from "../common/infrastructure/Atomic.js";
// ...
export default class CoolPlayerSource extends MemorySource {
constructor(/* ... */) {
super(/* ... */);
// ...
// tell MS it should try to get backlogged tracks on startup
this.canBacklog = true;
}
// ...
protected getBackloggedPlays = async (options: RecentlyPlayedOptions): Promise<PlayObject[]> => {
try {
const resp = await request
.get(`${this.baseUrl}/recent`)
.set('Authorization', `Token ${this.config.token}`);
// assuming list from body looks like track info returned in
// "Implement Play Object Transform" section
const {
body = []
} = resp;
return body.map(x => CoolPlayerSource.formatPlayObj(x));
} catch (e) {
throw new Error('Error occurred while getting recently played', {cause: e});
}
}
}
```
### Other Source Types
There are some scenarios where polling and/or state machine is not the right tool to handle determining if incoming data should be scrobbled:
* The Source service handles scrobble threshold internally, the data being received should always be scrobbled (WebScrobbler, Plex, Tautulli, Listenbrainz, Last.fm)
* You prefer to handle the scrobble determination yourself
#### Music History Source
If the Source is still polling but the track returned should always be scrobbled if not already seen IE the Source service is a **music history source** (Listenbrainz, Last.fm), rather than a music player, then simply indicate to MS the source of truth type by setting it in the constructor. The state machine will always return a track if it is new and not seen, regardless of how recently it was seen:
```ts title="src/backend/sources/CoolPlayerSource.ts"
import { SOURCE_SOT } from "../../core/Atomic.js";
// ...
export default class CoolPlayerSource extends MemorySource {
constructor(/* ... */) {
super(/* ... */);
// ...
// tell MS it should immediately scrobble any new, unseen tracks from the upstream service
this.playerSourceOfTruth = SOURCE_SOT.HISTORY;
}
}
```
#### Non-Polling Source
**Ingress** Sources (like Plex, Tautulli, Webscrobbler, Jellyfin) do not having a polling mechanism because the upstream service contacts MS when there is an event, rather than MS contacting the upstream service.
For these Sources you will need to implement endpoints in `src/service/api.ts` and corresponding files. See the existing Sources in the project as references for how to do this.
You may still wish to use the state machine `MemorySource` (like Jellyfin) if the events received are not "scrobble" events but instead of implementing `getRecentlyPlayed` you will implement your own function in your Source class, like `handle()`, that receives data and then uses `processRecentPlays`.
After new plays have been determined see the next section for how to scrobble...
#### Basic Source
At the core of a Source that implements `AbstractSource`'s functionality is the ability to **Discover** and **Scrobble** plays.
These functions are not seen in the MVP `CoolPlayerSource` because they are automatically done by the polling functionality after being returned from `getRecentlyPlayed`.
##### Discovery
A Source keeps track of all the "plays" that are determined to be valid for scrobbling. When a play is valid it is checked to see if it has already been "discovered" by comparing the track info and timestamp of the play against already discovered plays. This prevents duplicate scrobbling by using the Source's own data and simplifies scrobbling for Sources by allowing your implementation to "always" ingest track data without having to worry about whether its new or not -- `AbstractSource` and `discover()` will take care of that for you.
```ts title="src/backend/sources/MyBasicSource.ts"
export default class MyBasicSource extends AbstractSource {
handle(somePlay: PlayObject) {
// if the track is "new" and not seen before it is returned in the discovered list
// we then know it is OK to be sent to Clients for scrobbling
const discovered: PlayObject[] = this.discover([somePlay]);
}
}
```
This additionally will be surfaced to the user in the Dashboard in the "Tracks Discovered" page.
##### Scrobbling
After a play is verified to be discovered we can then scrobble it. This will emit the plays to the ScrobbleClients service which then disseminates the play to all Clients that were configured to listen in the Source's config.
```ts title="src/backend/sources/MyBasicSource.ts"
export default class MyBasicSourceSource extends AbstractSource {
handle(somePlay: PlayObject) {
const discovered: PlayObject[] = this.discover([somePlay]);
// emit plays that can be scrobbled by clients
this.scrobble(discovered);
}
}
```
If your service only emits an event when a play is scrobbled you can _technically_ skip using `discover()` but it is good practice to use it unless you have a very good reason not to.
:::note
Using `scrobble()` does not guarantee a track is actually scrobbled! The Scrobble Clients also check the play against their own "recently scrobbled" list to prevent duplicates.
:::
+96
View File
@@ -0,0 +1,96 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 4
title: Flatpak
description: Building Flatpak App locally
---
## Install Requirements
### Flatpak and flatpak-builder
Install [Flatpak](https://flatpak.org/setup/)
Install [flatpak-builder](https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak)
#### [flatpak-node-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/node)
Requires python 3.7+, [pip](https://pip.pypa.io/en/stable/)/[pipx](https://pypa.github.io/pipx/)
## Update Project source
Set the `branch` `tag` or `commit` to use for MS in the `source` section of [`io.github.foxxmd.multiscrobbler.yml`](https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/io.github.foxxmd.multiscrobbler.yml)
## Generate Sources and Build
### Use Setup Script
A convenience bash script is provided that automates generating offline sources and building the flatpak app for you. This is the recommend method to use.
Located in the project at [`flatpak/setup.sh`](https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/setup.sh), run it from the `flatpak` directory with this syntax:
```shell
./setup.sh -o -b /path/to/flatpak/build/dir
```
```
Args:
-o => Delete and overwrite any existing generated sources
-b => The absolute path to the directory that should be used for flatpak build/artifacts. If not defined will use `CWD/build`
-y => Proceed without confirming settings
```
### Manual Setup
If you cannot use `setup.sh` follow the below to manually generate sources and build the flatpak app:
<details>
<summary>Instructions</summary>
#### Use `flatpak-node-generator` to generate sources
First, [make sure `node_modules` is deleted or empty](https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442) for both the project and `docsite` directories.
Then, from MS project root:
```shell title="PROJECT_DIR"
flatpak-node-generator npm package-lock.json
```
Move `generated-sources.json` into `PROJECT_DIR/flatpak` and rename `generated-sources.0.json`
Then, generate `docsite` sources:
```shell title="PROJECT_DIR"
flatpak-node-generator npm docsite/package-lock.json
```
Move `generated-sources.json` into `PROJECT_DIR/flatpak` and rename `generated-sources.1.json`
#### Build flatpak
In the below command replace `/home/yourUser/multi-scrobbler-flatpak` with a directory to use for storing flatpak build/artifacts.
```shell title="PROJECT_DIR/flatpak"
flatpak-builder --repo=/home/yourUser/multi-scrobbler-flatpak/repo --state-dir=/home/yourUser/multi-scrobbler-flatpak/state /home/yourUser/multi-scrobbler-flatpak/build io.github.foxxmd.multiscrobbler.yml --force-clean
```
:::info
Append `--install --user` to the above command to have the app installed immediately.
::::
</details>
# Run App
If built with `--install --user` (default when using `setup.sh`) you can now run MS using the command
```shell
flatpak run -u io.github.foxxmd.multiscrobbler
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Installation",
"position": 1,
"position": 2,
"link": {
"type": "generated-index",
"description": "Way to install Multi-Scrobbler"
-175
View File
@@ -1,175 +0,0 @@
---
sidebar_position: 1
title: 'Overview'
---
# Installation
# Local
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
## Nodejs
Clone this repository somewhere and then install from the working directory
```shell
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
nvm use # optional, to set correct Node version
npm install
npm run build
npm run start
```
#### Rollup build error
During building if you encounter an error like: `Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.`
Modify `overrides` in `package.json` to use `@rollup/wasm-node` as a drop-in replacement for rollup:
```json
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"
}
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.9.6"
}
}
```
See [this issue](https://github.com/FoxxMD/multi-scrobbler/issues/135#issuecomment-1927080260) for more detail.
### Usage Examples
* The web UI and API is served on port `9078`. This can be modified using the `PORT` environmental variable.
#### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
```shell
npm run start
```
#### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
```shell
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret MALOJA_URL="http://domain.tld" node src/index.js
```
## Flatpak
You must have [Flatpak](https://flatpak.org/) installed on your system.
```shell
flatpak install flathub io.github.foxxmd.multiscrobbler
```
**Note:** Flatpak users have experienced issues when using multi-scrobbler as a long-running process. Due to the relative difficulty in debugging issues with flatpak installations it is recommended:
* to use a [Docker](#docker) installation if possible or
* only if you need access to host-level resources like dbus for [MPRIS](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris) and cannot run a [nodejs](#nodejs) installation
### Usage Examples
#### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
The config directory for multi-scrobbler as a flatpak can be found under `/home/YourUser/.var/app/io.github.foxxmd.multiscrobbler/config`
```shell
flatpak run io.github.foxxmd.multiscrobbler
```
#### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
There are a few [options for running flatpak applications with temporary or permanent environmental variables.](https://ardasevinc.dev/launch-flatpak-apps-with-custom-args-and-environment-variables)
```shell
flatpak run --env=SPOTIFY_CLIENT_ID=yourId --envSPOTIFY_CLIENT_SECRET=yourSecret --env=MALOJA_URL="http://domain.tld" io.github.foxxmd.multiscrobbler
```
## Docker
Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
[Dockerhub](https://hub.docker.com/r/foxxmd/multi-scrobbler)
```
docker.io/foxxmd/multi-scrobbler:latest
```
[Github Packages](https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler)
```
ghcr.io/foxxmd/multi-scrobbler:latest
```
Or use the provided [docker-compose.yml](../../../docker-compose.yml) after modifying it to fit your configuration.
Recommended configuration steps for docker or docker-compose usage:
#### Storage
You **must** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
* [Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount): `-v /path/on/host/config:/config`
* [Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3): `- /path/on/host/config:/config`
#### Networking
If you are using a [bridge network](https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/) (default docker setup) you **must** map a port to the container in order to access the dashboard and use MS with some sources (Plex, Jellyfin).
The default container port is `9078`. To map container to host port:
* With [docker](https://docs.docker.com/engine/reference/commandline/run/#publish): `-p 9078:9078` (first port is the port on the host to use)
* With [docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1): `- "9078:9078"`
##### Base URL
Optionally, when
* using a [Source or Client](../configuration/configuration.md) that has a "Redirect URI" that you have not explicitly defined
* and
* using a bridge network or
* installing MS on a different machine than the one used to view the dashboard
set the [Base URL](../configuration/configuration.md#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
* With docker: `-e BASE_URL="http://hostMachineIP"` (first port is the port on the host to use)
* With docker-compose: [see comments in docker-compose.yml](../../../docker-compose.yml)
#### Other
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
### Linux Host
If you are
* using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
* running docker on MacOS or Windows
this **DOES NOT** apply to you.
If you are running Docker on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
To get the UID and GID for the current user run these commands from a terminal:
* `id -u` -- prints UID
* `id -g` -- prints GID
## Docker Usage Examples
If installing on a different machine make sure all redirect URIs are defined or that you have set a [Base URL](#base-url).
### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
```bash
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
```
### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
```bash
docker run -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
```
See the [docker-compose.yml](../../../docker-compose.yml) file for how to use with docker-compose.
+320
View File
@@ -0,0 +1,320 @@
---
sidebar_position: 1
title: 'Overview'
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import ComposeStack from '!!raw-loader!../../../docker-compose.yml';
:::tip
For the difference between **ENV** and **File** examples in this document see [Configuration Types](../configuration/configuration.mdx#configuration-types).
:::
## Docker
Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
:::info[Available Images]
<Tabs>
<TabItem value="docker" label="Dockerhub">
[Repository Page](https://hub.docker.com/r/foxxmd/multi-scrobbler)
```
docker.io/foxxmd/multi-scrobbler:latest
```
</TabItem>
<TabItem value="ghcr" label="Github Packages">
[Repository Page](https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler)
```
ghcr.io/foxxmd/multi-scrobbler:latest
```
</TabItem>
</Tabs>
:::
Or use the example `docker-compose.yml`, found in the repo, after modifying it to fit your configuration:
<details>
<summary>`docker-compose.yml`</summary>
<CodeBlock language="yaml">{ComposeStack}</CodeBlock>
</details>
Recommended configuration steps for docker or docker-compose usage:
#### Storage
You should bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
[Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount):
```shell
docker run ... -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
[Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
volumes:
- "./config:/config"
```
</TabItem>
</Tabs>
</details>
#### Networking
If you are using a [bridge network](https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/) (default docker setup) you must map a port to the container in order to access the dashboard and use MS with some sources (Webscrobbler, LFM/LZ Endpoints). The default container port is `9078`.
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
[Docker `run` publish options](https://docs.docker.com/engine/reference/commandline/run/#publish):
```shell
docker run ... -p 9078:9078 foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
ports:
- "9078:9078"
```
</TabItem>
</Tabs>
</details>
##### Base URL
Optionally, when
* using a [Source or Client](../configuration/configuration.mdx) that has a "Redirect URI" that you have not explicitly defined
* and
* using a bridge network or
* installing MS on a different machine than the one used to view the dashboard
set the [Base URL](../configuration/configuration.mdx#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
```shell
docker run ... -e BASE_URL="http://hostMachineIP" foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
environment:
- BASE_URL=http://hostMachineIP
```
</TabItem>
</Tabs>
</details>
#### Other
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
#### Linux Host
::::info
If you are running this container with **Docker** on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
To get the UID and GID for the current user run these commands from a terminal:
* `id -u` -- prints UID
* `id -g` -- prints GID
::::
### Docker Usage Example
:::tip
See the [**Quick Start Guide**](../quickstart.mdx) for another guided docker-compose example
:::
The example scenario:
* [Jellyfin **Source**](../configuration/configuration.mdx#jellyfin)
* [Maloja **Client**](../configuration/configuration.mdx#maloja)
* Serving app on port `9078`
* Docker container located on a different IP (`192.168.0.100`) so use [Base URL](../configuration/configuration.mdx#base-url)
* Config/data directory on host machine in a directory next to `docker-compose.yml`
* Linux uid/gid is `1000:1000`
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
```bash
docker run --name multi-scrobbler -e "JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c" -e "JELLYFIN_URL=192.168.0.101:8096" -e "JELLYFIN_USER=MyUser" -e "BASE_URL=192.168.0.100" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /home/myUser/ms:/config foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="file" label="File">
```bash
docker run --name multi-scrobbler -e "PUID=1000" -e "PGID=1000" -e "BASE_URL=192.168.0.100" -p 9078:9078 -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
- JELLYFIN_URL=192.168.0.101:8096
- JELLYFIN_USER=MyUser
- BASE_URL=http://192.168.0.100:9078
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- PUID=1000
- PGID=1000
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
</TabItem>
</Tabs>
## Local Installation
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
### Nodejs
Clone this repository somewhere and then install from the working directory
```shell
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
nvm use # optional, to set correct Node version
npm install
npm run docs:install && npm run build
npm run start
```
#### Rollup build error
During building if you encounter an error like: `Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.`
Modify `overrides` in `package.json` to use `@rollup/wasm-node` as a drop-in replacement for rollup:
```json
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"
}
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.9.6"
}
}
```
See [this issue](https://github.com/FoxxMD/multi-scrobbler/issues/135#issuecomment-1927080260) for more detail.
#### Usage Examples
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
```shell
JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c JELLYFIN_URL=192.168.0.101:8096 JELLYFIN_USER=MyUser MALOJA_URL="http://domain.tld" node src/index.js
```
</TabItem>
<TabItem value="file" label="File">
<details>
<summary>`./config/config.json`</summary>
```json title="./config/config.json"
{
"sources": [
{
"type": "jellyfin",
"clients": ["myConfig"],
"name": "myJellyfinSource",
"data": {
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
"url": "http://192.168.0.101:8096",
"user": "MyUser"
}
}
],
"clients": [
{
"type": "maloja",
"name": "myConfig",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
}
}
],
}
```
</details>
```shell
npm run start
```
</TabItem>
</Tabs>
:::tip
The web UI and API is served on port `9078`. This can be modified using the `PORT` environmental variable.
:::
### Flatpak
:::warning[Unsupported]
Flatpak/Flathub installs are no longer supported. You can still build MS as a [Flatpak app from source.](../development/flatpak) See more information about [Flatpak EOL.](https://github.com/FoxxMD/multi-scrobbler/issues/287)
:::
+5 -18
View File
@@ -3,7 +3,7 @@ sidebar_position: 2
title: 'As a Service'
---
If you have multi-scrobbler installed [locally](installation.md#local) you can enable it to run as a background service when you login.
If you have multi-scrobbler installed [locally](installation.mdx#nodejs) you can enable it to run as a background service when you login.
Before running as a service you should run it at least once in the foreground to ensure it can start up correctly!
@@ -17,7 +17,7 @@ This setup will create a [user service](https://wiki.archlinux.org/title/systemd
Create a new service file for multi-scrobbler under your HOME config:
```console
```bash
mkdir -p ~/.config/systemd/user
touch ~/.config/systemd/user/multi-scrobbler.service
```
@@ -31,32 +31,19 @@ After=network.target
[Service]
Type=simple
ExecStart=flatpak run io.github.multiscrobbler
WorkingDirectory=/path/to/multi-scrobbler/directory
ExecStart=npm run start
Restart=no
[Install]
WantedBy=default.target
```
The above assumes you [installed multi-scrobbler using flatpak](installation.md#flatpak)
### Node.js Installs
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](installation.md#nodejs) you should modify the `[Service]`:
```ini
[Service]
Type=simple
WorkingDirectory=/path/to/multi-scrobbler/directory
ExecStart=node src/index.js
Restart=no
```
## Start the Service
Save the file then run:
```console
```bash
systemctl daemon-reload
systemctl --user enable multi-scrobbler.service
systemctl --user start multi-scrobbler.service
+191
View File
@@ -0,0 +1,191 @@
---
title: 'Quickstart'
sidebar_position: 1
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
This guide will get you up and running with multi-scrobbler using [Docker](https://www.docker.com/) and [`docker compose`](https://docs.docker.com/compose/). At the end of the guide you will have:
* the dashboard served on port `9078` of a host machine which has an IP of `192.168.0.100`
* data saved to the same directory as the `docker-compose.yml` file
* multi-scrobbler monitoring [Spotify](./configuration/configuration.mdx#spotify) and/or [Jellyfin](./configuration/configuration.mdx#jellyfin) for listening activity
* multi-scrobbler scrobbling to [Lastfm](./configuration/configuration.mdx#lastfm) and/or [Maloja](./configuration/configuration.mdx#maloja)
:::note
If the multi-scrobbler container is on the same machine you will be viewing the dashboard from (IE `localhost`) you can remove and ignore `BASE_URL` usage below. Additionally, replace usage of `192.168.0.100` with `localhost`.
:::
## Create Docker Compose File
Create a new folder for multi-scrobbler related data and then create `docker-compose.yml` with this content:
```yaml title="~/msData/docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- BASE_URL=http://192.168.0.100:9078
# all Environmental Variables in below examples go here!
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
## Setup Sources
**Sources** are the services multi-scrobbler monitors to look for listening activity.
<Tabs>
<TabItem value="jellyfin" label="Jellyfin">
Follow the Jellyfin configuration [instructions for setting up an **API Key**.](./configuration/configuration.mdx#jellyfin)
Then, add these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- JELLYFIN_URL=192.168.0.110:8096 # URL to Jellyfin UI
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b # Api Key created in previous step
- JELLYFIN_USER=MyUser # Your username in Jellyfin
```
</TabItem>
<TabItem value="spotify" label="Spotify">
To access your Spotify activity you must [create a Spotify App](https://developer.spotify.com/dashboard) to get a
**Client ID/Secret** and set a valid **Redirect URI**.
Refer to the [Spotify Authentication instructions in the docs.](./configuration/configuration.mdx#authenticate-spotify-with-multi-scrobbler)
Assuming you used the **HTTPS** method for your **Redirect URI** from the instructions above...after the application is created add these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- SPOTIFY_CLIENT_ID=yourClientId
- SPOTIFY_CLIENT_SECRET=yourClientSecret
- SPOTIFY_REDIRECT_URI=https://multi-scrobbler.mydomain.com/callback
```
Later, after [starting multi-scrobbler](#start-multi-scrobbler), visit the dashboard at `http://192.168.0.100:9078` and click **(Re)authenticate** on the Spotify card to authorize multi-scrobbler to use your account. Monitoring will begin automatically after authorization is complete.
</TabItem>
</Tabs>
## Setup Clients
**Clients** are services that store scrobbles. Multi-scrobbler will scrobble all listening activity from the **Sources** you configured to all **Clients** you configure here.
<Tabs>
<TabItem value="maloja" label="Maloja">
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
<details>
<summary>Maloja Setup Intructions</summary>
Using Maloja's example `docker-compose.yml`:
```yaml reference title="~/malojaData/docker-compose.yml"
https://github.com/krateng/maloja/blob/master/example-compose.yml
```
Uncomment `environment` and add `MALOJA_FORCE_PASSWORD=CHANGE_ME` to set an admin password
Start the container:
```shell title="~/malojaData"
docker compose up -d
```
</details>
* Navigate to the Admin Panel (Cog in upper-right corner) -> API Keys (or at http://192.168.0.100:42010/admin_apikeys)
* Create a **New Key** and then copy the generated key value
Finally, add these values to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- MALOJA_URL=http://192.168.0.100:42010
- MALOJA_API_KEY=myApiKey
```
</TabItem>
<TabItem value="lastfm" label="Last.fm">
[Register for an API account at Last.fm.](https://www.last.fm/api/account/create)
Use the following for **Callback URL**:
```
http://192.168.0.100:9078/lastfm/callback
```
After account creation use the displayed information and add these values to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- LASTFM_API_KEY=myApiKey
- LASTFM_SECRET=myApiSecret
```
</TabItem>
<TabItem value="koito" label="Koito">
Setup a [Koito server](https://koito.io/guides/installation/) if you have not already done this.
* [Create (or get) an API Key](https://koito.io/guides/scrobbler/)
* From your Koito server dashboard navigate to **Settings** (bottom-left icon) => **API Keys**
* Create a new Key (or use an existing) and Copy the key using the copy icon to the right of the key
* EX `pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL`
* Take note of your Koito username (used below as `myUser`)
* Determine your Koito URL
* This is the URL you use to access your Koito dashboard OR whatever URL is accessible from your multi-scrobbler instance.
* EX dashboard at `http://192.168.0.100:4110` => replace `myURL` below with `http://192.168.0.100:4110`
* [More info on the URL to use is the full docs](./configuration/configuration.mdx#koito)
Add/Substitute values above into these examples values and add them to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- KOTIO_TOKEN=myToken
- KOTIO_USER=myUser
- KOITO_URL=myURL
```
:::tip
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
:::
</TabItem>
</Tabs>
## Start Multi-Scrobbler
:::tip
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](./installation/installation.mdx#linux-host)
:::
From the same directory as the [`docker-compose.yml` you created earlier](#create-docker-compose-file) start the container:
```shell title="~/msData"
docker compose up -d
```
You're done! Multi-scrobbler is now running. It will monitor the sources you configured and scrobble to clients you set up.
Visit `http://192.168.0.100:9078` to see the dashboard where
* configured Sources/Clients
* show current status and authentication options
* display statistics about discovered/scrobbled tracks and Now Playing status
* a real-time log shows multi-scrobbler's activity
## Next Steps
* See more advanced docker options as well as other install methods in the [**Installation**](./installation/installation.mdx#docker) docs
* Review the [**Configuration**](./configuration/configuration.mdx) docs
* Learn about how to configure multi-scrobbler using files for more complicated Source/Client scenarios
* See all available Sources/Clients alongside configuration examples
* Learn how to set up [notification webhooks](./configuration/configuration.mdx#webhook-configurations)
* Check out the [kitchensink example](./configuration/kitchensink.md)
* Consult the [**FAQ**](./FAQ.md) for solutions to common problems
+8
View File
@@ -0,0 +1,8 @@
{
"label": "Updating",
"position": 3,
"link": {
"type": "generated-index",
"description": "Way to update Multi-Scrobbler"
}
}
+46
View File
@@ -0,0 +1,46 @@
---
sidebar_position: 1
title: 'Overview'
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
## Updating
Currently, multi-scrobbler does not have any databases or dependencies that require additional interaction/consideration when updating.
The majority of major/minor version releases introduce only *new* functionality but occasionally may also include breaking changes in behavior or configuration. These changes, and how to update configs accordingly, are detailed in [Github Release Notes](https://github.com/FoxxMD/multi-scrobbler/releases). It is recommended to check this page before upgrading.
Alternatively, multi-scrobbler version can be pinned using [git tags](https://github.com/FoxxMD/multi-scrobbler/tags) or docker image tags that match release versions IE `foxxmd/multi-scrobbler:0.9.7`
### Docker
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
```shell
docker image pull foxxmd/multi-scrobbler:latest
docker container stop multi-scrobbler
docker container rm multi-scrobbler
docker run --name multi-scrobbler ...
```
The last command is the [same as the command used to initialy run multi-scrobbler.](../installation/installation.mdx?runType=docker#docker-usage-example)
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
```shell
docker compose pull
docker compose up -d
```
</TabItem>
</Tabs>
### Nodejs
Assuming the [repository was used for initial installation with `git clone`](../installation/installation.mdx#nodejs), update by pulling the latest commit:
```
git pull
```
and restart multi-scrobbler.
@@ -1,11 +1,19 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
import type * as Preset from '@docusaurus/preset-classic';
import type { Config } from '@docusaurus/types';
import * as themes from 'prism-react-renderer';
//import sidebars from './sidebars';
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
const config: Config = {
future: {
v4: {
removeLegacyPostBuildHeadAttribute: true, // required
},
experimental_faster: {
ssgWorkerThreads: true,
rspackBundler: true, // required flag
rspackPersistentCache: true, // new flag
},
},
title: 'Multi-Scrobbler',
tagline: 'Scrobble all the things',
favicon: 'img/favicon.ico',
@@ -14,16 +22,16 @@ const config = {
url: 'https://foxxmd.github.io',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/multi-scrobbler',
baseUrl: process.env.DOCS_BASE !== undefined && process.env.DOCS_BASE !== '' ? process.env.DOCS_BASE : '/docs',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'foxxmd', // Usually your GitHub org/user name.
projectName: 'multi-scrobbler', // Usually your repo name.
trailingSlash: false,
trailingSlash: true,
onBrokenLinks: 'throw',
onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internalization, you can use this field to set useful
@@ -38,10 +46,9 @@ const config = {
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
{
docs: {
sidebarPath: require.resolve('./sidebars.js'),
sidebarPath: './sidebars.ts',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
@@ -57,30 +64,42 @@ const config = {
blog: false,
theme: {
customCss: require.resolve('./src/css/custom.css'),
customCss: './src/css/custom.css',
},
}),
} satisfies Preset.Options,
],
],
themes: ["docusaurus-json-schema-plugin"],
plugins: [
themes: [
"docusaurus-json-schema-plugin",
[
require.resolve("@cmfcmf/docusaurus-search-local"),
"@easyops-cn/docusaurus-search-local",
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
{
// ... Your options.
// `hashed` is recommended as long-term-cache of index file is possible.
hashed: true,
indexBlog: false,
// For Docs using Chinese, The `language` is recommended to set to:
// ```
// language: ["en", "zh"],
// ```
},
],
'docusaurus-theme-github-codeblock'
],
plugins: [
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
{
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
image: 'img/icon.png',
navbar: {
title: 'Multi-Scrobbler',
hideOnScroll: true,
logo: {
alt: 'Logo',
src: 'img/icon.svg',
height: 40
},
items: [
{
@@ -99,6 +118,11 @@ const config = {
label: 'GitHub',
position: 'right',
},
{
href: 'https://foxxmd.github.io/multi-scrobbler/',
label: 'Website',
position: 'right',
},
],
},
footer: {
@@ -113,31 +137,18 @@ const config = {
},
{
label: 'Installation',
to: '/docs/installation',
to: 'docs/installation',
},
{
label: 'Configuration',
to: '/docs/configuration',
to: 'docs/configuration',
},
{
label: 'Development',
to: 'docs/development/dev-common',
},
],
},
/* {
title: 'Community',
items: [
{
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
},
{
label: 'Discord',
href: 'https://discordapp.com/invite/docusaurus',
},
{
label: 'Twitter',
href: 'https://twitter.com/docusaurus',
},
],
},*/
{
title: 'More',
items: [
@@ -145,26 +156,65 @@ const config = {
label: 'GitHub',
href: 'https://github.com/foxxmd/multi-scrobbler',
},
{
label: 'Website',
href: 'https://foxxmd.github.io/multi-scrobbler/',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Multi-Scrobbler. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
theme: themes.themes.github,
darkTheme: themes.themes.dracula,
additionalLanguages: ['json','json5','typescript', 'docker', 'bash', 'ini']
},
}),
colorMode: {
defaultMode: 'dark',
disableSwitch: false,
respectPrefersColorScheme: false,
},
codeblock: {
showGithubLink: true,
githubLinkLabel: 'View on GitHub',
showRunmeLink: false,
runmeLinkLabel: 'Checkout via Runme'
}
} satisfies Preset.ThemeConfig,
headTags: [
{
tagName: 'meta',
attributes: {
name: 'google-site-verification',
content: process.env.GSITEVERIFICATION ?? 'none'
}
}
]
};
if (process.env.ANALYTICS !== undefined && process.env.ANALYTICS !== '') {
const script = {
src: process.env.ANALYTICS,
defer: true
}
if (process.env.ANALYTICS_DOMAIN !== undefined && process.env.ANALYTICS_DOMAIN !== '') {
script['data-domain'] = process.env.ANALYTICS_DOMAIN;
}
config.scripts.push(script)
}
if (process.env.RY_ANALYTICS !== undefined && process.env.RY_ANALYTICS !== '') {
const script = {
src: process.env.RY_ANALYTICS,
defer: true
}
if (process.env.RY_ANALYTICS_SITEID !== undefined && process.env.RY_ANALYTICS_SITEID !== '') {
script['data-site-id'] = process.env.RY_ANALYTICS_SITEID;
}
if (process.env.RY_ANALYTICS_REPLAY !== undefined && process.env.RY_ANALYTICS_REPLAY !== '') {
script['data-session-replay'] = process.env.RY_ANALYTICS_REPLAY;
}
config.scripts.push(script)
}
module.exports = config;
export default config;
+11071 -5197
View File
File diff suppressed because it is too large Load Diff
+24 -18
View File
@@ -3,8 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"start": "docusaurus start --host 0.0.0.0",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
@@ -15,20 +14,27 @@
"typecheck": "tsc"
},
"dependencies": {
"@cmfcmf/docusaurus-search-local": "^1.1.0",
"@docusaurus/core": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-json-schema-plugin": "^1.6.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"@docusaurus/core": "3.8.1",
"@docusaurus/faster": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
"@easyops-cn/docusaurus-search-local": "0.51.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-json-schema-plugin": "^1.14.0",
"docusaurus-theme-github-codeblock": "^2.0.2",
"json5": "^2.2.3",
"micromark-extension-directive": "^3.0.1",
"prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
"@docusaurus/module-type-aliases": "^3.6.3",
"@docusaurus/tsconfig": "^3.6.3",
"@docusaurus/types": "^3.6.3",
"@types/react": "^18.2.29",
"typescript": "~5.6.2"
},
"browserslist": {
"production": [
@@ -37,12 +43,12 @@
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=16.14"
"node": ">=18.0"
}
}
+4 -5
View File
@@ -1,3 +1,5 @@
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
@@ -9,10 +11,7 @@
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
const sidebars: SidebarsConfig = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [
{type: 'autogenerated', dirName: '.'},
@@ -37,4 +36,4 @@ const sidebars = {
*/
};
module.exports = sidebars;
export default sidebars;
+54
View File
@@ -0,0 +1,54 @@
import React, { Fragment } from "react"
import CodeBlock from '@theme/CodeBlock';
import Admonition from '@theme/Admonition';
import ErrorBoundary from "@docusaurus/ErrorBoundary"
import Error from "@theme/Error"
import { Simulate } from "react-dom/test-utils";
import error = Simulate.error;
import json5 from 'json5';
export interface AIOProps {
data: string
client?: boolean
name: string
}
const AIOExample = (props: AIOProps) => {
const {
data,
name,
client = false
} = props;
let configObj;
// eslint-disable-next-line prefer-const
try {
configObj = json5.parse(data) as object[];
} catch (e) {
console.error(e);
return <Admonition type="danger" title="Unexpected Error">
<p>Example component crashed because of error!</p>
<CodeBlock>{e.message}</CodeBlock>
</Admonition>
}
const configType = client ? 'clients' : 'sources';
configObj = configObj.filter(x => x.configureAs === undefined || x.configureAs === (client ? 'client' : 'source'));
configObj = configObj.map(x => ({...x, type: name}));
const aio = {[configType]: configObj};
return <CodeBlock title="CONFIG_DIR/config.json" language="json5">{JSON.stringify(aio, null, 2)}</CodeBlock>
}
const WrappedAIOExample = (props: AIOProps) => {
return <ErrorBoundary
fallback={({error}) => (
<div>
<p>Example component crashed because of error: {error.message}.</p>
</div>
)}
><AIOExample {...props} /></ErrorBoundary>
}
export default WrappedAIOExample;
+49
View File
@@ -0,0 +1,49 @@
import React, { Fragment } from "react"
import CodeBlock, {Props as CodeBlockProps} from '@theme/CodeBlock';
import Admonition from '@theme/Admonition';
import ErrorBoundary from "@docusaurus/ErrorBoundary"
import Error from "@theme/Error"
import { Simulate } from "react-dom/test-utils";
import error = Simulate.error;
import json5 from 'json5';
export interface FileProps extends Omit<CodeBlockProps, 'children'> {
data: string
client?: boolean
}
const FileExample = (props: FileProps) => {
const {
data,
client = false,
...rest
} = props;
let configObj;
// eslint-disable-next-line prefer-const
try {
configObj = json5.parse(data) as object[];
} catch (e) {
console.error(e);
return <Admonition type="danger" title="Unexpected Error">
<p>Example component crashed because of error!</p>
<CodeBlock>{e.message}</CodeBlock>
</Admonition>
}
configObj = configObj.filter(x => x.configureAs === undefined || x.configureAs === (client ? 'client' : 'source'));
return <CodeBlock {...rest} language="json5">{JSON.stringify(configObj, null, 2)}</CodeBlock>
}
const WrappedFileExample = (props: FileProps) => {
return <ErrorBoundary
fallback={({error}) => (
<div>
<p>Example component crashed because of error: {error.message}.</p>
</div>
)}
><FileExample {...props} /></ErrorBoundary>
}
export default WrappedFileExample;
+27
View File
@@ -0,0 +1,27 @@
import React, {PropsWithChildren, Fragment} from "react"
export interface SchemaLinkProps {
objectName: string
lower?: boolean
client?: boolean
}
const sourceURL = 'https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json';
const clientURL = 'https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json';
const SchemaLink = (props: PropsWithChildren<SchemaLinkProps>) => {
const {
children,
lower,
client = false
} = props;
let content = children;
if(content === undefined) {
content = <Fragment>{lower ? 'explore' : 'Explore'} the schema with an example and live editor/validator</Fragment>
}
const definition = `https://json-schema.app/view/%23/%23%2Fdefinitions%2F${props.objectName}`;
const url = client ? clientURL : sourceURL;
return <a target="_blank" href={`${definition}?url=${url}`}>{content}</a>
}
export default SchemaLink;
+39 -17
View File
@@ -10,35 +10,51 @@ title: Overview
<img src={require('/img/icon.png').default} align="right" alt="multi-scrobbler logo" width="180" height="180"/>
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), [ListenBrainz](https://listenbrainz.org), and [Koito](https://koito.io/).
* Supports scrobbling from many **Sources**
* [Spotify](docs/configuration#spotify)
* [Plex](docs/configuration#plex) or [Tautulli](docs/configuration#tautulli)
* [Subsonic-compatible APIs](docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [Jellyfin](docs/configuration#jellyfin)
* [WebScrobbler](docs/configuration#webscrobbler)
* [Youtube Music](docs/configuration#youtube-music)
* [Last.fm](docs/configuration#lastfm-source)
* [ListenBrainz](docs/configuration#listenbrainz--source-)
* [Azuracast](docs/configuration#azuracast)
* [Deezer](docs/configuration#deezer)
* [MPRIS (Linux Desktop)](docs/configuration#mpris)
* [Mopidy](docs/configuration#mopidy)
* [Google Cast (Chromecast)](docs/configuration#google-cast-chromecast)
* [Icecast](docs/configuration#icecast)
* [Jellyfin](docs/configuration#jellyfin)
* [JRiver](docs/configuration#jriver)
* [Kodi](docs/configuration#kodi)
* [Google Cast (Chromecast)](/docs/configuration#google-cast--chromecast-)
* [Koito](docs/configuration#koito-source)
* [Last.fm](docs/configuration#lastfm-source)
* [Last.fm (Endpoint)](docs/configuration#lastfm-endpoint)
* [ListenBrainz](docs/configuration#listenbrainz-source)
* [ListenBrainz (Endpoint)](docs/configuration#listenbrainz-endpoint)
* [Maloja](docs/configuration#maloja-source)
* [Mopidy](docs/configuration#mopidy)
* [MPD (Music Player Daemon)](docs/configuration#mpd-music-player-daemon)
* [MPRIS (Linux Desktop)](docs/configuration#mpris)
* [Musikcube](docs/configuration#musikcube)
* [Plex](docs/configuration#plex) or [~~Tautulli~~](docs/configuration#tautulli)
* [Spotify](docs/configuration#spotify)
* [Subsonic-compatible APIs](docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [WebScrobbler](docs/configuration#webscrobbler)
* [VLC](docs/configuration#vlc)
* [Yamaha MusicCast](docs/configuration#yamaha-musiccast)
* [Youtube Music](docs/configuration#youtube-music)
* Supports scrobbling to many **Clients**
* [Maloja](docs/configuration#maloja)
* [Koito](docs/configuration#koito)
* [Last.fm](docs/configuration#lastfm)
* [ListenBrainz](docs/configuration#listenbrainz)
* [Maloja](docs/configuration#maloja)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](docs/configuration#webhook-configurations) or [healthcheck endpoint](docs/configuration#health-endpoint)
* Supports [Now Playing](docs/configuration#now-playing) for scrobble Clients
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
* Install using [Docker images for x86/ARM](docs/installation#docker), [flatpak](docs/installation#flatpak), or [locally with NodeJS](docs/installation#nodejs)
* Modify data before scrobbling with [regular expression or search patterns](docs/configuration/transforms)
* Install using [Docker images for x86/ARM](docs/installation#docker) or [locally with NodeJS](docs/installation#nodejs)
[**Quick Start Guide**](docs/quickstart)
<img src={require('/img/status-ui.png').default} width="800"/>
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -49,9 +65,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](docs/configuration#lastfm--source-) or [Listenbrainz as a **Source**](docs/configuration#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
<img src={require('/img/status-ui.png').default} width="800"/>
Yes! You can use [Last.fm as a **Source**](docs/configuration#lastfm-source) or [Listenbrainz as a **Source**](docs/configuration#listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
@@ -76,6 +90,10 @@ Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Quick Start
[See the **Quick Start Guide**](docs/quickstart)
## Installation
[See the **Installation** documentation](docs/installation)
@@ -99,6 +117,10 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
Having issues with connections or configuration? Check the [FAQ](docs/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](docs/development/dev-common)
## License
MIT
+2
View File
@@ -17,7 +17,9 @@ function PlaygroundComponent(): JSX.Element {
>
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
// required to keep SSR from trying to render monaco/etc...
const PlaygroundInnerComponent =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("@site/src/components/PlaygroundInner").default
return <PlaygroundInnerComponent />
}}
+1 -2439
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"resolveJsonModule": true,
+29 -36
View File
@@ -4,40 +4,33 @@ import eslint from '@eslint/js';
import tsEslint from 'typescript-eslint';
import arrow from 'eslint-plugin-prefer-arrow-functions';
export default tsEslint.config(
eslint.configs.recommended,
...tsEslint.configs.recommended,
// use to enable typed linting (way more errors) https://typescript-eslint.io/linting/typed-linting
/* ...tsEslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigDirName: import.meta.dirname,
},
},
},*/
{
files: ['src/backend/**/*.ts'],
ignores: ['eslint.config.js'],
plugins: {
"prefer-arrow-functions": arrow
},
rules: {
'no-useless-catch': 'off',
'@typescript-eslint/no-unused-vars': 'off',
"prefer-arrow-functions/prefer-arrow-functions": [
"warn",
{
"allowNamedFunctions": false,
"classPropertiesAllowed": false,
"disallowPrototype": false,
"returnStyle": "unchanged",
"singleReturnOnly": false
}
],
"arrow-body-style": ["warn", "as-needed"],
"@typescript-eslint/no-explicit-any": "warn"
}
export default tsEslint.config({
files: ['src/backend/**/*.ts'],
plugins: {
"prefer-arrow-functions": arrow
},
ignores: [
'eslint.config.js',
'src/backend/tests/**/*.ts'
],
extends: [
eslint.configs.recommended,
...tsEslint.configs.recommended,
],
rules: {
'no-useless-catch': 'off',
'@typescript-eslint/no-unused-vars': 'off',
"prefer-arrow-functions/prefer-arrow-functions": [
"warn",
{
"allowNamedFunctions": false,
"classPropertiesAllowed": false,
"disallowPrototype": false,
"returnStyle": "unchanged",
"singleReturnOnly": false
}
],
"arrow-body-style": ["warn", "as-needed"],
"@typescript-eslint/no-explicit-any": "warn"
}
);
});
+1 -48
View File
@@ -1,48 +1 @@
NOTE: This steps are for building the flatpak entirely locally, from source. If you want to install the application normally then [get it through flathub](/docs/installation.md#flatpak)
The final build repo for the flathub version can be found at [flathub/io.github.foxxmd.multiscrobbler](https://github.com/flathub/io.github.foxxmd.multiscrobbler)
# 1. Install Requirements
## Flatpak and flatpak-builder
Install [Flatpak](https://flatpak.org/setup/)
Install [flatpak-builder](https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak)
## [flatpak-node-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/node)
Requires python 3.7+, [pip](https://pip.pypa.io/en/stable/)/[pipx](https://pypa.github.io/pipx/)
# 2. Update Project source
Set the `branch` `tag` or `commit` to use for MS in the `git` source in [`io.github.foxxmd.multiscrobbler.yml`](/flatpak/io.github.foxxmd.multiscrobbler.yml)
# 3. Use `flatpak-node-generator` to generate sources
First, [make sure `node_modules` is deleted or empty.](https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442)
Then, from MS project root:
```shell
flatpak-node-generator npm package-lock.json
```
Move `generated-sources.json` into [`/flatpak`](/flatpak)
# 4. Build flatpak
From MS project root:
```shell
cd flatpak
flatpak-builder --repo=/home/yourUser/multi-scrobbler-flatpak/repo --state-dir=/home/yourUser/multi-scrobbler-flatpak/state /home/yourUser/multi-scrobbler-flatpak/build io.github.foxxmd.multiscrobbler.yml --force-clean --install --user
```
Add `--install --user` to have the app installed immediately.
# 5. Run (Locally)
If built with `--install --user` you can now run MS using the command
```shell
flatpak run -u io.github.foxxmd.multiscrobbler
```
See flatpak docs on the [docs website](https://foxxmd.github.io/multi-scrobbler/docs/development/flatpak) or at [`/docsite/docs/development/flatpak.md`](../docsite/docs/development/flatpak.md)
@@ -22,11 +22,10 @@
<ul>
<li>Spotify</li>
<li>Plex (Tautulli)</li>
<li>Any subsonic-based music player (Airsonic)</li>
<li>Any subsonic-based music player (Airsonic or Navidrome)</li>
<li>Jellyfin</li>
<li>WebScrobbler</li>
<li>Mopidy</li>
<li>Deezer</li>
<li>JRiver</li>
<li>Kodi</li>
<li>Webscrobbler</li>
@@ -34,6 +33,12 @@
<li>MPRIS (linux desktop)</li>
<li>Google Cast (Chromecast)</li>
<li>Musikcube</li>
<li>MPD (Music Player Daemon)</li>
<li>VLC</li>
<li>Azuracast</li>
<li>Icecast</li>
<li>Yamaha MusicCast</li>
<li>Listenbrainz and Last.fm Endpoints</li>
</ul>
<p>and consolidate your listens (scrobbles) into one or more scrobbling servers/services:</p>
<ul>
@@ -50,6 +55,12 @@
</screenshot>
</screenshots>
<releases>
<release version="0.9.2" date="2025-03-24"/>
<release version="0.9.0" date="2025-01-15"/>
<release version="0.8.4" date="2024-09-27"/>
<release version="0.8.3" date="2024-08-28"/>
<release version="0.8.2" date="2024-08-20"/>
<release version="0.8.1" date="2024-07-17"/>
<release version="0.8.0" date="2024-07-08"/>
<release version="0.7.1" date="2024-04-11"/>
<release version="0.7.0" date="2024-03-26"/>

Some files were not shown because too many files have changed in this diff Show More