Compare commits

...
216 Commits
Author SHA1 Message Date
FoxxMD 232c802b44 chore(release): Bump version for prerelease 2026-08-28 17:54:55 +00:00
FoxxMD e335b44abd feat(ui): Add welcome toast for new ui 2026-08-28 17:36:21 +00:00
FoxxMD 115911c63a feat(ui): add link to new ui on old dashboard 2026-08-28 17:05:57 +00:00
FoxxMD f45493d10e fix(ui): don't hide bulk play actions menu button on mobile 2026-08-28 16:56:32 +00:00
FoxxMD 574b723fa4 fix(spotify): Patch in a hardcoded 5-9 sec timeout for requests 2026-08-28 16:31:23 +00:00
FoxxMD 02f1eca649 fix(ui): hydrate queue states for bulk scrobbling 2026-08-28 15:57:53 +00:00
Matt Foxx 212afaa5e5 Merge pull request #674 from FoxxMD/writeApi
Write api
2026-08-28 11:37:09 -04:00
Matt Foxx 006fbe1079 Merge pull request #685 from FoxxMD/catchConsoleDiff
fix(transform): Catch and log errors during console diff generation
2026-08-28 11:35:51 -04:00
FoxxMD 5cecac937b feat(ui): Implement bulk play retries 2026-08-28 15:28:31 +00:00
FoxxMD 297c7dee95 fix(ui): Change component state values to be lowercase so they pass validation 2026-08-28 15:27:58 +00:00
FoxxMD 42a6887141 fix: fix method for getting last transformed data 2026-08-28 12:34:19 +00:00
FoxxMD cbdb58b36e fix(transform): fix method for getting last transformed play data
Instead of assuming the previous step had data, reverse search the history array for the most current data to use for patching
2026-08-28 00:48:13 +00:00
FoxxMD ae20d46456 one more fix for queue filter state 2026-08-27 20:09:10 +00:00
FoxxMD 6b12bd2ef2 fix(ui): fix dead queue filtering 2026-08-27 19:55:38 +00:00
FoxxMD dde19cfc83 fix stale error on requeue 2026-08-27 19:50:00 +00:00
FoxxMD 0938e95e1d last fix for dead letter queue counts 2026-08-27 19:39:19 +00:00
FoxxMD 3a40478cb1 fix: fix queue status in requeued event 2026-08-27 19:31:19 +00:00
FoxxMD 935845a976 fix: further queue state cleanup before converting to event 2026-08-27 19:11:16 +00:00
FoxxMD b494e8f93f fix: don't need queue context after processing 2026-08-27 18:51:46 +00:00
FoxxMD 9e13ead40f feat: Add more queue context and render context in timeline 2026-08-27 18:35:43 +00:00
FoxxMD 3b2f5ee1c8 fix: Fix queue event names 2026-08-27 17:25:51 +00:00
FoxxMD 51de79b92f fix(ui): Fix missed statt text arrow color 2026-08-27 17:25:35 +00:00
FoxxMD b6f7e0e586 refactor(ui): Simplify and make stats code reusable 2026-08-27 17:10:45 +00:00
FoxxMD c69af47978 fix(transform): Catch and log errors during console diff generation
This functionality is not critical to transforms, just helpful for console debugging. It should not stop transforms from occuring so catch all errors and log.

#684
2026-08-27 15:05:35 +00:00
FoxxMD edde8f9e74 try moving openapi generate into docs script 2026-08-27 00:27:46 +00:00
FoxxMD 72d702acf0 docs: Add stability disclaimer 2026-08-27 00:18:07 +00:00
FoxxMD 67b13ffe40 docs: Add npm scripts to build openapi and generate docs 2026-08-27 00:10:17 +00:00
FoxxMD b464c7507f add openapi.json to gitignore 2026-08-27 00:05:59 +00:00
FoxxMD 3cf5b82247 working poc for openapi doc generation 2026-08-27 00:05:35 +00:00
FoxxMD 1571f06662 docs: Update docusaurs to 3.10.2 2026-08-26 20:05:29 +00:00
FoxxMD b3fb04a6ae docs: generate openapi spec 2026-08-26 20:01:13 +00:00
FoxxMD cb2a6168fd Update openapi doc meta for friendly names, descriptions, and categories 2026-08-26 15:01:39 +00:00
FoxxMD 56fcbedff9 feat: Implement zod validation for endpointlz requests 2026-08-26 14:32:05 +00:00
FoxxMD 548c614b0a test(lastfm): Add smoke tests for 2.0 endpoints 2026-08-26 13:28:10 +00:00
FoxxMD d3469aee7a fix(lastfm): Fix xml builder when fields are undefined 2026-08-26 12:57:35 +00:00
FoxxMD 0109cc6fcd fix(lastfm): Fix urlencoded number string coercion in request validation 2026-08-26 12:57:08 +00:00
FoxxMD 1988fc5f54 mostly figured out lfm validation... 2026-08-26 01:48:50 +00:00
FoxxMD aa93600166 chore: bump express-typed-router 2026-08-26 00:09:35 +00:00
FoxxMD 09e9d0f79a feat: Use new InferSchemaHandler type to simplify route handlers 2026-08-26 00:07:37 +00:00
FoxxMD 9c84e2d8e3 fxi(webscrobbler): Fix test data generation 2026-08-25 23:26:11 +00:00
FoxxMD 4b8d4932aa test: fix signal used to wait for queued played to finish processing before checking queue length 2026-08-25 23:06:47 +00:00
FoxxMD 970e200e76 fix types for matchRequest 2026-08-25 22:39:11 +00:00
FoxxMD 3e05b3f964 feat: Implement tests for listenbrainz and webscrobbler routes 2026-08-25 21:16:30 +00:00
Matt Foxx 86b8b4ad90 Merge pull request #681 from DirazCoder/master
confirm before removing all failed scrobbles
2026-08-25 14:15:08 -04:00
FoxxMD 64d160782b replace webscrobbler types with zod 2026-08-25 17:28:35 +00:00
FoxxMD 39f8367392 fill in the majority of routes with new typed routes for regex 2026-08-25 17:02:52 +00:00
FoxxMD 62905e8010 feat(ui): Implement retry options and delete flow 2026-08-25 13:15:22 +00:00
FoxxMD 203f4c343e feat(ui): Wire up non-form play actions 2026-08-25 13:15:22 +00:00
FoxxMD 3b93073694 chore: Bump express-typed-routes for type fixes 2026-08-25 13:15:22 +00:00
FoxxMD 649280582e feat(ui): Implement play actions 2026-08-25 13:15:22 +00:00
FoxxMD 0ad86ad692 feat: Implement api endpoitns for requeuing, cancelling, and deleting dead states 2026-08-25 13:15:22 +00:00
FoxxMD e80f35f1b1 feat: dead queue to ingress migration 2026-08-25 13:15:22 +00:00
FoxxMD a885079a07 Finish queue implementation for source and various fixes 2026-08-25 13:15:22 +00:00
FoxxMD ab994770ab fix(deezer): Fix missing closest play for some fuzzy scenarios 2026-08-25 13:15:22 +00:00
FoxxMD 17d7882710 feat(client): Unify queue processing for regular and dead plays 2026-08-25 13:15:22 +00:00
FoxxMD 1dcbf92fed fix(ui): Fallback to source for music service icon 2026-08-25 13:15:22 +00:00
FoxxMD 8686309993 fix(ui): Fix music service icon string variable assumption 2026-08-25 13:15:22 +00:00
FoxxMD b83f8872c2 start simplifying queue scrobble for clients 2026-08-25 13:15:22 +00:00
FoxxMD 9f22c56e87 feat(source): Implement queue context option usage during play processing 2026-08-25 13:15:22 +00:00
FoxxMD 6b86ef8629 feat(transform): Implement path to control cache usage 2026-08-25 13:15:22 +00:00
FoxxMD d0001d2f62 feat(source): Implement Play retry logic 2026-08-25 13:15:22 +00:00
FoxxMD e381be2f7e feat: Implement queue context with migration 2026-08-25 13:15:22 +00:00
FoxxMD c1e9757aed feat(ui): Optimize musicbrainz/external links layout in play data 2026-08-25 13:15:22 +00:00
FoxxMD 61fbc39a91 feat(ui): add play source to activity summary 2026-08-25 13:15:22 +00:00
FoxxMD 4c764879d7 more ignore behavior fixes 2026-08-25 13:15:22 +00:00
FoxxMD 692b2160bf fix: Fix auth routes with new typed routing 2026-08-25 13:15:22 +00:00
FoxxMD 344c0a15ad feat: Add now playing behavior for monitoring state 2026-08-25 13:15:22 +00:00
FoxxMD f44835dfba feat: Migrate to typed router 2026-08-25 13:15:22 +00:00
FoxxMD 204cc0bdb8 Improved monitoring behavior
* Discard plays early to avoid unnecessary transforms and existing checks
* Don't throw client scrobble processing, just set play as discarded
* Fix/improve source play processing on failure
2026-08-25 13:15:22 +00:00
FoxxMD 4b7542ddd9 fix: Add missing play update events 2026-08-25 13:15:22 +00:00
FoxxMD 1fc3ad1bd4 fix(source): Missing original data on play create 2026-08-25 13:15:22 +00:00
FoxxMD 9f34929140 create transform events with createdAt as now 2026-08-25 13:15:22 +00:00
FoxxMD 1178b0b6fb fix: Queue event added in repo by explicit parameter 2026-08-25 13:15:22 +00:00
FoxxMD cad42cba04 refactor play event data into factory functions 2026-08-25 13:15:22 +00:00
FoxxMD 68b7385f31 test: Fix useless polling wait slowing tests 2026-08-25 13:15:22 +00:00
FoxxMD 9a93f13a7d fix: fix missing events in apifixtures 2026-08-25 13:15:22 +00:00
FoxxMD 649ccde43d refactor: db retention cleanup for transform data 2026-08-25 13:15:22 +00:00
FoxxMD fc06c6a8a9 feat(ui): Add state change timeline event 2026-08-25 13:15:22 +00:00
FoxxMD d089b960f9 feat: Refactor activity timeline elements into proper event structure
* Implement new database playEvents table/entity
* Refactor usage of lifecycle,scrobbler,etc... with write-only play events
* Refactor/simplify activity timeline to use new event structure
2026-08-25 13:15:21 +00:00
FoxxMD c210590c8f fix: Add queue id to consumeQueue and implement queued id map to prevent processing an already processing play 2026-08-25 13:15:21 +00:00
FoxxMD 78aa418b7d feat: Filter next queued by optional id 2026-08-25 13:15:21 +00:00
FoxxMD 48cc18d373 fix(source): Use playDate SOT when generating temporal range for sql query 2026-08-25 13:15:21 +00:00
FoxxMD 258cd61b4b feat(source): Improved queue prunning for backlog and history sources 2026-08-25 13:15:21 +00:00
FoxxMD 185ccdf71c fix(source): Refactor existing play list used for existing check to use *all* processed plays
Instead of just discovered, since we now dupe/discard plays that persist in db, change the cached existing list logic to include all processed plays
2026-08-25 13:15:21 +00:00
FoxxMD b9dad6149b feat(deezer): Improve documentation on fuzzyDiscovery option usage 2026-08-25 13:15:21 +00:00
FoxxMD 9ae74a193f fix: using incorrect variable for existing 2026-08-25 13:15:21 +00:00
FoxxMD 99b30956ba fix: check for undefined event payload 2026-08-25 13:15:21 +00:00
FoxxMD 93a5775363 feat: add parsed from meta 2026-08-25 13:15:21 +00:00
FoxxMD e8a998ddda finish source queue discovery migration 2026-08-25 13:15:21 +00:00
FoxxMD 1b02dc684e test: Fix app migrations relative folder 2026-08-25 13:15:21 +00:00
FoxxMD 23813a65ef feat(source): Implement discovery queue 2026-08-25 13:15:21 +00:00
FoxxMD 0583c66743 db: Add playHash to to play input 2026-08-25 13:15:21 +00:00
FoxxMD cd4e88b232 refactor: rename queue constant to remove client
will be using for both source and client so name is less restrictive now
2026-08-25 13:15:21 +00:00
FoxxMD 8be739efb4 feat: Simplify component state updates for start/stop/restart and replace error usage 2026-08-25 13:15:21 +00:00
FoxxMD 3f2e29c5c8 feat: Implement convenience functions for replacing/clearning component errors 2026-08-25 13:15:21 +00:00
FoxxMD b109157350 disable ignore actions for now (not yet implemented) 2026-08-25 13:15:21 +00:00
FoxxMD ce15c4049c fix: component start invocation runs heartbeat immediately 2026-08-25 13:15:21 +00:00
FoxxMD 5a7c01a6d9 feat(ui): Implement test auth action 2026-08-25 13:15:21 +00:00
FoxxMD befd1ac043 feat(ui): Implement auth actions 2026-08-25 13:15:21 +00:00
FoxxMD 714678fdab finish implementation of AuthError 2026-08-25 13:15:21 +00:00
FoxxMD 2a58edd9b2 refactor: Strongly type and simplify auth error logic
* Use separate errors for init auth check throws and root auth error throws (to be implemented in each component)
* Consolidate auth issue discovery to one reusable function
* rethrow on testAuth based on auth error type and unrecoverable flag
2026-08-25 13:15:21 +00:00
FoxxMD abfceeb5f8 partial auth api 2026-08-25 13:15:21 +00:00
FoxxMD d7c88d4191 feat: Implement multiple stateful errors/warnings on component 2026-08-25 13:15:21 +00:00
FoxxMD a44fe48314 feat(ui): Implement basic component state change actions #500
Start/Stop/Restart client/sources from the ui
2026-08-25 13:15:21 +00:00
Matt Foxx 29ffc6b804 Merge pull request #680 from FoxxMD/listenbrainzUA
feat: Version and user-agent improvements
2026-08-24 18:10:39 -04:00
FoxxMD 3bdc9447b8 chore(release): Bump version for release 2026-08-24 21:38:46 +00:00
DirazCoder 4a27806825 Add confirmation for removing all dead scrobbles
Added confirmation dialog before removing all failed scrobbles.
2026-08-24 22:23:02 +01:00
FoxxMD b8cd53f554 fix(listenbrainz): Add user-agent to requests #679 2026-08-24 21:05:30 +00:00
Matt Foxx e7218074f8 Merge pull request #678 from halkeye/patch-1
fix(docsite): path.join doesn't really handle http links
2026-08-24 15:20:11 -04:00
Gavin Mogan a48ada9ac3 fix(docsite): path.join doesn't really handle http links 2026-08-22 23:30:43 -07:00
Matt Foxx bc28de66b1 Merge pull request #673 from mmattbtw/feature/teal-production-namespace
feat(teal): migrate play records to production namespace
2026-08-13 10:05:16 -04:00
FoxxMD bc18993cd3 fix(tealfm): Fix AI mistakes
* Remove gitignore entries and remove json lexicon files, we don't redistribute the lexicon
* regenerate lexicon with full definitions so we get functionality non-alpha namespace and re-add alpha to barrel export
* fix type narrowing for musicservice/origin
  * reuse isGenericUri instead of reinventing the regex wheel
* fix property assertions for renamed musicservice/origin(uri)
* use clientresponse type guard in a way typescript can actually understand it, fixes cursor type error
* reuse sortByNewestPlayDate instead of reinventing the sort wheel
2026-08-13 13:48:16 +00:00
mmattbtw d280ca5dd2 feat(teal): migrate play records to production namespace 2026-08-12 20:47:00 -05:00
FoxxMD 155d8a3252 fix(docs): Add zockerId keyword to ajv options 2026-08-12 18:01:08 +00:00
FoxxMD 68d9417cf7 fix(docs): override sharp version to fix node-gyp on arm
An old version of libvips was failing(?) to downloading
causing sharp to use node-gyp rebuild which errors due
to missing python on the base image

