Compare commits

...
Author SHA1 Message Date
FoxxMD 6d8d840a94 feat(jellyfin): Add more server choice logging 2025-11-28 16:39:04 +00:00
FoxxMD aac4f0f5e7 fix(jellyfin): Refactor library parsing and valid activity when user is non-admin
#402
2025-11-24 19:14:37 +00:00
FoxxMD c5269791af docs: Fix kitchensink links 2025-11-23 19:22:21 +00:00
FoxxMD fb5ffd053d fix(lastfm): Fix network error default value 2025-11-21 16:16:05 +00:00
Matt Foxx 0c04e7b79f Merge pull request #366 from FoxxMD/transformPipeline
feat: transform pipeline
2025-11-20 14:35:56 -05:00
FoxxMD dc5fab6f9a Refactor metadata stage so atomic parts are objects or booleans 2025-11-20 18:34:23 +00:00
FoxxMD 128c48947e feat(transforms): Allow for config with stage type
As long as it conforms to user stage type. Preserve backwards compatibility
2025-11-20 17:36:03 +00:00
FoxxMD 4cab02e88d fixed tests and isArray arg 2025-11-20 15:26:45 +00:00
FoxxMD a360687264 first attempt at folding in stages
Not working. Tests fail and compiler has errors.
2025-11-20 15:11:46 +00:00
FoxxMD 6a677b6fe1 refactor: Move transform typings to own file 2025-11-20 15:11:46 +00:00
FoxxMD 5c418d5618 chore(tealfm): Round duration for good measure 2025-11-19 17:25:51 +00:00
FoxxMD e6e782dec8 fix(rocksky): Round duration so value is an integer instead of float
#400
2025-11-19 14:44:04 +00:00
FoxxMD ab7d20d7d3 docs(rocksky): Add scrobbling guarantee disclaimer 2025-11-18 15:15:29 +00:00
FoxxMD ec4c2cdd56 fix(rocksky): Increase scrobble delay to respect PDS operation limits 2025-11-18 14:45:11 +00:00
FoxxMD 88d3e13b52 feat(rocksky): Use duration_ms instead of duation 2025-11-17 15:49:27 +00:00
FoxxMD 7bf70d1973 fix(rocksky): Use correct URL for auth validation 2025-11-17 15:48:16 +00:00
FoxxMD 8eae2dc69d chore: Bump version for release 2025-11-17 15:23:15 +00:00
FoxxMD aa44a08e9e fix(rocksky): Fix wrong property used for api auth header 2025-11-17 15:22:46 +00:00
FoxxMD 6071abca49 docs: Update rocksky link to main page rather than docs 2025-11-13 12:56:22 -05:00
FoxxMD 86a719ba4a docs: Add new clients to repo readme 2025-11-13 17:18:20 +00:00
FoxxMD 39b6f6e602 docs: Fix kitchensink link 2025-11-13 17:16:17 +00:00
FoxxMD bfce5dcc54 chore: Bump version for release 2025-11-13 17:14:36 +00:00
FoxxMD 18537e4e08 docs: Add subsonic guidance for lz scrobbling 2025-11-13 17:13:45 +00:00
FoxxMD 30db8712fc docs: Add redirects for old pages 2025-11-13 17:08:30 +00:00
Matt Foxx 1701a5e5f1 Merge pull request #397 from FoxxMD/lzParsingCleanup
refactor: Cleanup and consolidate listenbrainz listen parsing
2025-11-13 11:46:18 -05:00
FoxxMD 406c39b6b2 fix(tealfm): Fix scrobble play date context 2025-11-13 16:44:56 +00:00
FoxxMD ce7ad36db4 refactor(listenbrainz): Consolidate lz endpoint submit parsing into lz response parsing
Consolidtes logic for parsing lz listen data structure
2025-11-13 16:25:06 +00:00
FoxxMD fa4bf5ab10 refactor(listenbrainz): Move parsing functions out of static 2025-11-13 15:43:44 +00:00
FoxxMD 83d9af2aea fix(listenbrainz): Fix interfaces for submit/listen and consolidate 2025-11-13 15:36:17 +00:00
FoxxMD 433761525c fix(rocksky): Add scrobble to deadletter when ignored 2025-11-13 14:00:30 +00:00
FoxxMD f7a7a36d8b docs: Add robots 2025-11-12 16:59:36 +00:00
Matt Foxx 7e375f1531 Merge pull request #392 from FoxxMD/rocksky
feat(rocksky): Implement Rocksky Source/Client
2025-11-12 10:42:02 -05:00
FoxxMD 357397f225 chore: Add markdown extension to devcontainer 2025-11-12 15:36:14 +00:00
FoxxMD 78bf507226 docs: Update docs to include Rocksky Source and updated config 2025-11-12 15:35:39 +00:00
Matt Foxx f892851b60 Merge pull request #396 from FoxxMD/nowPlayingAccuracyImprovement
Now playing accuracy improvement
2025-11-12 10:10:16 -05:00
FoxxMD 4244ddb8c2 fix(listenbrainz): Use duration_ms if duration not provided 2025-11-11 19:54:34 +00:00
FoxxMD 3e67e9189f feat(listenbrainz): Add request body to logging for debugging 2025-11-11 19:50:14 +00:00
FoxxMD de1cbb2aae fix: Improve timing and update frequency to match scrobbler behavior
* Aggressively start Now Playing update (based on reported) but aggressibely end, based on stale/orphaned, so updates are early but not often
* Refactor update thresholds to use scrobble client behavior
  * LFM and LZ use track duration as "time visible" for Now Playing, don't re-update during that time period
  * Add thresholds to logging for easier troubleshooting

#395
2025-11-11 19:49:58 +00:00
FoxxMD db17455b2e fix: Revert accidental debug commit 2025-11-11 15:18:21 +00:00
FoxxMD a7729fbfa6 feat: Make ATProto handle transform usable by RockSky and teal.fm 2025-11-11 15:10:39 +00:00
FoxxMD 771fdc0686 chore: Regenerate schema with rebased results 2025-11-11 14:48:58 +00:00
FoxxMD 8cad2d4efd feat(rocksky): Implement Rocksky Source/Client 2025-11-11 14:47:59 +00:00
Matt Foxx a094a1aa65 Merge pull request #389 from FoxxMD/tealfm
feat: Implement teal.fm as source/client
2025-11-11 09:37:10 -05:00
Matt Foxx f2d45c5622 Merge pull request #386 from FoxxMD/GH-375/subsonicLoop
feat(subsonic): Add repeat detection
2025-11-10 13:15:50 -05:00
FoxxMD a429bccf07 fix(teal): Adding pds with env when it no longer exists 2025-11-07 22:44:02 +00:00
FoxxMD 04d950980d feat(tealfm): Massage handle for users who don't read docs 2025-11-07 22:41:30 +00:00
FoxxMD 7ec90b97d6 docs: Add tealfm to landing page 2025-11-07 22:30:38 +00:00
FoxxMD a4f989c5e4 feat(tealfm): Resolve pds from handle 2025-11-07 22:29:54 +00:00
FoxxMD da4fbd2f27 docs: Fix tealfm source link 2025-11-07 18:56:22 +00:00
FoxxMD b9e6f32500 chore: Update schema 2025-11-07 18:55:40 +00:00
FoxxMD 5c5c653c3b fix: Fix Source/Client data types 2025-11-07 18:55:27 +00:00
FoxxMD a9e7a4bf16 docs(tealfm): Add docs 2025-11-07 18:46:57 +00:00
FoxxMD 1b4baafa67 feat(teal): Change pds viewer web url 2025-11-07 17:35:00 +00:00
FoxxMD 794ef08aa2 feat(tealfm): Api improvements
* Implement PDS check for App auth clients
* Fix error logging for app loging
2025-11-07 17:31:16 +00:00
FoxxMD fa6ed323ff feat(tealfm): Add tealfm Source 2025-11-07 17:17:50 +00:00
FoxxMD 8110e8e9fa refactor(tealfm): Move common functionality to api client 2025-11-07 16:44:41 +00:00
FoxxMD 12a6eec740 test(tealfm): Add example config to pass config tests 2025-11-07 16:40:27 +00:00
FoxxMD 472021dd2c feat(tealfm): Add ENV config 2025-11-07 16:35:39 +00:00
FoxxMD ebb74d1b5c feat: Implement working tealfm client
* Add pds option
2025-11-07 16:31:41 +00:00
FoxxMD ea23a89101 refactor(tealfm): Implement abstract api client with concrete classes for different auth methods 2025-11-07 15:02:56 +00:00
FoxxMD 9062619d38 feat(tealfm): Implement app password auth 2025-11-07 14:51:31 +00:00
FoxxMD 3fb64be54f feat(tealfm): Initial oauth attempt 2025-11-06 17:22:06 +00:00
Matt Foxx 3e083b19f7 Merge pull request #382 from FoxxMD/GH-380/lzEndpoint
fix(endpointlz): Return expected OK response body
2025-11-05 13:22:47 -05:00
Matt Foxx e197c71bbb Merge pull request #388 from FoxxMD/ghWorkflowHarden
ci: Harden workflows using zizmor
2025-11-05 12:35:11 -05:00
FoxxMD 7b36d95dfc ci: Harden remaining workflows using zizmor
* Pin action digests
* Remove template injection where possible
2025-11-05 12:08:46 -05:00
FoxxMD 0427b6e945 try with double quotes 2025-11-05 11:48:00 -05:00
FoxxMD 326d9e58d7 ci: Use zizmor to fix and audit image publish action
* Pin actions to digest
* Replace template substitution with envs
2025-11-05 11:26:41 -05:00
FoxxMD 63c079f08e docs: Improve sources documentation 2025-11-04 19:18:46 +00:00
FoxxMD ff74fd7e4c docs: Update kitchensink 2025-11-04 19:04:17 +00:00
FoxxMD 680f862ee8 docs: Change installation to category doc link 2025-11-04 18:52:42 +00:00
FoxxMD abdd7f0b55 docs: Improved linking on config/landing and configuration type refinement 2025-11-04 18:49:16 +00:00
FoxxMD c017dd734b feat(subsonic): Add basic repeat detection based on listened duration
#375
2025-11-04 16:31:36 +00:00
Matt Foxx ad478adefd Merge pull request #385 from FoxxMD/depCleanup
refactor: frontend dependency cleanup
2025-11-04 10:40:35 -05:00
FoxxMD b11bd52963 chore(frontend): Update sass package and vite config to remove deprecation warnings 2025-11-04 14:36:22 +00:00
FoxxMD 538df6b6d0 refactor(frontend): Migrate tailwind config to v4 2025-11-04 14:17:51 +00:00
FoxxMD 819afcdb8a refactor(frontend): Update tailwind and move generation into vite
* Use vite tailwind plugin instead of postcss
* Update tailwind so it actually works with plugin
* Remove postcss
* Merge vite config into inline vite-express config
2025-11-04 13:55:31 +00:00
FoxxMD a5693f7c03 chore: Remove unused typedoc config 2025-11-04 13:35:02 +00:00
FoxxMD 22eb5b1168 docs: Add missing client flag for example json usage for clients
Fixes #383
2025-11-03 21:54:44 +00:00
FoxxMD 9402caa4ae docs(endpointlz): Add navidrome url setup instructions 2025-11-03 16:56:32 +00:00
FoxxMD 0aff56e891 feat(endpointlz): Add more logging for invalid routes 2025-11-03 16:10:44 +00:00
FoxxMD a5f6aaa7c0 feat(listenbrainz): Use end-of-string token and wildcard route to better log malformed LZ endpoint routes 2025-11-03 16:08:17 +00:00
FoxxMD 4ee503655c fix(endpointlz): Return expected OK response body
Fixes #380
2025-11-03 15:47:24 +00:00
FoxxMD e53a82782c docs: fix lastfm aio name 2025-11-01 03:50:20 +00:00
FoxxMD 020c6815ba docs: Fix readme links 2025-10-31 17:58:46 +00:00
FoxxMD 5281a92e81 docs: A word 2025-10-31 15:56:05 +00:00
Matt Foxx 8577eb44af Merge pull request #381 from tsirysndr/master
Fix typos in Rocksky documentation
2025-10-31 11:46:08 -04:00
Tsiry Sandratraina edd3cf25b5 Fix typos in Rocksky documentation 2025-10-31 17:24:29 +03:00
FoxxMD b17719c868 docs: Fix missing search index 2025-10-31 13:02:30 +00:00
FoxxMD 971ec9d4db feat(jellyfin): Add frontend url override as env #374 2025-10-30 20:20:10 +00:00
Matt Foxx 6607654e8a Merge pull request #379 from FoxxMD/docsRevamp
Docs revamp
2025-10-30 16:16:52 -04:00
FoxxMD 891e591ceb docs: Reorganize doc contents 2025-10-30 19:56:07 +00:00
FoxxMD 2540ed3945 docs: Add client own files 2025-10-30 18:52:59 +00:00
FoxxMD 7ac3f33cd4 docs: Move Source configs to own files 2025-10-30 18:35:58 +00:00
FoxxMD 9b91f6897e Add new Source overview using category 2025-10-30 17:06:39 +00:00
FoxxMD bc62b99401 docs: Add commonly used components to global scope 2025-10-30 17:06:22 +00:00
FoxxMD a5d1cea910 Add some snippers 2025-10-30 15:21:08 +00:00
FoxxMD 3e3473f6a8 chore: Ignore docusaurus build folders when searching 2025-10-30 15:19:14 +00:00
FoxxMD a0bdd15d15 docs: Move landing page to docs to simply url structure
Set docs as base for router so there isn't an additional docs in url
2025-10-30 15:19:01 +00:00
FoxxMD 920eb10c05 docs: Fix rocksky name/link typo 2025-10-30 09:30:14 -04:00
FoxxMD ae7a436758 chore: Bump version for release 2025-10-30 13:18:49 +00:00
FoxxMD 92d246b45a docs: Add rocksky to repo readme 2025-10-30 13:18:36 +00:00
Matt Foxx 0af480ac1a Merge pull request #377 from FoxxMD/GH-376/plexUrlParseFix
fix(plex): Use better url parsing method
2025-10-30 08:47:48 -04:00
Matt Foxx 9f52c2ec67 Merge pull request #369 from gabeklavans/fix/apprise-tags
fix: parse and use the tags in Apprise config
2025-10-29 11:33:05 -04:00
FoxxMD cfb278087e docs: Update listenbrainz json example to include url 2025-10-29 14:50:21 +00:00
FoxxMD 402b0d2cc7 docs: Add Rockysky setup instructions 2025-10-29 14:47:59 +00:00
FoxxMD 67c79f2eb7 fix(plex): Use better url parsing method
Fixes #376
2025-10-29 13:48:47 +00:00
FoxxMD d759b00c7e test: Add more tests for url parsing 2025-10-29 13:43:34 +00:00
Matt Foxx cac8163e58 Merge pull request #374 from wolffshots/feature/optionally-redirect-image-paths
feat(JellyfinApiSource): add option to override the Jellyfin base url
2025-10-24 09:16:21 -04:00
wolffshots 11dbdaf6cc chore(JellyfinApiSource): extract URL replacement to func and add tests 2025-10-23 17:53:51 +02:00
wolffshots ac6b04f492 feat(JellyfinApiSource): add option to override the Jellyfin base url used in the frontend 2025-10-23 17:22:57 +02:00
Matt Foxx 0790da2e0d feat(plex): Enable accepting self-signed certs #372 (#373)
* feat(plex): Enable accepting self-signed certs #372
* docs(plex): Add invalidcert param to plex example config
2025-10-21 09:10:38 -04:00
Matt Foxx dffb1c79ec Merge pull request #370 from h-wb/feat/add-source-as-lb-ms-name
feat(listenbrainz): add source as music service name
2025-10-08 08:27:01 -04:00
h-wb 46cc9f0b6e feat(listenbrainz): add source as music service name 2025-10-07 23:01:59 -04:00
gabe 140e9f0b20 fix(notification): parse and use the tags in Apprise config 2025-09-26 14:51:04 -04:00
FoxxMD 0fe5779c4b chore: Bump for release 2025-09-23 15:48:46 +00:00
FoxxMD 06b1475b63 chore: remove unused object-hash package 2025-09-23 15:48:21 +00:00
Matt Foxx 6bc415120d Merge pull request #359 from FoxxMD/objectHashReplace
fix: Update object hashing function
2025-09-23 11:43:13 -04:00
FoxxMD e061540e6b fix(jellyfin): Guard against undefined NowPlaying Path
#355
2025-09-18 14:15:20 +00:00
FoxxMD 38bf31a67f feat(lzendpoint): Accept artist and release artist names in submit payload
#360
2025-09-18 13:45:29 +00:00
Matt Foxx c8c3b4888f Merge pull request #343 from FoxxMD/caching
feat: Caching and persistent scrobble queues
2025-09-17 08:54:44 -04:00
FoxxMD 70bb30406a test: fix/workaround tests for new transformrule assumptions 2025-09-16 15:27:47 +00:00
FoxxMD d9e71f6770 fix: Do not use transform until after client is ready
Fixes #356
2025-09-16 15:16:27 +00:00
FoxxMD 7cee9f04cf fix: Update object hashing function
Might be a solution for #358
2025-09-15 17:37:30 +00:00
FoxxMD 0a673fd333 refactor: Decrease persist interval to 10 seconds
Faster interval means less likely user will update queue and exit MS before cache is saved
2025-09-11 19:14:01 +00:00
FoxxMD fd316a8e84 fix: Add full coverage for cached queue updating during client lifecycle 2025-09-11 19:14:01 +00:00
FoxxMD c1e1de2255 Increase file cache size to 2000
Should be ~700kb, full up. This should be a generous upper limit for number of scrobbles queued/dead at the same time.
2025-09-11 19:14:01 +00:00
FoxxMD 9bea31e1b2 docs: Add caching docs 2025-09-11 19:14:01 +00:00
FoxxMD 351edd8102 fix: Fix cache test missing awaits 2025-09-11 19:14:01 +00:00
FoxxMD 10dcb7f30d feat: Implement cache file streaming with progress logging
If cache files get large enough to cause noticeable delay it will be helpful to log progress
2025-09-11 19:14:01 +00:00
FoxxMD ffb41b4e30 test: Add scrobble client caching test 2025-09-11 19:14:01 +00:00
FoxxMD d78bc48e6f feat: Allow passing file cache options other than connection 2025-09-11 19:14:01 +00:00
FoxxMD b4154bb0d5 refactor: Move transient cache to exportable function 2025-09-11 19:14:01 +00:00
FoxxMD 48467b08d3 chore: Bump with-local-tmp-dir version
tmp dependency has been removed so previous buggy behavior(?) is gone
2025-09-11 19:14:01 +00:00
FoxxMD 682d507683 build: move net-interceptor to dev packages 2025-09-11 19:14:01 +00:00
FoxxMD 814e609efc fix: Add clone as runtime dependency 2025-09-11 19:14:01 +00:00
FoxxMD 652fa5a14c feat(cache): Improve data marshalling and cache init
* Use typeson for stringified data marshalling
* Use clone for in-memory serializing to preserve dayjs
* Refactor cache backend init into indepedent, testable functions
* Add `false` as cache provider type to allow disabling secondary cache
2025-09-11 19:14:01 +00:00
FoxxMD 39bc9cd410 build(vscode): Update devcontainer to use compose stack with valkey for caching integration 2025-09-11 19:14:01 +00:00
FoxxMD 63b6012488 feat(cache): Rehydrate dayjs data on cached scrobble parsing 2025-09-11 19:14:01 +00:00
FoxxMD 32e2aa6974 test: Setup transient di cache 2025-09-11 19:14:01 +00:00
FoxxMD 78f4767dd2 refactor: Refactor di cache to allow singleton or transient 2025-09-11 19:14:01 +00:00
FoxxMD 05a9ca15a9 refactor(cache): Update file cache name and gitignore 2025-09-11 19:14:01 +00:00
FoxxMD e2c8158aec feat: Log file cache save 2025-09-11 19:14:01 +00:00
FoxxMD 4eee6d4322 somewhat working scrobble caching 2025-09-11 19:14:01 +00:00
FoxxMD 56f5431dd9 chore: Update iti usage
* Update package to improve typing support/esm
* Remove dependency on sources/client/notifiers to prevent circular dependency
2025-09-11 19:14:01 +00:00
FoxxMD a00f94f9b1 feat: Implement caching backend
* Two-tier caching
  * primary is always in-memory LRU
  * secondary supports file and valkey