This shouldn't happen since we can def use a prebuild binary
so update sharp which updates libvips version used.
2026-08-12 17:24:00 +00:00
FoxxMD 9b7483db91 chore(release): Bump version for release 2026-08-12 16:40:23 +00:00
FoxxMD 7c0797b496 feat: Better validatation for normalized config urls 2026-08-12 16:32:42 +00:00
FoxxMD 1b3cbe9ab3 feat: Normalize domain:port url input 2026-08-12 13:27:10 +00:00
FoxxMD 15e0332ac2 feat: Update json schema to draft-2020-12 2026-08-11 20:37:24 +00:00
FoxxMD d532c85c2a fix: Relax some config url schema requirements with normalization 2026-08-11 20:36:47 +00:00
FoxxMD eecbea4cab add missing error clear when building clients 2026-08-11 18:40:48 +00:00
FoxxMD 823dd5d011 fix: Hand-roll applemusic env fixture data 2026-08-11 18:32:42 +00:00
FoxxMD 0fa9a8df10 fix(discord): Fix discord ENV parsing validation 2026-08-11 18:21:58 +00:00
FoxxMD a76213a7a0 docs: Hand-roll discord ENV updates 2026-08-11 15:56:54 +00:00
FoxxMD a42ab3d8fd fix: Hand-rolled env data fixtures for some edge cases 2026-08-11 15:46:19 +00:00
FoxxMD 475a3f1276 fix: narrow env config key collection to include prefix underscore 2026-08-11 15:14:08 +00:00
FoxxMD 6dcfd6028b fix: Narrow url/IP config zod types from string to purpose-built regex types 2026-08-11 15:06:52 +00:00
FoxxMD 54247e89c2 feat: Add ENV config test suite 2026-08-11 14:53:38 +00:00
Matt Foxx 433ea532ae Merge pull request #670 from FoxxMD/stronglyTypedEvents
feat: strongly type internal events
2026-08-11 08:56:24 -04:00
FoxxMD b615172ed1 feat: strongly-type internal events
* refactor wildcard emitter for better "any" usage
* strongly type emitter with event map
* fix webhook payload accessor #666
2026-08-10 21:03:31 +00:00
FoxxMD aa2beb5846 fix: Check for ENV prefix with startsWith instead of includes
#668
2026-08-10 20:53:53 +00:00
FoxxMD 06d3c6a3a8 docs: Add more guidance for File config example/editor
* Add configureAs callout for ambiguous sources
* Add editor validation limitation for ambiguous components
2026-08-10 18:15:49 +00:00
FoxxMD c0cfc5fa9f feat: relax configureAs requirements for AIO configs
* Do not require configureAs for AIO sources since we can determine from context
* Break out aio/file validation for better DX
2026-08-10 17:42:28 +00:00
FoxxMD 2195ac7057 chore(release): Bump for release 2026-08-10 16:36:23 +00:00
Matt Foxx 3fc67146c7 Merge pull request #669 from FoxxMD/envPrefixUsage
fix: Fix incorrect prefix not being used for env key collection
2026-08-10 12:35:34 -04:00
FoxxMD fe4e625824 docs: Improve scenario guidance for lfm/lz endpoints 2026-08-10 16:21:39 +00:00
FoxxMD b98756a262 fix: Fix incorrect prefix not being used for env key collection
#668
2026-08-10 14:21:59 +00:00
Matt Foxx 2b7521ebac Merge pull request #667 from Exerra/master
fix(AppleMusicSource): include library-songs in recent tracks
2026-08-09 16:55:29 -04:00
Exerra ecd48ca234 fix(AppleMusicSource): include library-songs in recent tracks
Updated the Apple Music recently played tracks query to include 'library-songs' types in addition to 'songs', ensuring a more comprehensive history retrieval.
2026-08-09 23:49:42 +03:00
FoxxMD 41aaaca530 docs: In 0.14.0 upgrade guide add note about ID requirement in 0.16.0+ 2026-08-08 21:06:31 -04:00
FoxxMD ed42da6843 chore(release): Bump version for release 2026-08-08 20:56:01 -04:00
Matt Foxx 34044401d4 Merge pull request #664 from FoxxMD/fixNewPlayRecent
fix(source): Fix missing id/uid on cached recent plays
2026-08-08 20:53:51 -04:00
FoxxMD f61e50fd45 fix(source): Fix missing id/uid on cached recent plays
#662
2026-08-08 17:12:13 -04:00
FoxxMD f5ad2ad29b chore(release): Bump version for release 2026-08-08 19:29:38 +00:00
FoxxMD 2e4acd0f00 fix: Add missing configureAs for ENV parsed components
#661
2026-08-08 19:24:45 +00:00
FoxxMD b9517bd066 docs: Add more type guidance for aio/file config 2026-08-07 17:22:20 +00:00
FoxxMD 335880249f chore(release): Bump version for release 2026-08-07 16:49:36 +00:00
Matt Foxx 5c2917f906 Merge pull request #660 from FoxxMD/ingressSot
Ingress sot
2026-08-07 12:34:15 -04:00
FoxxMD 124e857d9f fix: now playing playerUpdate event data 2026-08-07 14:53:02 +00:00
FoxxMD 5dee499381 test: fix failing tests from np data structure change 2026-08-07 14:53:02 +00:00
FoxxMD 10325fd4e6 refactor(now playing): Use more efficient solution for ingress now playing
Re-queue valid ingress NP update on client rather than spamming all clients with duplicate updates from memory source

This should have the same functional affect with way less log noise
2026-08-07 14:53:02 +00:00
FoxxMD 045890e243 feat(sourc): Add Ingress SOT type and force-push ingress player updates
#659
2026-08-07 14:53:02 +00:00
FoxxMD 685e48a7a6 fear(ui): Implement cache clearing mechanism #634 2026-08-06 20:20:11 +00:00
FoxxMD 7eb5104f0b feat(ui): Implement basic settings page 2026-08-06 19:34:02 +00:00
FoxxMD 4272e8ea58 feat(ui): Improve UX for theme switcher
* Remove three-state toggle
* Revert to system theme only when *user* initiated toggle results in same theme as system
2026-08-06 18:24:57 +00:00
FoxxMD 8e6d35a85b fix(lz endpoint): Fix missing early return when no sources for playing now response 2026-08-05 19:08:08 +00:00
FoxxMD 917d05f295 docs: Update configuration types language 2026-08-05 15:02:21 +00:00
FoxxMD 287c994643 docs: Some corrections and updated language 2026-08-05 14:15:25 +00:00
Matt Foxx 7980a0c856 Merge pull request #657 from FoxxMD/lastfmEndpointResponses
feat(lfm endpoint): Implement more api emulation
2026-08-05 09:28:43 -04:00
FoxxMD 9d2557cf6a feat(lfm endpoint): Handle scrobble payload variants 2026-08-04 22:14:03 +00:00
FoxxMD 6f8debc304 docs: Add lastfm endpoint alternative guidance 2026-08-04 18:50:59 +00:00
FoxxMD b53ae78d55 fix(lfm endpoint): Fix no response on no slug matched 2026-08-04 18:30:16 +00:00
FoxxMD 8723bc8890 fix(lfm endpoint): Improve regex slug matching to handle more variants 2026-08-04 18:29:41 +00:00
FoxxMD eca8ad2ce1 fix(lfm endpoint): Fix slug regex match 2026-08-04 18:16:39 +00:00
FoxxMD b6b8d3423f fix(lfm endpoint): Detect desired json response from accept header 2026-08-04 18:06:45 +00:00
FoxxMD fc027e8554 fix(lfm endpoint): remove duplicated method restrictions 2026-08-04 16:07:11 +00:00
FoxxMD 707da834bb docs: regenerate tealfm source env 2026-08-04 15:10:17 +00:00
FoxxMD 002520cd8a docs: Fix config.json example 2026-08-04 15:10:06 +00:00
FoxxMD 7ca84ce27f feat(lfm endpoint): Implement more api emulation #656
* Add optional username/apiKey config fields
* Identify Source by optional username/sk/api_key
* Emulate auth.getMobileSession response with sk derived from Source uid
* Update docs with guidance for multiple sources and improved setup language
2026-08-04 14:59:05 +00:00
FoxxMD 8af56cb561 revert: rollback tealfm source optional app password for now
This needs to be part of a larger refactor to require app password/permissions only for specific operations in the api client
2026-08-03 18:29:03 +00:00
FoxxMD 0e249b13a9 fix(source): Fix player cleanup for ingress sources
This bool check was backward. The scheduled task should no-op for polling sources and run cleanup for ingress sources