* Different caches for metadata and scrobbles
  * no file support for metadata (too big)
2025-09-11 19:14:01 +00:00
Matt Foxx df4fe87178 Merge pull request #351 from FoxxMD/ntfyToken
feat: Add Access Token auth for Ntfy webhook
2025-09-11 15:07:11 -04:00
Matt Foxx 89c39d2d25 Merge pull request #354 from FoxxMD/lastfmRecent
fix(lastfm): Full destructure recent response and log respon on error
2025-09-11 15:01:41 -04:00
FoxxMD b60422c26c fix: Try auth prefixed with Bearer 2025-09-11 18:57:30 +00:00
FoxxMD 59fce8271e feat: Add Access Token auth for Ntfy webhook 2025-09-11 18:57:30 +00:00
FoxxMD 506faf39dc fix(lastfm): Full destructure recent response and log respon on error
Maybe recenttracks/track does not exist for totally new accounts, though it should.

Guard against this and log response if something unexpected happens so we can troubleshoot

#353
2025-09-11 18:27:56 +00:00
Matt Foxx adf5401fff Merge pull request #347 from FoxxMD/ytm400
Youtube Music updates
2025-09-10 10:15:55 -04:00
FoxxMD dbf5cba7cb chore: Bump logging version
Fixes LOG_LEVEL non-output issue
2025-09-10 13:04:34 +00:00
FoxxMD b3a6beb8f4 docs(ytm): Add oauth warning 2025-09-10 12:57:42 +00:00
Matt Foxx 1291f60e62 Merge pull request #260 from FoxxMD/improvedArtistStringParsing
testing and improvements for artist string parsing
2025-09-10 08:47:50 -04:00
FoxxMD e86945355c fix: Add additional logging for ytm history request 2025-09-05 18:56:16 +00:00
FoxxMD 8dd9958b30 Use default scopes 2025-09-05 18:55:53 +00:00
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 33fda80d0a feat: Improve artist string parsing
* Don't split artist with joiner when only one joiner is present
* Override naive parsing if metabrainz mapped artists fit with provided joiners
2025-08-07 19:32:51 +00:00
FoxxMD a3d8372f48 feat: More artist parsing improvements
* Use more context-aware list parsing with ampersands
* Ignore artists with slashes when wrapped by word-boundary
2025-08-07 13:23:10 +00:00
FoxxMD b5dbde66c4 WIP testing and improvements for artist astring parsing
* Implement faker data generation for artist string
* Add test suite for parsing different styles of artist strings
2025-08-07 13:23:10 +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
231 changed files with 15976 additions and 7711 deletions
+25
View File
@@ -0,0 +1,25 @@
version: '3.7'
services:
# Update this to the name of the service you want to work with in your docker-compose.yml file
app:
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
# array). The sample below assumes your primary file is in the root of your project.
#
# build:
# context: .
# dockerfile: .devcontainer/Dockerfile
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ..:/workspaces:cached
command: sleep infinity
valkey:
image: valkey/valkey
env_file:
- path: ./.env
required: false
+10 -2
View File
@@ -3,7 +3,13 @@
{
"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-18-bookworm",
"dockerComposeFile": [
"compose.yml"
],
"service": "app",
"workspaceFolder": "/workspaces",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
@@ -20,7 +26,9 @@
"extensions": [
"hbenl.vscode-mocha-test-adapter",
"dbaeumer.vscode-eslint",
"unifiedjs.vscode-mdx"
"unifiedjs.vscode-mdx",
"bradlc.vscode-tailwindcss",
"TakumiI.markdowntable"
]
}
},
+9 -3
View File
@@ -12,8 +12,10 @@ jobs:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
with:
node-version: 20
cache: npm
@@ -30,6 +32,10 @@ jobs:
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
@@ -37,7 +43,7 @@ jobs:
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
permissions:
packages: write
steps:
- uses: dataaxiom/ghcr-cleanup-action@v1
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 #1.0.16
with:
token: ${{ secrets.GITHUB_TOKEN }}
+29 -16
View File
@@ -48,10 +48,13 @@ jobs:
# 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}}")
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
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
# 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
@@ -73,14 +76,14 @@ jobs:
- name: Login to Docker Hub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -89,13 +92,13 @@ jobs:
# metadata extract for docker labels/image names is done in merge job
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
# 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
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
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}}
@@ -114,7 +117,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -133,7 +136,7 @@ jobs:
- test
steps:
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
with:
path: /tmp/digests
pattern: digests-*
@@ -141,25 +144,25 @@ jobs:
- name: Login to Docker Hub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ vars.GHCR_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
with:
images: |
${{ vars.DOCKERHUB_SLUG }}
@@ -180,24 +183,34 @@ jobs:
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 ' *)
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
- 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 ' *)
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
- name: Inspect image dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
- name: Inspect image ghcr
if: ${{ vars.GHCR_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
combine-and-comment:
@@ -207,7 +220,7 @@ jobs:
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
steps:
- name: Create comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #2.9.4
with:
recreate: true
header: "pr-release"
+36 -19
View File
@@ -62,13 +62,19 @@ jobs:
# 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}}")
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
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
with:
persist-credentials: false
- name: Set short git commit SHA
id: appvars
@@ -94,14 +100,14 @@ jobs:
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
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
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -110,13 +116,13 @@ jobs:
# metadata extract for docker labels/image names is done in merge job
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
# 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
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
with:
build-args: |
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
@@ -129,11 +135,12 @@ jobs:
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
digest="${VARS_DIGEST}"
touch "/tmp/digests/${digest#sha256:}"
env:
VARS_DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -152,7 +159,7 @@ jobs:
- test
steps:
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
with:
path: /tmp/digests
pattern: digests-*
@@ -160,25 +167,25 @@ jobs:
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
uses: docker/login-action@v3
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
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
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
with:
images: |
${{ vars.DOCKERHUB_SLUG }}
@@ -208,21 +215,31 @@ jobs:
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 ' *)
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
- 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 ' *)
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
- name: Inspect image dockerhub
if: ${{ vars.DOCKERHUB_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
- name: Inspect image ghcr
if: ${{ vars.GHCR_SLUG != '' }}
run: |
docker buildx imagetools inspect ${{ vars.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
env:
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
+3 -2
View File
@@ -19,11 +19,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
+3
View File
@@ -118,8 +118,11 @@ dist
.pnp.*
config/*.json
config/mscache
config/yti-*
config/*.cache
*.txt
!robots.txt
.idea/
src/**/**.js
+3
View File
@@ -3,5 +3,8 @@
"extension": "ts",
"import": "tsx/esm",
"spec": "./src/backend/tests/**/*.test.ts",
"file": [
"./src/backend/tests/setup.ts"
],
"exit": true
}
+19
View File
@@ -16,6 +16,25 @@
"${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",
+12 -1
View File
@@ -1,5 +1,16 @@
{
"mochaExplorer.require": "tsx/esm",
"mochaExplorer.timeout": 1200000,
"mochaExplorer.exit": true
"mochaExplorer.exit": true,
"search.exclude": {
"**/.docusaurus": true,
"**/build": true
},
"tailwindCSS.files.exclude": [
"docsite/**"
],
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.experimental.configFile": "src/client/index.css"
}
+50 -42
View File
@@ -9,45 +9,53 @@
<img src="/assets/icon.png" 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 listen to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), [ListenBrainz](https://listenbrainz.org), and other services.
* Supports scrobbling from many **Sources**
* [Spotify](https://foxxmd.github.io/multi-scrobbler/docs/configuration#spotify)
* [Plex](https://foxxmd.github.io/multi-scrobbler/docs/configuration#plex) or [~~Tautulli~~](https://foxxmd.github.io/multi-scrobbler/docs/configuration#tautulli)
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jellyfin)
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webscrobbler)
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/docs/configuration#youtube-music)
* [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)
* [~~Deezer~~](https://foxxmd.github.io/multi-scrobbler/docs/configuration#deezer)
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris)
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mopidy)
* [JRiver](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jriver)
* [Kodi](https://foxxmd.github.io/multi-scrobbler/docs/configuration#kodi)
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#google-cast-chromecast)
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/docs/configuration#muikcube)
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpd-music-player-daemon)
* [VLC](https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc)
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#azuracast)
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#yamaha-musiccast)
* [Icecast](https://foxxmd.github.io/multi-scrobbler/docs/configuration#icecast)
* Supports scrobbling to many **Clients**
* [Maloja](https://foxxmd.github.io/multi-scrobbler/docs/configuration#maloja)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz)
* 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 scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/azuracast)
* [Deezer](https://foxxmd.github.io/multi-scrobbler/configuration/sources/deezer)
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/google-cast)
* [Icecast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/icecast)
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jellyfin)
* [JRiver](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jriver)
* [Kodi](https://foxxmd.github.io/multi-scrobbler/configuration/sources/kodi)
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/sources/koito-source)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source)
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-endpoint)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source)
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-endpoint)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/sources/maloja)
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mopidy)
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpd)
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpris)
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/configuration/sources/musikcube)
* [Plex](https://foxxmd.github.io/multi-scrobbler/configuration/sources/plex)
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/sources/rocksky-source)
* [Spotify](https://foxxmd.github.io/multi-scrobbler/configuration/sources/spotify)
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/tealfm-source)
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/configuration/sources/webscrobbler)
* [VLC](https://foxxmd.github.io/multi-scrobbler/configuration/sources/vlc)
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/yamaha-musiccast)
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/configuration/sources/youtube-music)
* Supports scrobbling to many [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients)
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/clients/koito)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/lastfm)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/clients/listenbrainz)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/clients/maloja)
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/clients/rocksky)
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/tealfm)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/configuration#webhook-configurations) or [healthcheck endpoint](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint)
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/configuration/clients#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
* 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), [flatpak](https://foxxmd.github.io/multi-scrobbler/docs/installation#flatpak), or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/docs/installation#nodejs)
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
<img src="/assets/status-ui.png" width="800">
@@ -60,15 +68,15 @@ 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**](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.
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/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?
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
### Source
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, like a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
Source configurations consist of:
@@ -78,7 +86,7 @@ Source configurations consist of:
### Client
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
Client configurations consist of:
@@ -87,15 +95,15 @@ Client configurations consist of:
## Quick Start
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
## Installation
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/docs/installation)
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
## Configuration
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration)
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
## Usage
@@ -110,11 +118,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](https://foxxmd.github.io/multi-scrobbler/docs/FAQ) before creating an issue!
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/docs/development/dev-common)
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
## License
+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",
}
}
]
+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"
}
}
]
+4 -2
View File
@@ -5,7 +5,8 @@
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
"username": "FoxxMD",
"url": "https://api.listenbrainz.org"
}
},
{
@@ -14,7 +15,8 @@
"configureAs": "source",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
"username": "FoxxMD",
"url": "https://api.listenbrainz.org"
}
}
]
+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"
+2 -1
View File
@@ -15,7 +15,8 @@
},
"options": {
"logPayload": true,
"logFilterFailure": "debug"
"logFilterFailure": "debug",
"ignoreInvalidCert": "false"
}
}
]
+18
View File
@@ -0,0 +1,18 @@
[
{
"name": "myRockskyClient",
"configureAs": "client",
"data": {
"key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",
"handle": "alice.bsky.social"
}
},
{
"name": "myRockskySource",
"configureAs": "source",
"data": {
"key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",
"handle": "alice.bsky.social"
}
}
]
+2 -1
View File
@@ -5,7 +5,8 @@
"data": {
"url": "http://localhost:4040/airsonic",
"user": "yourUser",
"password": "yourPassword"
"password": "yourPassword",
"usersAllow": ["yourUser"]
}
}
]
+18
View File
@@ -0,0 +1,18 @@
[
{
"name": "myTeal",
"configureAs": "client",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3"
}
},
{
"name": "myTealSource",
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
"appPassword": "twog-phu7-4dhe-y4j3"
}
}
]
+3 -3
View File
@@ -4,9 +4,9 @@ services:
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
+15 -69
View File
@@ -5,22 +5,21 @@ toc_max_heading_level: 5
## Connection Issues
### Plex (Webhook)/Tautulli/Jellyfin(Webhook)/Webscrobbler don't connect
### Webscrobbler/Listenbrainz Endpoint doesn't connect {#ingress-connection}
These sources are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server (or Webscrobbler extension) to contact multi-scrobbler, as opposed to multi-scrobbler contacting them.
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 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/Webscrobbler 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.
@@ -34,9 +33,9 @@ First, turn on **debug** logging for multi-scrobbler by setting the environmenta
##### Check Host name and URL
The URLs examples in the [configuration](configuration/configuration.mdx) documentation assume you are running Plex/Tautulli/Jellyfin/Webscrobbler 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.**
The URLs examples in the [configuration](/configuration#base-url) 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.**
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
EX `http://localhost:9078/api/webscrobbler` -> `http://192.168.0.140:9078/api/webscrobbler`
##### Check Firewall and Port Forwarding
@@ -44,53 +43,10 @@ If the machine multi-scrobbler is running on has a firewall ensure that port **9
##### 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.
###### Plex
Settings -> Manage -> Console
###### Tautulli
Check the command-line output of the application or docker logs.
###### Jellyfin
Administration -> Dashboard -> Advanced -> Logs
###### Webscrobbler
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.
### Jellyfin (Webhook) has warnings about undefined or missing data
Make sure you have
* [Configured the webhook plugin correctly](configuration/configuration.mdx#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 enabling Debug Mode with the env `DEBUG_MODE=true` which will output the raw payload to DEBUG level logging.
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
### Jellyfin (Webhook) 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.mdx#jellyfin)
* Add Request Header...
* **Key:** `Content-Type`
* **Value:** `application/json`
* Then Save
### Spotify/Deezer/LastFM won't authenticate
Ensure any **client id** or **secrets** are correct in your configuration.
@@ -104,10 +60,6 @@ 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`
### Deezer is not working
Deezer has discontinued support for their API and the Deezer Source is now [**deprecated.**](configuration/configuration.mdx#deezer) See [this issue for more discussion.](https://github.com/FoxxMD/multi-scrobbler/issues/175#issuecomment-2296776625)
### Youtube Music fails after some time
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
@@ -118,7 +70,7 @@ 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.**
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#authentication) 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
@@ -128,7 +80,7 @@ Refresh your authentication by using the **(Re)authenticate** link from MS's web
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.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
## Configuration Issues
@@ -152,17 +104,11 @@ This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scr
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 (Webhook) 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)
You can avoid this entirely by using the newer [Jellyfin API Source](configuration/configuration.mdx#jellyfin) which correctly handles multiple artists.
### 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.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.
To diagnose these issues you [**must enable payload logging**](/configuration/sources/google-cast#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
@@ -174,17 +120,17 @@ 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.mdx#allow-unknown-media-type) section to fix this
Refer to [Allow Unknown Media Type](/configuration/sources/google-cast#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.mdx#forcing-media-tracking) section to fix this
Refer to [Force Media Tracking](/configuration/sources/google-cast#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)
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](/configuration/sources/vlc#vlc-information-reporting)
### Youtube Music misses or duplicates scrobbles
@@ -223,7 +169,7 @@ In your YTM configuration (`ytmusic.json`) add `logDiff` under `options` like th
or set either ENVs:
* `YTM_LOG_DIFF=true`
* [`DEBUG_MODE=true`](configuration/configuration.mdx?#debug-mode)
* [`DEBUG_MODE=true`](/configuration#debug-mode)
This will cause MS to log YTM history changes similar to this:
+3 -3
View File
@@ -1,8 +1,8 @@
{
"label": "Configuration",
"position": 3,
"position": 5,
"link": {
"type": "generated-index",
"description": "Configuring Multi-Scrobbler and examples"
"type": "doc",
"id": "configuration/configuration"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,7 @@
{
"label": "Scrobble Clients",
"link": {
"type": "doc",
"id": "configuration/clients/clients"
}
}
@@ -0,0 +1,66 @@
---
sidebar_position: 2
title: Overview
toc_max_heading_level: 4
---
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
## Clients
| Name | Now Playing |
| :-------------------------------------------------- | :---------- |
| [Last.fm](/configuration/clients/lastfm) | ✅ |
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
| [Koito](/configuration/clients/koito) | ❌ |
| [Maloja](/configuration/clients/maloja) | ❌ |
| [Rocksky](/configuration/clients/rocksky) | ❌ |
| [Teal.fm](/configuration/clients/tealfm) | ❌ |
## Features
### Dead Scrobbles
If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up.
After it stops _automatically_ retrying the scrobble still exists and can be retried from the dashboard. Dead scrobbles persist between restart so you will never lose a scrobble that hasn't been successfully submitted yet.
### Now Playing
Multi-scrobbler can report the currently **playing** tracks it is monitoring to some Scrobble Clients via their individual **Now Playing** functionality.
Now Playing is **default enabled** for all Clients that support it.
The behavior multi-scrobbler uses for determining Now Playing reporting:
* Now Playing can be explicitly enabled or disabled globally using ENV `NOW_PLAYING=true` or `NOW_PLAYING=false`
* This only affects Clients that don't have behavior set via File/AIO (below)
* Now Playing will be only be reported to the same Clients a Source is configured to scrobble to
* A Source's Player status must be **Playing**, IE **active**, for reporting to occur
* This status can be verified via Player UI on the Dashboard
* If multiple Sources having active Players then the scrobble Client will default to reporting the track based on Source **configuration** name, alphabetically
* This is the `name` property set in [File or AIO source configs](/configuration?configType=file#configuration-types)
Clients can customize the Now Playing behavior individually using [File or AIO source configs:](/configuration?configType=file#configuration-types)
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"name": "myLastFmClient",
"configureAs": "client",
"data": {
// ...
},
"options": {
// disable or enable
//"nowPlaying": true
//
// OR define a list of Source *config* names that should be allowed to report Now Playing
// order of the list determines priority for reporting Now Playing
//"nowPlaying": ["mySpotify1","myJellyfin2"]
}
},
]
```
@@ -0,0 +1,60 @@
---
title: Koito
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/koito.json.example';
:::tip[Other Uses]
To monitor and re-scrobble activity from a Koito account create a [Koito (Source)](/configuration/sources/koito-source)
:::
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 `KOITO_USER`)
* 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` => `KOITO_URL` is set to `http://192.168.0.100:4110`
:::note[Koito Base URL]
Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/guides/scrobbler/) **is not required.** The Koito URL used for multi-scrobbler should only be the **base** URL. Only include a URL path if your Koito **base** url is not at `host:port`. See the table below for examples.
<details>
<summary>Base URL Examples</summary>
| `KOITO_URL` | MS Detected Base URL | Example Scrobble Api URL |
| :-------------------------------------------- | :------------------------------ | ---------------------------------------------------- |
| `http://192.168.0.100:4110` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
| `https://koito.mydomain.com` | `https://koito.mydomain.com` | `https://koito.mydomain.com/apis/listenbrainz/1` |
| `http://192.168.0.100:4110/apis/listenbrainz` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
| `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito/apis/listenbrainz/1` |
</details>
:::
:::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.
:::
## Configuration
<Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| KOITO_TOKEN | Yes | | API Key from your Koito Account |
| KOITO_USER | Yes | | Your Koito username |
| KOITO_URL | No | | The base URL for the Koito server |
</Config>
@@ -0,0 +1,36 @@
---
title: Last.fm
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
:::tip[Other Users]
* To monitor and re-scrobble activity from your Last.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
:::
[Register for an API account here.](https://www.last.fm/api/account/create)
The Callback URL is actually specified by multi-scrobbler but to keep things consistent you should use
```
http://localhost:9078/lastfm/callback
```
or replace `localhost:9078` with your own base URL.
## Configuration
<Config config="LastfmClientConfig" fileContent={JsonConfig} client name="lastfm">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-----------------------------------------|-------------------------------------------------------------------------------|
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
</Config>
@@ -0,0 +1,31 @@
---
title: Listenbrainz
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/listenbrainz.json.example';
:::tip[Other Users]
* To monitor and re-scrobble activity from a Listenbrainz account create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
:::
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
## Configuration
<Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-------------------------------|---------------------------------|
| LZ_TOKEN | Yes | | User token from your LZ profile |
| LZ_USER | Yes | | Your LZ username |
| LZ_URL | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
</Config>
@@ -0,0 +1,45 @@
---
title: Maloja
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/maloja.json.example';
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://myMalojaServerIP/admin_apikeys)
* Create a **New Key** and then copy the generated key value
Finally, add the Maloja server URL and API Key to the configuration type you choose to use, below.
## Configuration
<Config config="MalojaClientConfig" fileContent={JsonConfig} client name="maloja">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|-------------------------------|
| `MALOJA_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
</Config>
@@ -0,0 +1,72 @@
---
title: Rocksky
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
:::tip[Other Uses]
To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)](/configuration/sources/rocksky-source)
:::
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
<details>
<summary>How does that work?</summary>
<Bluesky name="Rocksky" lexicon="app.rocksky"/>
</details>
:::warning[Public Data]
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
:::
:::warning[No Scrobbling Guarantee]
Rocksky **does not guarantee** that every scrobble you send will be persisted. Rocksky's design goal is to store scrobbles with **metdata-complete, 3rd party verifiable track information** so that it can be a metadata-rich discovery platform, rather than an exhaustive scrobble data source-of-truth.
To do this it *asynchronously* matches your scrobble data against Musicbrainz's and/or Spotify's catalogs, *after the scrobble has been submitted.* **If the scrobble does not end up matching then it is not persisted.**
If you need your scrobbles to be 100% captured then you should scrobble to an *additional* [Scrobble Client](/configuration/clients).
:::
## Setup
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
:::note
Usage is adapted from [Rocksky docs](https://docs.rocksky.app/migrating-to-rocksky-scrobble-api-957839m0) on [docs.rocksky.app](https://docs.rocksky.app)
:::
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Bluesky account
* Then, navigate to the [RockSky developer dashboard](https://rocksky.app/apikeys) and obtain an API Key
## Configuration
### Handle
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
* For regular Bluesky account this will be like: `alice.bsky.social`
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
* For non-Bluesky users, you probably already know your handle
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
<Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| ROCKSKY_HANDLE | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
| ROCKSKY_KEY | Yes | | API Key obtained from Rocksky developer dashboard |
</Config>
@@ -0,0 +1,72 @@
---
title: Teal.fm
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/tealfm.json.example';
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
:::tip[Other Uses]
To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (Source)](/configuration/sources/tealfm-source)
:::
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
<details>
<summary>How does that work?</summary>
<Bluesky name="teal.fm" lexicon="fm.teal"/>
</details>
:::warning
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
:::
:::note
[teal.fm is still in developement](https://notes.teal.fm/3lzlwe6puis2l) and does not have a public website, yet.
To view your teal.fm scrobbles you can:
* use [wrapped.baileytownsend.dev](https://wrapped.baileytownsend.dev/) which gives a simple stats overview of your scrobbles
* use [atproto.at](https://atproto.at) to view the raw data (convenience URLs are generated in multi-scrobbler for you to do this)
:::
## Setup
#### Bluesky
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
* Navigate to https://bsky.app/settings/app-passwords
* Settings -> Privacy and Security -> App passwords
* Create a new **App Password** and save it somewhere, it will not be shown again
## Configuration
### Handle
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
* For regular Bluesky account this will be like: `alice.bsky.social`
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
* For non-Bluesky users, you probably already know your handle
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
<Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| TEALFM_IDENTIFIER | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
| TEALFM_APP_PW | Yes | | Bluesky/ATProto network App Password |
</Config>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@@ -3,21 +3,22 @@ sidebar_position: 3
title: Kitchen Sink
---
# Example Config using all Possible Features
# Example Complex Scenario
Scenario:
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
* Each person has their own Maloja server
* Each person has their own [Maloja](/configuration/clients/maloja) server
* Each person has their own Spotify account
* You have your own Airsonic (subsonic) server you to scrobble from
* You have your own Youtube Music account you want to scrobble from
* Mary has her own Last.fm account she also wants to scrobble to
* Fred has his own Spotify application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
* You have your own [Youtube Music](/configuration/soures/youtube-mysic) account you want to scrobble from
* Mary has her own [Last.fm](/configuration/clients/lastfm) account she also wants to scrobble to
* Fred has his own [Spotify](/configuration/sources/spotify) application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
* Fred has a Plex server and wants to scrobble everything he plays
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
* The three of you have a shared library on Plex called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
* Fred also has his own Jellyfin server and wants to scrobble everything he plays
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
* Fred also has his own [Jellyfin server](/configurion/sources/jellyfin) and wants to scrobble everything he plays
* You have an android music app that can scrobble to a [custom listenbrainz server](/configuration/sources/listenbrainz-endpoint)
### All-in-one Config
@@ -124,6 +125,14 @@ Scenario:
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
}
},
{
"type": "endpointlz",
"name": "listenbrainzfoxx",
"clients": ["foxxMaloja"],
"data": {
"token": "myToken"
}
}
],
"clients": [
{
@@ -246,7 +255,6 @@ Scenario:
```json5 title="CONFIG_DIR/ytmusic.json"
[
{
"type": "ytmusic",
"name": "foxxYoutube",
"clients": ["foxxMaloja"],
"data": {
@@ -255,6 +263,17 @@ Scenario:
}
]
```
```json5 title="CONFIG_DIR/endpointlz.json"
[
{
"name": "listenbrainzfoxx",
"clients": ["foxxMaloja"],
"data": {
"token": "myToken",
}
}
]
```
```json5 title="CONFIG_DIR/maloja.json"
[
-81
View File
@@ -1,81 +0,0 @@
---
sidebar_position: 2
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)
# Using Tautulli
## Create a new Notification Agent
* Navigate to the **Notification Agents** page in **Settings**
* Click **Add a new notification agent**
* Select **Webhook**
## Configure the Agent
The below sections correspond with the tabs available in the notification agent configuration popup.
### Configuration
* Webhook URL -- `http://localhost:9078/tautulli` (substitute your domain if different than the default)
* Webhook Method -- POST
### Triggers
Select **Watched**
### Conditions
Refer to [Tautulli's documentation](https://github.com/Tautulli/Tautulli-Wiki/wiki/Custom-Notification-Conditions) if you need help here. It may be a good idea to restrict notifications to only one library (if you have a Music library, for instance)
**This app will only scrobble an item if `media_type` is a "track", which is the default for all music.**
### Data
Expand the **Watched** dropdown and add the following code block to the **JSON Data** text field:
```
{
"artist_name": "{artist_name}",
"track_name": "{track_name}",
"track_artist": "{track_artist}",
"album_name": "{album_name}",
"media_type": "{media_type}",
"title": "{title}",
"duration": "{duration_sec}",
"username": "{username}",
"server": "{server_name}",
"version": "{server_version}",
"library": "{library_name}",
"player": "{player}",
"device": "{device}",
"platform": "{platform}",
"action": "{action}",
"machine_id": "{machine_id}",
"session_key": "{session_key}"
}
```
**Click the Save button to finish.**
Your agent is now configured and ready to scrobble.
# Using Plex Webhooks
* Navigate to your **Account/Settings** and find the **Webhooks** page
* Click **Add Webhook**
* URL -- `http://localhost:9078/plex` (substitute your domain if different than the default)
* **Save Changes**
Plex is now configured to scrobble.
@@ -0,0 +1,7 @@
{
"label": "Scrobble Sources",
"link": {
"type": "doc",
"id": "configuration/sources/sources"
}
}
@@ -0,0 +1,82 @@
---
title: Azuracast
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example';
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
:::tip[Scrobbling Threshold]
Azuracast may not correctly report track length or position. If this is the case you should reduce the "listened to" **duration** using [Scrobble Thresholds](/configuration/sources#scrobble-thresholds) so that multi-scrobbler scrobbles correctly.
:::
The Azuracast server should have **Use High-Performance Now Playing Updates** enabled in _Administration -> System Settings_
#### URL
The URL used by MS to connect to Azuracast has the syntax:
```
[ws|wss]://HOST:[PORT]
```
MS will automatically add the path required for websockets, [`/api/live/nowplaying/websocket`](https://www.azuracast.com/docs/developers/now-playing-data/#websockets), to your URL if none is provided. If you use a reverse proxy with a path-based URL or otherwise need a custom path to access the websockets path correctly then explicitly provide it. Examples:
```
URL From Config => MS Uses
'ws://192.168.0.101' => ws://192.168.0.101/api/live/nowplaying/websocket
'ws://azura.mydomain.com' => ws://azura.mydomain.com.com/api/live/nowplaying/websocket
'wss://mydomain.com/custom/azura/ws' => wss://mydomain.com/custom/azura/ws
```
#### Scrobble Behavior
:::info
<ShouldScrobble filename="azuracast"/>
:::
Multi-scrobbler will set the default **scrobbling behavior** based on if either of these settings is configured:
* live status (`AZURA_LIVE` or `monitorWhenLive`) or
* listener count (`AZURA_LISTENERS_NUM` or `monitorWhenListeners`)
If either is configured then MS will automatically scrobble based on when these conditions are met.
If neither is configured automatic scrobble defaults to **off** and you should either manually initiate it or explicitly set the default behavior.
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
| `AZ_URL` | Yes | | Azuracast URL *without station name* |
| `AZ_STATION` | Yes | | The station name shown on the public page |
| `AZURA_LIVE` | No | Yes | Only scrobble when station status is ONLINE |
| `AZURA_LISTENERS_NUM` | No | `true` | Only scrobble if station has any listeners (`true`) or listeners are equal-to/greater-than `X` |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/azuracast.json" language="json5">{AzuracastConfig}</CodeBlock>
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={AzuracastConfig} name="azuracast"/>
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
</TabItem>
</Tabs>
@@ -0,0 +1,153 @@
---
title: Deezer
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import DeezerConfig from '!!raw-loader!@site/../config/deezer.json.example';
import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecated.json.example';
<Tabs groupId="deezerSource" queryString>
<TabItem value="arl" label="Unofficial ARL">
:::warning
This Source uses unofficial methods to access Deezer data that are likely in violation of Deezer's TOS. Deezer may change or remove these methods at any time, breaking functionality. Use this Source at your own risk.
:::
:::info
This Source will work only if you have a **Premium** Deezer account.
:::
### Retrieve ARL
The `arl` is a browser cookie that Deezer uses to store your login information. Multi-scrobbler uses the `arl` value to make authorized requests to Deezer's API where it can retrieve listening history.
**After logging into Deezer**, use one of the methods below to retrieve the `arl` cookie value:
<Tabs groupId="arlRetrival">
<TabItem value="chrome" label="Chrome">
1. On deezer.com, press F12 to open the Developer Tools window.
2. Go to the Applications tab. Press the '>>' More tabs icon if it's hidden.
3. Under the Cookies Filter, click deezer.com, then the text bar name `arl`.
4. Copy all the contents shown in the **Cookie Value** pane.
</TabItem>
<TabItem value="firefox" label="Firefox">
1. On deezer.com, hit Ctrl+Shift+I to open Developer Tools
2. Go to the Storage Tab, then expand Cookies in the sidebar and select deezer.com
3. Find the row with `arl` as the name, then double click the Value column and right click -> copy the value
</TabItem>
</Tabs>
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ------------------------------------------------ |
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerConfig}</CodeBlock>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={DeezerConfig} name="deezer"/>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
</Tabs>
### Duplicate detection
Third party integrations with Deezer (Sonos) may cause Deezer to [report the same track many times in listening history.](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2922374738) If you experience this issue modify your Deezer Source config (file-based only) to include the option `"fuzzyDiscoveryIgnore": "aggressive"`
<details>
<summary>deezer.json example</summary>
```json title="deezer.json"
[
{
"name": "MyDeezer",
"data": {
"arl": "UOsRPjT3U5Dhaaup3x...",
},
"options": {
"fuzzyDiscoveryIgnore": "aggressive"
}
}
]
```
</details>
This option comes with some trade-offs: MS will aggressively detect repeated tracks within a window of time that should eliminate all duplicates. However, this will also prevent *intentionally* repeated tracks from being scrobbled. See [this thread](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2970417070) for more information on how this works.
</TabItem>
<TabItem value="official" label="Official API">
:::warning
**This Source is DEPRECATED because Deezer has dropped official API support.** This Source will **not** be removed but no further support or fixes will be given. [See this issue for more discussion.](https://github.com/FoxxMD/multi-scrobbler/issues/175#issuecomment-2296776625)
Users cannot create new applications on Deezer Developers and there is no guarantee existing applications will continue to work.
As a workaround consider:
* Using the alternative [Deezer Source that uses unofficial, internal API via ARL](./?deezerSource=arl)
* Connect Deezer with Last.fm and then use [Last.fm as a Source](/configuration/sources/lastfm-source)
:::
Create a new application at [Deezer Developers](https://developers.deezer.com/myapps)
* Application Domain must be the same as your multi-scrobbler domain. Default is `localhost:9078`
* Redirect URL must end in `deezer/callback`
* Default would be `http://localhost:9078/deezer/callback`
After application creation you should have credentials displayed in the "My Apps" dashboard. You will need:
* **Application ID**
* **Secret Key**
* **Redirect URL** (if not the default)
**If no access token is provided...**
After starting multi-scrobbler with credentials in-place open the dashboard (`http://localhost:9078`) and find your Deezer source. Click **(Re)authenticate and (re)start polling** to start the login process. After login is complete polling will begin automatically.
###### Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-----------------------------------------|-----------------------------------|
| `DEEZER_CLIENT_ID` | Yes | | Your **Application ID** |
| `DEEZER_CLIENT_SECRET` | Yes | | Your **Secret Key** |
| `DEEZER_REDIRECT_URI` | No | `http://localhost:9078/deezer/callback` | URI must end in `deezer/callback` |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerDeprecatedConfig}</CodeBlock>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={DeezerDeprecatedConfig} name="deezer"/>
or <SchemaLink lower objectName="DeezerSourceConfig"/>
</TabItem>
</Tabs>
</TabItem>
</Tabs>
@@ -0,0 +1,184 @@
---
title: Google Cast (Chromecast)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import ChromecastConfig from '!!raw-loader!@site/../config/chromecast.json.example';
If your media device can be **Cast** to using this button ![Chromecast Icon](https://upload.wikimedia.org/wikipedia/commons/2/26/Chromecast_cast_button_icon.svg) on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.
:::note
Google Cast support is **experimental**. You may experience crashes and errors while using this Source. Please open an issue if you experience problems and include all information detailed in the issue template to help debug your issue.
:::
:::note
This source relies on common, **basic** music data provided by the cast device which will always be less exhaustive than data parsed from full source integrations. If there is an existing [Source](/configuration/sources) it is recommended to configure for it and blacklist the app on Google Cast, rather than relying solely on Google Cast for scrobbling.
:::
#### Connecting Devices
Cast devices can be manually configured using file-based configuration OR automatically discovered using **mDNS.**
###### mDNS Discovery
The host machine running multi-scrobbler must be configured to allow [mDNS traffic on port 5353/UDP](https://book.hacktricks.xyz/network-services-pentesting/5353-udp-multicast-dns-mdns).
<details>
<summary>OS Specific Instructions</summary>
<Tabs>
<TabItem value="linux" label="Linux">
**Docker**
The host machine must have [avahi-daemon](https://avahi.org/) running to circumvent limitations with DNS resolution due to musl in Alpine. Most major linux distributions package avahi and many have it built-in. Once avahi is running you must pass D-Bus and the avahi daemon socket to your container like so:
```
docker run ... -v /var/run/dbus:/var/run/dbus -v /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket ... foxxmd/multi-scrobbler
```
**Flatpak/Nodejs**
No additional steps are required.
</TabItem>
<TabItem value="windows" label="Windows">
**Docker**
Unsupported at this time.
**Nodejs**
No additional steps are required.
</TabItem>
</Tabs>
</details>
#### What Media Does MS Scrobble?
Cast devices report what type of media the current activity is [(see `metadata` property here)](https://developers.google.com/cast/docs/media/messages#MediaInformation). The reported type is dependent on the application playing the media to correctly report it, the cast device does not magically know what the media is. If an application does not report a type it is always classified as `unknown`.
**By default, MS will only track media that is reported as `MusicTrack`.**
##### Allow Unknown Media Type
Media with an Unknown (`Generic`) media type can be explicitly allowed by setting `"allowUnknownMedia": true` in the file-based configuration. This can also be configured to only allow unknown media types for specific applications by using a list of application names.
<details>
<summary>Example</summary>
```json5 title="chromecast.json"
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
// only allow unknown if app name contains any of these phrases
"allowUnknownMedia": ["smarttube", "default media receiver"]
},
}
]
```
</details>
##### Forcing Media Tracking
MS can be forced to track media from an application regardless of media type. This is useful if an application incorrectly reports a media type you are sure should be music. Set `"forceMediaRecognitionOn"` in the file-based configuration to a list of application names that should always be tracked.
<details>
<summary>Example</summary>
```json5 title="chromecast.json"
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
// media from applications that contains these phrases will always be tracked, regardless of media type reported
"forceMediaRecognitionOn": ["smarttube", "default media receiver"]
},
}
]
```
</details>
#### Cast Troubleshooting
Please include any/all logs with raw output if there are any errors encountered as this is critical to diagnosing issues.
To diagnose bad/incomplete track information or strange MS player behavior please turn on **payload logging** and include log output of the source running to help diagnose this issue. Either enable in config using the below example OR enable [Debug Mode.](/configuration#debug-mode)
<details>
<summary>Example</summary>
```json5 title="chromecast.json"
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
//...
},
"options": {
"logPayload": true
}
}
]
```
</details>
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
:::note
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
:::
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------------------------------------------------------|
| CC_ENABLE | No | | Set to 'true' to enable Cast monitoring without needing to define other ENVs |
| CC_WHITELIST_DEVICES | No | | Only scrobble from these Cast devices. Comma-delimited list. EX mini-home, family-tv |
| CC_BLACKLIST_DEVICES | No | | Do not scrobble from these Cast devices. Comma-delimited list |
| CC_WHITELIST_APPS | No | | Only scrobble from these casted Apps. Comma-delimited list. EX spotify, pandora |
| CC_BLACKLIST_APPS | No | | Do not scrobble from these casted Apps. Comma-delimited list |
</TabItem>
<TabItem value="file" label="File">
<details>
<summary>Example</summary>
<CodeBlock title="CONFIG_DIR/chromecast.json" language="json5">{ChromecastConfig}</CodeBlock>
</details>
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<details>
<summary>Example</summary>
<AIOExample data={ChromecastConfig} name="chromecast"/>
</details>
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
</TabItem>
</Tabs>
@@ -0,0 +1,97 @@
---
title: Icecast
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import IcecastConfig from '!!raw-loader!@site/../config/icecast.json.example';
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
Parse metadata from any Icecast Station (streams) to use for scrobbling.
:::note
There is no standard format for presenting track information in an Icecast stream and the majority of streams that do include metadata only use a single string (`StreamTitle`) from which metadata can be parsed.
Multi-scrobbler will do its best to parse Artist/Album/Track information from this data but the quality and consistency for this is solely based on how the Icecast broadcaster chooses to format their tracks.
:::
:::note[Scrobbling Behavior]
Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, **automatic scrobbling for Icecast Sources is off by default.**
Use [Should Scrobble Behavior](/configuration/sources#should-scrobble-behavior) to initiate scrobbling or configure automatic scrobble.
:::
:::tip[Scrobbling Threshold]
Icecast does not report track lengths so multi-scrobbler cannot determine if it should scrobble based on percent played. You should instead set a shorter "listened to" **duration** using [Scrobble Thresholds.](/configuration/sources#scrobble-thresholds)
:::
#### URL and Sources
The URL should be the URL you would use to stream audio.
Additional sources are supported based on those found in [`icecast-metadata-stats`](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#supported-status-apis). To use these with automatic parsing [based on the regular URL](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options) include a `sources` array in your config the same way as shown in `icecast-metadata-stats`:
<details>
<summary>Example</summary>
```json5 title="config.json"
[
{
"name": "myIcy",
"enable": true,
"data": {
"url": "http://myIcecast.com/radio",
"sources": ["icy", "ogg", "sevenhtml"]
}
}
]
```
</details>
To manually set these sources include the same [`*Endpoint` options](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options) found in `icecast-metadata-stats`.
If no `sources` are configured for multi-scrobbler it will automatically try to use `icy` and `ogg`.
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ----------- |
| `ICECAST_URL` | Yes | | Icecast URL |
</TabItem>
<TabItem value="file" label="File">
<details>
<summary>Example</summary>
<CodeBlock title="CONFIG_DIR/icecast.json" language="json5">{IcecastConfig}</CodeBlock>
</details>
or <SchemaLink lower objectName="IcecastSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<details>
<summary>Example</summary>
<AIOExample data={IcecastConfig} name="icecast"/>
</details>
or <SchemaLink lower objectName="IcecastSourceConfig"/>
</TabItem>
</Tabs>
@@ -0,0 +1,67 @@
---
title: Jellyfin
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JellyfinConfig from '!!raw-loader!@site/../config/jellyfin.json.example';
Must be using Jellyfin 10.7 or greater
* Create an **API Key** for multi-scrobbler
* In the Jellyfin desktop web UI Navigate to -> Administration -> Dashboard -> API Keys (`http://YOUR_JELLYIN_URL/web/index.html#!/apikeys.html`)
* Click `+` button and create a new key with **App name** `multi-scrobbler`
* Copy the created API Key value for use in configuration below
It is **recommended** to use API Key + username but if you are not an admin for your Jellyfin instance you can also authenticate with your Jellyfin username and **password.**
:::tip[Important Defaults]
By default...
* multi-scrobbler will **only** scrobble for the user authenticated with the API.
* Allowed Users (`usersAllow` or `JELLYFIN_USERS_ALLOW`) are only necessary if you want to scrobble for additional users.
* multi-scrobbler will **only** scrobble media found in Jellyfin libraries that were labelled as **Music.**
* `librariesAllow` or `JELLYFIN_LIBRARIES_ALLOW` will override this
* OR use `additionalAllowedLibraryTypes` to allow more types (like `mixed` or `book` for audiobooks)
* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio.**
* To force multi-scrobbler to scrobble when media is detected as **Unknown** use `"allowUnknown": true` in file/aio configuration.
:::
#### Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
| -------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
</TabItem>
<TabItem value="file" label="File">
<CodeBlock title="CONFIG_DIR/jellyfin.json" language="json5">{JellyfinConfig}</CodeBlock>
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOExample data={JellyfinConfig} name="jellyfin"/>
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
</TabItem>
</Tabs>
@@ -0,0 +1,76 @@
---
title: JRiver
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JriverConfig from '!!raw-loader!@site/../config/jriver.json.example';
In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:
* Tools -> Options -> Media Network
* Check `Use Media Network to share this library...`
* If you have `Authentication` checked you will need to provide the **Username** and **Password** in the ENV/File configuration below.
### URL
The URL used to connect to JRiver, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
```
Syntax => [protocol]://[hostname]:[port]/[path]
Default => http://localhost:52199/MCWS/v1/
```
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
:::tip[Jriver Settings]
* Make sure the port number matches what is found in `Advanced` section in the Media Network options.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
:::
<details>
<summary>URL Transform Examples</summary>
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to JRiver:
| `url` | Transformed URL |
|------------------------------------------|---------------------------------------------|
| (none set) | `http://localhost:52199/MCWS/v1/` |
| `jriver.mydomain.com` | `http://jriver.mydomain.com:52199/MCWS/v1/` |
| `192.168.0.101:3456` | `http://192.168.0.101:3456/MCWS/v1/` |
| `mydomain.com:80/jriverReverse/MCWS/v1/` | `mydomain.com:80/jriverReverse/MCWS/v1/` |
</details>
## Configuration
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
<EnvType>
| Environmental Variable | Required | Default | Description |
|------------------------|----------|---------------------------------|------------------------------------------------|
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
</EnvType>
</TabItem>
<TabItem value="file" label="File">
<FileType>
<CodeBlock title="CONFIG_DIR/jriver.json" language="json5">{JriverConfig}</CodeBlock>
or <SchemaLink lower objectName="JRiverSourceConfig"/>
</FileType>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOType>
<AIOExample data={JriverConfig} name="jriver"/>
or <SchemaLink lower objectName="JRiverSourceConfig"/>
</AIOType>
</TabItem>
</Tabs>
@@ -0,0 +1,59 @@
---
title: Kodi
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import KodiConfig from '!!raw-loader!@site/../config/kodi.json.example';
import Config from '@site/src/components/GenericConfiguration.mdx';
In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can be done in the Kodi GUI:
* Settings -> Services -> Control
* Check `Allow remote control via HTTP`
* Ensure you have a **Username** and **Password** set, you will need to provide them in the ENV/File configuration below.
### URL
The URL used to connect to Kodi, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
```
Syntax => [protocol]://[hostname]:[port]/[path]
Default => http://localhost:8080/jsonrpc
```
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
:::tip[Kodi Settings]
* Make sure the port number matches what is found in **Port** in the Control section mentioned above.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
:::
<details>
<summary>URL Transform Examples</summary>
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to Kodi:
| `url` | Transformed URL |
|---------------------------------------|----------------------------------------------|
| (none set) | `http://localhost:8080/jsonrpc` |
| `kodi.mydomain.com` | `http://kodi.mydomain.com:8080/jsonrpc` |
| `192.168.0.101:3456` | `http://192.168.0.101:3456/jsonprc` |
| `mydomain.com:80/kodiReverse/jsonrpc` | `http://mydomain.com:80/kodiReverse/jsonrpc` |
</details>
## Configuration
<Config config="KodiSourceConfig" fileContent={KodiConfig} name="kodi">
| Environmental Variable | Required | Default | Description |
|------------------------|----------|-------------------------------|----------------------------|
| KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
| KODI_USERNAME | No | | The username set |
| KODI_PASSWORD | No | | The password set |
</Config>
@@ -0,0 +1,31 @@
---
title: Koito
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import KoitoConfig from '!!raw-loader!@site/../config/koito.json.example';
This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured [Clients.](/configuration/clients)
:::tip[Other Uses]
To _scrobble to_ a Koito server, create a [Koito (Client)](/configuration/clients/koito)
:::
See the [Koito (Client)](/configuration/clients/koito) configuration for general setup. The only difference for **Source** configuration:
* Cannot be setup with ENV config
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
#### Configuration
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito">
:::note
You cannot use ENV variables shown in the [Koito Client config](/configuration/clients/koito) -- multi-scrobbler assumes Koito ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Koito as a Source.
:::
</Config>
@@ -0,0 +1,46 @@
---
title: Lastfm (Endpoint)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import LastfmEndpointConfig from '!!raw-loader!@site/../config/endpointlfm.json.example';
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.
:::tip[Other Uses]
* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
* To monitor and re-scrobble activity from your Last.fm account, create a [Last.fm (Source)](/configuration/sources/lastfm-source)
:::
### URL
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Last.fm scrobbles at
```
http://localhost:9078/2.0/
```
which is the "standard" Last.fm server path for scrobbling
Use a slug only if you need to setup multiple Last.fm Endpoint sources and cannot use different tokens.
If a slug is used then the URL will be:
```
http://localhost:9078/api/lastfm/mySlug
```
## Configuration
<Config config="LastFMEndpointSourceConfig" fileContent={LastfmEndpointConfig} name="endpointlfm">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `LFMENDPOINT_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
| `LFM_SLUG` | No | | (Optional) The URL suffix to use for accepting LFM scrobbles |
</Config>
@@ -0,0 +1,29 @@
---
title: Lastfm
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import LastfmConfig from '!!raw-loader!@site/../config/lastfm.json.example';
This Source **monitors your Last.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
:::tip[Other Uses]
* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
:::
To configure this Source use the same registration instructions and configuration data shown in [Last.fm (Client)](/configuration/clients/lastfm) with the difference being `"configureAs": "source"` defined in the File/AIO configuration below.
## Configuration
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm">
:::note
You cannot use ENV variables shown in the [Last.fm Client config](/configuration/clients/lastfm) -- multi-scrobbler assumes Last.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Last.fm as a Source.
:::
</Config>
@@ -0,0 +1,68 @@
---
title: Listenbrainz (Endpoint)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import ListenbrainzEndpointConfig from '!!raw-loader!@site/../config/endpointlz.json.example';
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.
:::tip[Other Uses]
* To **scrobble to** Listenbrainz, create a [Listenbrainz (Client)](/configuration/clients/listenbrainz)
* To monitor and re-scrobble activity from your Listenbrainz account, create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
:::
### URL
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Listenbrainz scrobbles at
```
http://myMultiScrobblerIP:9078/1/submit-listens
```
which is the "standard" Listenbrainz server path for scrobbling.
In general, you should use `http://myMultiScrobblerIP:9078/1/` as the **base path** when configuring your application to scrobble to multi-scrobbler.
<details>
<summary>URL Setup for Popular Apps</summary>
##### Navidrome
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
</details>
Use a slug only if you need to setup multiple Listenbrainz Endpoint sources and cannot use different tokens.
If a slug is used then the URL will be:
```
http://myMultiScrobblerIP:9078/api/listenbrainz/mySlug
```
:::note
Some Listenbrainz applications may require custom Listenbrainz URLs to be a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
:::
### Token
Most Listenbrainz clients require a token (Authentication Token) to be provided during setup. This value can be anything you want, just make sure to use the same value for `token` in your multi-scrobbler configuration for the endpoint.
## Configuration
<Config config="ListenbrainzEndpointSourceConfig" fileContent={ListenbrainzEndpointConfig} name="endpointlz">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `LZENDPOINT_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
| `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application |
| `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles |
</Config>
@@ -0,0 +1,31 @@
---
title: Listenbrainz
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import ListenbrainzConfig from '!!raw-loader!@site/../config/listenbrainz.json.example';
This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients.
:::tip[Other Uses]
* To **scrobble to** a Listenbrainz server, create a [Listenbrainz (Client)](/configuration/clients/listenbrainz)
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
:::
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/).
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
## Configuration
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz">
:::note
You cannot use ENV variables shown in the [Listenbrainz Client config](/configuration/clients/listenbrainz) -- multi-scrobbler assumes Listenbrainz ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Listenbrainz as a Source.
:::
</Config>
@@ -0,0 +1,29 @@
---
title: Maloja
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import MalojaConfig from '!!raw-loader!@site/../config/maloja.json.example';
:::tip[Other Uses]
To _scrobble to_ a Maloja server, create a [Maloja (Client)](/configuration/clients/maloja)
:::
See the [Maloja (Client)](/configuration/clients/maloja) configuration for general setup. The only difference for **Source** configuration:
* Cannot be setup with ENV config
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
## Configuration
<Config config="MalojaSourceConfig" fileContent={MalojaConfig} name="maloja">
:::note
You cannot use ENV variables shown in the [Maloja Client config](/configuration/clients/maloja) -- multi-scrobbler assumes Maloja ENVs are always used for the **client** configuration. You must use the [[File or AIO](/configuration?configType=file#configuration-types) config to setup Maloja as a Source.
:::
</Config>
@@ -0,0 +1,102 @@
---
title: Mopidy
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import MopidyConfig from '!!raw-loader!@site/../config/mopidy.json.example';
Mopidy is a headless music server that supports playing music from many [standard and non-standard sources such as Pandora, Bandcamp, and Tunein.](https://mopidy.com/ext/)
multi-scrobbler can scrobble tracks played from any Mopidy backend source, regardless of where you listen to them.
:::note
You must have [Mopidy-HTTP extension](https://mopidy.com/ext/http) enabled for this integration to work.
:::
#### URL
The URL used to connect to the Mopidy server, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
```
Syntax => [protocol]://[hostname]:[port]/[path]
Default => ws://localhost:6680/mopidy/ws
```
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
:::tip[Mopidy Configuration]
Make sure the hostname and port number match what is found in the Mopidy configuration file `mopidy.conf`:
```ini
[http]
hostname = localhost
port = 6680
```
:::
<details>
<summary>URL Transform Examples</summary>
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to Mopidy:
| `url` | Transformed URL |
|--------------------------------|--------------------------------------------|
| (none set) | `ws://localhost:6680/mopidy/ws` |
| `mopidy.mydomain.com` | `ws://mopidy.mydomain.com:6680/mopidy/ws/` |
| `192.168.0.101:3456` | `ws://192.168.0.101:3456/mopidy/ws/` |
| `mopidy.mydomain.com:80/MOPWS` | `ws://mopidy.mydomain.com:80/MOPWS` |
</details>
### URI Blacklist/Whitelist
If you wish to disallow or only allow scrobbling from some sources played through Mopidy you can specify these using `uriBlacklist` or `uriWhitelist` in your config. multi-scrobbler will check the list to see if any string matches the START of the `uri` on a track. If whitelist is used then blacklist is ignored. All strings are case-insensitive.
<details>
<summary>Example</summary>
```json
{
"uriBlacklist": ["soundcloud"]
}
```
Will prevent multi-scrobbler from scrobbling any Mopidy track that start with a `uri` like `soundcloud:song:MySong-1234`
</details>
### Album Blacklist
For certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use "Soundcloud" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake Album name to this list. Multi-scrobbler will still scrobble the track, just without the bad data. All strings are case-insensitive.
<details>
<summary>Example</summary>
```json
{
"albumBlacklist": ["SoundCloud", "Mixcloud"]
}
```
If a track would be scrobbled like
```
Album: Soundcloud, Track: My Cool Track, Artist: A Cool Artist
```
then multi-scrobbler will instead scrobble
```
Track: My Cool Track, Artist: A Cool Artist
```
</details>
## Configuration
<Config config="MopidySourceConfig" fileContent={MopidyConfig} name="mopidy">
No ENV support
</Config>
@@ -0,0 +1,28 @@
---
title: MPD (Music Player Daemon)
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import MPDConfig from '!!raw-loader!@site/../config/mpd.json.example';
MS communicates with MPD using the [TCP client connection.](https://mpd.readthedocs.io/en/stable/user.html#client-connections)
You should uncomment/create the following settings in your mpd config:
```
bind_to_address "any" # or a specific ipv4/v6 address
port "6600"
```
## Configuration
<Config config="MPDSourceConfig" fileContent={MPDConfig} name="mpd">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|------------------|-------------|
| `MPD_URL` | No | `localhost:6600` | |
| `MPD_PASSWORD` | No | | |
</Config>
@@ -0,0 +1,79 @@
---
title: MPRIS
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import MPRISConfig from '!!raw-loader!@site/../config/mpris.json.example';
[MPRIS](https://specifications.freedesktop.org/mpris-spec/latest) is a standard interface for communicating with Music Players on **linux operating systems.**
If you run Linux and have a notification tray that shows what media you are listening to, you likely have access to MPRIS.
![Notification Tray](../mpris.jpg)
multi-scrobbler can listen to this interface and scrobble tracks played by **any media player** that communicates to the operating system with MPRIS.
##### Host Setup
MPRIS communication requires multi-scrobbler to have access to the host's dbus-daemon.
<Tabs groupId="dbus" queryString>
<TabItem value="local" label="Local">
If multi-scrobbler is running as a [**Local Installation**](../../installation/installation.mdx#nodejs), directly with node/npm, on the same host as MPRIS then no setup is required.
Remote setup is also possible, see the **Remote Unix Socket** section in the Docker tab.
</TabItem>
<TabItem value="docker" label="Docker">
If multi-scrobbler is running as a [**Docker Container**](../../installation/installation.mdx#docker) some modifications are required...
##### Local Unix Socket
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation/#linux-host) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
```yaml title="docker-compose.yaml"
environment:
- DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/UID/bus
volumes:
- /run/user/UID/bus:/run/user/UID/bus:ro
```
##### Remote Unix Socket
:::warning
This method is **insecure.** You should not expose dbus over a network unless access to the port is restricted and the network is trusted.
:::
Use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from the host's dbus socket to a listening TCP port.
To make dbus for user with uid `1000` available on port `6644`:
```shell
socat TCP-LISTEN:6644,reuseaddr,fork UNIX-CONNECT:/run/user/1000/bus
```
Then, add the following value to multi-scrobbler's ENVs:
```
DBUS_SESSION_BUS_ADDRESS=tcp:host=yourHostIP,port=6644
```
</TabItem>
</Tabs>
## Configuration
<Config config="MPRISSourceConfig" fileContent={MPRISConfig} name="mpris">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|----------------------------------------------------------------------------------|
| MPRIS_ENABLE | No | | Use MPRIS as a Source (useful when you don't need any other options) |
| MPRIS_BLACKLIST | No | | Comma-delimited list of player names not to scrobble from |
| MPRIS_WHITELIST | No | | Comma-delimited list of players names to ONLY scrobble from. Overrides blacklist |
</Config>
@@ -0,0 +1,38 @@
---
title: Musikcube
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import MusikcubeConfig from '!!raw-loader!@site/../config/musikcube.json.example';
In order to use [Musikcube](https://musikcube.com) configure it to accept [websocket connections](https://github.com/clangen/musikcube/wiki/remote-api-documentation) in **server setup**:
* Enable the **Metadata Server**
* Set a **Password**
Both of these settings are found in _Musikcube -> (s)ettings -> server setup_
![Server Setup](../musikcube.jpg)
The URL used by MS has the syntax:
```
[ws|wss]://HOST:[PORT]
```
The **port** is the same as shown in the server setup screenshot from above, under **metadata server enabled**. If no port is provided to MS it will default to `7905`.
If no URL is provided to MS it will try to use `ws://localhost:7905`
## Configuration
<Config config="MusikcubeSourceConfig" fileContent={MusikcubeConfig} name="musikcube">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-----------------------|--------------------------------------|
| `MC_URL` | No | `ws://localhost:7905` | Use port set for **metadata server** |
| `MC_PASSWORD` | Yes | | |
</Config>
@@ -0,0 +1,79 @@
---
title: Plex
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example';
import PlexConfig from '!!raw-loader!@site/../config/plex.json.example';
Find your [**Plex Token**](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) and make note of the **URL** and **Port** used to connect to your Plex instance.
<details>
<summary>Allowed Users and Defaults</summary>
**Multi-scrobbler will automatically scrobble for these users by default:**
* The User authenticated with the Plex Token
* and the **Local User**
The Local User (`PLEX_LOCAL_USER`) is how Plex identifies anyone directly accessing the Plex UI from a local IP (who does not need to login).
To allow MS to scrobble for other users use `usersAllow` or `PLEX_USERS_ALLOW` (env) from the below configuration docs. However, because you are overriding the default settings you must also explicitly list the authenticated user and the Local User if you want them to also be able to scrobble.
<details>
<summary>Examples</summary>
###### Defaults
If `usersallow` and `PLEX_USERS_ALLOW` are not defined then the Plex Token authenticated User and Local User will be scrobbled for.
###### Only A Specific User
* `"usersallow": ["SomeUser"]` or
* `PLEX_USERS_ALLOW: SomeUser`
Only the Plex user `SomeUser` will be scrobbled for. The Plex Token authenticated user and the Local User will not be scrobbled for.
###### A Specific User + Defaults
(Assuming the plex authenticated user is `FoxxMD`)
* `"usersallow": ["FoxxMD", "PLEX_LOCAL_USER", "SomeUser"]` or
* `PLEX_USERS_ALLOW: FoxxMD,PLEX_LOCAL_USER,SomeUser`
The Plex user SomeUser, the Plex Token authenticated user (FoxxMD) and the Local User will be scrobbled for.
</details>
</details>
<details>
<summary>Allowed Libraries and Defaults</summary>
By default multi-scrobbler will only scrobble media found in Plex libraries that are labelled as **Music.**
* `librariesAllow` or `PLEX_LIBRARIES_ALLOW` will override this
</details>
#### Configuration
<Config config="PlexApiSourceConfig" fileContent={PlexConfig} name="plex">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ---------------------------------------------------------------------- |
| `PLEX_URL` | **Yes** | | The URL of the Plex server IE `http://localhost:32400` |
| `PLEX_TOKEN` | **Yes** | | The **Plex Token** to use with the API |
| `PLEX_USERS_ALLOW` | No | | Comma-separated list of usernames (from Plex) to scrobble for |
| `PLEX_USERS_BLOCK` | No | | Comma-separated list of usernames (from Plex) to disallow scrobble for |
| `PLEX_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
| `PLEX_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
| `PLEX_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
| `PLEX_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
</Config>
@@ -0,0 +1,39 @@
---
title: Rocksky
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
:::tip[Other Uses]
* To **scrobble to** Rocksky, create a [Rocksky (Client)](/configuration/clients/rocksky)
:::
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
This Source monitors the Rocksky scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients.
Learn more about how Rocksky scrobbles work in the [Rocksky (Client)](/configuration/clients/rocksky) docs.
## Setup
Follow the **Setup** instructions in the [Rocksky (Client)](/configuration/clients/rocksky#setup) docs to get access to your Bluesky PDS repository and scrobble data.
## Configuration
See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration for general setup. The only difference for **Source** configuration:
* Cannot be setup with ENV config
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
<Config config="RockskyClientConfig" fileContent={JsonConfig} name="rocksky">
:::note
You cannot use ENV variables shown in the [Rocksky Client config](/configuration/clients/rocksky#configuration) -- multi-scrobbler assumes Rocksky ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Rocksky as a Source.
:::
</Config>
@@ -0,0 +1,155 @@
---
sidebar_position: 2
title: Overview
toc_max_heading_level: 4
---
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
## Sources
| Name | Networking | Scrobble SOT | [Multi Device/User](#multi-deviceuser) | [Scrobble Destination](#limiting-scrobble-destination) | [Thresholds](#scrobble-thresholds) | [Should Scrobble](#should-scrobble-behavior) |
| :---------------------------------------------------------------------- | :------------------------------------------------------- | :-------------------------------------------------- | :------------------------------------- | :----------------------------------------------------- | :--------------------------------- | :------------------------------------------- |
| [Azuracast](/configuration/sources/azuracast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ✅ |
| [Deezer](/configuration/sources/deezer) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ✅ | ✅ | ❌ | ❌ |
| [Google Cast (Chromecast)](/configuration/sources/google-cast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Icecast](/configuration/sources/icecast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ✅ |
| [Jellyfin](/configuration/sources/jellyfin) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [JRiver](/configuration/sources/jriver) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Kodi](/configuration/sources/kodi) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Koito](/configuration/sources/koito-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Last.fm](/configuration/sources/lastfm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint) | [Ingress](./?sourceComm=ingress#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [ListenBrainz](/configuration/sources/listenbrainz-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [ListenBrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) | [Ingress](./?sourceComm=ingress#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Maloja](/configuration/sources/maloja) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Mopidy](/configuration/sources/mopidy) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [MPD (Music Player Daemon)](/configuration/sources/mpd) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Musikcube](/configuration/sources/musikcube) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Plex](/configuration/sources/plex) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Rocksky](/configuration/sources/rocksky-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Spotify](/configuration/sources/spotify) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [Subsonic-compatible APIs](/configuration/sources/subsonic) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
| [teal.fm](/configuration/sources/tealfm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [WebScrobbler](/configuration/sources/webscrobbler) | [Ingress](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [VLC](/configuration/sources/vlc) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Yamaha MusicCast](/configuration/sources/yamaha-musiccast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
| [Youtube Music](/configuration/sources/youtube-music) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
## Features
### Types of Sources
The Sources implemented in multi-scrobbler can be broken down into two categories.
#### By Communication Method
How does this Source get data from the service/application?
<Tabs groupId="sourceComm" queryString>
<TabItem value="active" label="Active">
The MS Source makes network requests to the service which returns a response with data MS can use to monitor listening activity or scrobbles.
</TabItem>
<TabItem value="ingress" label="Ingress">
The MS Source waits for the service/application to send data to multi-scrobbler.
These types of Sources require that multi-scrobbler is accessible to the service, generally through an open port or reverse proxy.
</TabItem>
</Tabs>
#### By Data Source of Truth
How does MS determine if/when a song has been played and is scrobbable?
<Tabs groupId="sot" queryString>
<TabItem value="activity" label="Listening Activity">
These Sources expose some kind of real-time listening data for users, like:
* player state (paused, stopped, playing)
* player position (1:50/2:40)
* current listened duration (Track A playing for 1m:20s)
For theses Sources multi-scrobbler keeps track of the real-time state of your activity and scrobbles a track after it meets certain [scrobble thresholds](#scrobble-thresholds).
</TabItem>
<TabItem value="history" label="Listening History">
Some Sources expose their own version of "this track has been scrobbled."
* Listenbrainz, Koito, and Last.fm are all scrobble services that have "definitive" histories of scrobbled tracks.
For these Sources multi-scrobbler monitors the listening history data and scrobbles when it sees a new entry in the list.
</TabItem>
</Tabs>
### Multi-Device/User
Some Sources report which User is playing a track and/or on what device the track is being played. These Sources can be filtered with configuration to only monitor activity for specific devices or users.
### Limiting Scrobble Destination
To limit which Clients a Source will scrobble to use the `clients` property in a Source's [File/AIO configuration](/configuration?configType=file#configuration-types) to specify the **name** property of the Clients that it should scrobble to:
```json5 title="jellyfin.json"
{
"name": "MyJellyfin",
"clients": ["myMaloja","koitoA","listenbrainzFoo"]
"data": {
// ...
},
}
```
:::note
If `clients` is empty (`"clients": []`) or is omitted then the default is for the Source to scrobble to **all** configured Clients.
:::
:::note
If a Client is configured using [ENV](/configuration?configType=env#configuration-types) then it is given a generated name. Check the multi-scrobbler Dashboard to find their name in a subheader under the Client type. Example:
![Client Name Subheader](../client-subheader.png)
Otherwise, use the `name` property you gave the client in it's File/AIO config.
:::
### Scrobble Thresholds
<ScrobbleThreshold filename="spotify"/>
These can also be configured **globally** for all Sources using ENVs:
```
SOURCE_SCROBBLE_DURATION=40
SOURCE_SCROBBLE_PERCENT=20
```
### Should Scrobble Behavior
:::note
This feature is currently available only for some Sources. Check each Source's documentation to see if it supported.
:::
For _most_ Sources multi-scrobbler can determine if a user is listening and scrobble automatically. For Sources which multi-scrobbler cannot do this or require additional configuration, a user can indicate to MS that it should or should not scrobble. This action can be taken on the Web Dashboard by clicking the link next to **Should Scrobble** found in each Source's status card.
Additionally, for these Sources, the default behavior for Scrobbling can be explicitly set by adding the following value to a Source's `options` in [file or aio config](/configuration?configType=file#configuration-types):
```json5 title="azuracast.json"
{
"data": {
// ...
},
"options": {
// set to true to always scrobble automatically
// set to false to never scrobble automatically
"systemScrobble": true
}
}
```
@@ -0,0 +1,105 @@
---
title: Spotify
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import SpotifyConfig from '!!raw-loader!@site/../config/spotify.json.example';
<details>
<summary>Spotify and Automix</summary>
If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-is-Automix/ta-p/5257278) enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is "mixed" in your queue with a shorter play time than its actual length and [Spotify does not report this modified play time in its API.](https://community.spotify.com/t5/Spotify-for-Developers/Wrong-duration-ms-of-track-with-Automix/m-p/5429147) This **does not affect MS's ability to scrobble** from Spotify but it will affect the accuracy of the duration MS reports was played.
</details>
#### Authenticate Spotify with Multi-Scrobbler
To access your Spotify history you must [create a Spotify App](https://developer.spotify.com/dashboard) to get a
Client ID/Secret.
1. **Login to https://developer.spotify.com** with your existing Spotify account and accept Developer Terms
2. Navigate to your Spotify Developer [Dashboard](https://developer.spotify.com/dashboard) and start the **Create App** process
3. Determine the correct **Redirect URI** to use and set it in your App settings
<details>
<summary>Redirect URI Instructions</summary>
A Redirect URI is the URL that Spotify will navigate your **browser** to after you complete authorization. The URL will contain the code necessary for multi-scrobbler to get a Spotify access token.
Spotify [**no longer allows insecure URIs**](https://developer.spotify.com/documentation/web-api/tutorials/migration-insecure-redirect-uri) (start with `http://`) unless the address is `127.0.0.1` -- so `localhost` and internal IPs (`192.168.0.xxx`) no longer work.
Use **one** of the following methods to specify a valid Redirect URI and complete Spotify authentication with multi-scrobbler:
<Tabs groupId="spotifyRedirect" queryString>
<TabItem value="https" label="HTTPS">
**Use a domain you control, with SSL certificates/HTTPS enabled, to create a valid redirect URI.**
For example, if you are already running multi-scrobbler behind a reverse proxy (nginx/traefik/caddy) at `https://scrobbler.mydomain.com` then set the Spotify Redirect URI for your Spotify App, and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV, to `https://scrobbler.mydomain.com/callback`
Alternatively, if multi-scrobbler is not accessible behind your domain, then use the [**Echo**](./?spotifyRedirect=echo) method with your own domain instead of 127.0.0.1.
</TabItem>
<TabItem value="local" label="Run MS Locally">
**Run multi-scrobbler, configured for a Spotify Source, from the same machine as your browser in order to get credentials.**
In this scenario you can use `http://127.0.0.1:9078/callback` as the Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV.
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation#storage) to the final location of where multi-scrobbler will be run.
</TabItem>
<TabItem value="echo" label="Echo">
**Use a docker container on the same machine your browser is on to "intercept" the callback URL from Spotify authentication, then manually finish the process by changing the domain to your multi-scrobbler instance.**
1. Create a container on the same machine as your browser using the [`mendhak/http-https-echo:36`](https://github.com/mendhak/docker-http-https-echo) image. This container runs a simple HTTP server that echos back any request it recieves.
```shell
docker run --rm -p 127.0.0.1:5079:8080 mendhak/http-https-echo:36
```
2. Set the Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV as `http://127.0.0.1:5079/callback`
3. In [Authenticate Spotify with Multi-Scrobbler](#authenticate-spotify-with-multi-scrobbler) (outside these Redirect Instructions) go to **Step 4** and continue the instructions
Eventually, Spotify will redirect you to a web page with a URL like `http://127.0.0.1:5079/callback?code=xxxxxx...`
![echo](../echo.png)
4. In the URL, replace `127.0.0.1:5079` with the address/IP for your multi-scrobbler instance so the URL is similar to `http://192.168.0.101:9078/callback?code=xxxxxx...`. Then navigate to this URL to complete authentication.
</TabItem>
<TabItem value="msEcho" label="Echo from echo.multi-scrobbler.app">
For your convenience, I run the [`mendhak/http-https-echo:36`](https://github.com/mendhak/docker-http-https-echo) container from the [**Echo**](./?spotifyRedirect=echo) approach on a domain I, the multi-scrobbler developer [FoxxMD](https://github.com/FoxxMD), control and self-host. Please be aware:
* I **do not collect personal data** or any querystrings/data sent in requests to this site
* It's **not possible** for me to use the code sent by Spotify to this URL without also having your Client ID/Secret
* However, there is **always** risk involved with trusting a service you do not control. Evaluate the risk of using this site for yourself.
Use the [**Echo**](./?spotifyRedirect=echo) approach, starting from **Step 2**, with **https://echo.multi-scrobbler.app/callback** as Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV.
</TabItem>
</Tabs>
</details>
4. Save your new App, then copy the Client ID/Secret from the App's Basic Information page.
5. Add the Client ID, Secret, and Redirect URI to the respective field/ENV in the [configuration section below](#configuration)
6. Start multi-scrobbler, then visit the Web Dashboard and click **(Re)authenticate** on the Spotify card to start the auth process
## Configuration
<Config config="SpotifySourceConfig" fileContent={SpotifyConfig} name="spotify">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | -------------------------------- | -------------------------- |
| `SPOTIFY_CLIENT_ID` | Yes | | |
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
| `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` |
</Config>
@@ -0,0 +1,39 @@
---
title: Subsonic
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import SubsonicConfig from '!!raw-loader!@site/../config/subsonic.json.example';
This Source can be used for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) and supports the [`getNowPlaying`](http://www.subsonic.org/pages/api.jsp#getNowPlaying) endpoint (such as [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
:::warning[Known Issues]
* Multiple artists are reported as one value and cannot be separated
* If using [Airsonic Advanced](https://github.com/airsonic-advanced/airsonic-advanced) the password used (under **Credentials**) must be **Decodable**
:::
:::tip
If your serivce supports scrobbling to **Listenbrainz** (such as Navidrome), considering using a [**Listenbrainz (Endpoint)** Source](/configuration/sources/listenbrainz-endpoint) instead of this one.
Service-specific scrobble implementations tend to be more accurate and provide more information (multiple artists) compared to the Subsonic API.
:::
Use the optional `usersAllow` property with **File** or **AIO** configuration to restrict scrobbling to a list of defined users.
## Configuration
<Config config="SubsonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------|
| `SUBSONIC_USER` | Yes | | |
| `SUBSONIC_PASSWORD` | Yes | | |
| `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server |
</Config>
@@ -0,0 +1,39 @@
---
title: Teal.fm
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/tealfm.json.example';
:::tip[Other Uses]
* To **scrobble to** an ATProto PDS, create a [teal.fm (Client)](/configuration/clients/tealfm)
:::
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
This Source monitors the teal.fm scrobbles in your PDS and then re-scrobbles discovered tracks to configured clients.
Learn more about how teal.fm scrobbles work in the [teal.fm (Client)](/configuration/clients/tealfm) docs.
## Setup
Follow the **Setup** instructions in the [teal.fm (Client)](/configuration/clients/tealfm#setup) docs to get access to your Bluesky PDS repository and scrobble data.
## Configuration
See the [teal.fm (Client)](/configuration/clients/tealfm#setup) configuration for general setup. The only difference for **Source** configuration:
* Cannot be setup with ENV config
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
<Config config="TealClientConfig" fileContent={JsonConfig} name="tealfm">
:::note
You cannot use ENV variables shown in the [teal.fm Client config](/configuration/clients/tealfm#configuration) -- multi-scrobbler assumes teal.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup teal.fm as a Source.
:::
</Config>
@@ -0,0 +1,94 @@
---
title: VLC
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/vlc.json.example';
MS communicates with [VLC](https://www.videolan.org/vlc/) using the [Web (`http`) interface module](https://wiki.videolan.org/VLC_HTTP_requests/)
#### Enable HTTP Interface
Open VLC:
* **Tools** Menu -> Preferences
* In the bottom left change **Show Settings** from `Simple` to `All`
* In the updated window select `Interface -> Main interfaces`
* In the **Extra interface modules** section enable **Web** and verify that `http` is shown in the textbox
* Select `Interface -> Main interfaces -> Lua`
* In Lua HTTP -> Password -> set a password in this box
* Click **Save** at the bottom to persist your changes
* Restart VLC
* Verify the HTTP interface is accessible by navigating to `http://localhost:8080` in your browser
If the interface is accessible, after typing in your password, VLC is now ready to be used by MS.
##### Setting VLC Interface and Port
These can be set by starting VLC from command line with specific options:
* `vlc --http-host yourHostIp:yourPort` or
* `vlc --http-port yourPort` or
* modify the configuration file for VLC by editing keys with the same values as from the command line (`http-host` and `http-port`)
#### Filename Parsing
If the file being played in VLC does not have ID3 tags/metadata information multi-scrobbler can attempt to extract artist, title, and album information from the raw filename using regular expressions. Mutli-scrobbler will only do this if there is **no other information** reported by VLC about these fields.
To extract this information use [named capture groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group) in your expression matching the field that should be extracted: `artist` `title` and/or `album`. MS accepts multiple expressions, the first one that matches a filename will be used.
Example usage in [a file-based config:](/configuration?configType=file#configuration-types)
```json5 title="config/vlc.json" {10-15}
[
{
"name": "MyVlc",
"enable": true,
"data": {
// ...
},
"options": {
"filenamePatterns": [
// Extracts artist and title from filenames that look like:
// My Artist - My Cool title.mp4
"/^\\s*(?<artist>.+?) - (?<title>.+?)\\.\\w+$/i"
],
// logs to DEBUG when MS tries to extract data from a filename
"logFilenamePatterns": true,
}
}
]
```
#### Vlc Information Reporting
If you find that VLC is incorrectly reporting track information (in its interface) but multi-scrobbler is not parsing it correctly it may be due to the _name_ of the fields VLC is reporting. mutli-scrobbler uses the most common field name but does not cover all use cases. Before reporting an issue please turn on metadata logging and include output from your logs while it is turned on:
```json5 title="config/vlc.json" {9-9}
[
{
"name": "MyVlc",
"enable": true,
"data": {
// ...
},
"options": {
"dumpVlcMetadata": true
}
}
]
```
## Configuration
<Config config="VLCSourceConfig" fileContent={JsonConfig} name="vlc">
| Environmental Variable | Required? | Default | Description |
|------------------------|------------|------------------|-------------|
| `VLC_URL` | No | `localhost:8080` | |
| `VLC_PASSWORD` | Yes | | |
</Config>
@@ -0,0 +1,95 @@
---
title: Webscrobbler
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/webscrobbler.json.example';
After installing the [Webscrobbler extension](https://web-scrobbler.com/) open the preferences/settings for it:
* Under **Accounts**
* **Add Webhook**
* API URL: `[YourMultiScrobblerInstance]/api/webscrobbler` EX `http://localhost:9078/api/webscrobbler`
* Application name: `(whatever you want)`
Reload the extension after adding the webhook.
:::note
* **On Firefox** - Only FQNs (domain.tld), `localhost`, and `127.0.0.1` are supported for API URL due to [firefox requiring https](https://github.com/web-scrobbler/web-scrobbler/issues/4183#issuecomment-1749222006)
* **On Chromium-based Browsers** - Any domain will work for API URL
* All other browsers are untested
:::
:::warning
Multi-scrobbler is **not** designed to be publicly accessible from the internet. To use Webscrobbler outside your local network you should be accessing MS through a VPN or through a reverse proxy with some kind of authentication (though this is still not recommended).
:::
### Multiple Users
If you would like use multiple WebScrobbler sources they can be matched using a **slug** at the end of the **API URL.** This requires using [a file-based config.](/configuration?configType=file#configuration-types)
<details>
<summary>Example</summary>
```json title="webscrobbler.json"
[
{
"name": "aUserWS",
"clients": [
"client1Maloja"
],
"data": {
"slug": "usera"
}
},
{
"name": "bUserWS",
"clients": [
"client2Maloja"
],
"data": {
"slug": "userb"
}
}
]
```
* To use `aUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/usera`
* To use `bUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/userb`
:::tip
`http://localhost:9078/api/webscrobbler` is matched with the first source that _that does not have a slug defined._
:::
</details>
### Connectors Black/Whitelist
MS can be configured to only scrobble, or NOT scrobble, from some WS connectors. Use the name of the website from the [supported websites](https://web-scrobbler.com/) or from the **Connectors** tab in the extension.
:::note
This affects **only** MS's behavior and does not affect the general connector behavior you have configured within the WebScrobbler extension.
:::
## Configuration
<Config config="WebscrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------------------------------------------|
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
| WS_WHITELIST | No | | Only scrobble from these WebScrobbler Connectors. Comma-delimited list |
| WS_BLACKLIST | No | | Do not scrobble from these WebScrobbler Connectors. Comma-delimited list |
</Config>
@@ -0,0 +1,37 @@
---
title: Yamaha MusicCast
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/musiccast.json.example';
Monitor [MusicCast](https://usa.yamaha.com/products/contents/audio_visual/musiccast/index.html) device/receivers for music played on Network/USB/CD inputs.
:::tip[Scrobbling Threshold]
Musiccast may not correctly report track length. If this is the case you should reduce the "listened to" **duration** using [Scrobble Thresholds](/configuration/sources#scrobble-thresholds) so that multi-scrobbler scrobbles correctly.
:::
##### URL
If only a domain/IP:PORT is provided to multi-scrobbler it will automatically append the default Musiccast API path. If your Musiccast device is located behind a custom URL (or reverse proxy) you should explicitly provide the base path to use.
EX
| `url` | Transformed URL |
| --------------------------------------- | :---------------------------------------------- |
| `http://192.168.0.101` | `http://192.168.0.101/YamahaExtendedControl/v1` |
| `http://mydomain.com/reverseProxy/cast` | `http://mydomain.com/reverseProxy/cast` |
## Configuration
<Config config="MusicCastSourceConfig" fileContent={JsonConfig} name="musiccast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :----------------------- |
| `MCAST_URL` | Yes | | The Musiccast device URL |
</Config>
@@ -0,0 +1,189 @@
---
title: Youtube Music
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/ytmusic.json.example';
:::warning
Communication with YT Music is **unofficial** and not supported or endorsed by Google. This means that **this integration may stop working at any time** if Google decides to change how YT Music works in the browser.
:::
:::tip[Scrobble Troubleshooting]
Due to monitoring being unofficial, listening history from YTM can be inconsistent and can cause missed scrobbles.
[**See the FAQ**](/FAQ.md#youtube-music-misses-or-duplicates-scrobbles) for a detailed explanation, how to see more details about MS's detection of tracks, and how to properly report an issue.
:::
#### Authentication {#ytm-auth}
Only one of these methods needs to be used.
<Tabs groupId="ytmAuth" queryString>
<TabItem value="cookie" label="Cookies">
Use instructions from
* https://github.com/patrickkfkan/Volumio-YouTube.js/wiki/How-to-obtain-Cookie or
* https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers
to get the **Cookie** value from a browser.
It is highly recommended to [get the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) to limit the chance the session is invalidated from normal browsing.
Add the cookie to your `ytmusic.json` config in `data` or as an ENV:
```json
{
"type": "ytmusic",
"enable": true,
"name": "MyYTM",
"data": {
"cookie": "__Secure-1PSIDTS=sidts-CjEB3EgAEvCd-......"
},
"options": {
"logAuthUpdateChanges": true,
"logDiff": true
}
}
```
If MS gives you authentication errors (session invalidated) at some point in the future follow the same instructions to get new cookies.
</TabItem>
<TabItem value="oauth" label="OAuth Client">
:::warning
As of Sept 25' OAuth clients no longer seem to have permission to access the YTM service, as reported by the upstream YTM library developer and [several other users.](https://github.com/FoxxMD/multi-scrobbler/issues/345#issuecomment-3258518769)
If you have existing, working OAuth credentials keep using them but there is no gaurantee they will continue to work.
If you are setting up the YTM Source for the first time you should first try to use [Cookies](./?ytmAuth=cookie#ytm-auth).
:::
[Based on the instructions from here...](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2479689924)
* Login to [Google Cloud console](https://console.cloud.google.com/) (create an account, if necessary)
* [Create a new project](https://console.cloud.google.com/projectcreate)
* Go to APIs and services.
* Configure the OAuth consent screen
* Use the old experience if possible
* If new is unavoidable then do not fill out any branding and under Authorized Domains you can delete the empty one (in order to save)
* Add yourself as an authorized user
* Navigate to Credentials
* Create Credentials -> choose "OAuth client ID"
* Application Type is **Web Application**
* **Name** is whatever you want, leave Authorization Javascript origins blank
* Authorized redirect URIs
* This must be **exactly** the same as what is displayed in MS! For now leave it blank so we can generate it from MS first
* Create
* In the newly created client popup save the **Client ID** and **Client Secret**, then copy them into `ytmusic.json` or appropriate ENVs:
```json
{
"type": "ytmusic",
"enable": true,
"name": "MyYTM",
"data": {
"clientId": "8910....6jqupl.apps.googleusercontent.com",
"clientSecret": "GOCSPX-WGXL6BSuQ343..."
},
"options": {
"logAuthUpdateChanges": true,
"logDiff": true
}
}
```
Now, start MS and during the YTMusic startup it will log something like this:
```
Will use custom OAuth Client:
Client ID: ...
Client Secret: ...
Redirect URI: http://localhost:9078/api/ytmusic/callback?name=MyYTM
```
If the beginning of the Redirect URI (before `api`) is EXACTLY how you would reach the MS dashboard from your browser (EX `http://localhost:9078`) then edit your google oauth client section for `Authorized redirect URIs` and add the URL MS has displayed.
If it is NOT EXACTLY the same you either need to set MS's [base url](https://foxxmd.github.io/multi-scrobbler/docs/configuration/#base-url) or you can provide your own (Custom) Redirect URI for MS to use by setting it in `ytmusic.json` or ENV.
<details>
<summary>Using a Custom Redirect URI</summary>
The three parts of the URL that must be the same:
* it must start with `api` (after domain or subdirectory IE `my.domain.tld/api...` or `whatever.tld/subDir/api...`
* it must end in `ytmusic/callback`
* It must include `name=[NameOfSource]` in the query string
Remember to add your custom URL to the `Authorized redirect URIs` section in the google oauth client!
```json
{
"type": "ytmusic",
"enable": true,
"name": "MyYTM",
"data": {
"clientId": "8910....6jqupl.apps.googleusercontent.com",
"clientSecret": "GOCSPX-WGXL6BSuQ343...",
"redirectUri": "http://my.custom.domain/api/ytmusic/callback?name=MyYTM"
},
"options": {
"logAuthUpdateChanges": true,
"logDiff": true
}
}
```
</details>
AFTER changing the Authorized redirect URIs on Google Cloud console you may need to wait a few minutes for it to take affect. Then restart MS. From the dashboard click `(Re)authenticate` on the YTmusic source card and follow the auth flow:
* On the screen about "testing" make sure you hit **Continue** (not Back To Safety)
* Make sure to select ALL scopes/permissions/grants it asks you about
* `Select what [YourAppName] can access` -> Select all
Once the flow is finished MS will get the credentials and start polling automatically. You should not need to re-authenticate again after restarting MS as it saves the credentials to the `/config` folder.
</TabItem>
<TabItem value="ytt" label="YoutubeTV">
:::warning
Using the built-in YoutubeTV authentication is unlikely to work due to [Google restricting what permissions TV clients can have](https://github.com/yt-dlp/yt-dlp/issues/11462#issuecomment-2471703090). This authentication method should not be used.
:::
To authenticate start multi-scrobbler with an empty YT Music configuration. An authentication URL/code will be logged in additon to being available from the dashboard.
```
ERROR: Sign in with the code 'CLV-KFA-BVKY' using the authentication link on the dashboard or https://www.google.com/device
```
Visit the authentication URL and enter the code that was provided (also available on the dashboard). After completing the setup flow MS will log `Auth success` and the YT Music dashboard card will display as **Idle** after refreshing. Click the **Start** link to begin monitoring.
</TabItem>
</Tabs>
## Configuration
<Config config="YTMusicSourceConfig" fileContent={JsonConfig} name="ytmusic">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|-----------------------------------------------|
| YTM_COOKIE | No | | Value for Cookie Authentication |
| YTM_CLIENT_ID | No | | Client ID for OAuth Athentication |
| YTM_CLIENT_SECRET | No | | Client Secret for OAuth Athentication |
| YTM_REDIRECT_URI | No | | A custom redirect URI for OAuth Athentication |
| YTM_LOG_DIFF | No | false | Log YTM history changes |
</Config>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Development",
"position": 4,
"position": 6,
"link": {
"type": "generated-index",
"description": "Developing for Multi-Scrobbler and tutorials"
-8
View File
@@ -6,14 +6,6 @@ title: Flatpak
description: Building Flatpak App locally
---
:::note
These steps are for building the flatpak from source. If you want to install the application normally then [get it through flathub](../installation/installation.mdx#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)
## Install Requirements
### Flatpak and flatpak-builder
+130
View File
@@ -0,0 +1,130 @@
---
slug: /
title: Overview
sidebar_position: 1
---
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)
<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), [ListenBrainz](https://listenbrainz.org), and [Koito](https://koito.io/).
* Supports scrobbling from many [**Sources**](/configuration/sources)
* [Azuracast](/configuration/sources/azuracast)
* [Deezer](/configuration/sources/deezer)
* [Google Cast (Chromecast)](/configuration/sources/google-cast)
* [Icecast](/configuration/sources/icecast)
* [Jellyfin](/configuration/sources/jellyfin)
* [JRiver](/configuration/sources/jriver)
* [Kodi](/configuration/sources/kodi)
* [Koito](/configuration/sources/koito-source)
* [Last.fm](/configuration/sources/lastfm-source)
* [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
* [ListenBrainz](/configuration/sources/listenbrainz-source)
* [ListenBrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
* [Maloja](/configuration/sources/maloja)
* [Mopidy](/configuration/sources/mopidy)
* [MPD (Music Player Daemon)](/configuration/sources/mpd)
* [MPRIS (Linux Desktop)](/configuration/sources/mpris)
* [Musikcube](/configuration/sources/musikcube)
* [Plex](/configuration/sources/plex)
* [Rocksky](/configuration/sources/rocksky-source)
* [Spotify](/configuration/sources/spotify)
* [Subsonic-compatible APIs](/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [teal.fm](/configuration/sources/tealfm-source)
* [WebScrobbler](/configuration/sources/webscrobbler)
* [VLC](/configuration/sources/vlc)
* [Yamaha MusicCast](/configuration/sources/yamaha-musiccast)
* [Youtube Music](/configuration/sources/youtube-music)
* Supports scrobbling to many [**Clients**](/configuration/clients)
* [Koito](/configuration/clients/koito)
* [Last.fm](/configuration/clients/lastfm)
* [ListenBrainz](/configuration/clients/listenbrainz)
* [Maloja](/configuration/clients/maloja)
* [Rocksky](/configuration/clients/rocksky)
* [teal.fm](/configuration/clients/tealfm)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](/configuration#webhook-configurations) or [healthcheck endpoint](/configuration#health-endpoint)
* Supports [Now Playing](/configuration/clients#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](/configuration#configuration-types)
* Modify data before scrobbling with [regular expression or search patterns](/configuration/transforms)
* Install using [Docker images for x86/ARM](/installation#docker) or [locally with NodeJS](/installation#nodejs)
[**Quick Start Guide**](/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?**
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
* **Open-source** -- Get peace of mind knowing exactly how your personal data is being handled.
* **Track your activity regardless of where you listen** -- Scrobble from many Sources to one Client with ease and without duplicating tracks.
* **Manage scrobbling for others** -- Scrobble for your friends and family without any setup on their part. Easily silo sources to specific clients to keep plays separate.
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](configuration/sources/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?
You set up [configurations](/configuration) for one or more [**Sources**](/configuration/sources) and one or more [**Clients**](/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
### Source
A [**Source**](/configuration/sources) is a data source that contains information about music you are playing or have listened to, like a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
Source configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Source.
* An optional list of Client names that the Source should scrobble to. If omitted the Source also scrobbles to all configured Clients.
### Client
A [**Client**](/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Quick Start
[See the **Quick Start Guide**](/quickstart)
## Installation
[See the **Installation** documentation](/installation)
## Configuration
[See the **Configuration** documentation](/configuration)
## Usage
A status page with statistics, recent logs, and some runtime configuration options can be found at
```
http://localhost:9078
```
Output is also provided to stdout/stderr as well as file if specified in configuration.
On first startup you may need to authorize Spotify and/or Last.fm by visiting the callback URL (which can also be accessed from the status page). Visit the status page above to find the applicable link to trigger this.
## Help/FAQ
Having issues with connections or configuration? Check the [FAQ](/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](/development/dev-common)
## License
MIT
+3 -3
View File
@@ -1,8 +1,8 @@
{
"label": "Installation",
"position": 2,
"position": 3,
"link": {
"type": "generated-index",
"description": "Way to install Multi-Scrobbler"
"type": "doc",
"id": "installation/installation"
}
}
+109 -143
View File
@@ -14,131 +14,6 @@ For the difference between **ENV** and **File** examples in this document see [C
:::
## 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
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret 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": "spotify",
"clients": ["myConfig"],
"name": "mySpotifySource",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
}
}
],
"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
You must have [Flatpak](https://flatpak.org/) installed on your system.
```shell
flatpak install flathub io.github.foxxmd.multiscrobbler
```
:::warning
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
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
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
```
</TabItem>
<TabItem value="file" label="File">
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
```
</TabItem>
</Tabs>
## Docker
Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
@@ -176,7 +51,7 @@ 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.
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>
@@ -206,7 +81,7 @@ You **should** bind a host directory into the container for storing configuratio
#### 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`.
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>
@@ -274,17 +149,11 @@ set the [Base URL](../configuration/configuration.mdx#base-url) as the IP of the
* (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
#### Linux Host
::::info
:::note
If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) or are running docker on MacOS/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)
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:
@@ -297,16 +166,16 @@ To get the UID and GID for the current user run these commands from a terminal:
:::tip
See the [**Quick Start Guide**](../quickstart.mdx) for another guided docker-compose example
See the [**Quick Start Guide**](/quickstart) for another guided docker-compose example
:::
The example scenario:
* [Spotify **Source**](../configuration/configuration.mdx#spotify)
* [Maloja **Client**](../configuration/configuration.mdx#maloja)
* [Jellyfin **Source**](/configuration/sources/jellyfin)
* [Maloja **Client**](/configuration/sources/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)
* Docker container located on a different IP (`192.168.0.100`) so use [Base URL](/configuration#base-url)
* Config/data directory on host machine in a directory next to `docker-compose.yml`
* Linux uid/gid is `1000:1000`
@@ -315,12 +184,12 @@ The example scenario:
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
```bash
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -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
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 -e "PUID=1000" -e "PGID=1000" -e "BASE_URL=192.168.0.100" -p 9078:9078 -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
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>
@@ -335,8 +204,9 @@ The example scenario:
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=yourId
- SPOTIFY_CLIENT_SECRET=yourSecret
- 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
@@ -351,4 +221,100 @@ The example scenario:
</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)
:::
+2 -15
View File
@@ -31,27 +31,14 @@ 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.mdx#flatpak)
### Node.js Installs
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](installation.mdx#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:
+47 -22
View File
@@ -1,6 +1,6 @@
---
title: 'Quickstart'
sidebar_position: 1
sidebar_position: 2
---
import Tabs from '@theme/Tabs';
@@ -10,8 +10,8 @@ This guide will get you up and running with multi-scrobbler using [Docker](https
* 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)
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify) and/or [Jellyfin](/configuration/sources/jellyfin) for listening activity
* multi-scrobbler scrobbling to [Lastfm](/configuration/clients/lastfm) and/or [Maloja](/configuration/clients/maloja)
:::note
@@ -42,11 +42,11 @@ services:
## Setup Sources
**Sources** are the services multi-scrobbler monitors to look for listening activity.
[**Sources**](/configuration/sources) are the services/music players 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)
Follow the Jellyfin configuration [instructions for setting up an **API Key**.](/configuration/sources/jellyfin)
Then, add these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
@@ -57,20 +57,17 @@ services:
```
</TabItem>
<TabItem value="spotify" label="Spotify">
To access your Spotify activity you must [register a Spotify application](https://developer.spotify.com/dashboard) to get a
**Client ID/Secret**.
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/sources/spotify#authenticate-spotify-with-multi-scrobbler)
When creating the application add this to **Redirect URIs**
```
http://192.168.0.100:9078/callback
```
After the application is created add these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
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.
@@ -79,7 +76,7 @@ services:
## 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.
[**Clients**](/configuration/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">
@@ -130,13 +127,41 @@ services:
- 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/clients/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)
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation#linux-host)
:::
@@ -157,10 +182,10 @@ Visit `http://192.168.0.100:9078` to see the dashboard where
## 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
* See more advanced docker options as well as other install methods in the [**Installation**](/installation#docker) docs
* Review the [**Configuration**](/configuration) 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
* See all available [**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) alongside configuration examples
* Learn how to set up [notification webhooks](/configuration#webhook-configurations)
* Check out the [kitchensink example](/configuration/kitchensink)
* Consult the [**FAQ**](/FAQ) for solutions to common problems
+8
View File
@@ -0,0 +1,8 @@
{
"label": "Updating",
"position": 4,
"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.
+70 -5
View File
@@ -5,7 +5,14 @@ import * as themes from 'prism-react-renderer';
const config: Config = {
future: {
experimental_faster: true
v4: {
removeLegacyPostBuildHeadAttribute: true, // required
},
experimental_faster: {
ssgWorkerThreads: true,
rspackBundler: true, // required flag
rspackPersistentCache: true, // new flag
},
},
title: 'Multi-Scrobbler',
tagline: 'Scrobble all the things',
@@ -42,6 +49,7 @@ const config: Config = {
{
docs: {
sidebarPath: './sidebars.ts',
routeBasePath: '/',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
@@ -72,6 +80,7 @@ const config: Config = {
// `hashed` is recommended as long-term-cache of index file is possible.
hashed: true,
indexBlog: false,
docsRouteBasePath: "/",
// For Docs using Chinese, The `language` is recommended to set to:
// ```
// language: ["en", "zh"],
@@ -81,16 +90,50 @@ const config: Config = {
'docusaurus-theme-github-codeblock'
],
plugins: [
[
'@docusaurus/plugin-client-redirects',
{
redirects: [
// /docs/oldDoc -> /docs/newDoc
{
to: '/configuration',
from: '/docs/configuration',
},
{
to: '/configuration/kitchensink',
from: '/docs/configuration/kitchensink',
},
{
to: '/installation',
from: '/docs/installation',
},
{
to: '/quickstart',
from: '/docs/quickstart',
},
{
to: '/FAQ',
from: '/docs/FAQ',
},
{
to: '/',
from: '/docs',
},
]
}
]
],
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: [
{
@@ -128,15 +171,15 @@ const config: Config = {
},
{
label: 'Installation',
to: 'docs/installation',
to: '/installation',
},
{
label: 'Configuration',
to: 'docs/configuration',
to: '/configuration',
},
{
label: 'Development',
to: 'docs/development/dev-common',
to: '/development/dev-common',
},
],
},
@@ -173,6 +216,15 @@ const config: Config = {
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 !== '') {
@@ -185,5 +237,18 @@ if (process.env.ANALYTICS !== undefined && process.env.ANALYTICS !== '') {
}
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)
}
export default config;
+2886 -2518
View File
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -14,20 +14,21 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.6.3",
"@docusaurus/faster": "^3.7.0",
"@docusaurus/preset-classic": "^3.6.3",
"@easyops-cn/docusaurus-search-local": "^0.48.3",
"@docusaurus/core": "3.8.1",
"@docusaurus/faster": "3.8.1",
"@docusaurus/plugin-client-redirects": "^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.12.1",
"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": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.6.3",
+5 -2
View File
@@ -23,7 +23,7 @@ const AIOExample = (props: AIOProps) => {
let configObj;
// eslint-disable-next-line prefer-const
try {
configObj = json5.parse(data);
configObj = json5.parse(data) as object[];
} catch (e) {
console.error(e);
return <Admonition type="danger" title="Unexpected Error">
@@ -31,9 +31,12 @@ const AIOExample = (props: AIOProps) => {
<CodeBlock>{e.message}</CodeBlock>
</Admonition>
}
configObj[0].type = name;
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>
}
+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;
@@ -0,0 +1,28 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
<EnvType>
{props.children}
</EnvType>
</TabItem>
<TabItem value="file" label="File">
<FileType>
<FileExample title={`CONFIG/${props.name}.json`} client={props.client ?? false} data={props.fileContent}/>
or <SchemaLink lower objectName={props.config}/>
</FileType>
</TabItem>
<TabItem value="aio" label="AIO">
<AIOType>
<AIOExample data={props.fileContent} client={props.client ?? false} name={props.name}/>
or <SchemaLink lower objectName={props.config}/>
</AIOType>
</TabItem>
</Tabs>
@@ -0,0 +1,2 @@
[**AIO** Config Type](/configuration?configType=aio#configuration-types)
{props.children}
@@ -0,0 +1,14 @@
import CodeInline from '@theme/CodeInline';
The [AT Protocol](https://docs.bsky.app/docs/advanced-guides/atproto) is a standard to allow different applications to share and interpret your social data.
Your data is stored in a personal [repository](https://atproto.com/guides/data-repos) in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds), which is data-type agnostic (it's just JSON). Each application defines their own datatypes and pulls data from your repository to render to you, your network, etc...
Even though the application and the PDS/repository are independent, all of this is done for you automatically when you create a [Bluesky](https://bsky.social/about) account.
So...
* multi-scrobbler "scrobbles" your plays to your repository
* in a JSON data structure which includes [{props.name}'s data type and structure](https://atproto.com/specs/lexicon)
* at a [known location for {props.name}'s data](https://atproto.com/guides/glossary#collection) (<CodeInline>{props.lexicon}</CodeInline>), in a list that is appended to as you continue to scrobble
* when accessing {props.name}, or another application that knows how to read it, your data is pulled from the repository to render the app
@@ -0,0 +1,3 @@
[**ENV** Config Type](/configuration?configType=env#configuration-types)
{props.children}
@@ -0,0 +1,3 @@
[**File** Config Type](/configuration?configType=file#configuration-types)
{props.children}
@@ -0,0 +1,27 @@
import CodeBlock from '@theme/CodeBlock';
A **Source** that monitors active listening uses one of two metrics to determine if a Song is scrobbable based on your listening:
* **percent** of track listened to (50% or more)
* or amount of time (**duration**) track was listened to (4 minutes or more)
These default values are based on [last.fm's scrobble guidance.](https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble)
These values can be customized in the `options` of Source's [file or aio config](/configuration#configuration-types):
<CodeBlock
language="json5"
title={`${props.filename}.json`}>
{`{
"data": {
// ...
},
"options": {
// highlight-start
"scrobbleThresholds": {
"duration": 40, // scrobbable if listened to for 40 seconds or more
"percent": 20 // scrobbable if listened to for 20% or more of the track's length
}
// highlight-end
}
}`}
</CodeBlock>
@@ -0,0 +1,22 @@
import CodeBlock from '@theme/CodeBlock';
This **Source** supports toggling scrobbling on or off manually. This action can be taken on the Web Dashboard by clicking the link next to **Should Scrobble** found in each Source's status card.
Additionally, the default behavior for scrobbling can be explicitly set by adding the following value to a Source's `options` in [file or aio config](/configuration?configType=file#configuration-types):
<CodeBlock
language="json5"
title={`${props.filename}.json`}>
{`{
"data": {
// ...
},
"options": {
// highlight-start
// set to true to always scrobble automatically
// set to false to never scrobble automatically
"systemScrobble": true
// highlight-end
}
}}`}
</CodeBlock>
@@ -0,0 +1,3 @@
This component **recieves data** from outside services, rather than actively contacting the outside service for data itself.
In order for it to work multi-scrobbler must be accessible, on an open port or through reverse-proxy, to the service that will be sending data to it.
-122
View File
@@ -1,122 +0,0 @@
---
title: Overview
---
# Overview
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)
<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)
* 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)
* [Last.fm (Endpoint)](docs/configuration#lastfm-endpoint)
* [ListenBrainz](docs/configuration#listenbrainz-source)
* [ListenBrainz (Endpoint)](docs/configuration#listenbrainz-endpoint)
* [~~Deezer~~](docs/configuration#deezer)
* [MPRIS (Linux Desktop)](docs/configuration#mpris)
* [Mopidy](docs/configuration#mopidy)
* [JRiver](docs/configuration#jriver)
* [Kodi](docs/configuration#kodi)
* [Google Cast (Chromecast)](docs/configuration#google-cast-chromecast)
* [Musikcube](docs/configuration#musikcube)
* [MPD (Music Player Daemon)](docs/configuration#mpd-music-player-daemon)
* [VLC](docs/configuration#vlc)
* [Azuracast](docs/configuration#azuracast)
* [Yamaha MusicCast](docs/configuration#yamaha-musiccast)
* [Icecast](docs/configuration#icecast)
* Supports scrobbling to many **Clients**
* [Maloja](docs/configuration#maloja)
* [Last.fm](docs/configuration#lastfm)
* [ListenBrainz](docs/configuration#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](docs/configuration#webhook-configurations) or [healthcheck endpoint](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
* Modify data before scrobbling with [regular expression or search patterns](docs/configuration/transforms)
* Install using [Docker images for x86/ARM](docs/installation#docker), [flatpak](docs/installation#flatpak), 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?**
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
* **Open-source** -- Get peace of mind knowing exactly how your personal data is being handled.
* **Track your activity regardless of where you listen** -- Scrobble from many Sources to one Client with ease and without duplicating tracks.
* **Manage scrobbling for others** -- Scrobble for your friends and family without any setup on their part. Easily silo sources to specific clients to keep plays separate.
**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.
## How Does multi-scrobbler (MS) Work?
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
### Source
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
Source configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Source.
* An optional list of Client names that the Source should scrobble to. If omitted the Source also scrobbles to all configured Clients.
### Client
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
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)
## Configuration
[See the **Configuration** documentation](docs/configuration)
## Usage
A status page with statistics, recent logs, and some runtime configuration options can be found at
```
http://localhost:9078
```
Output is also provided to stdout/stderr as well as file if specified in configuration.
On first startup you may need to authorize Spotify and/or Last.fm by visiting the callback URL (which can also be accessed from the status page). Visit the status page above to find the applicable link to trigger this.
## Help/FAQ
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
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
// Import the original mapper
import MDXComponents from '@theme-original/MDXComponents';
import SchemaLink from "@site/src/components/SchemaLink";
import AIOExample from "@site/src/components/AIOExample";
import FileExample from "@site/src/components/FileExample";
import EnvType from "@site/src/components/snippets/_env-config.mdx";
import FileType from "@site/src/components/snippets/_file-config.mdx";
import AIOType from "@site/src/components/snippets/_aio-config.mdx";
import Config from "@site/src/components/GenericConfiguration.mdx";
export default {
// Re-use the default mapping
...MDXComponents,
SchemaLink,
AIOExample,
FileExample,
EnvType,
FileType,
AIOType,
Config
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
Sitemap: https://foxxmd.github.io/multi-scrobbler/sitemap.xml
User-agent: *
Allow: /
File diff suppressed because one or more lines are too long
@@ -33,8 +33,12 @@
<li>MPRIS (linux desktop)</li>
<li>Google Cast (Chromecast)</li>
<li>Musikcube</li>
<li>MPDF (Music Player Daemon)</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>
+1639 -675
View File
File diff suppressed because it is too large Load Diff
+26 -19
View File
@@ -1,6 +1,6 @@
{
"name": "multi-scrobbler",
"version": "0.9.2",
"version": "0.10.3",
"type": "module",
"description": "scrobble plays from multiple sources to multiple clients",
"scripts": {
@@ -43,6 +43,9 @@
"homepage": ".",
"dependencies": {
"@astronautlabs/mdns": "^1.0.7",
"@atcute/identity-resolver-node": "^1.0.3",
"@atproto/api": "^0.18.0",
"@atproto/oauth-client-node": "^0.3.10",
"@awaitjs/express": "^0.6.3",
"@curvenote/ansi-to-react": "^7.0.0",
"@donedeal0/superdiff": "^1.1.1",
@@ -51,16 +54,18 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@foxxmd/chromecast-client": "^1.0.4",
"@foxxmd/get-version": "^0.0.3",
"@foxxmd/logging": "^0.2.2",
"@foxxmd/logging": "^0.2.3",
"@foxxmd/redact-string": "^0.1.2",
"@foxxmd/regex-buddy-core": "^0.1.2",
"@foxxmd/string-sameness": "^0.4.0",
"@gr2m/net-interceptor": "^1.0.0",
"@jellyfin/sdk": "^0.11.0",
"@kenyip/backoff-strategies": "^1.0.4",
"@lukehagar/plexjs": "^0.32.1",
"@keyv/valkey": "^1.0.8",
"@lukehagar/plexjs": "^0.39.0",
"@react-nano/use-event-source": "^0.13.0",
"@reduxjs/toolkit": "^1.9.5",
"@supercharge/promise-pool": "^3.0.0",
"@tailwindcss/vite": "^4.1.16",
"@types/express-serve-static-core": "^4.19.6",
"address": "^1.2.2",
"ajv": "^8.12.0",
@@ -68,7 +73,9 @@
"avahi-browse": "^1.1.4",
"better-sse": "^0.8.0",
"body-parser": "^1.19.0",
"cacheable": "^1.10.4",
"castv2": "^0.1.10",
"clone": "^2.1.2",
"clsx": "^2.0.0",
"common-tags": "^1.8.2",
"compare-versions": "^4.1.2",
@@ -81,47 +88,53 @@
"express-session": "^1.17.2",
"fast-deep-equal": "^3.1.3",
"fixed-size-list": "^0.3.0",
"flat-cache": "^6.1.13",
"formidable": "^3.5",
"glob": "^11.0.0",
"google-auth-library": "^9.15.0",
"gotify": "^1.1.0",
"http-cookie-agent": "^7.0.1",
"icecast-metadata-stats": "^0.1.12",
"iso-websocket": "^0.3.0",
"iti": "^0.6.0",
"iti": "^0.7.0",
"json5": "^2.2.3",
"keyv": "^5.5.0",
"kodi-api": "^0.2.1",
"lastfm-node-client": "^2.2.0",
"mopidy": "^1.3.0",
"mpd-api": "^1.1.2",
"nanoid": "^3.3.1",
"node-object-hash": "^3.1.1",
"normalize-url": "^8.0.1",
"ntfy": "^1.6.2",
"object-hash": "^3.0.0",
"ntfy": "^1.7.6",
"p-event": "^4.2.0",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"patch-package": "^8.0.0",
"postcss": "^8.4.41",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"sass": "^1.70.0",
"sass": "^1.93.3",
"spotify-web-api-node": "^5.0.2",
"superagent": "^8.0.9",
"tailwindcss": "^3.4.4",
"tailwindcss": "^4.1.16",
"toad-scheduler": "^3.0.0",
"tough-cookie": "^5.1.2",
"ts-json-schema-generator": "^2.3.0",
"tsx": "^4.7.0",
"typeson": "^9.0.4",
"typeson-registry": "^11.1.1",
"vite-express": "^0.16.0",
"vlc-client": "^1.1.1",
"xml2js": "0.6.1",
"youtubei.js": "^13.1.0"
"youtubei.js": "^15.0.1"
},
"devDependencies": {
"@dbus-types/notifications": "^0.0.5",
"@eslint/js": "^8.56.0",
"@faker-js/faker": "^9.0.1",
"@gr2m/net-interceptor": "^1.0.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
@@ -146,12 +159,11 @@
"@types/react-dom": "^18.2.7",
"@types/react-window": "^1.8.5",
"@types/spotify-web-api-node": "^5.0.7",
"@types/superagent": "^4.1.16",
"@types/superagent": "^8.1.9",
"@types/xml2js": "^0.4.11",
"@vitejs/plugin-react": "^4.2.1",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"clone": "^2.1.2",
"eslint": "^8.56.0",
"eslint-plugin-prefer-arrow-functions": "^3.2.4",
"mocha": "^10.3.0",
@@ -162,7 +174,7 @@
"typescript": "5.5.4",
"typescript-eslint": "^7.0.1",
"vite": "^5.2.12",
"with-local-tmp-dir": "^5.1.1"
"with-local-tmp-dir": "^6.0.0"
},
"browserslist": {
"production": [
@@ -179,11 +191,6 @@
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"
},
"with-local-tmp-dir": {
"tmp-promise": {
"tmp": "0.2.1"
}
}
}
}
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+63 -19
View File
@@ -9,23 +9,25 @@ import { buildTrackString, truncateStringToLength } from "../../core/StringUtils
import {
configPartsToStrongParts, countRegexes,
isUserStage,
transformPlayUsingParts
} from "../utils/PlayTransformUtils.js";
import { hasNodeNetworkException } from "./errors/NodeErrors.js";
import { hasUpstreamError } from "./errors/UpstreamError.js";
import {
ConditionalSearchAndReplaceRegExp,
PlayTransformParts, PlayTransformPartsArray,
PlayTransformRules,
TRANSFORM_HOOK,
TransformHook
} from "./infrastructure/Atomic.js";
import { CommonClientConfig } from "./infrastructure/config/client/index.js";
import { CommonSourceConfig } from "./infrastructure/config/source/index.js";
import play = Simulate.play;
import { WebhookPayload } from "./infrastructure/config/health/webhooks.js";
import { AuthCheckError, BuildDataError, ConnectionCheckError, PostInitError, TransformRulesError } from "./errors/MSErrors.js";
import { AuthCheckError, BuildDataError, ConnectionCheckError, ParseCacheError, PostInitError, TransformRulesError } from "./errors/MSErrors.js";
import { messageWithCauses, messageWithCausesTruncatedDefault } from "../utils/ErrorUtils.js";
import {
ConditionalSearchAndReplaceRegExp,
PlayTransformParts,
PlayTransformPartsArray,
PlayTransformRules,
TRANSFORM_HOOK,
TransformHook
} from "./infrastructure/Transform.js";
export default abstract class AbstractComponent {
requiresAuth: boolean = false;
@@ -35,12 +37,13 @@ export default abstract class AbstractComponent {
buildOK?: boolean | null;
connectionOK?: boolean | null;
cacheOK?: boolean | null;
initializing: boolean = false;
config: CommonClientConfig | CommonSourceConfig;
transformRules!: PlayTransformRules;
transformRules: PlayTransformRules = {};
regexCache!: ReturnType<typeof cacheFunctions>;
logger: Logger;
@@ -65,6 +68,7 @@ export default abstract class AbstractComponent {
await this.buildComponentLogger();
}
await this.buildInitData(force);
await this.parseCache(force);
this.buildTransformRules();
await this.checkConnection(force);
await this.testAuth(force);
@@ -105,6 +109,45 @@ export default abstract class AbstractComponent {
}
}
public async parseCache(force: boolean = false) {
if(this.cacheOK) {
if(!force) {
return;
}
this.logger.debug('Cache OK but step was forced');
}
try {
const res = await this.doParseCache();
if(res === undefined) {
this.cacheOK = null;
this.logger.debug('No cache to parse.');
return;
}
if (res === true) {
this.logger.verbose('Parsing caching succeeded');
} else if (typeof res === 'string') {
this.logger.verbose(`Parsing caching succeeded => ${res}`);
}
this.cacheOK = true;
} catch (e) {
this.cacheOK = false;
throw new ParseCacheError('Parsing cache for initialization failed', {cause: e});
}
}
/**
* Build or parse any cache required for this Component
*
* * Return undefined if not possible or not required
* * Return TRUE if build succeeded
* * Return string if build succeeded and should log result
* * Throw error on failure
* */
protected async doParseCache(): Promise<true | string | undefined> {
return;
}
public async buildInitData(force: boolean = false) {
if(this.buildOK) {
if(!force) {
@@ -166,7 +209,6 @@ export default abstract class AbstractComponent {
} = this.config;
if (playTransform === undefined) {
this.transformRules = {};
return;
}
@@ -347,17 +389,19 @@ export default abstract class AbstractComponent {
let transformedPlay: PlayObject = play;
const transformDetails: string[] = [];
for(const hookItem of hook) {
const newTransformedPlay = transformPlayUsingParts(transformedPlay, hookItem, {
logger: getLogger,
regex: {
searchAndReplace: this.regexCache.searchAndReplace,
testMaybeRegex: this.regexCache.testMaybeRegex,
if(hookItem.type === 'user') {
const newTransformedPlay = transformPlayUsingParts(transformedPlay, hookItem, {
logger: getLogger,
regex: {
searchAndReplace: this.regexCache.searchAndReplace,
testMaybeRegex: this.regexCache.testMaybeRegex,
}
});
if(!deepEqual(newTransformedPlay, transformedPlay)) {
transformDetails.push(buildTrackString(transformedPlay, {include: ['artist', 'track', 'album']}));
}
});
if(!deepEqual(newTransformedPlay, transformedPlay)) {
transformDetails.push(buildTrackString(transformedPlay, {include: ['artist', 'track', 'album']}));
transformedPlay = newTransformedPlay;
}
transformedPlay = newTransformedPlay;
}
if(transformDetails.length > 0) {
+303
View File
@@ -0,0 +1,303 @@
import { Cacheable, createKeyv, Keyv, KeyvStoreAdapter, KeyvOptions, CacheableOptions } from 'cacheable';
import { FlatCache, FlatCacheOptions } from 'flat-cache';
import KeyvValkey from '@keyv/valkey';
import dayjs, { Dayjs } from 'dayjs';
import duration from 'dayjs/plugin/duration.js';
import isBetween from 'dayjs/plugin/isBetween.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
import isToday from 'dayjs/plugin/isToday.js';
import timezone from 'dayjs/plugin/timezone.js';
import utc from 'dayjs/plugin/utc.js';
import clone from 'clone';
import { childLogger, Logger } from '@foxxmd/logging';
import { projectDir } from './index.js';
import path from 'path';
import { fileOrDirectoryIsWriteable } from '../utils.js';
import { asCacheAuthProvider, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js';
import { Typeson } from 'typeson';
import { builtin } from 'typeson-registry';
import { MaybeLogger } from './logging.js';
import { ListenProgressPositional, ListenProgressTS } from '../sources/PlayerState/ListenProgress.js';
const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`);
dayjs.extend(utc)
dayjs.extend(isBetween);
dayjs.extend(relativeTime);
dayjs.extend(duration);
dayjs.extend(timezone);
dayjs.extend(isToday);
const typeson = new Typeson().register([
builtin,
]);
typeson.register({
Dayjs: [
(x) => dayjs.isDayjs(x),
(d: Dayjs) => d.toJSON(),
(date) => dayjs(date)
],
ListenProgressTS,
ListenProgressPositional
});
export class MSCache {
config: Required<CacheConfigOptions>
cacheMetadata: Cacheable;
cacheScrobble: Cacheable;
cacheAuth: Cacheable;
logger: Logger;
constructor(logger: Logger, config: CacheConfigOptions = {}) {
this.logger = childLogger(logger, 'Cache');
const {
metadata: {
provider: mProvider = (process.env.CACHE_METADATA as (CacheMetadataProvider | undefined) ?? 'memory'),
connection: mConn = process.env.CACHE_METADATA_CONN,
...restMetadata
} = {},
scrobble: {
provider: sProvider = (process.env.CACHE_SCROBBLE as (CacheScrobbleProvider | undefined) ?? 'file'),
connection = (process.env.CACHE_SCROBBLE_CONN ?? configDir),
...restScrobble
} = {},
auth: {
provider: aProvider = (process.env.CACHE_AUTH as (CacheAuthProvider | undefined) ?? 'file'),
connection: aConn = (process.env.CACHE_AUTH_CONN ?? configDir),
...restAuth
} = {},
} = config;
this.config = {
metadata: {
provider: mProvider,
connection: mConn,
...restMetadata,
},
scrobble: {
provider: sProvider,
connection,
...restScrobble
},
auth: {
provider: aProvider,
connection: aConn,
...restAuth
}
};
}
init = async () => {
// disabled for now
//await this.initMetadataCache();
await this.initScrobbleCache();
await this.initAuthCache();
}
protected initCacheable = async (config: CacheConfig, cacheFor: string) => {
let logger = childLogger(this.logger, cacheFor);
const providerHints = ['In-Memory (Primary)'];
if(config.provider !== false) {
providerHints.push(`${config.provider} (Secondary)`)
}
logger.verbose(`Cache Providers: ${providerHints.join(' | ')}`)
const ns = `ms-${cacheFor.toLocaleLowerCase()}`;
const cacheOpts: CacheableOptions = {
primary: initMemoryCache({ namespace: ns })
}
let secondaryCache: Keyv | KeyvStoreAdapter | undefined;
if (config.provider === 'valkey') {
logger.debug(`Building valkey cache from ${config.connection}`);
try {
secondaryCache = await initValkeyCache(ns, config.connection);
logger.debug('valkey cache connected');
} catch (e) {
this.logger.warn(e);
}
} else if (config.provider === 'file') {
logger.debug(`Building file cache from ${path.join(config.connection, `${ns}.cache`)}`);
try {
const [keyvFile] = await initFileCache({ ...config, cacheDir: config.connection, cacheId: `${ns}.cache` }, logger);
secondaryCache = keyvFile;
} catch (e) {
logger.warn(e);
}
}
if(secondaryCache !== undefined) {
cacheOpts.secondary = secondaryCache;
}
return new Cacheable(cacheOpts);
}
initScrobbleCache = async () => {
if (this.cacheScrobble === undefined) {
if (!asCacheScrobbleProvider(this.config.scrobble.provider)) {
throw new Error(`Cache Scrobble provider '${this.config.scrobble.provider}' must be one of: memory, valkey, file`);
}
this.cacheScrobble = await this.initCacheable(this.config.scrobble, 'Scrobble');
}
}
initMetadataCache = async () => {
if (this.cacheMetadata === undefined) {
if (!asCacheMetadataProvider(this.config.metadata.provider)) {
throw new Error(`Cache Metadata provider '${this.config.metadata.provider}' must be one of: memory, valkey`);
}
this.cacheMetadata = await this.initCacheable(this.config.metadata, 'Metadata');
}
}
initAuthCache = async () => {
if (this.cacheAuth === undefined) {
if (!asCacheAuthProvider(this.config.auth.provider)) {
throw new Error(`Cache Auth provider '${this.config.auth.provider}' must be one of: memory, valkey, file`);
}
this.cacheAuth = await this.initCacheable(this.config.auth, 'Auth');
}
}
}
export const initMemoryCache = (opts: Parameters<typeof createKeyv>[0] = {}): Keyv | KeyvStoreAdapter => {
const memory = createKeyv({
ttl: '1h',
lruSize: 200,
...opts,
useClone: false,
});
// structuredClone does not work well with dayjs https://github.com/iamkun/dayjs/issues/2236
// but deep cloning is fine so disable useClone and provide our own cloning function
memory.serialize = (data) => {
return clone(data) as string;
}
return memory;
}
export const flatCacheCreate = (opts: FlatCacheOptions) => {
return new FlatCache({
ttl: 0,
lruSize: 2000,
cacheDir: opts.cacheDir ?? configDir,
cacheId: opts.cacheId ?? 'scrobble.cache',
persistInterval: 1 * 1000 * 10,
expirationInterval: 1 * 1000 * 10, // 10 seconds
...opts
});
}
export const flatCacheLoad = async (flatCache: FlatCache, logger: MaybeLogger): Promise<void> => {
const cachePath = path.join(flatCache.cacheDir, flatCache.cacheId);
try {
fileOrDirectoryIsWriteable(cachePath);
} catch (e) {
throw new Error(`Unable to use path for file cache at ${cachePath}`, { cause: e })
}
const streamPromise = new Promise((resolve, reject) => {
flatCache.loadFileStream(cachePath, (progress: number, total: number) => {
logger.debug(`Loading ${progress}/${total} chunks...`);
}, () => {
resolve(true);
}, (err: Error) => {
reject(err);
});
});
try {
await streamPromise;
logger.debug(`File cache loaded`);
return;
} catch (e) {
if (null !== e.message.match(/Cache file .+ does not exist/)) {
let loadError: Error;
try {
const onlySaveError = (e: Error) => {
loadError = e;
};
flatCache.on('error', onlySaveError);
flatCache.load();
if (loadError !== undefined) {
throw loadError;
}
flatCache.off('error', onlySaveError);
logger.debug(`File cache loaded`);
return;
} catch (e) {
throw new Error(`Unable to use file cache at ${cachePath}`, { cause: e });
}
} else {
throw new Error(`Unable to use file cache at ${cachePath}`, { cause: e });
}
}
}
export const initFileCache = async (opts: FlatCacheOptions = {}, logger: MaybeLogger = new MaybeLogger()): Promise<[Keyv | KeyvStoreAdapter | undefined, FlatCache | undefined]> => {
const flatCache = flatCacheCreate(opts);
try {
await flatCacheLoad(flatCache, logger);
flatCache.on('error', (e) => {
logger.warn(e);
});
flatCache.on('save', () => {
logger.debug('Saved cache to file');
});
const cache = new Keyv({
store: flatCache,
throwOnErrors: true,
...typesonMarshalling
});
return [cache, flatCache];
} catch (e) {
throw e;
}
}
export const valkeyCacheCreate = (ns: string, ...args: ConstructorParameters<typeof KeyvValkey>): Keyv => {
const [connection, valkeyOpts = {}] = args;
const valkey = new KeyvValkey(connection, { maxRetriesPerRequest: 5, connectTimeout: 1100, ...valkeyOpts });
const kv = new Keyv({
store: valkey,
throwOnErrors: true,
namespace: ns,
...typesonMarshalling
});
return kv;
}
export const initValkeyCache = async (ns: string, ...args: ConstructorParameters<typeof KeyvValkey>): Promise<Keyv> => {
const kv = valkeyCacheCreate(ns, ...args);
try {
await kv.get('test');
return kv;
} catch (e) {
throw new Error(`Unable to connect to cache ${args[0]}`, { cause: e })
}
}
const typesonMarshalling: Pick<KeyvOptions, 'serialize' | 'deserialize'> = {
serialize: (data) => {
const str = typeson.stringifySync(data);
return str;
},
deserialize: (str) => {
const data = typeson.parseSync(str);
return data;
}
}
+5 -1
View File
@@ -8,12 +8,16 @@ export class BuildDataError extends StageError {
name = 'Init Build Data';
}
export class ParseCacheError extends StageError {
name = 'Init Parse Cache';
}
export class TransformRulesError extends StageError {
name = 'Transform Rules';
}
export class ConnectionCheckError extends StageError {
name = 'Conenction Check';
name = 'Connection Check';
}
export class AuthCheckError extends StageError {

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