#652
2026-08-03 18:25:12 +00:00
FoxxMD 591b5c6618 fix(tealfm): app password for source is optional 2026-08-03 16:55:16 +00:00
FoxxMD 3c5b9cc402 fix(teal): teal client app password is required 2026-08-03 16:52:32 +00:00
FoxxMD 420a053e25 docs: update now playing for clients 2026-08-03 16:06:23 +00:00
FoxxMD 72f64953f6 chore: remove unused ts-zod config 2026-08-03 15:55:00 +00:00
Matt Foxx 8b8c2765c7 Merge pull request #654 from FoxxMD/configBuildSimplify
Simplify config validation and building
2026-08-03 11:53:23 -04:00
FoxxMD e1324921e9 fix: Fix missing name passed to component classes 2026-08-03 15:38:35 +00:00
FoxxMD 9f71fbef97 fix: prevent creating source/client with same id 2026-08-03 15:21:11 +00:00
FoxxMD dd22418c41 fix(tests): Fix missing required id 2026-08-03 14:56:33 +00:00
FoxxMD d296b825d6 Id logging and schema cleanup
* Guard against undefined name when logging component disabled
* Add id/name hint to config location
* inherit name/id field from common schema
2026-08-03 14:31:24 +00:00
FoxxMD 792ccc3c09 feat: Improve location wording for config errors 2026-08-03 13:39:14 +00:00
FoxxMD 6772e9934d refactor!: Enforce mandatory id for file/aio 2026-08-03 13:38:49 +00:00
FoxxMD 4ce7219214 fix: Skip ambigious configs during source parsing if configureAs is not present 2026-08-02 20:21:55 +00:00
FoxxMD b2ce42fecf simplify jellyfin mediatype usage for now 2026-08-01 19:12:04 +00:00
FoxxMD 46a061a602 docs: Update comma-separated list zod types for envs 2026-08-01 19:07:49 +00:00
FoxxMD ff7360fced docs: More env row generation improvements
* Always use `out` of common envs
* Wrapper function for normalizing descriptions with comma-separate lists
2026-08-01 19:07:35 +00:00
FoxxMD 425a0b2807 Partial fix for flattening complex zod types for docs 2026-08-01 17:47:44 +00:00
FoxxMD ad632f38d8 docs: regenerate env docs 2026-08-01 17:31:37 +00:00
FoxxMD 1d1b3b6f56 docs: Update env descriptions for id, name, and enable 2026-08-01 17:31:28 +00:00
FoxxMD 7575e8bff7 fix: Fix unhandled ZodTransform type generation 2026-08-01 17:31:03 +00:00
FoxxMD 31546ed4c1 fix: Early return on component add to prevent dynamic loading 2026-08-01 15:09:52 +00:00
FoxxMD bf9cdc141c re-add clients to aio json schema 2026-08-01 00:05:07 +00:00
FoxxMD 02ab1571b6 refactor: Convert validation client structures to dynamically import 2026-07-31 23:57:59 +00:00
FoxxMD 699bd02782 refactor: Convert validation structures to dynamically import 2026-07-31 23:41:56 +00:00
FoxxMD 0d45d06966 docs: Italicize required env properties 2026-07-31 23:41:12 +00:00
FoxxMD a93d02e1b7 env doc generation fixes 2026-07-31 20:50:41 +00:00
FoxxMD 513e2dde15 feat: Use env config generated from schema 2026-07-31 16:37:59 -04:00
FoxxMD 2fc90dcfda feat: Generate env markdown for components 2026-07-31 20:15:11 +00:00
FoxxMD fd2a178808 feat: Add env prefix to schema interface 2026-07-31 20:08:00 +00:00
FoxxMD 9f54d45576 fix(source): Fix missing defaults 2026-07-31 19:43:00 +00:00
FoxxMD 78aff32334 refactor: Extract source/client building into a function to reduce verbosity 2026-07-31 15:33:34 -04:00
FoxxMD 0115ccb862 refactor: Simplify ScrobbleSources config builder 2026-07-31 19:01:10 +00:00
FoxxMD 8a71663c9f Implement remaining source env schemas 2026-07-31 18:27:01 +00:00
FoxxMD 40eff0b901 refactor: Simplify ScrobbleClients config builder 2026-07-31 17:27:44 +00:00
FoxxMD 13acc56424 refactor client env schemas to reuse zod shape where possible 2026-07-31 10:39:14 -04:00
FoxxMD 09fc0ce15c Implement majority of source env schemas 2026-07-31 10:32:21 -04:00
FoxxMD 4fbdd71588 add EnvSourceSchema interface 2026-07-31 14:03:04 +00:00
FoxxMD 7cf105935e feat: Implement remaining env schemas for clients 2026-07-31 09:59:57 -04:00
FoxxMD 1bd10326eb add generic client env data validation interface 2026-07-31 13:31:52 +00:00
FoxxMD 74605caf15 add interface for env schemas 2026-07-31 13:10:13 +00:00
FoxxMD 7dec9a39e6 chore(release): Bump version for release 2026-07-30 17:50:57 +00:00
FoxxMD 90a8426748 Proof of concept: generate ENV docs from zod schema 2026-07-30 17:19:32 +00:00
FoxxMD 8469d161e9 feat: Reduce db write frequency when idle by buffering date updates #653 2026-07-30 12:37:38 +00:00
Matt Foxx 941f50bff2 Merge pull request #651 from FoxxMD/mbTrackGuard
fix: Guard against tracks/track in mb recording medium
2026-07-30 00:09:59 -04:00
FoxxMD f8d882cfcf docs: Clarify when activity is scrobbled 2026-07-29 19:37:40 +00:00
FoxxMD 0704cebe72 fix(webscrobbler): Fix bad access of PlayerState #650 2026-07-29 15:49:54 +00:00
326 changed files with 18927 additions and 5954 deletions
+1 -1
View File
@@ -159,4 +159,4 @@ storybook-static
bun.lock
.DS_Store
**/.DS_Store
**/.DS_Store
+4 -2
View File
@@ -22,7 +22,8 @@
"type": "spotify",
"enable": true,
"clients": ["myConfig"],
"name": "mySpotifySource",
"name": "Cool Spotify Name",
"id": "mySpotifyId",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
@@ -34,7 +35,8 @@
{
"type": "maloja",
"enable": true,
"name": "myConfig",
"name": "Foxx Maloja",
"id": "myMalojaId",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
+4 -1
View File
@@ -18,4 +18,7 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lib
lib
docs/api/
static/openapi.json
@@ -0,0 +1,12 @@
| Environmental Variable | Type | Default | Description |
| ---------------------------------- | -------------------------- | --------------------- | --------------------------------------- |
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
| `DISCORD_ENABLE` | boolean | true | Should this component be used? |
| `DISCORD_TOKEN` | string | | |
| `DISCORD_ARTWORK` | boolean OR string OR array | | |
| `DISCORD_APPLICATION_ID` | string | | |
| `DISCORD_IPC_LOCATIONS` | string | | |
| `DISCORD_ARTWORK_DEFAULT_URL` | string | | |
| `DISCORD_STATUS_OVERRIDE_ALLOW` | string | | |
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | string | | |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | --------------------------------------- |
| _**`KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name EX `My Cool Component` |
| `KOITO_ENABLE` | boolean | true | Should this component be used? |
| _**`KOITO_URL`**_ | string | | URL for the Koito server |
| _**`KOITO_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`KOITO_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------------------------- | ------------------------------------------------------- |
| _**`LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name EX `My Cool Component` |
| `LASTFM_ENABLE` | boolean | true | Should this component be used? |
| _**`LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account |
| _**`LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account |
| `LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. |
| `LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- |
| _**`LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name EX `My Cool Component` |
| `LIBREFM_ENABLE` | boolean | true | Should this component be used? |
| `LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account |
| `LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account |
| `LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. |
| `LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow |
| `LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- |
| _**`LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LZ_NAME` | string | Value of `LZ_ID` | A vanity name EX `My Cool Component` |
| `LZ_ENABLE` | boolean | true | Should this component be used? |
| `LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default |
| _**`LZ_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`LZ_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | -------------------- | --------------------------------------- |
| _**`MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name EX `My Cool Component` |
| `MALOJA_ENABLE` | boolean | true | Should this component be used? |
| _**`MALOJA_URL`**_ | string | | URL for maloja server |
| _**`MALOJA_API_KEY`**_ | string | | API Key for Maloja server |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| _**`ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name EX `My Cool Component` |
| `ROCKSKY_ENABLE` | boolean | true | Should this component be used? |
| `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account |
| `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account |
| _**`ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ------------------------- | ------- | -------------------- | --------------------------------------------------------------------------------------------- |
| _**`TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name EX `My Cool Component` |
| `TEALFM_ENABLE` | boolean | true | Should this component be used? |
| _**`TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as |
| _**`TEALFM_APP_PW`**_ | string | | The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account |
@@ -17,10 +17,10 @@ A **Client** is an application that stores the historical information about what
| [Last.fm](/configuration/clients/lastfm) | ✅ |
| [Libre.fm](/configuration/clients/librefm) | ✅ |
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
| [Koito](/configuration/clients/koito) | |
| [Koito](/configuration/clients/koito) | |
| [Maloja](/configuration/clients/maloja) | ❌ |
| [Rocksky](/configuration/clients/rocksky) | ❌ |
| [Teal.fm](/configuration/clients/tealfm) | |
| [Teal.fm](/configuration/clients/tealfm) | |
## Features
@@ -557,10 +557,12 @@ Remember, this URL need to be accessible on the public internet.
## Configuration
<Config config="DiscordClientConfig" fileContent={JsonConfig} client name="discord">
| Environmental Variable | Required? | Default | Description |
| :---------------------------- | --------- | ------- | :--------------------------------------------------------------------------------------------------------------- |
| `DISCORD_APPLICATION_ID` | Yes | | Application ID |
| `DISCORD_IPC_LOCATIONS` | No | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
| Environmental Variable | Type | Default | Description |
| :----------------------------- | ----------------- | --------------------- | :--------------------------------------------------------------------------------------------------------------- |
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
| _**`DISCORD_APPLICATION_ID`**_ | string | | Application ID |
| `DISCORD_IPC_LOCATIONS` | string | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
| `DISCORD_ARTWORK` | boolean | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | string or boolean | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
</Config>
@@ -323,12 +323,14 @@ The order in which multi-scrobbler determines what image to use is the same as t
## Configuration
<Config config="DiscordClientConfig" fileContent={JsonConfig} client name="discord">
| Environmental Variable | Required? | Default | Description |
| :--------------------------------- | --------- | ----------------- | :---------------------------------------------------------------------------------------------------------------- |
| `DISCORD_TOKEN` | Yes | | User Token acquired from an active Discord sessions |
| `DISCORD_APPLICATION_ID` | No | | Application ID used to display album art |
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
| `DISCORD_STATUS_OVERRIDE_ALLOW` | No | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | No | | A commera-seperated list of listening activity names MS **is allowed** to broadcast presence at the same time as. |
| Environmental Variable | Type | Default | Description |
| :--------------------------------- | ----------------- | --------------------- | :---------------------------------------------------------------------------------------------------------------- |
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
| _**`DISCORD_TOKEN`**_ | string | | User Token acquired from an active Discord sessions |
| `DISCORD_APPLICATION_ID` | string | | Application ID used to display album art |
| `DISCORD_ARTWORK` | boolean | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
| `DISCORD_ARTWORK_DEFAULT_URL` | string or boolean | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
| `DISCORD_STATUS_OVERRIDE_ALLOW` | string | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | string | | A commera-seperated list of listening activity names MS **is allowed** to broadcast presence at the same time as. |
</Config>
+3 -8
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_koito.md"
:::tip[Other Uses]
@@ -51,12 +52,6 @@ For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLO
## Configuration
<Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| `KOITO_ID` | Yes | | A unique id |
| `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 |
| `KOITO_NAME` | No | | A vanity name, different than ID |
<Config config="KoitoClientConfig" fileContent={JsonConfig} client ambiguous name="koito">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_lastfm.md"
:::tip[Other Users]
@@ -62,13 +63,6 @@ The Redirect URL can be explicitly specified in the configuration below, or by u
## Configuration
<Config config="LastfmClientConfig" fileContent={JsonConfig} client name="lastfm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `LASTFM_ID` | Yes | | A unique id |
| `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. |
| `LASTFM_NAME` | No | | A vanity name, different than ID |
<Config config="LastfmClientConfig" fileContent={JsonConfig} client ambiguous name="lastfm">
<ENVConfig/>
</Config>
+3 -11
View File
@@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_librefm.md"
:::tip[Other Users]
@@ -53,15 +54,6 @@ https://my.cool.libre.com/2.0/
## Configuration
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client name="librefm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `LIBREFM_ID` | Yes | | A unique ID |
| `LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
| `LIBREFM_API_KEY` | No | `anything` | Optional API Key |
| `LIBREFM_SECRET` | No | `anything` | Optional shared secret |
| `LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
| `LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
| `LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
| `LIBREFM_NAME` | No | | A vanity name different than ID |
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client ambiguous name="librefm">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_listenbrainz.md"
:::tip[Other Users]
@@ -22,12 +23,6 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
## Configuration
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `LZE_ID` | Yes | | A unique ID |
| `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 |
| `LZ_NAME` | No | | A vanity name different than ID |
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client ambiguous name="listenbrainz">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_maloja.md"
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
@@ -37,11 +38,6 @@ Finally, add the Maloja server URL and API Key to the configuration type you cho
## Configuration
<Config config="MalojaClientConfig" fileContent={JsonConfig} client name="maloja">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------- |
| `MALOJA_ID` | Yes | | A unique ID |
| `MALOJA_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
| `MALOJA_NAME` | No | | A vanity name different than ID |
<Config config="MalojaClientConfig" fileContent={JsonConfig} client ambiguous name="maloja">
<ENVConfig/>
</Config>
@@ -6,6 +6,7 @@ toc_max_heading_level: 5
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_rocksky.md"
:::tip[Other Uses]
@@ -75,11 +76,6 @@ Keys are now **deprecated** and functionality for them will be removed in a futu
</DetailsAdmo>
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client name="rocksky">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `ROCKSKY_ID` | Yes | | A unique ID used in MS |
| `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
| `ROCKSKY_TOKEN` | Yes | | Access Token obtained from Rocksky |
| `ROCKSKY_NAME` | No | | A vanity name different than ID |
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client ambiguous name="rocksky">
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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";
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_tealfm.md"
:::tip[Other Uses]
@@ -77,11 +78,6 @@ If using a handle:
If your identifier does not look like a DID and does 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 |
| :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `TEAMFM_ID` | Yes | | A unique ID |
| `TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
| `TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
| `TEALFM_NAME` | No | | A vanity name different than ID |
<Config config="TealClientConfig" fileContent={JsonConfig} client ambiguous name="tealfm">
<ENVConfig/>
</Config>
+110 -52
View File
@@ -13,7 +13,7 @@ import FileExample from "../../src/components/FileExample";
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
import AIOConfig from '!!raw-loader!../../../config/config.json.example';
import SpotifyConfig from '!!raw-loader!../../../config/spotify.json.example';
import SingleConfig from '!!raw-loader!../../../config/jellyfin.json.example';
:::tip
@@ -29,40 +29,35 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
* client/source specific json config files
* an all-in-one json config file
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one type.
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
MS will parse environmental variables present in the OS/container when it is run. **This method means MS does not require files to run.**
MS will parse environmental variables present in the OS/container when it is run.
This method means MS **does not require config files** but it will still create and use data files in the [local](/installation/#config-and-data-directories) or [docker](/installation/?dockerSetting=storage#recommended-settings) data directory.
<details>
<summary>Use ENV-based configuration if...</summary>
* You are the only person for whom MS is scrobbling for
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Plex -> Maloja
* You are the only person for whom MS is scrobbling for or
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Jellyfin -> Koito, or
* You are not comfortable editing JSON files
</details>
<details>
<summary>Config Example</summary>
For Docker container...
```shell
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...
```
For Docker Compose
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
environment:
- SPOTIFY_CLIENT_ID=yourId
- SPOTIFY_CLIENT_SECRET=yourSecret
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- JELLYFIN_URL=192.168.0.110:8096
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
- JELLYFIN_USER=MyUser
- JELLYFIN_ID=myJellyin
# ...
# ...
```
@@ -77,77 +72,114 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
<TabItem value="file" label="File">
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
* Local installation -> `PROJECT_DIR/config`
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
* Docker -> `/config` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) for how to configure this correctly
<details>
<summary>Use File-based configuration if...</summary>
* You have many [Sources](/configuration/sources)
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only to scrobble to specific **Clients**
* You are comfortable editing JSON files
* You have more than one of the same type of **Source/Client** you want to use EX 2x Last.fm accounts, 3x Jellyfin accounts
* You want activity from specific **Sources** to scrobble to specific **Clients**
* You need to setup more advanced configuration for a Source/Client
* Most Source/Clients only support basic configuration through ENV, all configuration is possible using File/AIO
* Most Source/Clients only support basic configuration through ENV, more configuration is possible using File. AIO enables complete configuration.
</details>
:::tip
* There are **example configurations** for all Source/Client types and AIO config located in the [`/config`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
* For docker installations these examples are copied to your configuration directory on first-time use.
* These are the same examples you will find in the **Configuration** section of each Source/Client
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
:::
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
Each config file is named after the **type** of the Client/Source. Within each file, a JSON **array** defines all of the instances for that type of Client/Source.
Example directory structure:
```
/CONFIG_DIR
plex.json
spotify.json
maloja.json
jellyfin.json
lastfm.json
koito.json
```
<details>
<summary>Config Example</summary>
<CodeBlock title="CONFIG_DIR/spotify.json" language="json5">{SpotifyConfig}</CodeBlock>
```yaml title="jellyfin.json"
[
{
"name": "Foxx JF Server",
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
}
]
```
</details>
<details>
<summary>Multiple of Same Config Example</summary>
```yaml title="jellyfin.json"
[
{
"name": "Foxx JF Server",
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
},
// highlight-start
{
"name": "Foo's JF Server",
"id": "fooJf",
"data": {
"url": "http://192.168.0.150:8096",
"user": "foo",
"apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"
}
}
// highlight-end
]
```
</details>
You can find an example of the file name that should be used under the **File** Configuration for each Source/Client. Note: this is the same value that is used for `type` in AIO configs.
</TabItem>
<TabItem value="aio" label="File AIO">
MS will parse an **all-in-one** configuration file located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
MS will parse an **all-in-one** configuration file (`config.json`) located in the directory specified by the `CONFIG_DIR` environmental variable. The file's location defaults to:
* Local installation -> `PROJECT_DIR/config/config.json`
* Docker -> `/config/config.json` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
* Docker -> `/config/config.json` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) for how to configure this correctly
<details>
<summary>Use AIO-based configuration if...</summary>
* You have many [Sources](/configuration/sources)
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only to scrobble to specific **Clients**
* You need to setup [monitoring/webhooks](#monitoring)
* You want to setup defaults for all Sources/Clients
* ...All of the reasons listed in [File](./?configType=file#configuration-types)
* You want to setup [monitoring/webhooks](#monitoring), default settings for all Sources/Clients, or global config options
* You want to use [Transformers](/configuration/transforms) (other than what is provided for [Musicbrainz ENV](/configuration/transforms/musicbrainz/#env-configuration))
* You want complete control over all potential MS configuration
</details>
**The AIO config also enables setting default options for sources/clients as well as global options for MS itself.**
:::tip
* An example AIO config files can be found in the project directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example)
* For docker installations this example is copied to your configuration directory on first-time use.
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
* **AIO config is the "complete" way to configure MS.**
* All settings available in File/ENV are available in AIO and AIO has *more* exclusive settings.
* AIO enables configuring **default settings** for all Sources/Clients
* There is a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
* Use the [**Config Playground**](/playground) to explore the entire config schema with an editable example
* The example used can also be found in the repo directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example) in the example below
:::
[**Explore the schema for this configuration, along with an example generator and validator, here**](/playground)
<details>
<summary>Config Example</summary>
@@ -155,6 +187,32 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
<CodeBlock title="CONFIG_DIR/config.json" language="json5">{AIOConfig}</CodeBlock>
</details>
<DetailsAdmo type="important" summary="Source/Client AIO Configs require a 'type' property">
Compared to their [File](./?configType=file#configuration-types) equivalent, Sources/Clients configured using AIO require an additional `type` property.
```yaml title="config.json"
{
"sources": [
{
"name": "Foxx JF Server",
// highlight-start
"type: "jellyfin",
// highlight-end
"id": "foxxJf",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
}
]
}
```
You can find an example of the required `type` value by referring to the **AIO** tab in each Source/Client's Configuration section example. This is the same value that is used as the name of individual [File](./?configType=file#configuration-types) config files.
</DetailsAdmo>
</TabItem>
</Tabs>
@@ -248,7 +306,7 @@ WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECR
## Base URL
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
Defines the URL that is used to generate default redirect URLs for authentication for services like [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
* Default => `http://localhost:9078`
* Set with [ENV](./?configType=env#configuration-types) `BASE_URL` or `baseUrl` [all-in-one configuration](./?configType=aio#configuration-types)
@@ -279,7 +337,7 @@ Useful when running with [docker](../installation/installation.mdx#docker) so th
## Caching
Multi-scrobbler implements caching to persist important data across restarts, reduce external API calls, and make some actions faster.
Multi-scrobbler implements caching to reduce external API calls and make some actions faster.
A default **in-memory** cache store is used so that you always benefit from some caching. An optional, [**secondary** store](#secondary-caching) can be configured for greater caching capabilities.
@@ -301,7 +359,7 @@ API Calls to external (metadata) services used to [Enhance Scrobbles](/configura
<DetailsAdmo type="important" summary="Auth Cache Configuration">
Auth caching defaults to a **file** that is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-auth.cache`.
Auth caching defaults to a **file** that is stored in the `DATA_DIR` directory using the pre-defined file name `ms-auth.cache`.
This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)).
@@ -448,7 +506,7 @@ Multi-scrobbler depends on a SQLite database (`ms.db`) that is created on first
The database stores *all* Plays for your Sources/Clients as well as metadata and debugging information to help troubleshoot issues. Each Play is associated with a Source/Config in the database based on your configuration.
You **should set IDs for each Source/Client** so that the database can identify these even when the configuration is changed.
You **must set IDs for each Source/Client** so that the database can identify these even when the configuration is changed.
### Retention
@@ -0,0 +1,13 @@
| Environmental Variable | Type | Default | Description |
| ------------------------------------------ | ------- | ------------------------ | -------------------------------------------------------------------- |
| _**`APPLEMUSIC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name EX `My Cool Component` |
| `APPLEMUSIC_ENABLE` | boolean | true | Should this component be used? |
| `APPLEMUSIC_KEY_ID` | string | | |
| `APPLEMUSIC_TEAM_ID` | string | | |
| `APPLEMUSIC_KEY_P8` | string | | |
| _**`APPLEMUSIC_MEDIA_USER_TOKEN`**_ | string | | |
| `APPLEMUSIC_TOKEN` | string | | |
| `APPLEMUSIC_ORIGIN_HEADER` | string | | Origin header to include in every Apple Music API request. |
| `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | boolean | true | Fixes a quirk where Apple Music's history API hides duplicate plays. |
| `APPLEMUSIC_NORMALIZE_ALBUM` | boolean | true | Removes extraneous suffixes from album data |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | ------------------------------------------------- |
| _**`AZURA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name EX `My Cool Component` |
| `AZURA_ENABLE` | boolean | true | Should this component be used? |
| _**`AZURA_STATION`**_ | string | | The specific station to monitor |
| _**`AZURA_URL`**_ | string | | Base URL of the Azuracast instance |
| `AZURA_KEY` | string | | API Key used to access data about private streams |
| `AZURA_LISTENERS_NUM` | string | | |
| `AZURA_LIVE` | boolean | | |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ---------------- | ----------------------------------------------------------------------------------------------- |
| _**`CC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `CC_NAME` | string | Value of `CC_ID` | A vanity name EX `My Cool Component` |
| `CC_ENABLE` | boolean | true | Should this component be used? |
| `CC_BLACKLIST_DEVICES` | string | | DO NOT scrobble from any cast devices on this comma-delimited list that START WITH these values |
| `CC_WHITELIST_DEVICES` | string | | ONLY scrobble from any cast device on this comma-delimited list that START WITH these values |
| `CC_BLACKLIST_APPS` | string | | DO NOT scrobble from any application on this comma-delimited list that START WITH these values |
| `CC_WHITELIST_APPS` | string | | ONLY scrobble from any application on this comma-delimited list that START WITH these values |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| _**`DEEZER_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `DEEZER_NAME` | string | Value of `DEEZER_ID` | A vanity name EX `My Cool Component` |
| `DEEZER_ENABLE` | boolean | true | Should this component be used? |
| _**`DEEZER_ARL`**_ | string | | ARL retrieved from Deezer response header |
| `DEEZER_ACCOUNT_ID` | string | | The ID (USER_ID) of the linked account to monitor. |
| `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _**`LFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LFM_NAME` | string | Value of `LFM_ID` | A vanity name EX `My Cool Component` |
| `LFM_ENABLE` | boolean | true | Should this component be used? |
| `LFM_SLUG` | string | | When using **multiple sources without apikey/user**, or **not** using the standard base URL, this is the URL base path that should be used to identify scrobbles for this source |
| `LFM_USERNAME` | string | | A fake username to differentiate LFM Endpoint Sources |
| `LFM_API_KEY` | string | | A fake api key to differentiate LFM Endpoint Sources |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _**`LZE_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `LZE_NAME` | string | Value of `LZE_ID` | A vanity name EX `My Cool Component` |
| `LZE_ENABLE` | boolean | true | Should this component be used? |
| `LZE_SLUG` | string | | When using **multiple sources without tokens**, or **not** using a standard base URL, this is the URL base path that should be used to identify scrobbles for this source |
| `LZE_TOKEN` | string | | If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source |
| `LZE_USERNAME` | string | | The listenbrainz "username" to associate with this Source |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | --------------------- | --------------------------------------- |
| _**`ICECAST_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `ICECAST_NAME` | string | Value of `ICECAST_ID` | A vanity name EX `My Cool Component` |
| `ICECAST_ENABLE` | boolean | true | Should this component be used? |
| _**`ICECAST_URL`**_ | string | | The Icecast stream URL |
| `ICECAST_AUTO_MONITOR` | boolean | | |
@@ -0,0 +1,17 @@
| Environmental Variable | Type | Default | Description |
| -------------------------------- | -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| _**`JELLYFIN_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name EX `My Cool Component` |
| `JELLYFIN_ENABLE` | boolean | true | Should this component be used? |
| _**`JELLYFIN_USER`**_ | string | | The username of the user to authenticate for or track scrobbles for |
| `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for |
| `JELLYFIN_APIKEY` | string | | API Key to authenticate with. |
| _**`JELLYFIN_URL`**_ | string | | HOST:PORT of the Jellyfin server to connect to |
| `JELLYFIN_USERS_ALLOW` | string OR true | | Only scrobble for specific users from this comma-delimited list. If `true`, scrobble for all users. |
| `JELLYFIN_USERS_BLOCK` | string | | Do not scrobble for users from this comma-delimited list |
| `JELLYFIN_DEVICES_ALLOW` | string | | Only scrobble if device or application name contains strings from this comma-delimited list |
| `JELLYFIN_DEVICES_BLOCK` | string | | Do not scrobble if device or application name contains strings from this comma-delimited list |
| `JELLYFIN_LIBRARIES_ALLOW` | string | | Only scrobble if library name contains string from this comma-delimited list |
| `JELLYFIN_LIBRARIES_BLOCK` | string | | Do not scrobble if library name contains strings from this comma-delimited list |
| `JELLYFIN_FRONTEND_URL_OVERRIDE` | string | | HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) |
| `JELLYFIN_MEDIATYPES_ALLOW` | string | | Allow media types from this comma-delimited list to be scrobbled |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------------------- | -------------------------------------------------------- |
| _**`JRIVER_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `JRIVER_NAME` | string | Value of `JRIVER_ID` | A vanity name EX `My Cool Component` |
| `JRIVER_ENABLE` | boolean | true | Should this component be used? |
| _**`JRIVER_URL`**_ | string | http://localhost:52199/MCWS/v1/ | URL of the JRiver HTTP server to connect to |
| `JRIVER_USER` | string | | If you have enabled authentication, the username you set |
| `JRIVER_PASSWORD` | string | | If you have enabled authentication, the password you set |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------------------- | ------------------------------------------------- |
| _**`KODI_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `KODI_NAME` | string | Value of `KODI_ID` | A vanity name EX `My Cool Component` |
| `KODI_ENABLE` | boolean | true | Should this component be used? |
| _**`KODI_URL`**_ | string | http://localhost:8080/jsonrpc | URL of the Kodi HTTP server to connect to |
| _**`KODI_USER`**_ | string | | The username set for Remote Control via Web Sever |
| _**`KODI_PASSWORD`**_ | string | | The password set for Remote Control via Web Sever |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| -------------------------- | ------- | -------------------------- | --------------------------------------- |
| _**`SOURCE_KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_KOITO_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_KOITO_URL`**_ | string | | URL for the Koito server |
| _**`SOURCE_KOITO_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`SOURCE_KOITO_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | ------------------------------------- | ------------------------------------------------------- |
| _**`SOURCE_LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_LASTFM_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account |
| _**`SOURCE_LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account |
| `SOURCE_LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. |
| `SOURCE_LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- |
| _**`SOURCE_LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_LIBREFM_NAME` | string | Value of `SOURCE_LIBREFM_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_LIBREFM_ENABLE` | boolean | true | Should this component be used? |
| `SOURCE_LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account |
| `SOURCE_LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account |
| `SOURCE_LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. |
| `SOURCE_LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow |
| `SOURCE_LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ----------------------- | ------- | ----------------------------- | --------------------------------------------------------- |
| _**`SOURCE_LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_LZ_ENABLE` | boolean | true | Should this component be used? |
| `SOURCE_LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default |
| _**`SOURCE_LZ_TOKEN`**_ | string | | User token for the user to scrobble for |
| _**`SOURCE_LZ_USER`**_ | string | | Username of the user to scrobble for |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | --------------------------- | --------------------------------------- |
| _**`SOURCE_MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_MALOJA_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_MALOJA_URL`**_ | string | | URL for maloja server |
| _**`SOURCE_MALOJA_API_KEY`**_ | string | | API Key for Maloja server |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ----------------------- | ------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
| _**`MOPIDY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MOPIDY_NAME` | string | Value of `MOPIDY_ID` | A vanity name EX `My Cool Component` |
| `MOPIDY_ENABLE` | boolean | true | Should this component be used? |
| `MOPIDY_URL` | string | ws://localhost:6680/mopidy/ws/ | URL of the Mopidy HTTP server to connect to |
| `MOPIDY_URI_DENYLIST` | string | | Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive |
| `MOPIDY_URI_ALLOWLIST` | string | | Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive |
| `MOPIDY_ALBUM_DENYLIST` | string | Soundcloud | Remove album data that matches any case-insensitive string from this list when scrobbling, |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------- |
| _**`MPD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MPD_NAME` | string | Value of `MPD_ID` | A vanity name EX `My Cool Component` |
| `MPD_ENABLE` | boolean | true | Should this component be used? |
| `MPD_URL` | string | localhost:6600 | URL:PORT of the MPD server to connect to |
| `MPD_PASSWORD` | string | | Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------- |
| _**`MPRIS_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name EX `My Cool Component` |
| `MPRIS_ENABLE` | boolean | true | Should this component be used? |
| `MPRIS_BLACKLIST` | string | | DO NOT scrobble from any players that START WITH values from this comma-delimited list |
| `MPRIS_WHITELIST` | string | | ONLY scrobble from any players that START WITH values from this comma-delimited list |
@@ -0,0 +1,6 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | ------------------------------------------------------------ |
| _**`MCAST_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MCAST_NAME` | string | Value of `MCAST_ID` | A vanity name EX `My Cool Component` |
| `MCAST_ENABLE` | boolean | true | Should this component be used? |
| _**`MCAST_URL`**_ | string | | The host or URL of the YamahaExtendedControl endpoint to use |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------- |
| _**`MC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `MC_NAME` | string | Value of `MC_ID` | A vanity name EX `My Cool Component` |
| `MC_ENABLE` | boolean | true | Should this component be used? |
| `MC_URL` | string | ws://localhost:7905 | URL of the Musikcube Websocket (Metadata) server to connect to |
| _**`MC_PASSWORD`**_ | string | | Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation |
@@ -0,0 +1,13 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------ | --------------------------------------------------------------------------------------------- |
| _**`PLEX_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name EX `My Cool Component` |
| `PLEX_ENABLE` | boolean | true | Should this component be used? |
| _**`PLEX_URL`**_ | string | | http(s)://HOST:PORT of the Plex server to connect to |
| `PLEX_TOKEN` | string | | |
| `PLEX_USERS_ALLOW` | string | | Only scrobble for specific users from this comma-delimited list |
| `PLEX_USERS_BLOCK` | string | | Do not scrobble for users from this comma-delimited list |
| `PLEX_DEVICES_ALLOW` | string | | Do not scrobble if device or application name contains strings from this comma-delimited list |
| `PLEX_DEVICES_BLOCK` | string | | Only scrobble if device or application name contains strings from this comma-delimited list |
| `PLEX_LIBRARIES_ALLOW` | string | | Only scrobble if library name contains string from this comma-delimited list |
| `PLEX_LIBRARIES_BLOCK` | string | | Do not scrobble if library name contains strings from this comma-delimited list |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| _**`SOURCE_ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_ROCKSKY_ENABLE` | boolean | true | Should this component be used? |
| `SOURCE_ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account |
| _**`SOURCE_ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account |
@@ -0,0 +1,10 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
| _**`SONOS_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name EX `My Cool Component` |
| `SONOS_ENABLE` | boolean | true | Should this component be used? |
| _**`SONOS_HOST`**_ | string | | IP address of any connected Sonos speaker or device |
| `SONOS_DEVICES_ALLOW` | string | | Only scrobble if device name contains strings from this comma-delimited list |
| `SONOS_DEVICES_BLOCK` | string | | Do not scrobble if device name contains strings from this comma-delimited list |
| `SONOS_GROUPS_ALLOW` | string | | Only scrobble if the name of a group the playing device belongs to contains strings from this comma-delimited list |
| `SONOS_GROUPS_BLOCK` | string | | Do not scrobble if the name of a group the playing device belongs to contains strings from this comma-delimited list |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ----------------------------- | ------- | ------------------------------ | -------------------------------------------------------------------- |
| _**`SPOTIFY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name EX `My Cool Component` |
| `SPOTIFY_ENABLE` | boolean | true | Should this component be used? |
| _**`SPOTIFY_CLIENT_ID`**_ | string | | spotify client id |
| _**`SPOTIFY_CLIENT_SECRET`**_ | string | | spotify client secret |
| `SPOTIFY_REDIRECT_URI` | string | http://localhost:9078/callback | spotify redirect URI -- required only if not the default shown here. |
@@ -0,0 +1,8 @@
| Environmental Variable | Type | Default | Description |
| ------------------------- | ------- | ---------------------- | ------------------------------------------------- |
| _**`SUBSONIC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name EX `My Cool Component` |
| `SUBSONIC_ENABLE` | boolean | true | Should this component be used? |
| _**`SUBSONIC_USER`**_ | string | | Username to login to the server with |
| _**`SUBSONIC_PASSWORD`**_ | string | | Password for the user to login to the server with |
| _**`SUBSONIC_URL`**_ | string | | URL of the subsonic media server to query |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| -------------------------------- | ------- | --------------------------- | --------------------------------------------------------------------------------------------- |
| _**`SOURCE_TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name EX `My Cool Component` |
| `SOURCE_TEALFM_ENABLE` | boolean | true | Should this component be used? |
| _**`SOURCE_TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as |
| _**`SOURCE_TEALFM_APP_PW`**_ | string | | The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ---------------------------------------- |
| _**`VLC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `VLC_NAME` | string | Value of `VLC_ID` | A vanity name EX `My Cool Component` |
| `VLC_ENABLE` | boolean | true | Should this component be used? |
| `VLC_URL` | string | localhost:8080 | URL:PORT of the VLC server to connect to |
| _**`VLC_PASSWORD`**_ | string | | Password for the server |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ---------------- | ----------------------------------------------------------- |
| _**`WS_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `WS_NAME` | string | Value of `WS_ID` | A vanity name EX `My Cool Component` |
| `WS_ENABLE` | boolean | true | Should this component be used? |
| `WS_BLACKLIST` | string | | Block scrobbling from specific WebScrobbler Connectors |
| `WS_WHITELIST` | string | | Only allow scrobbling from specific WebScrobbler Connectors |
@@ -0,0 +1,7 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ---------------------- | --------------------------------------------------------------------------- |
| _**`YMBRIDGE_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `YMBRIDGE_NAME` | string | Value of `YMBRIDGE_ID` | A vanity name EX `My Cool Component` |
| `YMBRIDGE_ENABLE` | boolean | true | Should this component be used? |
| _**`YMBRIDGE_URL`**_ | string | | URL of the local Python bridge, for example http://yandex-music-bridge:9980 |
| `YMBRIDGE_API_KEY` | string | | Optional API key sent as X-API-Key to the bridge |
@@ -0,0 +1,9 @@
| Environmental Variable | Type | Default | Description |
| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------ |
| _**`YTM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
| `YTM_NAME` | string | Value of `YTM_ID` | A vanity name EX `My Cool Component` |
| `YTM_ENABLE` | boolean | true | Should this component be used? |
| `YTM_REDIRECT_URI` | string | | Google Cloud Console project OAuth Client Authorized redirect URI |
| `YTM_CLIENT_ID` | string | | Google Cloud Console project OAuth Client ID |
| `YTM_CLIENT_SECRET` | string | | Google Cloud Console project OAuth Client Secret |
| `YTM_COOKIE` | string | | The cookie retrieved from the Request Headers of music.youtube.com after logging in. |
@@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import AppleMusicConfig from '!!raw-loader!@site/../config/applemusic.json.example';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_applemusic.md"
This Source **monitors your Apple Music listening history** via the official Apple Music API and scrobbles new activity to your configured [Clients](/configuration/clients).
@@ -246,17 +247,5 @@ Here is an example of creating a User Stage to only strip suffixes for a specifi
## Configuration Reference
<Config config="AppleMusicSourceConfig" fileContent={AppleMusicConfig} name="applemusic">
| Environment Variable | Required? | Default | Description |
| ------------------------------------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APPLEMUSIC_ID` | Yes | | A unique ID for this source. |
| `APPLEMUSIC_MEDIA_USER_TOKEN` | Yes | | The media-user-token extracted from the browser. |
| `APPLEMUSIC_KEY_ID` | No | | Key ID from your MusicKit key. |
| `APPLEMUSIC_TEAM_ID` | No | | Team ID from your Apple Developer account. |
| `APPLEMUSIC_KEY_P8` | No | | The contents of your MusicKit `.p8` private key file. |
| `APPLEMUSIC_TOKEN` | No | | The authentication JWT extracted from the browser. |
| `APPLEMUSIC_INTERVAL` | No | `60` | Polling interval in seconds. |
| `APPLEMUSIC_ORIGIN_HEADER` | No | | Origin header for API requests, e.g. `https://music.apple.com`. Required when using a browser token. |
| `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | No | `true` | Apple Music deduplicates its history by bumping re-played tracks to the top. If you listen to Song A → Song B → Song A, the history changes from `[A]` to `[A, B]` (bumping A). MS detects this "top-rebound" to recover the intermediate play (B) and the re-listen (A). Disable this only if you see incorrect duplicate scrobbles. |
| `APPLEMUSIC_NORMALIZE_ALBUM` | No | `true` | Strips extraneous `- EP` and `- Single` suffixes from album names |
| `APPLEMUSIC_NAME` | No | | A vanity name different than the ID. |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_azuracast.md"
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
:::tip[Scrobbling Threshold]
@@ -112,12 +113,5 @@ For users who aren't administrators of Azuracast: if you can access the station
## Configuration
<Config config="AzuracastSourceConfig" fileContent={AzuracastConfig} name="azuracast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
| `AZURA_ID` | Yes | | A unique ID |
| `AZURA_URL` | Yes | | Azuracast URL *without station name* |
| `AZURA_STATION` | Yes | | The station URL Stub or shortcode |
| `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` |
| `AZURA_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_deezer.md"
<Tabs groupId="deezerSource" queryString>
<TabItem value="arl" label="Unofficial ARL">
@@ -134,9 +135,7 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
## Configuration
<Config config="DeezerCompatConfig" fileContent={DeezerConfig} name="deezer">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | ------------------------------------------------ |
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
<ENVConfig/>
</Config>
</TabItem>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_chromecast.md"
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.
@@ -149,13 +150,5 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
:::
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------ |
| `CC_ID` | Yes | | A unique ID |
| `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 |
| `CC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_icecast.md"
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
Parse metadata from any Icecast Station (streams) to use for scrobbling.
@@ -64,9 +65,5 @@ If no `sources` are configured for multi-scrobbler it will automatically try to
## Configuration
<Config config="IcecastSourceConfig" fileContent={IcecastConfig} name="icecast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ------------------------------- |
| `ICECAST_ID` | Yes | | A unique ID |
| `ICECAST_URL` | Yes | | Icecast URL |
| `ICECAST_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_jellyfin.md"
Must be using Jellyfin 10.7 or greater
@@ -52,20 +53,5 @@ Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for
#### Configuration
<Config config="JellyApiSourceConfig" fileContent={JellyfinConfig} name="jellyfin">
| Environmental Variable | Required? | Default | Description |
| -------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `JELLYFIN_ID` | **Yes** | | A unique ID |
| `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 |
| `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` |
| `JELLYFIN_FRONTEND_URL_OVERRIDE` | No | | The external URL to use for album art assets instead of `JELLYFIN_URL` |
| `JELLYFIN_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_jriver.md"
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 be found in the JRiver GUI:
@@ -50,11 +51,5 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
## Configuration
<Config config="JRiverSourceConfig" fileContent={JriverConfig} name="jriver">
| Environmental Variable | Required | Default | Description |
| ---------------------- | -------- | ------------------------------- | ---------------------------------------------- |
| JRIVER_ID | Yes | | A unique ID |
| 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 |
| JRIVER_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -7
View File
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_kodi.md"
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:
@@ -51,11 +52,5 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
## Configuration
<Config config="KodiSourceConfig" fileContent={KodiConfig} name="kodi">
| Environmental Variable | Required | Default | Description |
| ---------------------- | -------- | ----------------------------- | ------------------------------- |
| KODI_ID | Yes | | A unique ID |
| 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 |
| KODI_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_koito.md"
This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured [Clients.](/configuration/clients)
@@ -24,12 +25,6 @@ See the [Koito (Client)](/configuration/clients/koito) configuration for general
#### Configuration
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | --------- | ------- | --------------------------------- |
| `SOURCE_KOITO_ID` | Yes | | A unique ID |
| `SOURCE_KOITO_TOKEN` | Yes | | API Key from your Koito Account |
| `SOURCE_KOITO_USER` | Yes | | Your Koito username |
| `SOURCE_KOITO_URL` | No | | The base URL for the Koito server |
| `SOURCE_KOITO_NAME` | No | | A vanity name differen than ID |
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} configureAs ambiguous name="koito">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_endpointlfm.md"
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.
@@ -18,31 +19,147 @@ This Source enables multi-scrobbler to accept scrobbles from outside application
:::
### URL
<DetailsAdmo type="tip" summary="Recommended Alternative">
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Last.fm scrobbles at
This Source has the same data limitations as the [Last.fm (Source)](/configuration/sources/lastfm-source) and [Last.fm (Client)](/configuration/clients/lastfm) have: Last.fm does not support separating artists/album artists fields which means any multi-artist tracks will have all artists combined into one string. This makes scrobbling to other services, and using metadata corrections like [Musicbrainz](/configuration/transforms/musicbrainz), more difficult.
If your application has the option to scrobble using Listenbrainz then use the [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) Source instead.
</DetailsAdmo>
## Setup
:::tip[Scenario Picker]
Use **one** of the scenarios below based on how on your service(s) (things "scrobbling" to Multi-Scrobbler using this Source) will need to interact with MS.
<Tabs groupId="scenario" queryString>
<TabItem value="single" label="One Service">
> You have **one** service, like Panoscrobbler, that will scrobble to Multi-Scrobbler.
Configure **one** Last.fm Endpoint Source. Use the [standard base URL](#standard-base-url) to configure your service and [apikey/user](#authentication), if necessary. Use the same apikey/user in MS as you do in your service.
</TabItem>
<TabItem value="multiple" label="Many Services, Same User">
> You may have **more than one** service that will scrobble to Multi-Scrobbler, like ArchiveTune and Panoscrobbler.
>
> All of these services will scrobble for the same user (you) and all of the scrobbles should go to the same [Clients](/configuration/clients).
Configure **one** Lastfm Endpoint Source. Use the [standard base URL](#standard-base-url) to configure your service and configure [apikey/user](#authentication) in MS, if necessary. Use the same apikey/user for all your services as you configure in MS.
</TabItem>
<TabItem value="multiple-tokens" label="Many Services, Many Users, Can use LFM Account">
> You have **more than one** service that will scrobble to Multi-Scrobbler, like ArchiveTune and Panoscrobbler.
>
> Some of these services should scrobble for different users, scrobble to different [Clients](/configuration/clients), or generally be treated differently (such as for transforms).
>
> **All** of these services can accept a [**apikey/user**](#authentication) to authenticate a Last.fm user/account.
Configure one Last.fm Endpoint Sources **per service that should be treated differently**. That is, if multiple services are all "the same user" then you only need one Source for that entire group of services.
For each Lastfm Endpoint Source, configure a different [apikey/user](#authentication). Use that same apikey/user when configuring Last.fm for the associated service.
All Last.fm Endpoint Sources should use the same [standard base URL](#standard-base-url).
</TabItem>
<TabItem value="multiple-slug" label="Many Services, Many Users, Cannot use LFM Account">
> You have **more than one** service that will scrobble to Multi-Scrobbler, like ArchiveTune and Panoscrobbler.
>
> Some of these services should scrobble for different users, scrobble to different [Clients](/configuration/clients), or generally be treated differently (such as for transforms).
>
> All, or some, of these services **cannot** accept an [apikey/user](#authentication) to authenticate a Last.fm user/account.
**This is not a common scenario** but if it fits your usecase then use these guidelines to differentiate your services:
Configure one Last.fm Endpoint Sources **per service that should be treated differently**. That is, if multiple services are all "the same user" then you only need one Source for that entire group of services.
* For any groups of services that **can use [apikey/user](#authentication)**, use the instructions for [Many Services, Many Users, Can use LFM Account](./?scenario=multiple-tokens#setup)
* For any groups of services that **cannot accept** an apikey/user...
* You can still configure a Source **without an [apikey/user](#authentication)**. Any services not using an account will use this "default" Source.
* If multiple groups cannot use an apikey/user, or the groups cannot use the [standard base URL](#standard-base-url), then use different [base url **Slugs**](#multiple-sources).
</TabItem>
<TabItem value="slug" label="Service cannot use standard URL">
> You have **one** service, like PanoScrobbler, that will scrobble to Multi-Scrobbler.
>
> You have already tried the [**One Service**](./?scenario=single#setup) scenario with the [standard base URL](#standard-base-url) but it is not working.
**This is not a common scenario.** Check the [**Troubleshooting URL**](#troubleshooting-url) section before trying this.
At this point, you can try to use a different [base url **Slug**](#multiple-sources). Your service must be able to set a "fully custom" URL for Last.fm IE it must allow you to set more than just a domain/host.
</TabItem>
</Tabs>
:::
### Standard Base URL
When setting up your Last.fm client to communicate with Multi-Scrobbler replace the Last.fm domain with your Multi-Scrobbler domain:
https://**last.fm** => https://**yourMSDomain**
MS accepts Last.fm API calls with the same **url base** and structure as the [last.fm api](https://www.last.fm/api/intro), IE `http://yourMSDomain/2.0/`
### Authentication
If you are only setting up **one** Lastfm Endpoint Source then you do not need to configure any explicit username/apiKey/password for MS. If your Last.fm Client requires these credentials use any fake values you want.
<DetailsAdmo type="important" summary="Supported Auth Types">
Currently, Multi-Scrobbler only supports the [**Mobile Application**](https://www.last.fm/api/mobileauth) auth flow. If your client requires one of the other authentication flows please [open an issue](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=02-feature-request.yml).
**Note:** Your client does not **need** to implement any auth in order to use a Lastfm Endpoint Source. You can directly make [`track.scrobble`](https://www.last.fm/api/show/track.scrobble) or [`track.updateNowPlaying`](https://www.last.fm/api/show/track.updateNowPlaying) api calls to `http://yourMSDomain/2.0/` with any fake auth data you want.
</DetailsAdmo>
### Multiple Sources
If you need configure **more than one** Lastfm Endpoint Source then you should configure username/apiKey for MS, per Source. This enables MS to differentiate scrobbles for each Source.
Use the same **username** and/or **API Key** you configure with MS when setting up your Last.fm Client. These values can be anything you want, as long as they match between MS and your client. Password can be anything and is not checked.
However, if you cannot configure username/apiKeys for your scenario you may also use a URL **Slug** to tell MS which Source belongs to a specific user. **This is not a common scenario** and should only be used if username/apiKey configuration is not sufficient or not possible.
<DetailsAdmo type="tip" summary="Different URL Base (Slug)">
If you cannot use different username/apiKey per Source (or Last.fm Client) you can still differentiate Sources by using a different **url base (slug)** for Last.fm communication.
Setting a **slug** in config will change the **url base** for MS like this:
```
http://localhost:9078/2.0/
slug: "mySlug"
```
```
http://yourMsDomain/api/lastfm/mySlug
```
which is the "standard" Last.fm server path for scrobbling
The above url base is equivalent to making calls to `http://yourMSDomain/2.0/`
Use a slug only if you need to setup multiple Last.fm Endpoint sources and cannot use different tokens.
</DetailsAdmo>
If a slug is used then the URL will be:
## Troubleshooting URL
```
http://localhost:9078/api/lastfm/mySlug
```
If you think your service should be able to use the [standard base URL](#standard-base-url) but it is not working follow these steps before trying the [the base URL Slug](#multiple-sources).
#### Verify URL Format
Some service may require `http/https` in the URL format (`http://myMSDomainOrIp:9078`) while others may not (`myDomainOrIp:9078`). Check the docs for your service to see if they specify this or have an example. Try both versions, if possible, to see if one works.
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
Some services that can scrobble to a custom Last.fm URL may require the URL to have 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.
</DetailsAdmo>
#### Is MS is Reachable?
Verify that MS can be accessed by your service, or from the same host as the service is running. See the [**FAQ**](/FAQ/#ingress-connection) for guidance on troubleshooting ingress connection issues.
#### Service Logging
If you service has accessible logging, check to see if it outputs the URL it is trying to reach when authenticating the Last.fm apiKey/user, or trying to make a scrobble request.
If you decide to report this issue, please include this logging in your issue.
## Configuration
<Config config="LastFMEndpointSourceConfig" fileContent={LastfmEndpointConfig} name="endpointlfm">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `LFM_ID` | Yes | | A unique ID |
| `LFM_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 |
| `LFM_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_lastfm.md"
This Source **monitors your Last.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
@@ -23,13 +24,6 @@ To configure this Source use the same registration instructions and configuratio
## Configuration
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm">
| Environmental Variable | Required? | Default | Description |
| ---------------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
| `SOURCE_LASTFM_ID` | Yes | | A unique ID |
| `SOURCE_LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `SOURCE_LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `SOURCE_LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
| `SOURCE_LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
| `SOURCE_LASTFM_NAME` | No | | A vanity name different than ID |
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} configureAs ambiguous name="lastfm">
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_librefm.md"
This Source **monitors your Libre.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
@@ -22,15 +23,6 @@ To configure this Source use the same registration instructions and configuratio
## Configuration
<Config config="LibrefmSourceConfig" fileContent={JsonConfig} name="librefm">
| Environmental Variable | Required? | Default | Description |
| ----------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `SOURCE_LIBREFM_ID` | Yes | | A unique ID |
| `SOURCE_LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
| `SOURCE_LIBREFM_API_KEY` | No | `anything` | Optional API Key |
| `SOURCE_LIBREFM_SECRET` | No | `anything` | Optional shared secret |
| `SOURCE_LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
| `SOURCE_LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
| `SOURCE_LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
| `SOURCE_LIBREFM_NAME` | No | | A vanity name different than ID |
<Config config="LibrefmSourceConfig" fileContent={JsonConfig} configureAs ambiguous name="librefm">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_endpointlz.md"
This Source enables multi-scrobbler to accept scrobbles from outside applications **as if it was a Listenbrainz server.**
@@ -22,16 +23,76 @@ If your service/player has a "Scrobble to Listenbrainz" feature and there is an
## Setup
### URL
:::tip[Scenario Picker]
In general, you should use one of the following as the **base URL** when configuring your application to scrobble to multi-scrobbler:
Use **one** of the scenarios below based on how on your service(s) (things "scrobbling" to Multi-Scrobbler using this Source) will need to interact with MS.
<Tabs groupId="scenario" queryString>
<TabItem value="single" label="One Service">
> You have **one** service, like Navidrome, that will scrobble to Multi-Scrobbler.
Configure **one** Listenbrainz Endpoint Source. Use the [standard base URL](#standard-base-url) to configure your service and set a [Token](#token), if necessary. Use the same **Token** in MS as you do in your service.
</TabItem>
<TabItem value="multiple" label="Many Services, Same User">
> You may have **more than one** service that will scrobble to Multi-Scrobbler, like Navidrome and Panoscrobbler.
>
> All of these services will scrobble for the same user (you) and all of the scrobbles should go to the same [Clients](/configuration/clients).
Configure **one** Listenbrainz Endpoint Source. Use the [standard base URL](#standard-base-url) to configure your service and configure a [Token](#token) in MS, if necessary. Use the same token for all your services as you configure in MS.
</TabItem>
<TabItem value="multiple-tokens" label="Many Services, Many Users, Can use Tokens">
> You have **more than one** service that will scrobble to Multi-Scrobbler, like Navidrome and Panoscrobbler.
>
> Some of these services should scrobble for different users, scrobble to different [Clients](/configuration/clients), or generally be treated differently (such as for transforms).
>
> **All** of these services can accept a **Token** to authenticate a Listenbrainz user/account.
Configure one Listenbrainz Endpoint Sources **per service that should be treated differently**. That is, if multiple services are all "the same user" then you only need one Source for that entire group of services.
For each Listenbrainz Endpoint Source, configure a different [**Token**](#token). Use that same token when configuring Listenbrainz for the associated service.
All Listenbrainz Endpoint Sources should use the same [standard base URL](#standard-base-url).
</TabItem>
<TabItem value="multiple-slug" label="Many Services, Many Users, Cannot use Tokens">
> You have **more than one** service that will scrobble to Multi-Scrobbler, like Navidrome and Panoscrobbler.
>
> Some of these services should scrobble for different users, scrobble to different [Clients](/configuration/clients), or generally be treated differently (such as for transforms).
>
> All, or some, of these services **cannot** accept a **Token** to authenticate a Listenbrainz user/account.
**This is not a common scenario** but if it fits your usecase then use these guidelines to differentiate your services:
Configure one Listenbrainz Endpoint Sources **per service that should be treated differently**. That is, if multiple services are all "the same user" then you only need one Source for that entire group of services.
* For any groups of services that **can accept** a token, use the instructions for [Many Services, Many Users, Can use Tokens](./?scenario=multiple-tokens#setup)
* For any groups of services that **cannot accept** a token...
* You can still configure a Source **without a [Token](#token)**. Any services not using a token will use this "default" Source.
* If multiple groups cannot use a token, or the groups cannot use the [standard base URL](#standard-base-url), then use different [base url **Slugs**](#multiple-sources).
</TabItem>
<TabItem value="slug" label="Service cannot use standard URL">
> You have **one** service, like Navidrome, that will scrobble to Multi-Scrobbler.
>
> You have already tried the [**One Service**](./?scenario=single#setup) scenario with both [standard base URL](#standard-base-url) and neither is working.
**This is not a common scenario.** Check the [**Troubleshooting URL**](#troubleshooting-url) section before trying this.
At this point, you can try to use a different [base url **Slug**](#multiple-sources). Your service must be able to set a "fully custom" URL for Listenbrainz IE it must allow you to set more than just a domain/host.
</TabItem>
</Tabs>
:::
### Standard Base URL
In most scenarios you should use one of the following as the **base URL** when configuring your service to scrobble to multi-scrobbler:
* `http://myMultiScrobblerIP:9078`
* `http://myMultiScrobblerIP:9078/1/`
<DetailsAdmo type="tip" summary="Default Scrobble Submit URL">
If you are not using a [slug for multiple endpoints](#multiple-endpoints) then multi-scrobbler will accept Listenbrainz scrobbles at
Multi-scrobbler will accept Listenbrainz scrobbles at
```
http://myMultiScrobblerIP:9078/1/submit-listens
@@ -41,41 +102,34 @@ which is the **standard** Listenbrainz server path for scrobbling.
</DetailsAdmo>
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
Some services that can scrobble to a custom Listenbrainz URL may require the URL to have 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.
</DetailsAdmo>
### 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.
Most Listenbrainz clients require an Authentication Token to be provided during setup. This value can be anything you want but ensure you use the same value for `token` (ENV `LZE_TOKEN`) in your configuration for this Listenbrainz Endpoint Source.
### Multiple Endpoints
If your service does not require a token, or does not let you set one, then do not configure it for this Source either.
If you are setting up multiple [Listenbrainz (Endpoint) Sources](/configuration/sources/listenbrainz-endpoint) MS can differentiate scrobbles based on [Token](#token) so that you can use the same endpoint for many users.
### Multiple Sources
However, if you cannot configure multiple tokens for your scenario you may also use a URL **Slug** to tell MS which Source belongs to a specific user.
If you need configure **more than one** Listenbrainz Endpoint Source then MS can differentiate scrobbles based on [Token](#token) so that you can use the [**same, standard base URL**](#standard-base-url) for many users.
<details>
However, if you cannot configure multiple tokens for your scenario you may also use a URL **Slug** to tell MS which Source belongs to a specific user. **This is not a common scenario** and should only be used if **Token** configuration is not sufficient or not possible.
<summary>Using a Slug</summary>
<DetailsAdmo type="tip" summary="Different URL Base (Slug)">
When a **slug** is defined in ENV/File config MS will listen for listenbrainz routes using the slug as a the route path. Example:
When a **slug** is defined in ENV/File config MS will listen for listenbrainz `submit-listens` requests using the slug as the base route path. Example:
```
LZE_SLUG=foobar
```
MS will accept calls to
MS will accept requests for `submit-listens` (scrobble submissions and now playing) at
```
http://myMultiScrobblerIP:9078/api/listenbrainz/foobar
http://myMultiScrobblerIP:9078/api/listenbrainz/foobar/submit-listens
```
for scrobble submission.
</details>
</DetailsAdmo>
## Setup for Popular Apps
@@ -99,22 +153,39 @@ Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configur
* In the ListenBrainz Scrobbler configuration screen:
* User Token is the [Token](#token) you configured
* Toggle **Show advanced settings**
* Base URL is the [URL](#url) you can configured, or the equivalent of `http://myMultiScrobblerIP:9078` for your MS instance
* Base URL is the [URL](#standard-base-url) you can configured, or the equivalent of `http://myMultiScrobblerIP:9078` for your MS instance
* Save the configuration and you are ready to scrobble
To troubleshoot any errors, and assuming you are using Home Assistant, view the Logs in the Music Assistant Add-ons. Please include these logs in any reported issue.
</details>
## Troubleshooting URL
If you think your service should be able to use the [standard base URL](#standard-base-url) but it is not working follow these steps before trying the [the base URL Slug](#multiple-sources).
#### Verify URL Format
Some service may require `http/https` in the URL format (`http://myMSDomainOrIp:9078`) while others may not (`myDomainOrIp:9078`). Check the docs for your service to see if they specify this or have an example. Try both versions, if possible, to see if one works.
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
Some services that can scrobble to a custom Listenbrainz URL may require the URL to have 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.
</DetailsAdmo>
#### Is MS is Reachable?
Verify that MS can be accessed by your service, or from the same host as the service is running. See the [**FAQ**](/FAQ/#ingress-connection) for guidance on troubleshooting ingress connection issues.
#### Service Logging
If you service has accessible logging, check to see if it outputs the URL it is trying to reach when authenticating the Listenbrainz account/user, or trying to make a scrobble request.
If you decide to report this issue, please include this logging in your issue.
## Configuration
<Config config="ListenbrainzEndpointSourceConfig" fileContent={ListenbrainzEndpointConfig} name="endpointlz">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LZE_ID` | Yes | | A unique ID |
| `LZE_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 |
| `LZE_USERNAME` | No | | (Optional) A fake username that will be returned for token validation and on now-playing responses. If none is provided the Source's name is used instead. |
| `LZE_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_listenbrainz.md"
This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients.
@@ -25,12 +26,6 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
## Configuration
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
| `SOURCE_LZ_ID` | Yes | | A unique ID |
| `SOURCE_LZ_TOKEN` | Yes | | User token from your LZ profile |
| `SOURCE_LZ_USER` | Yes | | Your LZ username |
| `SOURCE_LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
| `SOURCE_LZ_NAME` | No | | A vanity name different than ID |d
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} configureAs ambiguous name="listenbrainz">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_maloja.md"
:::tip[Other Uses]
@@ -21,11 +22,6 @@ See the [Maloja (Client)](/configuration/clients/maloja) configuration for gener
## Configuration
<Config config="MalojaSourceConfig" fileContent={MalojaConfig} name="maloja">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | ------- | ------------------------------- |
| `SOURCE_MALOJA_ID` | Yes | | A unique ID |
| `SOURCE_MALOJA_URL` | Yes | | Base URL of your installation |
| `SOURCE_MALOJA_API_KEY` | Yes | | Api Key |
| `SOURCE_MALOJA_NAME` | Yes | | A vanity name different than ID |
<Config config="MalojaSourceConfig" fileContent={MalojaConfig} configureAs ambiguous name="maloja">
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mopidy.md"
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/)
@@ -98,5 +99,5 @@ For certain sources (Soundcloud) Mopidy does not have all track info (Album) and
## Configuration
<Config config="MopidySourceConfig" fileContent={MopidyConfig} name="mopidy">
No ENV support
<ENVConfig/>
</Config>
+2 -6
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mpd.md"
MS communicates with MPD using the [TCP client connection.](https://mpd.readthedocs.io/en/stable/user.html#client-connections)
@@ -23,10 +24,5 @@ Multi-scrobbler requires only the [`read`](https://mpd.readthedocs.io/en/stable/
## Configuration
<Config config="MPDSourceConfig" fileContent={MPDConfig} name="mpd">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------- | ------------------------------- |
| `MPD_ID` | Yes | | A unique ID |
| `MPD_URL` | No | `localhost:6600` | |
| `MPD_PASSWORD` | No | | |
| `MPD_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -7
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mpris.md"
[MPRIS](https://specifications.freedesktop.org/mpris-spec/latest) is a standard interface for communicating with Music Players on **linux operating systems.**
@@ -71,11 +72,5 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d
## Configuration
<Config config="MPRISSourceConfig" fileContent={MPRISConfig} name="mpris">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | -------------------------------------------------------------------------------- |
| MPRIS_ID | Yes | | A unique ID |
| 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 |
| MPRIS_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_musikcube.md"
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**:
@@ -31,10 +32,5 @@ 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_ID` | Yes | | A unique ID |
| `MC_URL` | No | `ws://localhost:7905` | Use port set for **metadata server** |
| `MC_PASSWORD` | Yes | | |
| `MC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+2 -12
View File
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_plex.md"
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.
@@ -194,16 +195,5 @@ To configure MS to use Musicbrainz with your Plex metadata use one of the method
#### Configuration
<Config config="PlexApiSourceConfig" fileContent={PlexConfig} name="plex">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ---------------------------------------------------------------------- |
| `PLEX_ID` | **Yes** | | A unique ID |
| `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 |
| `PLEX_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_rocksky.md"
:::tip[Other Uses]
@@ -32,11 +33,6 @@ See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration f
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
<Config config="RockskySourceConfig" fileContent={JsonConfig} name="rocksky">
| Environmental Variable | Required? | Default | Description |
| :---------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| `SOURCE_ROCKSKY_ID` | Yes | | A unique ID |
| `SOURCE_ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
| `SOURCE_ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard |
| `SOURCE_ROCKSKY_NAME` | No | | A vanity name different than ID |
<Config config="RockskySourceConfig" fileContent={JsonConfig} configureAs ambiguous name="rocksky">
<ENVConfig/>
</Config>
+2 -9
View File
@@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/sonos.json.example';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_sonos.md"
Multi-scrobbler can use the [Sonos](https://www.sonos.com) [local API](https://docs.sonos.com/docs/control) to monitor what your Sonos system is playing.
@@ -90,13 +91,5 @@ The **optional** `devices` and `groups` related properties found in the config b
You **must** define the IP address of at least one Sonos/accessory in order for Multi-scrobbler to connect to your Sonos system.
<Config config="SonosSourceConfig" fileContent={JsonConfig} name="sonos">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | :--------------------------------------------------------------------- |
| `SONOS_ID` | Yes | | A unique ID |
| `SONOS_HOST` | Yes | | The IP address of any active Sonos device/accessory EX `192.168.0.150` |
| `SONOS_DEVICES_ALLOW` | No | | Comma-separated list of Sonos device names to scrobble from |
| `SONOS_DEVICES_BLOCK` | No | | Comma-separated list of Sonos device names to disallow scrobbles from |
| `SONOS_GROUPS_ALLOW` | No | | Comma-separated list of device group names to scrobble from |
| `SONOS_GROUPS_BLOCK` | No | | Comma-separated list of devices group names to disallow scrobbles from |
| `SONOS_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+17 -1
View File
@@ -123,7 +123,23 @@ Otherwise, use the `name` property you gave the client in it's File/AIO config.
:::
### Scrobble Thresholds
### When Is Activity Scrobbled?
##### From Listening History
For [**Listening History**](./?sot=history#by-data-source-of-truth) Source-Of-Truth based Sources, like Listenbrainz and Last.fm, newely discovered history is immediately forwarded to relevant Clients.
##### From Ingress
For [**Ingress**](./?sot=activity&sourceComm=ingress#by-communication-method) communication based Sources, like [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) and [Webscrobbler](/configuration/sources/webscrobbler), the client sending data to Multi-Scrobbler is what determines when activity is scrobbled. This means that it up to the behavior and implementation of whatever application you point at these Source endpoints: when the client sends "this is scrobbled", MS immediately forwards it to relevant Clients.
##### From Listening Activity
For Sources that use [**Active**](./?sot=activity&sourceComm=active#by-communication-method) communication with [**Listening Activity**](./?sot=activity#by-data-source-of-truth) as the Source-Of-Truth -- like Jellyfin, Plex, Spotify, Subsonic, etc... -- *Multi-Scrobbler waits until the track changes or the active player becomes stale/disappears* before determining if the activity should be scrobbled.
This allows MS to record how long you listened to the track for as well as more accurately determine if **Scrobble Thresholds** have been met.
#### Scrobble Thresholds
<ScrobbleThreshold filename="spotify"/>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_spotify.md"
import IconWarning from '@theme/Admonition/Icon/Warning';
<DetailsAdmo type="note" summary="Spotify and Automix">
@@ -111,11 +112,5 @@ Use **one** of the following methods to specify a valid Redirect URI and complet
## Configuration
<Config config="SpotifySourceConfig" fileContent={SpotifyConfig} name="spotify">
| Environmental Variable | Required? | Default | Description |
| ----------------------- | --------- | -------------------------------- | ------------------------------- |
| `SPOTIFY_ID` | Yes | | A unique ID |
| `SPOTIFY_CLIENT_ID` | Yes | | |
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
| `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` |
| `SPOTIFY_NAME` | No | | A vantiy name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_subsonic.md"
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/))
@@ -47,11 +48,5 @@ If the server properly reports no playing song when playback has stopped or this
## Configuration
<Config config="SubSonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------ |
| `SUBSONIC_ID` | Yes | | A unique ID |
| `SUBSONIC_USER` | Yes | | |
| `SUBSONIC_PASSWORD` | Yes | | |
| `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server |
| `SUBSONIC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -9,6 +9,7 @@ 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 ENVConfig from "@site/docs/configuration/sources/_env_configs/_tealfm.md"
:::tip[Other Uses]
@@ -103,11 +104,6 @@ Allow scrobbles from plyr.fm and unknown services:
</details>
<Config config="TealClientConfig" fileContent={JsonConfig} name="tealfm">
| Environmental Variable | Required? | Default | Description |
| :------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
| `SOURCE_TEALFM_ID` | Yes | | A unique ID |
| `SOURCE_TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
| `SOURCE_TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
| `SORUCE_TEALFM_NAME` | No | | A vanity name different than ID |
<Config config="TealClientConfig" fileContent={JsonConfig} configureAs ambiguous name="tealfm">
<ENVConfig/>
</Config>
+2 -6
View File
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_vlc.md"
MS communicates with [VLC](https://www.videolan.org/vlc/) using the [Web (`http`) interface module](https://wiki.videolan.org/VLC_HTTP_requests/)
@@ -99,10 +100,5 @@ If you find that VLC is incorrectly reporting track information (in its interfac
## Configuration
<Config config="VLCSourceConfig" fileContent={JsonConfig} name="vlc">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------- | ------------------------------- |
| `VLC_ID` | Yes | | A unique ID |
| `VLC_URL` | No | `localhost:8080` | |
| `VLC_PASSWORD` | Yes | | |
| `VLC_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_webscrobbler.md"
After installing the [Webscrobbler extension](https://web-scrobbler.com/) open the preferences/settings for it:
@@ -87,11 +88,5 @@ This affects **only** MS's behavior and does not affect the general connector be
## Configuration
<Config config="WebScrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | ------------------------------------------------------------------------ |
| WS_ID | Yes | | A unique ID |
| 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 |
| WS_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_musiccast.md"
Monitor [MusicCast](https://usa.yamaha.com/products/contents/audio_visual/musiccast/index.html) device/receivers for music played on Network/USB/CD inputs.
@@ -31,9 +32,5 @@ EX
## Configuration
<Config config="MusicCastSourceConfig" fileContent={JsonConfig} name="musiccast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :------------------------------ |
| `MCAST_ID` | Yes | | A unique ID |
| `MCAST_URL` | Yes | | The Musiccast device URL |
| `MCAST_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JsonConfig from '!!raw-loader!@site/../config/ymbridge.json.example';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_ymbridge.md"
Monitor your [**Yandex Music**](https://music.yandex.com) listening activity in real-time.
@@ -72,10 +73,5 @@ If you have issues specifically with the MS Source please mention [**Druidblack*
## Configuration
<Config config="YandexMusicBridgeSourceConfig" fileContent={JsonConfig} name="ymbridge">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :------------------------------------------------------------------------ |
| `YMBRIDGE_ID` | Yes | | A unique ID |
| `YMBRIDGE_URL` | Yes | | `http://URL:PORT` for the `yandex-music-bridge` container |
| `YMBRIDGE_API_KEY` | No | | The same key used for `YM_API_KEY` on the `yandex-music-bridge` container |
| `YMBRIDGE_NAME` | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
@@ -8,6 +8,7 @@ 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';
import ENVConfig from "@site/docs/configuration/sources/_env_configs/_ytmusic.md"
:::warning
@@ -179,13 +180,5 @@ Only one of these methods needs to be used.
## Configuration
<Config config="YTMusicSourceConfig" fileContent={JsonConfig} name="ytmusic">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ------- | --------------------------------------------- |
| YTM_ID | Yes | | A unique ID |
| 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 |
| YTM_NAME | No | | A vanity name different than ID |
<ENVConfig/>
</Config>
+3 -4
View File
@@ -125,7 +125,7 @@ services:
</TabItem>
<TabItem value="linuxHost" label="Linux Host">
If you are running this container with **Docker** on a **Linux Host** you **should** 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 **should** specify `user:group` permissions of the user who owns the **configuration and data directories** 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:
@@ -150,7 +150,7 @@ services:
</TabItem>
<TabItem value="caching" label="Caching">
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration#secondary-caching) to take advantage of faster performance and reduced memory usage.
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration#secondary-caching) to take advantage of faster performance, reduced memory usage, and cached api calls to external services.
```yaml title="docker-compose.yml"
services:
@@ -215,8 +215,7 @@ services:
- PUID=1000
- PGID=1000
# uncomment along with valkey service/volume below for better caching
#- CACHE_METADATA=valkey
#- CACHE_METADATA_CONN=redis://valkey:6379
#- CACHE_VALKEY=redis://valkey:6379
volumes:
- ./config:/config
ports:
+3 -1
View File
@@ -100,10 +100,12 @@ Add an `id` to the top-level for each Source/Client configuration, next to `data
:::important[Default ID]
**If you do not add an ID then Multi-Scrobbler will automatically use the name of the Source/Client as the ID.** The name is shown in the Dashboard.
**In 0.14.0, If you do not add an ID then Multi-Scrobbler will automatically use the name of the Source/Client as the ID.** The name is shown in the Dashboard.
If you decide to add an ID later, you must use the Source/Client name as the ID in order to keep Plays/Scobbles associated with the same config.
**In [0.16.0+](https://github.com/FoxxMD/multi-scrobbler/releases/tag/0.16.0) you are required to have an ID or a configuration error is thrown.**
:::
## Cached Scrobble Migration
+60 -13
View File
@@ -3,6 +3,7 @@ import type { Config } from '@docusaurus/types';
import * as themes from 'prism-react-renderer';
import * as Renderers from './lib/socialCard/ImageRenderers.js';
import path from 'node:path';
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
//import sidebars from './sidebars';
let baseSite: string = 'https://foxxmd.github.io';
@@ -32,7 +33,7 @@ const config: Config = {
v4: {
removeLegacyPostBuildHeadAttribute: true, // required
},
experimental_faster: {
faster: {
ssgWorkerThreads: true,
rspackBundler: true, // required flag
rspackPersistentCache: true, // new flag
@@ -75,20 +76,29 @@ const config: Config = {
[
'classic',
{
docs: {
sidebarPath: './sidebars.ts',
docItemComponent: "@theme/ApiItem",
routeBasePath: '/',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: (params: {
version: string;
versionDocsDirPath: string;
docPath: string;
permalink: string;
locale: string;
}) => {
return `${path.join('https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite', params.versionDocsDirPath, params.docPath)}?plain=1`;
}
version: string;
versionDocsDirPath: string;
docPath: string;
permalink: string;
locale: string;
}) => {
return `https://github.com/FoxxMD/multi-scrobbler/blob/master${path.join('/docsite', params.versionDocsDirPath, params.docPath)}?plain=1`;
},
// based on this https://github.com/facebook/docusaurus/issues/6127#issuecomment-3937124499
// to filter out the autogenerated api folder sidebar item
sidebarItemsGenerator: async function ({ defaultSidebarItemsGenerator, ...args }) {
const items = await defaultSidebarItemsGenerator(args);
const filteredItems = items.filter(item => !(item.type === 'category' && item.label === 'api'));
return filteredItems
},
},
// blog: {
// showReadingTime: true,
@@ -100,7 +110,7 @@ const config: Config = {
blog: false,
theme: {
customCss: './src/css/custom.css',
customCss: ['./src/css/custom.css','./src/css/openapiBase.css','./src/css/evergreen.css'],
},
} satisfies Preset.Options,
],
@@ -122,10 +132,11 @@ const config: Config = {
// ```
},
],
'docusaurus-theme-github-codeblock'
'docusaurus-theme-github-codeblock',
"docusaurus-theme-openapi-docs"
],
plugins: [
'docusaurus-plugin-sass',
'docusaurus-plugin-sass',
[
'@docusaurus/plugin-client-redirects',
{
@@ -167,7 +178,38 @@ const config: Config = {
'docusaurus-plugin-content-pages': Renderers.docs,
},
},
]
],
[
'docusaurus-plugin-openapi-docs',
{
id: "api", // plugin id
docsPluginId: "classic", // configured for preset-classic
config: {
ms: {
specPath: "static/openapi.json",
outputDir: "docs/api",
showInfoPage: true,
maskCredentials: false, // Disable credential masking in code snippets
sidebarOptions: {
groupPathsBy: "tag",
},
} satisfies OpenApiPlugin.Options,
}
},
],
// [
// '@docusaurus/plugin-content-docs',
// {
// sidebarItemsGenerator: async function ({ defaultSidebarItemsGenerator, ...args }) {
// const items = await defaultSidebarItemsGenerator(args);
// for(const i of items) {
// console.log(i);
// }
// //const filteredItems = items.filter(item => item.type !== 'doc' || !item.id.endsWith('/index'));
// //return filteredItems
// },
// }
// ]
],
themeConfig:
{
@@ -193,6 +235,11 @@ const config: Config = {
position: 'left',
label: 'Config Playground',
},
{
to: 'api/multi-scrobbler-api',
position: 'left',
label: 'API',
},
{
href: 'https://github.com/foxxmd/multi-scrobbler',
label: 'GitHub',
+2796 -904
View File
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -14,14 +14,15 @@
"typecheck": "tsc",
"postinstall": "patch-package",
"previews:build-renderer": "tsc --project tsconfig.client.json || exit 0",
"prebuild": "npm run -s previews:build-renderer"
"prebuild": "npm run -s previews:build-renderer && npm run -s openapi:generate",
"openapi:generate": "docusaurus clean-api-docs ms && docusaurus gen-api-docs ms"
},
"dependencies": {
"@bony_chops/docusaurus-og": "^1.0.3-beta.3",
"@docusaurus/core": "3.9.2",
"@docusaurus/faster": "3.9.2",
"@docusaurus/plugin-client-redirects": "^3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@docusaurus/core": "3.10.2",
"@docusaurus/faster": "3.10.2",
"@docusaurus/plugin-client-redirects": "^3.10.2",
"@docusaurus/preset-classic": "3.10.2",
"@easyops-cn/docusaurus-search-local": "0.51.1",
"@mdx-js/react": "^3.0.0",
"ajv": "^8.18.0",
@@ -30,6 +31,7 @@
"clsx": "^2.0.0",
"docusaurus-plugin-sass": "^0.2.6",
"docusaurus-theme-github-codeblock": "^2.0.2",
"docusaurus-theme-openapi-docs": "^5.2.0",
"embla-carousel": "^9.0.0-rc02",
"embla-carousel-react": "^9.0.0-rc02",
"json5": "^2.2.3",
@@ -44,9 +46,9 @@
"sass": "^1.97.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.9.2",
"@docusaurus/tsconfig": "^3.9.2",
"@docusaurus/types": "^3.9.2",
"@docusaurus/module-type-aliases": "^3.10.2",
"@docusaurus/tsconfig": "^3.10.2",
"@docusaurus/types": "^3.10.2",
"@types/react": "^19.2.4",
"typescript": "~5.6.2"
},
@@ -69,6 +71,7 @@
"overrides": {
"lodash": "4.18.1",
"satori": "0.25.0",
"hast-util-to-jsx-runtime": "2.3.6"
"hast-util-to-jsx-runtime": "2.3.6",
"sharp": "0.35.3"
}
}
+6 -14
View File
@@ -1,5 +1,5 @@
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
import msSidebar from "./docs/api/sidebar.js";
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
@@ -19,21 +19,13 @@ const sidebars: SidebarsConfig = {
type: "link",
label: "Config Playground",
href: "/playground"
}
],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
{
type: "category",
label: "Api",
items: msSidebar,
},
],
*/
};
export default sidebars;
@@ -9,10 +9,15 @@ import AdmonitionIconImportant from './ImportantIcon';
export interface DetailsAdmoProps extends DetailProps {
type?: 'warning' | 'danger' | 'note' | 'tip' | 'important'
render?: boolean
}
const DetailsAdmo = (props: DetailsAdmoProps) => {
if(props.render === false) {
return null;
}
let cn: string = '';
let icon: ReactElement;
@@ -11,6 +11,8 @@ import {useWindowSize} from '@docusaurus/theme-common';
<TabItem value="env" label="ENV">
<EnvType>
{props.children}
_**Bold/Italic**_ = Required
</EnvType>
</TabItem>
<TabItem value="file" label="File">
@@ -35,10 +37,29 @@ import {useWindowSize} from '@docusaurus/theme-common';
* **text** mode lets you edit the JSON directly.
* **tree** mode gives you a guided point-and-click editing experience that always keeps the JSON *syntax* valid.
Both modes **validate** that the configuraion is correct. Any errors show up as squiggly lines.
Both modes **validate** that the configuration is correct. Any errors show up as squiggly lines.
<span style={{display: props.ambiguous === true ? 'inherit' : 'none'}}>
:::important
Only <strong children={props.client ? 'client' : 'source'}/> <span children={props.name}/> configs will validate correctly in the editor but *both* **source** and **client** <span children={props.name}/> configs are allowed in the real <code>CONFIG/<span children={props.name}/>.json</code>
:::
</span>
After you finish editing, switch to **text** and then copy all text to get a completed config.
</DetailsAdmo>
<DetailsAdmo type="important" summary="configureAs Required" render={props.configureAs === true}>
When configuring a <span children={props.name}/> **Source** in a **File** Config you **must** set
```
"configureAs": "source"
```
Without this property, Multi-Scrobbler will assume the config is for a <span children={props.name}/> **Client** config.
This requirement does not apply to **AIO** <span children={props.name}/> configs.
</DetailsAdmo >
</div>
<div className="fileEditorMobile">
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
+6 -3
View File
@@ -11,6 +11,7 @@ import "./json-schema-viewer-styles.css";
import "./modern-json-react-styles.css";
import { Mode, ReactJsonEditor, createAjvValidator, type Content } from 'modern-react-json-editor';
import f from "ajv-formats"
import Ajv2020 from 'ajv/dist/2020';
import DetailsAdmo from "./AdmonitionDetails";
const validator = createAjvValidator({
@@ -21,9 +22,11 @@ const validator = createAjvValidator({
allowUnionTypes: true,
},
onCreateAjv: (ajv) => {
ajv.addKeyword('deprecationMessage');
f.default(ajv);
return ajv;
const a = new Ajv2020();
a.addKeyword('deprecationMessage');
a.addKeyword('zockerId');
f.default(a);
return a;
}
});
@@ -5,6 +5,7 @@ import { useColorMode } from '@docusaurus/theme-common';
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t");
import "../modern-json-react-styles.css";
import { JSONSchema, Mode, ReactJsonEditor, createAjvValidator, type Content, type Validator } from 'modern-react-json-editor';
import Ajv2020 from 'ajv/dist/2020';
import f from "ajv-formats"
import json5 from 'json5';
@@ -29,9 +30,11 @@ function SchemaEditorInner(props: SchemaEditorProps): JSX.Element {
allowUnionTypes: true,
},
onCreateAjv: (ajv) => {
ajv.addKeyword('deprecationMessage');
f.default(ajv);
return ajv;
const a = new Ajv2020();
a.addKeyword('deprecationMessage');
a.addKeyword('zockerId');
f.default(a);
return a;
}
});
}, [props.schema]);
@@ -1,6 +1,6 @@
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:
A **Source** that monitors Listening Activity 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)
+323
View File
@@ -0,0 +1,323 @@
/**
* Evergreen theme for docusaurus-openapi-docs demo.
*
* A modern riff on the classic Docusaurus green. Keeps the signature
* #2e8555 primary family, adds Plus Jakarta Sans, a richer dark mode,
* and cleaner shadows — what the default template could look like with
* a bit more design polish.
*
* Load alongside custom.css in docusaurus.config.ts:
*
* customCss: ["./src/css/custom.css", "./src/css/themes/evergreen.css"]
*/
/* ============================================================
* Fonts — Plus Jakarta Sans (body) + Fira Code (code)
* ============================================================ */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap");
/* ============================================================
* Light mode
* ============================================================ */
:root {
/* Primary — the classic Docusaurus green, slightly enriched */
/* --ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e; */
/* Content */
/* --ifm-color-content: #1c1e21;
--ifm-heading-color: #1c1e21;
--ifm-color-content-secondary: #525860; */
/* Semantic */
/* --ifm-color-success: #2e8555;
--ifm-color-warning: #d97706;
--ifm-color-danger: #dc2626;
--ifm-color-info: #0284c7; */
/* Typography */
/* --ifm-font-size-base: 95%;
--ifm-code-font-size: 85%;
--ifm-font-family-base:
"Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--ifm-heading-font-family:
"Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--ifm-font-family-monospace:
"Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Courier New", monospace;
--font-mono:
"Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Courier New", monospace;
--ifm-font-weight-base: 400;
--ifm-font-weight-semibold: 600;
--ifm-heading-font-weight: 700; */
/* Heading scale */
/* --ifm-h1-font-size: 2rem;
--ifm-h2-font-size: 1.6rem;
--ifm-h3-font-size: 1.3rem;
--ifm-h4-font-size: 1.1rem;
--ifm-h5-font-size: 1rem;
--ifm-h6-font-size: 0.9rem; */
/* Theme hooks for custom.css */
--theme-primary-rgb: 46, 133, 85;
--site-primary-hue-saturation: 148 49%;
--theme-border-radius-sm: 4px;
--theme-border-radius-md: 8px;
--theme-border-radius-lg: 12px;
--theme-border-radius-xl: 16px;
--theme-border-radius-2xl: 24px;
/* Shadow scale — neutral, not green-tinted */
--theme-shadow-sm:
0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.06);
--theme-shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.06);
--theme-shadow-lg:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.06);
/* --ifm-hr-background-color: var(--ifm-color-emphasis-200);
--docusaurus-highlighted-code-line-bg: rgba(46, 133, 85, 0.1); */
/* Explorer — unified font size for code tabs and request body */
--openapi-explorer-font-size-code: 13px;
}
/* ============================================================
* Dark mode — richer than the Docusaurus default
* ============================================================ */
[data-theme="dark"] {
/* Lighter green — readable on dark surfaces */
/* --ifm-color-primary: #3cad6e;
--ifm-color-primary-dark: #359962;
--ifm-color-primary-darker: #33925d;
--ifm-color-primary-darkest: #2e8555;
--ifm-color-primary-light: #4db87c;
--ifm-color-primary-lighter: #5ec389;
--ifm-color-primary-lightest: #85d4a8; */
/* --ifm-color-content: #dadde1;
--ifm-color-content-secondary: #9ba1aa;
--ifm-heading-color: #f5f6f7; */
/* Richer dark background vs. the flat Docusaurus default */
/* --ifm-background-color: #18191a !important;
--ifm-navbar-background-color: #18191a; */
/* Warm-neutral dark gray scale */
/* --ifm-color-emphasis-100: #242526;
--ifm-color-emphasis-200: #3a3b3c;
--ifm-color-emphasis-300: #4e4f50;
--ifm-color-emphasis-400: #6b6c6d;
--ifm-color-emphasis-500: #8d8f91;
--ifm-color-emphasis-600: #b0b3b8;
--ifm-color-emphasis-700: #dadde1;
--ifm-color-emphasis-800: #e4e6eb;
--ifm-color-emphasis-900: #f0f2f5; */
/* --ifm-link-color: #3cad6e;
--ifm-link-hover-color: #5ec389; */
--docusaurus-highlighted-code-line-bg: rgba(60, 173, 110, 0.12);
/* --theme-primary-rgb: 60, 173, 110; */
}
/* ============================================================
* Method badge colors — sidebar pills
* ============================================================ */
.get > .menu__link::before {
background-color: rgba(46, 133, 85, 0.12);
color: #29784c;
}
.get > .menu__link--active::before {
background-color: #2e8555;
color: #ffffff;
}
.post > .menu__link::before {
background-color: rgba(2, 132, 199, 0.12);
color: #0369a1;
}
.post > .menu__link--active::before {
background-color: #0284c7;
color: #ffffff;
}
.delete > .menu__link::before {
background-color: rgba(220, 38, 38, 0.12);
color: #dc2626;
}
.delete > .menu__link--active::before {
background-color: #dc2626;
color: #ffffff;
}
.put > .menu__link::before {
background-color: rgba(217, 119, 6, 0.15);
color: #b45309;
}
.put > .menu__link--active::before {
background-color: #d97706;
color: #ffffff;
}
.patch > .menu__link::before {
background-color: rgba(217, 119, 6, 0.15);
color: #b45309;
}
.patch > .menu__link--active::before {
background-color: #d97706;
color: #ffffff;
}
/* Dark mode */
[data-theme="dark"] .get > .menu__link::before {
background-color: rgba(60, 173, 110, 0.15);
color: #3cad6e;
}
[data-theme="dark"] .get > .menu__link--active::before {
background-color: #3cad6e;
color: #18191a;
}
[data-theme="dark"] .post > .menu__link::before {
background-color: rgba(56, 189, 248, 0.12);
color: #38bdf8;
}
[data-theme="dark"] .post > .menu__link--active::before {
background-color: #0ea5e9;
color: #18191a;
}
[data-theme="dark"] .delete > .menu__link::before {
background-color: rgba(239, 68, 68, 0.12);
color: #f87171;
}
[data-theme="dark"] .delete > .menu__link--active::before {
background-color: #ef4444;
color: #ffffff;
}
[data-theme="dark"] .put > .menu__link::before {
background-color: rgba(251, 191, 36, 0.12);
color: #fbbf24;
}
[data-theme="dark"] .put > .menu__link--active::before {
background-color: #f59e0b;
color: #18191a;
}
[data-theme="dark"] .patch > .menu__link::before {
background-color: rgba(251, 191, 36, 0.12);
color: #fbbf24;
}
[data-theme="dark"] .patch > .menu__link--active::before {
background-color: #f59e0b;
color: #18191a;
}
/* ============================================================
* API page method badges (.badge--*)
* ============================================================ */
.badge--primary {
/* GET */
background-color: #2e8555;
color: #ffffff;
--ifm-badge-border-color: transparent;
}
.badge--success {
/* POST */
background-color: #0284c7;
color: #ffffff;
--ifm-badge-border-color: transparent;
}
.badge--danger {
/* DELETE */
background-color: #dc2626;
color: #ffffff;
--ifm-badge-border-color: transparent;
}
.badge--info {
/* PUT */
background-color: #d97706;
color: #ffffff;
--ifm-badge-border-color: transparent;
}
.badge--warning {
/* PATCH */
background-color: #d97706;
color: #ffffff;
--ifm-badge-border-color: transparent;
}
[data-theme="dark"] .badge--primary {
/* GET */
background-color: #3cad6e;
color: #18191a;
}
[data-theme="dark"] .badge--success {
/* POST */
background-color: #0ea5e9;
color: #18191a;
}
[data-theme="dark"] .badge--danger {
/* DELETE */
background-color: #ef4444;
color: #ffffff;
}
[data-theme="dark"] .badge--info {
/* PUT */
background-color: #f59e0b;
color: #18191a;
}
[data-theme="dark"] .badge--warning {
/* PATCH */
background-color: #f59e0b;
color: #18191a;
}
/* ============================================================
* API Explorer — request body editor font size
*
* react-simple-code-editor renders an inner textarea + pre that
* can slip past the outer !important font shorthand. Target them
* directly so the body editor matches the code snippet tabs.
* ============================================================ */
.openapi-explorer__playground-editor,
.openapi-explorer__playground-editor textarea,
.openapi-explorer__playground-editor pre {
font-size: var(--openapi-explorer-font-size-code) !important;
}
/* ============================================================
* API Explorer — Send button dark mode
* ============================================================ */
[data-theme="dark"] .openapi-explorer__request-btn {
background-color: #2e8555;
color: #ffffff;
}
[data-theme="dark"] .openapi-explorer__request-btn:hover {
background-color: #3cad6e;
}
+580
View File
@@ -0,0 +1,580 @@
/**
* Structural CSS for the docusaurus-openapi-docs demo site.
*
* This file contains layout, component behavior, animations, and utilities.
* It is intentionally free of hard-coded colors and fonts — those live in
* a theme file loaded alongside this one via docusaurus.config.ts:
*
* customCss: ["./src/css/custom.css", "./src/css/themes/panw.css"]
*
* To try a different palette, swap the theme file. The following CSS
* variables must be defined by the active theme:
*
* --theme-primary-rgb RGB triplet of primary color (for animations)
* --theme-border-radius-sm/md Border radius tokens
* --site-primary-hue-saturation Hue+saturation for the announcement bar
*
* Structure inspired by @gleanwork/docusaurus-theme-glean.
*/
/* ============================================================
* Typography
* ============================================================ */
.markdown h1,
article h1,
.main-wrapper h1 {
letter-spacing: 0.025em;
font-weight: var(--ifm-heading-font-weight);
line-height: 1.2;
margin-bottom: 1.5rem;
color: var(--ifm-heading-color);
}
.markdown h2,
article h2,
.markdown h3,
article h3 {
font-weight: var(--ifm-font-weight-semibold);
}
/* .markdown strong,
article strong,
.markdown b,
article b {
font-weight: var(--ifm-font-weight-semibold);
} */
.font-mono {
font-family: var(--font-mono);
/* font-variant-ligatures: none; */
}
.table-of-contents {
font-size: 0.95rem;
}
html {
scroll-padding-top: 50px;
}
/* ============================================================
* Links
* ============================================================ */
a:any-link:hover {
text-decoration: none;
}
article p a,
article ul a,
article li a,
.markdown p a,
.theme-doc-markdown p a {
text-decoration: underline 1px var(--ifm-color-primary);
transition: color 0.2s ease;
}
article p a:hover,
article ul a:hover,
article li a:hover,
.markdown p a:hover,
.theme-doc-markdown p a:hover {
text-decoration: underline 2px var(--ifm-color-primary);
}
/* ============================================================
* Sidebar structure
* ============================================================ */
/* Slightly off-white sidebar in light mode */
.theme-doc-sidebar-container {
background-color: #f6f6f6;
}
[data-theme="dark"] .theme-doc-sidebar-container {
background-color: unset;
}
/* Tighter nested indentation */
.menu__list .menu__list {
padding-left: 10px;
}
/* No underlines on menu links */
.menu__link {
text-decoration: none;
color: var(--ifm-color-emphasis-700);
}
/* Top-level category labels: uppercase, small caps, bold */
/* .theme-doc-sidebar-item-category-level-1
> .menu__list-item-collapsible
> .menu__link,
.theme-doc-sidebar-item-category-level-1
> .menu__list-item-collapsible
> .menu__link.menu__link--active {
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.5px;
line-height: 16px;
font-size: 12px;
color: var(--ifm-color-emphasis-900);
} */
/* Hover: text darkens, no background fill */
/* .menu__link:hover {
background-color: transparent;
color: var(--ifm-color-emphasis-800);
text-decoration: none;
} */
/* Active item: primary-colored text, no background */
/* .menu__link--active:not(
.theme-doc-sidebar-item-category-level-1
> .menu__list-item-collapsible
> .menu__link
) {
color: var(--ifm-color-primary);
background-color: transparent;
font-weight: 500;
}
.menu__link--active:not(
.theme-doc-sidebar-item-category-level-1
> .menu__list-item-collapsible
> .menu__link
):hover {
color: var(--ifm-color-primary);
background-color: transparent;
} */
/* [data-theme="dark"]
.menu__link--active:not(
.theme-doc-sidebar-item-category-level-1
> .menu__list-item-collapsible
> .menu__link
) {
color: var(--ifm-color-primary-light);
}
.menu__list-item-collapsible:hover,
.menu__list-item-collapsible:active,
.menu__list-item-collapsible:focus-within,
.menu__list-item-collapsible.menu__list-item-collapsible--active {
background: transparent;
} */
/* Custom caret */
.menu__link--sublist-caret:after,
.menu__caret::before {
--ifm-menu-link-sublist-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="8px" height="8px" viewBox="0 0 24 24"><path fill="rgba(0,0,0,0.5)" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"></path></svg>');
content: " ";
background-size: contain;
}
/* ============================================================
* Sidebar Method labels — Default (pill style)
*
* Shape and content labels only. Colors are in the theme file.
* ============================================================ */
.api-method > .menu__link,
.schema > .menu__link {
align-items: center;
justify-content: start;
}
.api-method > .menu__link::before,
.schema > .menu__link::before {
font-size: 0.55rem;
font-weight: 700;
padding: 0.125rem 0.25rem;
border-radius: 0.375rem;
line-height: 1.25;
width: 2.25rem;
height: auto;
border: none;
text-transform: uppercase;
margin-right: var(--ifm-spacing-horizontal);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
text-align: center;
}
/* Content labels */
.get > .menu__link::before {
content: "get";
}
.post > .menu__link::before {
content: "post";
}
.delete > .menu__link::before {
content: "del";
}
.put > .menu__link::before {
content: "put";
}
.patch > .menu__link::before {
content: "patch";
}
/* HEAD / EVENT / SCHEMA — neutral colors, no theme needed */
.head > .menu__link::before {
content: "head";
background-color: var(--ifm-color-emphasis-200);
color: var(--ifm-color-emphasis-700);
}
.event > .menu__link::before {
content: "event";
background-color: var(--ifm-color-emphasis-200);
color: var(--ifm-color-emphasis-700);
}
.schema > .menu__link::before {
content: "schema";
background-color: var(--ifm-color-emphasis-200);
color: var(--ifm-color-emphasis-700);
}
/* Deprecated */
.menu__list-item--deprecated > .menu__link,
.menu__list-item--deprecated > .menu__link:hover {
text-decoration: line-through;
}
/* ============================================================
* Sidebar Method labels — High Contrast variant (unchanged)
* ============================================================ */
.api-method-contrast > .menu__link,
.schema-contrast > .menu__link {
align-items: center;
justify-content: start;
}
.api-method-contrast > .menu__link::before,
.schema-contrast > .menu__link::before {
width: 55px;
height: 20px;
font-size: 12px;
line-height: 20px;
text-transform: uppercase;
font-weight: 600;
border-radius: 0.25rem;
border: 1px solid;
border-inline-start-width: 5px;
margin-right: var(--ifm-spacing-horizontal);
text-align: center;
flex-shrink: 0;
}
.get-contrast > .menu__link::before {
content: "get";
background-color: var(--ifm-color-info-contrast-background);
color: var(--ifm-color-info-contrast-foreground);
border-color: var(--ifm-color-info-dark);
}
.post-contrast > .menu__link::before {
content: "post";
background-color: var(--ifm-color-success-contrast-background);
color: var(--ifm-color-success-contrast-foreground);
border-color: var(--ifm-color-success-dark);
}
.delete-contrast > .menu__link::before {
content: "del";
background-color: var(--ifm-color-danger-contrast-background);
color: var(--ifm-color-danger-contrast-foreground);
border-color: var(--ifm-color-danger-dark);
}
.put-contrast > .menu__link::before {
content: "put";
background-color: var(--ifm-color-warning-contrast-background);
color: var(--ifm-color-warning-contrast-foreground);
border-color: var(--ifm-color-warning-dark);
}
.patch-contrast > .menu__link::before {
content: "patch";
background-color: var(--ifm-color-success-contrast-background);
color: var(--ifm-color-success-contrast-foreground);
border-color: var(--ifm-color-success-dark);
}
.head-contrast > .menu__link::before {
content: "head";
background-color: var(--ifm-color-secondary-contrast-background);
color: var(--ifm-color-secondary-contrast-foreground);
border-color: var(--ifm-color-secondary-dark);
}
.event-contrast > .menu__link::before {
content: "event";
background-color: var(--ifm-color-secondary-contrast-background);
color: var(--ifm-color-secondary-contrast-foreground);
border-color: var(--ifm-color-secondary-dark);
}
.schema-contrast > .menu__link::before {
content: "schema";
background-color: var(--ifm-color-secondary-contrast-background);
color: var(--ifm-color-secondary-contrast-foreground);
border-color: var(--ifm-color-secondary-dark);
}
/* ============================================================
* Sidebar Method labels — Simple variant (unchanged)
* ============================================================ */
.api-method-simple > .menu__link {
align-items: center;
justify-content: start;
}
.api-method-simple > .menu__link::before {
width: 55px;
height: 20px;
font-size: 12px;
line-height: 20px;
text-transform: uppercase;
font-weight: 600;
border-radius: 0.25rem;
align-content: start;
margin-right: var(--ifm-spacing-horizontal);
text-align: right;
flex-shrink: 0;
border-color: transparent;
}
.get-simple > .menu__link::before {
content: "get";
color: var(--ifm-color-info);
}
.post-simple > .menu__link::before {
content: "post";
color: var(--ifm-color-success);
}
.delete-simple > .menu__link::before {
content: "del";
color: var(--ifm-color-danger);
}
.put-simple > .menu__link::before {
content: "put";
color: var(--ifm-color-warning);
}
.patch-simple > .menu__link::before {
content: "patch";
color: var(--ifm-color-warning);
}
.head-simple > .menu__link::before {
content: "head";
color: var(--ifm-color-secondary-contrast-foreground);
}
.event-simple > .menu__link::before {
content: "event";
color: var(--ifm-color-secondary-contrast-foreground);
}
.schema-simple > .menu__link::before {
content: "schema";
color: var(--ifm-color-secondary-contrast-foreground);
}
/* ============================================================
* Admonitions
* ============================================================ */
.alert {
border: none;
border-radius: var(--theme-border-radius-md, 8px);
}
/* ============================================================
* Navbar header icons
* ============================================================ */
.header-github-link:hover {
opacity: 0.6;
}
.header-github-link:before {
content: "";
width: 24px;
height: 24px;
display: flex;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
no-repeat;
}
html[data-theme="dark"] .header-github-link:before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
no-repeat;
}
.header-medium-link:hover {
opacity: 0.6;
}
.header-medium-link:before {
content: "\f23a";
font-family: "Font Awesome 5 Brands";
font-size: x-large;
}
/* ============================================================
* Announcement bar
* ============================================================ */
div[class^="announcementBar_"] {
--site-announcement-bar-stripe-color1: hsl(
var(--site-primary-hue-saturation) 85%
);
--site-announcement-bar-stripe-color2: hsl(
var(--site-primary-hue-saturation) 95%
);
background: repeating-linear-gradient(
35deg,
var(--site-announcement-bar-stripe-color1),
var(--site-announcement-bar-stripe-color1) 20px,
var(--site-announcement-bar-stripe-color2) 10px,
var(--site-announcement-bar-stripe-color2) 40px
);
font-weight: bold;
}
/* ============================================================
* Schema parameter highlight animation
*
* Color driven by --theme-primary-rgb (defined in the theme file).
* ============================================================ */
[id]:target {
animation: highlight-flash 2s ease-out;
background-color: rgba(var(--theme-primary-rgb, 0, 76, 157), 0.08);
border-radius: 4px;
padding: 2px 4px;
margin: -2px -4px;
}
@keyframes highlight-flash {
0% {
background-color: rgba(var(--theme-primary-rgb, 0, 76, 157), 0.25);
box-shadow: 0 0 0 3px rgba(var(--theme-primary-rgb, 0, 76, 157), 0.25);
}
50% {
background-color: rgba(var(--theme-primary-rgb, 0, 76, 157), 0.15);
box-shadow: 0 0 0 2px rgba(var(--theme-primary-rgb, 0, 76, 157), 0.15);
}
100% {
background-color: rgba(var(--theme-primary-rgb, 0, 76, 157), 0.08);
box-shadow: none;
}
}
.openapi-markdown__details [id]:target,
.openapi-schema__list-item [id]:target {
animation: highlight-flash 2s ease-out;
background-color: rgba(var(--theme-primary-rgb, 0, 76, 157), 0.08) !important;
}
/* ============================================================
* Code tab scrollbar
* ============================================================ */
.tabs-container [role="tabpanel"] {
min-width: 0;
overflow-x: auto;
}
.tabs-container [role="tabpanel"] pre {
overflow-x: auto;
white-space: pre;
word-wrap: normal;
max-width: 100%;
}
.tabs-container [role="tabpanel"] pre::-webkit-scrollbar {
height: 6px;
}
.tabs-container [role="tabpanel"] pre::-webkit-scrollbar-track {
background: var(--ifm-color-emphasis-200);
border-radius: var(--theme-border-radius-sm, 4px);
}
.tabs-container [role="tabpanel"] pre::-webkit-scrollbar-thumb {
background: var(--ifm-color-emphasis-400);
border-radius: var(--theme-border-radius-sm, 4px);
}
.tabs-container [role="tabpanel"] pre::-webkit-scrollbar-thumb:hover {
background: var(--ifm-color-emphasis-500);
}
/* ============================================================
* OpenAPI code tab language icons
* ============================================================ */
.openapi-tabs__code-item--python {
color: var(--ifm-color-success);
}
.openapi-tabs__code-item--python::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-plain.svg")
no-repeat;
margin-block: auto;
}
.openapi-tabs__code-item--python.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-python);
border-color: var(--openapi-code-tab-border-color-python);
}
/* ============================================================
* Grid utilities
* ============================================================ */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.gap-3 {
gap: 0.75rem;
}
.gap-6 {
gap: 1.5rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-6 {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
@media (min-width: 768px) {
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
+16 -4
View File
@@ -1,6 +1,15 @@
{
"debugMode": false,
"disableWeb": false,
"sourceDefaults": {
"maxPollRetries": 0,
"logPayload": false,
"logFilterFailure": "warn",
"logPlayerState": false,
"scrobbleThresholds": {
"duration": 30,
"percent": 50
},
"maxPollRetries": 1,
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
@@ -8,12 +17,13 @@
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
"baseUrl": "http://localhost",
"sources": [
{
"type": "spotify",
"enable": true,
"clients": ["myConfig"],
"name": "mySpotifySource",
"name": "Cool Spotify Name",
"id": "mySpotifyId",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
@@ -24,7 +34,9 @@
"clients": [
{
"type": "maloja",
"name": "myConfig",
"enable": true,
"name": "Foxx Maloja",
"id": "myMalojaId",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
+602 -22
View File
@@ -1,12 +1,12 @@
{
"name": "multi-scrobbler",
"version": "0.15.0-RC1",
"version": "0.17.0-RC1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-scrobbler",
"version": "0.15.0-RC1",
"version": "0.17.0-RC1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -30,6 +30,7 @@
"@kenyip/backoff-strategies": "^1.0.4",
"@keyv/valkey": "^1.0.8",
"@lukehagar/plexjs": "^0.39.0",
"@minisylar/express-typed-router": "^1.9.8",
"@pierre/diffs": "^1.2.4",
"@rocksky/sdk": "^0.3.0",
"@supercharge/promise-pool": "^3.0.0",
@@ -132,6 +133,8 @@
"@storybook/addon-docs": "^10.4.3",
"@storybook/react-vite": "^10.4.3",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/form-core": "^2.0.0-alpha.2",
"@tanstack/react-form": "^2.0.0-alpha.2",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-query-devtools": "^5.101.0",
"@tanstack/react-virtual": "^3.14.3",
@@ -159,6 +162,7 @@
"@types/sinon": "^21.0.0",
"@types/spotify-web-api-node": "^5.0.7",
"@types/superagent": "^8.1.9",
"@types/supertest": "^7.2.1",
"@types/xml2js": "^0.4.11",
"@vitejs/plugin-react": "^6.0.2",
"@welldone-software/why-did-you-render": "^10.0.1",
@@ -173,7 +177,7 @@
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-boundaries": "^7.0.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-mocha": "^11.3.0",
"eslint-plugin-mocha": "^12.0.2",
"eslint-plugin-prefer-arrow-functions": "^3.2.4",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-storybook": "^10.3.6",
@@ -182,6 +186,7 @@
"git-cliff": "^2.12.0",
"globals": "^17.7.0",
"ky": "^2.0.2",
"markdown-table": "^3.0.4",
"mocha": "^10.3.0",
"mockdate": "^3.0.5",
"msw": "^2.14.6",
@@ -201,6 +206,7 @@
"sinon": "^21.0.2",
"storybook": "^10.4.3",
"storybook-addon-remix-react-router": "^6.1.0",
"supertest": "^7.2.2",
"tailwindcss": "^4.2.2",
"tinyexec": "^1.1.1",
"ts-essentials": "^10.2.0",
@@ -208,7 +214,9 @@
"typescript-eslint": "^8.59.3",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.12",
"vite-plugin-svgr": "^5.2.0",
"with-local-tmp-dir": "^7.0.1",
"zocker": "^3.0.0",
"zod": "^4.4.3"
},
"engines": {
@@ -2268,9 +2276,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2998,6 +3006,22 @@
"react": ">=16"
}
},
"node_modules/@minisylar/express-typed-router": {
"version": "1.9.8",
"resolved": "https://registry.npmjs.org/@minisylar/express-typed-router/-/express-typed-router-1.9.8.tgz",
"integrity": "sha512-C0VW62eAPNFYP/pPqpepVQLitsJLPI1/8CFSVGIfZO4uaPkJZchN9eDpcCE/S2Rc1RXC+71tZ1A/A/yCu8kp7A==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@standard-schema/utils": "^0.3.0"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"express": "^4.18.0 || ^5.0.0"
}
},
"node_modules/@mjackson/form-data-parser": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@mjackson/form-data-parser/-/form-data-parser-0.4.0.tgz",
@@ -5327,7 +5351,6 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@stdlib/error-tools-fmtprodmsg": {
@@ -5811,6 +5834,258 @@
"node": ">=8"
}
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
"integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz",
"integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz",
"integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz",
"integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-svg-dynamic-title": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz",
"integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-svg-em-dimensions": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz",
"integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-transform-react-native-svg": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz",
"integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-plugin-transform-svg-component": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz",
"integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/babel-preset": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz",
"integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@svgr/babel-plugin-add-jsx-attribute": "8.0.0",
"@svgr/babel-plugin-remove-jsx-attribute": "8.0.0",
"@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0",
"@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0",
"@svgr/babel-plugin-svg-dynamic-title": "8.0.0",
"@svgr/babel-plugin-svg-em-dimensions": "8.0.0",
"@svgr/babel-plugin-transform-react-native-svg": "8.1.0",
"@svgr/babel-plugin-transform-svg-component": "8.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@svgr/core": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
"camelcase": "^6.2.0",
"cosmiconfig": "^8.1.3",
"snake-case": "^3.0.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@svgr/core/node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0",
"path-type": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@svgr/hast-util-to-babel-ast": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz",
"integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.21.3",
"entities": "^4.4.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@svgr/plugin-jsx": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz",
"integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
"@svgr/hast-util-to-babel-ast": "8.0.0",
"svg-parser": "^2.0.4"
},
"engines": {
"node": ">=14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
},
"peerDependencies": {
"@svgr/core": "*"
}
},
"node_modules/@svrooij/sonos": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@svrooij/sonos/-/sonos-2.5.0.tgz",
@@ -6142,6 +6417,38 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tanstack/form-core": {
"version": "2.0.0-alpha.2",
"resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-2.0.0-alpha.2.tgz",
"integrity": "sha512-u/2s1rJ1pSdFgU/oKz2zt/Wdb3XQK9LY1IIL2pLMu0AVYK5f8Ue5UwaCjcUxEAI8g6XJpbmEuy2wLIwHZbZnwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tanstack/pacer-lite": "^0.2.2",
"@tanstack/store": "^0.11.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/pacer-lite": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.2.2.tgz",
"integrity": "sha512-eQ1MyLKCHyXiH7NbdmB80W77OhiMgGBUb+qDx/8WMGbwg5Lf/NlfD0TfNYAqY77i8V3AxoDoYdICrQE5ADw4Yw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.101.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
@@ -6164,6 +6471,27 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-form": {
"version": "2.0.0-alpha.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-2.0.0-alpha.2.tgz",
"integrity": "sha512-rIm6kRhpK9Fbb5p3xiwKKofOKHXAK3K28NeR4sM5T0h8CU0TpCLMz27SAHY2xjvG2iSgx8n4mLVEyTxw7iHJdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tanstack/form-core": "2.0.0-alpha.2",
"@tanstack/react-store": "^0.11.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.101.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
@@ -6199,6 +6527,25 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-store": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.1.tgz",
"integrity": "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tanstack/store": "0.11.1",
"use-sync-external-store": "^1.6.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.14.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.3.tgz",
@@ -6217,6 +6564,17 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/store": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.1.tgz",
"integrity": "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.1.tgz",
@@ -6858,6 +7216,17 @@
"form-data": "^4.0.0"
}
},
"node_modules/@types/supertest": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz",
"integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/methods": "^1.1.4",
"@types/superagent": "^8.1.0"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -10315,6 +10684,17 @@
"csstype": "^3.0.2"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
@@ -10324,6 +10704,16 @@
"node": ">=10"
}
},
"node_modules/drange": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz",
"integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/drizzle-kit": {
"version": "1.0.0-rc.4-ca0f029",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-1.0.0-rc.4-ca0f029.tgz",
@@ -11478,27 +11868,37 @@
}
},
"node_modules/eslint-plugin-mocha": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-11.3.0.tgz",
"integrity": "sha512-anENwrIwmdvunmmssjMn5a4nTd+mYMkqBlwjksxOECcIThLNhefWJIiTWY7pY/arMQFjNwHQjVOZb6pQ9PrLjg==",
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-12.0.2.tgz",
"integrity": "sha512-RzjnzVBoidGFjVcvzQShLbfWug5pL/6d6LZFbBZk1b5duZ0ooq6dasmpu6qLyNowUZBqaWvQbnR5U1hsKX/+5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.1",
"globals": "^15.14.0"
"@eslint-community/eslint-utils": "4.10.1",
"@eslint/core": "1.2.1",
"@types/estree": "1.0.9",
"globals": "17.8.0",
"json-schema-to-ts": "3.1.1",
"type-fest": "5.8.0"
},
"engines": {
"node": ">=22.0.0"
},
"peerDependencies": {
"eslint": ">=9.0.0"
"eslint": ">=10.2.0"
}
},
"node_modules/eslint-plugin-mocha/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"node_modules/eslint-plugin-mocha/node_modules/type-fest": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz",
"integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==",
"dev": true,
"license": "MIT",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -13189,9 +13589,9 @@
}
},
"node_modules/globals": {
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
"version": "17.8.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz",
"integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -14468,6 +14868,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
@@ -15088,6 +15502,16 @@
"get-func-name": "^2.0.1"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -15190,6 +15614,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -15973,6 +16408,17 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -17298,6 +17744,20 @@
"url": "https://github.com/sindresorhus/quote-js-string?sponsor=1"
}
},
"node_modules/randexp": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz",
"integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"drange": "^1.0.2",
"ret": "^0.2.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -17828,6 +18288,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ret": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz",
"integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -18509,6 +18979,17 @@
"node": ">=6"
}
},
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
@@ -18949,6 +19430,42 @@
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/supertest": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
"integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cookie-signature": "^1.2.2",
"methods": "^1.1.2",
"superagent": "^10.3.0"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/supertest/node_modules/superagent": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
"integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"component-emitter": "^1.3.1",
"cookiejar": "^2.1.4",
"debug": "^4.3.7",
"fast-safe-stringify": "^2.1.1",
"form-data": "^4.0.5",
"formidable": "^3.5.4",
"methods": "^1.1.2",
"mime": "2.6.0",
"qs": "^6.14.1"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -18974,6 +19491,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-parser": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.5.tgz",
"integrity": "sha512-FuT74puvVMJQ8sgFxgOKPex5MbxPeltqCXJV3wR9Xq+vPHaF+tTdE1lhJcwspUxjtjvXy1NwqcSLZx9UNwHawg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
@@ -19211,6 +19738,13 @@
"integrity": "sha512-0s7O4OFi0f8DpD03spao04Osrt2b4p1v2uags7JBMLsAe36pUI0mQU6XMgQuQSv5jOTqTFS0W50wTPrlzST80w==",
"license": "MIT"
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-dedent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
@@ -20127,6 +20661,21 @@
"picocolors": "^1.1.1"
}
},
"node_modules/vite-plugin-svgr": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz",
"integrity": "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.3.0",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0"
},
"peerDependencies": {
"vite": ">=3.0.0"
}
},
"node_modules/vlc-client": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vlc-client/-/vlc-client-1.1.1.tgz",
@@ -20494,6 +21043,37 @@
"node": ">=18.17"
}
},
"node_modules/zocker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/zocker/-/zocker-3.0.0.tgz",
"integrity": "sha512-xSJz2M9AELZKzgpuuZPbTQdMv9263p/zSHDIj81B58X3334Xl6iJ/C3u6OQiOlsrWAoJSJsveBcIdz1eEPL0EA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@faker-js/faker": "^10.0.0",
"randexp": "^0.5.3"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/zocker/node_modules/@faker-js/faker": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.5.0.tgz",
"integrity": "sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
"npm": ">=10"
}
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+12 -3
View File
@@ -1,11 +1,12 @@
{
"name": "multi-scrobbler",
"version": "0.15.0-RC1",
"version": "0.17.0-RC1",
"type": "module",
"description": "scrobble plays from multiple sources to multiple clients",
"scripts": {
"schema": "npm run -s schema:docs",
"schema:docs": "node src/backend/utils/SchemaDocsStaticUtils.ts",
"openapi:docs": "node src/backend/utils/OpenApiUtils.ts",
"circular": "madge --circular --extensions ts src/index.ts",
"test": "npm run -s test:backend",
"test:backend": "mocha --reporter spec",
@@ -18,7 +19,7 @@
"build:parallel": "concurrently --kill-others-on-fail --names backend,frontend,docs \"npm run -s build:backend\" \"npm run -s build:frontend\" \"npm run docs:build\"",
"docs:install": "cd docsite && npm install --no-audit",
"docs:start": "cd docsite && npm start",
"docs:build": "npm run -s schema:docs && cd docsite && npm run build",
"docs:build": "npm run -s schema:docs && npm run -s openapi:docs && cd docsite && npm run build",
"docs:netlify": "npm run -s docs:install && npm run -s docs:build",
"postinstall": "patch-package",
"install:parallel": "concurrently --kill-others-on-fail --names app,docs \"npm ci --no-audit\" \"npm run docs:install\"",
@@ -70,6 +71,7 @@
"@kenyip/backoff-strategies": "^1.0.4",
"@keyv/valkey": "^1.0.8",
"@lukehagar/plexjs": "^0.39.0",
"@minisylar/express-typed-router": "^1.9.8",
"@pierre/diffs": "^1.2.4",
"@rocksky/sdk": "^0.3.0",
"@supercharge/promise-pool": "^3.0.0",
@@ -172,6 +174,8 @@
"@storybook/addon-docs": "^10.4.3",
"@storybook/react-vite": "^10.4.3",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/form-core": "^2.0.0-alpha.2",
"@tanstack/react-form": "^2.0.0-alpha.2",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-query-devtools": "^5.101.0",
"@tanstack/react-virtual": "^3.14.3",
@@ -199,6 +203,7 @@
"@types/sinon": "^21.0.0",
"@types/spotify-web-api-node": "^5.0.7",
"@types/superagent": "^8.1.9",
"@types/supertest": "^7.2.1",
"@types/xml2js": "^0.4.11",
"@vitejs/plugin-react": "^6.0.2",
"@welldone-software/why-did-you-render": "^10.0.1",
@@ -213,7 +218,7 @@
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-boundaries": "^7.0.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-mocha": "^11.3.0",
"eslint-plugin-mocha": "^12.0.2",
"eslint-plugin-prefer-arrow-functions": "^3.2.4",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-storybook": "^10.3.6",
@@ -222,6 +227,7 @@
"git-cliff": "^2.12.0",
"globals": "^17.7.0",
"ky": "^2.0.2",
"markdown-table": "^3.0.4",
"mocha": "^10.3.0",
"mockdate": "^3.0.5",
"msw": "^2.14.6",
@@ -241,6 +247,7 @@
"sinon": "^21.0.2",
"storybook": "^10.4.3",
"storybook-addon-remix-react-router": "^6.1.0",
"supertest": "^7.2.2",
"tailwindcss": "^4.2.2",
"tinyexec": "^1.1.1",
"ts-essentials": "^10.2.0",
@@ -248,7 +255,9 @@
"typescript-eslint": "^8.59.3",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.12",
"vite-plugin-svgr": "^5.2.0",
"with-local-tmp-dir": "^7.0.1",
"zocker": "^3.0.0",
"zod": "^4.4.3"
},
"browserslist": {
+16
View File
@@ -0,0 +1,16 @@
diff --git a/node_modules/spotify-web-api-node/src/http-manager.js b/node_modules/spotify-web-api-node/src/http-manager.js
index 6c3991f..89e7a99 100644
--- a/node_modules/spotify-web-api-node/src/http-manager.js
+++ b/node_modules/spotify-web-api-node/src/http-manager.js
@@ -63,6 +63,11 @@ HttpManager._makeRequest = function(method, options, uri, callback) {
req.send(options.data);
}
+ req.timeout({
+ response: 5000, // Wait 5 seconds for the server to start sending,
+ deadline: 9000, // but allow 9 seconds for entire response to complete
+ });
+
req.end(function(err, response) {
if (err) {
if (err.timeout) {

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