Compare commits

...
Author SHA1 Message Date
FoxxMD fc85790df5 fix: Normalize ISRC values #643
Spotify does not gaurantee non-hyphenated ISRC and musicbrainz won't search for hyphenated, so normalize before ISRC values downstream
2026-07-22 17:19:52 +00:00
FoxxMD a30d1ae6a5 fix: Move project root dir constant location into backend-only scope
When in core its being pulled into vite bundler and causing build issues. It's only needed by backend anyways so move to backend-scope.
2026-07-22 15:40:44 +00:00
FoxxMD 5c586fb635 fix(storybook): Rename configure file causing ts to fail due to extension 2026-07-22 15:39:07 +00:00
FoxxMD de0d336325 fix(rocksky): Remove undefined keys from payload before sending 2026-07-22 15:38:18 +00:00
Matt Foxx 65d0c7c16b Merge pull request #641 from mattl/patch-1
Update librefm.mdx
2026-07-21 23:54:20 -04:00
Dr. Matt Lee fc6cc68d59 Update librefm.mdx 2026-07-21 18:10:16 -04:00
Matt Foxx 8c83969e95 Merge pull request #638 from FoxxMD/dataDirStandardize
Separate and standardize config/data dirs
2026-07-21 12:53:19 -04:00
FoxxMD 7ac57ca757 feat(ui): Add theme switcher to header
#500
2026-07-17 16:41:24 +00:00
FoxxMD 1dc8c21716 docs: Add preview UI callout to landing 2026-07-17 14:53:57 +00:00
FoxxMD 6f8e7d3862 docs: Update intallation instructions with config/data dir guidance
#616
2026-07-16 16:14:24 -04:00
FoxxMD a6d33fc2ef feat: Move config and data dir creation into app
* only use docker init for fixing permission
  * don't rely on docker init for dir creation as other distros may need this functionality (nix)
* check for and create config/data dirs recursively on app start

#616
2026-07-16 16:14:24 -04:00
FoxxMD 26121ace10 feat: Separate config and data directories and use OS-standard paths, if present
* Break configDir into config and data directories so config and database/cache can be stored in different directories
* Use `env-paths` to fallback to OS-standard paths for config and data if CONFIG_DIR and DATA_DIR envs are not present
* Additionally fallback to process CWD/config if none of the above are present for some reason

#616
2026-07-16 16:14:24 -04:00
FoxxMD 27e5656a04 refactor: Replace projectDir with explicit path to project root, projectRootDir 2026-07-16 16:14:24 -04:00
Matt Foxx f3d7d59af6 Merge pull request #636 from Exerra/feat/apple-music
feat: Apple Music source
2026-07-16 15:45:04 -04:00
FoxxMD 2a4afefe64 feat(applemusic): Make album normalization user configurable and add guidance on usage 2026-07-16 16:41:36 +00:00
FoxxMD 1dba73ccb2 feat(client)!: Overhaul source scrobble filtering
* Use id OR name for matching client to scrobble to, during transition period
* Deprecate using name-only and warn when this is detected
* Consolidate logging filtered clients to one statement to reduce log noise
* Improve filter matching by making strings case and whitepsace insensitive
* On startup log `clients` options for sources
2026-07-16 15:51:54 +00:00
FoxxMD 3e82e035ac feat: Update string normalization with granular options 2026-07-16 15:49:01 +00:00
Exerra b12153fba9 docs(applemusic): improve formatting of developer tools shortcut
Updated the Apple Music configuration documentation to use <kbd> tags for keyboard shortcuts, improving readability.
2026-07-16 18:12:33 +03:00
Exerra 8c82412de4 docs: add detailed Apple Music authorization token extraction guide
Updated Apple Music configuration documentation with step-by-step instructions and screenshots for extracting authorization tokens in Chrome, Edge, Firefox, and Safari.
2026-07-16 17:15:58 +03:00
FoxxMD e804ce33ef fix(listenbrainz): Fix play fetch more indicator
latest_listen_ts is *always* the latest of *all-time*, not for the current min/max_ts range requested.

Instead, use count < than desired (and non-zero) to indicate more results
2026-07-15 20:49:37 +00:00
FoxxMD 3907fa5474 fix: Fix play fetch options logging data for pageless functions 2026-07-15 20:48:32 +00:00
FoxxMD a7b3593314 stage import rule from eslint-plugin-import-x 2026-07-15 19:08:48 +00:00
FoxxMD 76ef994067 fix: Implement unicorn eslint rules and fix them 2026-07-15 18:31:13 +00:00
FoxxMD c3cb3cf3a1 fix(client): Correct SOT scrobble api requests to not have the same from/to value 2026-07-15 17:34:37 +00:00
FoxxMD 8096219526 fix(client): Correct create/update timestamps for queues and scrobbles
* Ensure ingress queue always has an updated timestamp before dead creation
* Don't re-assign createdAt for scrobble after dead queue has already been updated
2026-07-15 17:02:29 +00:00
FoxxMD 67c1823195 feat(ui): Clean up scrobble timeline items
* Use timeline summary formatting to fix spacing
* Provide client name from fetched component data, if present, so scrobble summary is explicit where it sent/response scrobble from
2026-07-15 16:22:04 +00:00
FoxxMD 2014e78ade fix(ui): Stabilize play timeline item order 2026-07-15 14:51:42 +00:00
Exerra b3c332deeb refactor: replace Apple Music headers config with origin field
Replaces the generic `headers` configuration field with a specific `origin` field for Apple Music. Updates the config schema, environment variable handling, documentation, and source implementation to simplify origin header management.
2026-07-15 17:12:21 +03:00
Exerra 93a1042e03 test(applemusic): add tests for stripping EP/Single album suffixes
Added test cases to ensure that ' - EP' and ' - Single' suffixes are correctly stripped from Apple Music album names during the formatting process.
2026-07-15 16:21:10 +03:00
Exerra ba010240c0 feat(AppleMusicSource): include track URL in metadata
Added the web URL to the track metadata in AppleMusicSource to provide a direct link to the track.
2026-07-15 16:20:54 +03:00
FoxxMD 7f491d6974 fix: Fix hash router type 2026-07-15 13:08:04 +00:00
Exerra 74e67e0e41 feat(AppleMusicSource): include ISRC in track metadata
Added the isrc field to the track object in AppleMusicSource to improve track identification accuracy.
2026-07-15 15:59:49 +03:00
FoxxMD 1853f9abba fix(musicbrainz): Catch scenario where all search prereqs fail
#636
2026-07-15 12:58:40 +00:00
Exerra c13e7ebeec feat(applemusic): strip ' - EP' and ' - Single' from album names
Implemented album name normalization in AppleMusicSource to ensure consistency with other sources by removing standard suffixes from EPs and singles. Updated documentation to reflect this behavior.
2026-07-15 15:38:14 +03:00
Exerra 207354e799 docs(docsite): fix heading hierarchy in applemusic.mdx
Adjusted markdown heading levels to ensure proper document structure.
2026-07-15 15:36:51 +03:00
FoxxMD d7f6b17308 update boundary element linting to be more lenient
* allow core-tests from backend
* downgrade linting to warn
2026-07-15 12:23:53 +00:00
Exerra c069020aac docs(README.md): add Apple Music to supported sources list
Updated the README to include Apple Music in the list of supported scrobbling sources with a link to its documentation.
2026-07-15 13:52:53 +03:00
Exerra f84a0f190d test(applemusic): add integration tests for Apple Music module
Add new test suite for Apple Music backend logic
2026-07-15 13:39:27 +03:00
FoxxMD d0f43fbd2d feat(ui): Increase virtual play list height 2026-07-15 01:29:12 +00:00
FoxxMD b38168975e feat(ui): Compact filters
* move refresh/loading indicator into list type header
* Remove filter card title to trim filter real estate
2026-07-15 01:22:40 +00:00
Exerra a845ac10ce feat(applemusic): implement top-rebound recovery for duplicate plays
Added logic to detect and recover 'top-rebound' history patterns where Apple Music deduplicates re-listened tracks by bumping them to the top. This ensures intermediate tracks are captured and re-listened top tracks are processed correctly. Includes a new configuration option `recoverUnchangedTopHistory` to toggle this behavior.
2026-07-15 02:44:41 +03:00
Exerra 56fdb12683 feat(sources): update Apple Music configuration and environment variables
Refactored Apple Music source configuration to support token-based authentication via key files and custom headers. Renamed environment variable prefixes from 'AM' to 'APPLEMUSIC' for consistency.
2026-07-15 00:04:14 +03:00
Exerra 1f40e1e414 docs(applemusic): update configuration guide and add APPLEMUSIC_HEADERS
Added documentation for the APPLEMUSIC_HEADERS environment variable and updated code blocks with titles for better clarity in the Apple Music source configuration.
2026-07-15 00:03:06 +03:00
Exerra c1a260dc56 fix(AppleMusicSource): add authentication verification to connection checks
Implemented status code validation for Apple Music authentication and connection checks. The module now throws errors instead of failing silently when the API is unreachable or auth is invalid.
2026-07-14 21:56:59 +03:00
Exerra 5d34ea68af docs: update Apple Music configuration guide
Reorganized the Apple Music documentation to prioritize the Browser Token (Free) authentication method and clarify requirements for the paid MusicKit Key approach.
2026-07-14 21:27:23 +03:00
FoxxMD 41ea9e0aa7 feat(ui): Make timeline items more compact
#500
2026-07-14 15:23:37 +00:00
FoxxMD a9d91c2c8a fix circular buffer 2026-07-14 14:04:19 +00:00
FoxxMD a696436a75 fix: Fix missing early return for skipped stage 2026-07-14 13:47:57 +00:00
Exerra 4ae388676e chore(package-lock.json): update dependencies
Bump node-musickit-api to 3.1.0-next.5
2026-07-14 14:12:42 +03:00
Exerra 89b698d48d docs: add Apple Music source documentation
Add Apple Music to the sources overview table and the main documentation index.
2026-07-14 14:06:20 +03:00
Exerra b71a4cbbd6 build(package.json): bump node-musickit-api to v3.1.0-next.5
Updated node-musickit-api dependency to the latest next version.
2026-07-14 13:08:36 +03:00
Exerra a870cdc6f4 chore(package.json): update node-musickit-api to v3.1.0-next.4
Switched node-musickit-api dependency from a local path to version 3.1.0-next.4 from the registry.
2026-07-14 01:11:29 +03:00
Exerra c404dd5396 feat(applemusic): add support for custom headers in API requests
Introduced a `headers` configuration option to allow setting custom request headers, such as 'Origin', for Apple Music API requests. Updated configuration schema and source implementation to apply these headers.
2026-07-14 01:03:26 +03:00
Exerra cae845028d refactor(AppleMusicSource): use HistoryConsistencyResult type alias
Update parseRecentAgainstResponse return type signature to use the HistoryConsistencyResult alias instead of an inline object definition.
2026-07-14 00:50:58 +03:00
Exerra 1698c5d71e refactor(AppleMusicSource): clarify timestamp calculation logic
Updated comments in applyCalculatedTimestamps to reflect that Apple Music lacks track timestamps, necessitating a hacky estimation method.
2026-07-14 00:48:29 +03:00
Exerra 765d86e02b refactor(AppleMusicSource): extract timestamp calculation to method
Moved the reduceRight logic for calculating play timestamps into a dedicated private method 'applyCalculatedTimestamps' to improve code readability and maintainability.
2026-07-14 00:46:58 +03:00
Exerra f347b82d33 refactor(AppleMusicSource): remove redundant unknown type assertion
Cleaned up the type casting in the AppleMusicSource fetch logic by removing unnecessary unknown assertion.
2026-07-14 00:44:38 +03:00
Exerra 87aa9c461e refactor(AppleMusicSource): replace magic numbers with class constants
Introduced constants for history limits and track polling constraints to improve maintainability and readability within AppleMusicSource.
2026-07-14 00:44:25 +03:00
Exerra 417ce85bc7 refactor(AppleMusicSource): improve type safety and code readability
Refactored AppleMusicSource to use defined types for history consistency, updated the getRecentlyPlayedTracks limit type, and renamed variable for clarity.
2026-07-14 00:42:01 +03:00
Exerra c8d917cfe6 refactor(AppleMusicSource): replace AppleMusicTrack interface with Song type
Removed local AppleMusicTrack interface and updated formatPlayObj to utilize the shared Song type for improved type consistency.
2026-07-14 00:34:47 +03:00
Exerra 8c789123f9 refactor(AppleMusicSource): remove unused backlogged plays logic
Removed the unused `getBackloggedPlays` method and cleaned up the `getRecentlyDiscoveredPlays` logic in `AppleMusicSource` to streamline track processing.
2026-07-14 00:31:06 +03:00
Exerra 06ed3f4388 feat: add Apple Music source support
Implement Apple Music source, including configuration types, environment variable handling, and source initialization logic. Refactored SourceType definitions to improve maintainability.
2026-07-14 00:24:44 +03:00
FoxxMD fab2e4938d feat(ui): Persist log window state #500
Store opened state, and position/size, to local storage so logs stay where user wants them across refreshes/initial load.

Resets window to default position if any part of log window is outside viewport bounds on open.
2026-07-12 16:19:13 +00:00
FoxxMD fbcd1e1b9a refactor(ui): Fix conditional hook for SSE and add stories for header elements 2026-07-12 15:43:33 +00:00
FoxxMD d48f2bd4ac feat(ui): Replace now playing player with summary with reduced motion #500 2026-07-11 20:28:13 +00:00
FoxxMD e2dad463f6 feat: reduce now playing animation #500 2026-07-11 20:05:20 +00:00
FoxxMD 642af13f3c fix(ui): fix shiki instance creation 2026-07-10 21:26:51 +00:00
FoxxMD 4146b159bd feat(ui): Component list container max width based on selected grid width 2026-07-10 21:11:13 +00:00
FoxxMD 3a3a97132d fix jellyfin import? 2026-07-10 21:03:06 +00:00
FoxxMD 04a5b91694 feat(ui): Implement grid for desktop layout
Allow user-selected max grid width on non-mobile breakpoints

#500
2026-07-10 20:57:03 +00:00
FoxxMD 945f494a2e fix: top-level type import syntax
see eslint.config.js comments
2026-07-10 16:47:38 +00:00
FoxxMD 4b09909b33 update cacheable for per-operation ttl 2026-07-10 15:17:01 +00:00
FoxxMD a5a1365b72 chore: remove tsx from mocha extension 2026-07-10 15:16:42 +00:00
FoxxMD 6bc618c481 always cache mb url so we can log to transform input 2026-07-10 15:16:31 +00:00
FoxxMD 85d1a0e648 chore: fix code styling in musicbrainz transformer 2026-07-10 15:15:57 +00:00
FoxxMD 1ae45c6060 add npm script for generating esbuild bundle metadata 2026-07-10 13:34:15 +00:00
FoxxMD 7daa43a883 tree-shake shiki for chakra usage 2026-07-10 13:34:15 +00:00
FoxxMD f8501b2a2e chore: Remove tsx usage from vscode launch profiles 2026-07-10 13:34:15 +00:00
Matt Foxx 14c35edadf Merge pull request #633 from FoxxMD/node-ts-native
feat: Migrate to native node runtime everywhere
2026-07-09 16:25:27 -04:00
FoxxMD cbb9774a18 chore: remove tsx and usage in npm scripts 2026-07-09 20:24:16 +00:00
FoxxMD 7b1eb7c2c1 disable flaky test 2026-07-09 20:06:13 +00:00
FoxxMD 8c8d1e1673 chore: Configure no-unused-vars 2026-07-09 20:01:08 +00:00
FoxxMD fbdd504453 fxi: Fix linting rules related to functions and error causes 2026-07-09 19:52:13 +00:00
FoxxMD 612a90d7e7 fix: Fix many useless or missed variable assignments 2026-07-09 19:43:11 +00:00
FoxxMD de8ad60db8 allow no-empty-object-type with single extends interfaces 2026-07-09 19:34:24 +00:00
FoxxMD 43fd8ea807 refactor: autofix (eslint) prefer-const prefer-as-const and var rules 2026-07-09 19:33:54 +00:00
FoxxMD ec3a7e2d20 fix: Fix case block declarations 2026-07-09 19:20:26 +00:00
FoxxMD 981a211e56 refactor: remove all unused imports 2026-07-09 19:04:40 +00:00
FoxxMD c42b5cacaa refactor: Move files/imports to implement folder import isolation 2026-07-09 18:31:18 +00:00
FoxxMD 919907f1cb feat: Add eslint rules to enforce folder import boundaries 2026-07-09 18:14:31 +00:00
FoxxMD 9eac9fdfa1 chore: prevent vscode from reporting comments in config json are errors 2026-07-09 18:14:01 +00:00
FoxxMD 86cea49ed4 fix(ci): Use node for stanity run 2026-07-09 13:31:02 +00:00
FoxxMD 8fc892a738 test: Fix db error test using transient db 2026-07-09 13:27:55 +00:00
FoxxMD fd591be3d6 remove tsx from mocha setup 2026-07-09 13:27:39 +00:00
FoxxMD 63b23689a4 remove debug file 2026-07-09 13:27:27 +00:00
FoxxMD 3035d4c5e8 feat(docker): Remove tsx and typescript dependency from build 2026-07-09 12:58:15 +00:00
FoxxMD 7891133002 fix remaining type imports preventing docker container from running with tsx 2026-07-09 12:26:56 +00:00
FoxxMD 66db4d017e more fixes to get all tests passing 2026-07-09 12:17:03 +00:00
FoxxMD 15e55dc6c5 more refactoring to remove server packages from bundled vite build 2026-07-09 04:13:28 +00:00
FoxxMD 239fb5919e fix(ui): Exclude discord.js from vite deps 2026-07-09 02:54:44 +00:00
FoxxMD 7e53553a8e more fixes for file extensions 2026-07-09 02:36:51 +00:00
FoxxMD 4bd0345d1b replace file extensions and more import type fixes 2026-07-09 02:27:41 +00:00
FoxxMD b207109697 add allowImportingTsExtensions compiler option 2026-07-09 02:03:03 +00:00
FoxxMD 77de2a7526 fix remaining erasure issues 2026-07-09 01:59:58 +00:00
FoxxMD 69b19da6aa fix(eslint): ignore no-undef 2026-07-09 01:59:46 +00:00
FoxxMD f2e2d67668 initial work for using erasable types 2026-07-08 21:14:34 +00:00
FoxxMD 9c44ccb663 chore(release): bump version for release 2026-07-08 17:25:23 +00:00
FoxxMD f431f4d5af refactor(ui): Remove non-mobile sidebar and ad header links
* Remove non-mobile sidebar to free up some more screen real estate
* Add quick Links menu to header that hides on mobile breakpoint
2026-07-08 16:49:37 +00:00
FoxxMD be872d5ce1 fix(ui): Fix double mobile menu icon 2026-07-08 16:09:04 +00:00
FoxxMD 0246f7dfa6 fix(now playing): Fix realtime NP state handling
* Revert removing state on "should clear" statuses, causing false positive updates
* Insteat, filter out NP state to api based on "should clear" statuses
2026-07-08 15:43:42 +00:00
FoxxMD b57f325684 feat: Implement error persistence and live updated for components 2026-07-08 15:02:02 +00:00
FoxxMD 153f37e927 standardize props and components used in timeline 2026-07-08 14:01:11 +00:00
FoxxMD de4cb8a5c2 fix(ui): Conditionally render queue timeline items based on timestamp sameness and context 2026-07-08 13:38:18 +00:00
FoxxMD eb133e4f7e fix(ui): Fallback datetime for scrobble timeline item 2026-07-08 13:32:52 +00:00
FoxxMD a3e056e7c5 feat: Implement virtual states to make filtering play state easier
* Convert 'queued' and 'dead queued' play states to queue filters
* Fix relation filter sql generation for count query
2026-07-08 13:25:41 +00:00
FoxxMD 7d8b00ab22 fix(musicbrainz): Fix finding stage inputs in error causes 2026-07-07 20:33:15 +00:00
FoxxMD 8b633dd901 feat(musicbrainz): Store all queries to lifecycle inputs and persist even on errors
Should make MB debugging easier
2026-07-07 20:05:10 +00:00
FoxxMD f055cf683f feat: Add toJSON to custom errors so they serialize correctly automatically 2026-07-07 18:19:05 +00:00
FoxxMD baaa954f31 chore: Add eslint plugin for mocha tests 2026-07-07 17:54:19 +00:00
FoxxMD c727de9c40 refactor(ui): Refactor timeline items to be rendered independently and order by createdAt 2026-07-07 17:42:27 +00:00
FoxxMD e6f4a1dbd2 feat(client): Implement createdAt dates for scrobble actions 2026-07-07 16:37:45 +00:00
FoxxMD 3d6e86f258 feat(ui): Add queue states to timeline 2026-07-07 15:21:29 +00:00
FoxxMD e6130eb5eb feat(ui): Better error/warning indicators for timeline 2026-07-07 14:23:06 +00:00
FoxxMD 97b6042768 feat(ui): Implement basic error summary detection 2026-07-07 03:07:29 +00:00
FoxxMD b3c1ce6d8a feat(ui): Indicate failed scrobble is dead queued 2026-07-07 02:32:35 +00:00
FoxxMD 5daba47ac7 fix(ui): Fix conditional hook rendering 2026-07-06 20:08:18 +00:00
FoxxMD 8d7760df6d chore: Update eslint config to include react hooks rules 2026-07-06 20:07:51 +00:00
FoxxMD 75c95fdaa8 more error boundaries 2026-07-06 18:53:57 +00:00
FoxxMD e874ccc2d3 Add some error boundaries around components 2026-07-06 18:34:32 +00:00
FoxxMD 17e97b3a6a fix(ui): Fix now playing mode for player on client component details 2026-07-06 18:18:58 +00:00
FoxxMD c6389590fe fix api fixture typing for players 2026-07-06 18:05:52 +00:00
FoxxMD 7151d96b59 feat(ui): Implement log copy 2026-07-06 18:03:38 +00:00
FoxxMD b2b2018764 feat(ui): Use custom hook for player queries and fix now-playing forced re-render 2026-07-06 17:07:46 +00:00
FoxxMD 655b97ce1c feat(ui): Implement now playing ui element
* Reuse source player in always-nowplaying mode, reuse api structure
* Add expiration to player data to stop rendering player for non-realtime clients
2026-07-06 16:40:01 +00:00
FoxxMD 5c6edefc56 feat(ui): track virtual play key by uid rather than index
Should prevent pop-in and false update-indicator triggering
2026-07-02 21:32:48 +00:00
FoxxMD bf7fb4f558 feat(storybook): More accurate play list api mocking
* Reusable function for generic behavior
* Append to list and create unique plays on every page request
2026-07-02 21:32:02 +00:00
FoxxMD 32b64ff384 fix update icons not expiring 2026-07-02 20:00:04 +00:00
FoxxMD f6e0f4be4a feat(ui): Use play updatedAt field for determining updated icon 2026-07-02 19:44:19 +00:00
FoxxMD fb66f0ca84 feat(db): update updatedAt fields automatically 2026-07-02 19:43:48 +00:00
FoxxMD c4b1d72f3f feat(ui): Implement plays refresh 2026-07-02 15:55:37 +00:00
FoxxMD fb452eb40b feat(ui): try to constrain date picker size for mobile 2026-07-02 14:37:52 +00:00
FoxxMD d3fd03a78c feat(ui): Wrap all filters 2026-07-02 14:28:57 +00:00
FoxxMD 7b04e76c3b fix(ui): Accessing target/children when no ref, yet 2026-07-02 14:13:54 +00:00
FoxxMD 9f34c45192 fix(ui): Fix n+1 insertion bug on new play and add logging 2026-07-02 14:07:46 +00:00
FoxxMD 7ab0f710ff feat(ui): Better control over ephemeral updates to plays
* Use structuralSharing to add signal that play data was updated from invalidated query
* Make update badge more generic and fix timeout/re-render
* Play update story updates random play
2026-07-02 14:01:28 +00:00
FoxxMD ea1fb54de1 fix(ui): Revert datepicker formatting for now
Causing end date to jump back one day after every change?
2026-07-02 12:29:31 +00:00
FoxxMD 35fd84a1d0 feat(ui): Improve date picker useability
* reduce preset options to make smaller viewports easier to use
* Add icon to mobile users can avoid keyboard popup
* Disable openOnClick to avoid popup annoyance when user is trying to directly type dates
2026-07-02 01:07:20 +00:00
FoxxMD e498db8ad3 feat(ui): Implement flex wrapped hook 2026-07-01 22:44:17 +00:00
FoxxMD 145960ca04 feat(ui): Implement placeholder component state actions 2026-07-01 20:23:16 +00:00
FoxxMD 91ba24e165 feat(ui): Add placeholder primary actions from component state 2026-07-01 19:49:08 +00:00
FoxxMD 66fc7a2b09 feat(ui): Add back button on component details 2026-07-01 18:30:44 +00:00
FoxxMD 97a7c7044c feat(ui): Implement play actions in summary 2026-07-01 17:57:07 +00:00
FoxxMD 53d7de7e8d refactor(ui): Use custom hook for activity fetching 2026-07-01 17:12:55 +00:00
FoxxMD 32da14c08e make entire component header a link to details 2026-07-01 00:33:45 +00:00
FoxxMD c7f370f121 feat(ui): de-emphasize mode badge 2026-07-01 00:31:48 +00:00
FoxxMD 9d105173da refactor(storybook): More re-organizing 2026-06-30 22:47:47 +00:00
FoxxMD 3e7bbab5b0 refactor(storybook): organize storybook file and loglical 2026-06-30 21:03:14 +00:00
FoxxMD e04965eb35 fix(storybook): Fix router wiring for component list stories 2026-06-30 20:41:51 +00:00
FoxxMD f2c5e82665 keep names and sourcemap on build? 2026-06-30 20:28:07 +00:00
FoxxMD 4c41f0976d Add placeholder action buttons to play detail 2026-06-30 20:27:57 +00:00
FoxxMD db333184cc fix(ui): Fix text alignment in timeline titles 2026-06-30 20:27:38 +00:00
FoxxMD a5963a2333 (bad) ignore weird type error on cause unknown 2026-06-25 15:38:38 +00:00
FoxxMD 0fe81d6164 fix(ui): Fix content wrapping in detailed component 2026-06-25 15:13:52 +00:00
FoxxMD 3ad9f5dcac Add api fixture for generating fake errors 2026-06-25 15:13:27 +00:00
FoxxMD d39abe40c6 feat(ui): Hide error stacks at sm and below 2026-06-25 15:12:46 +00:00
FoxxMD 6f8ca72ccf fix(db): Fix accessing undefined lifecycle during retention cleanup 2026-06-24 19:29:35 +00:00
FoxxMD 2ae32ee19f feat(ui): Polish error alert 2026-06-24 19:21:15 +00:00
FoxxMD 2ac89c5db3 fix: Serialize errors at api boundary and fix recursive cause bug in error alert render 2026-06-24 18:27:34 +00:00
FoxxMD b0db1dd8bc feat(ui): Add error boundaries and better error detection for rendering 2026-06-24 17:18:26 +00:00
FoxxMD a603be6e22 fix(ui): Fix total stats count 2026-06-24 16:12:58 +00:00
FoxxMD 7ad23a76de fix(ui): Fix component summary not rendering live updates when no initial players 2026-06-24 15:51:48 +00:00
FoxxMD 4481646153 refactor(ui): Move query functions into factory 2026-06-24 15:51:26 +00:00
FoxxMD 5d2c9b557f refactor(ui): Move component player live add/delete into players container 2026-06-24 15:17:54 +00:00
FoxxMD eb69ae48f1 fix(ui): Ok to ignore empty error when rendering error alert 2026-06-24 14:19:54 +00:00
FoxxMD c7479b2f00 fix(ui): Fix player keys and use of floats 2026-06-24 14:18:46 +00:00
FoxxMD 8ae7a8d338 feat(ui): Implement live play insert 2026-06-24 14:14:17 +00:00
FoxxMD 7573c762c0 fix(db): Include input in created play data since some systems depend on this existing 2026-06-24 14:14:03 +00:00
FoxxMD 6d503a1a15 fix(storybook): Fix duplicate msw workers due to vite hot reload 2026-06-24 14:13:19 +00:00
FoxxMD 94d3ea5da8 fix: Fix typing for play update events 2026-06-24 01:10:05 +00:00
FoxxMD e998006e6c feat(ui): Implement live updates to activity summary and details 2026-06-24 00:52:01 +00:00
FoxxMD 234fc67ab2 refactor(client): Remove unnecessary notifier dependency 2026-06-23 20:05:33 +00:00
FoxxMD 37bb507359 feat(ui): Global sse status indicator 2026-06-23 19:51:14 +00:00
FoxxMD 1237056d59 feat(ui): Implement statuses for clients 2026-06-23 19:50:54 +00:00
FoxxMD 7174702f7b feat(ui): Implement status and state updates for sources 2026-06-23 18:44:19 +00:00
FoxxMD 3b721673c8 refactor(ui): Replace native links with react-router links 2026-06-23 16:10:52 +00:00
FoxxMD b64f0d2dd5 fix(ui): Fix color contrast for log lines 2026-06-23 16:03:20 +00:00
FoxxMD 7738dc004a feat(ui): Animate sleep indicator 2026-06-23 15:54:34 +00:00
FoxxMD 5c5563a917 Fix layout of state badge and status in component details 2026-06-23 15:48:32 +00:00
FoxxMD 7d08939a18 feat(ui): Implement sleeping indicator 2026-06-23 15:38:53 +00:00
FoxxMD fabc50aa62 switch back to directdomupdates for virtuallist 2026-06-23 13:40:42 +00:00
FoxxMD 4c0615d427 finally working infinite query loading 2026-06-23 13:31:16 +00:00
FoxxMD b4dd729150 refactor: Replace hardcoded queryopts
* Convert to json with deepreplacevalue
* hydrate dayjs at api boundary with generic object walker
2026-06-23 12:33:18 +00:00
FoxxMD f336089755 fix(ui): Fix header icon 2026-06-22 20:38:28 +00:00
FoxxMD 4fc6285081 fix: remove import that not longer exists 2026-06-22 20:26:04 +00:00
FoxxMD 0cf3e33fe6 fix artist alignment 2026-06-22 20:24:39 +00:00
FoxxMD ab424aee0d feat(ui): Implement track/artist/album text search 2026-06-22 20:21:54 +00:00
FoxxMD 9fb55d479d feat(ui): Add play count total and per day 2026-06-22 19:14:22 +00:00
FoxxMD c8fb01bbab Improvements to infinite querying 2026-06-22 18:00:00 +00:00
FoxxMD f008c4074d feat(ui): Implement infinite queries 2026-06-22 16:54:47 +00:00
FoxxMD d66922c99a Improve layout for activity summary 2026-06-22 16:16:05 +00:00
FoxxMD c7689199cc fix styling for filters 2026-06-22 15:21:49 +00:00
FoxxMD 9a8b6bfa09 feat(ui): wire up play filters to api 2026-06-22 14:47:44 +00:00
FoxxMD 45c8141137 with card 2026-06-20 02:34:52 +00:00
FoxxMD 4e7b3e9ceb better styling 2026-06-20 02:30:51 +00:00
FoxxMD dfcc9c32a8 partially working layout for filters 2026-06-20 02:17:00 +00:00
FoxxMD a3d891aa2f Add activity list heading 2026-06-19 20:52:46 +00:00
FoxxMD 088ae4a26a default to virtual list 2026-06-19 20:32:16 +00:00
FoxxMD f425b5a906 disable wdyr
causing issues with storybook
2026-06-19 20:11:15 +00:00
FoxxMD a4ddaeff61 install react-compiler 2026-06-19 20:10:55 +00:00
FoxxMD 8932224b57 add why-did-you-render for debugging 2026-06-19 20:00:48 +00:00
FoxxMD ffe2203ba5 implement virtual list with tanstack/virtual 2026-06-19 19:30:08 +00:00
FoxxMD 987819cc7c fix missing key warnings on react nodes 2026-06-19 15:50:39 +00:00
FoxxMD 974a017e5a feat(ui): Implement query-key-factory and activity summary fetchable 2026-06-19 15:39:58 +00:00
FoxxMD 594a59347e refactor activity summary into own component 2026-06-19 14:09:36 +00:00
FoxxMD b0970c6653 chore: standardize component type 2026-06-19 13:47:54 +00:00
FoxxMD d8017dda62 fix: fix typing on api fixture for queues 2026-06-18 20:33:20 +00:00
FoxxMD a8c3e88ec9 fix(ui): Fix progress background when in a container 2026-06-18 20:31:39 +00:00
FoxxMD 7bf31a7fc7 add chevron left button 2026-06-18 20:26:11 +00:00
FoxxMD deb3310e3f fix: Fix preserving play original data until it is persisted 2026-06-18 20:18:15 +00:00
FoxxMD 5450b48eb3 fix(ui): correct query args for showing plays by seen/desc 2026-06-18 20:17:50 +00:00
FoxxMD 70a94cff9b fix(ui): Fix showing error indicator when error is null 2026-06-18 20:17:32 +00:00
FoxxMD ff31259092 feat(ui): Implement fetchable play lists and detailed components 2026-06-18 19:41:02 +00:00
FoxxMD 8b4f1849f6 feat: Implement api endpoints for individual components, plays, and individual plays 2026-06-18 19:40:29 +00:00
FoxxMD 110ffad7d7 docs: Update atproto descriptions 2026-06-18 16:49:35 +00:00
FoxxMD 62a0670b4d Add players to detailed source component 2026-06-18 15:54:00 +00:00
FoxxMD 7a1142fdd5 actions placeholder 2026-06-18 15:20:58 +00:00
FoxxMD 5def3cde99 fix(ui): Fix accordion context 2026-06-18 14:54:27 +00:00
FoxxMD 075fa877ed Move player tooltip to bottom right position 2026-06-18 14:36:22 +00:00
FoxxMD 6ffb46a65a chore: Fix storybook theming, again 2026-06-18 14:12:21 +00:00
FoxxMD ac5f34ea5f fix(ui): Fix width of tooltip popover 2026-06-18 14:09:48 +00:00
FoxxMD 9975f377d0 fix(ui): Fix colors in player buffer tooltip to match system colors with contrast 2026-06-18 14:05:55 +00:00
FoxxMD 76b4606d17 chore: Use system theme for storybook 2026-06-18 14:05:36 +00:00
FoxxMD 1244e0ee3e fix(ui): Use more lenient css selectors for progress bar 2026-06-18 13:53:15 +00:00
FoxxMD c56b16e2e6 fix: Fix auth cache validation and fallback 2026-06-18 13:43:10 +00:00
FoxxMD 831fd420ff feat(ui): Add player buffer tooltip 2026-06-18 13:27:16 +00:00
FoxxMD 2125f92bd8 feat(ui): Implement player buffering real time positiono 2026-06-18 12:42:26 +00:00
FoxxMD 42c9383dda partial buffered player updates working 2026-06-18 03:06:53 +00:00
FoxxMD f5eab006f5 feat(ui): Implement buffering player progress with mui 2026-06-18 02:19:08 +00:00
FoxxMD 5a2c664fe1 fix(atproto): Fix rate limit expiration and display 2026-06-18 00:56:30 +00:00
FoxxMD d7cbb7c9bd fix: Fix missing implementation on test component 2026-06-17 21:22:20 +00:00
FoxxMD 8afe30bfd4 feat(ui): Implement stats, stat change indicator, and iniitial component detail 2026-06-17 21:19:18 +00:00
FoxxMD 24b67fc430 feat(ui): Implement better live stat indicators 2026-06-17 19:59:06 +00:00
FoxxMD d01a00ad13 feat(atproto): Implement rate limit checks 2026-06-17 17:02:19 +00:00
FoxxMD ac250eba39 feat(ui): Implement real state reporting for components 2026-06-17 14:56:47 +00:00
FoxxMD 778a2d6565 feat(ui): add activity indicator to logs panel 2026-06-17 13:57:19 +00:00
FoxxMD 0b9a7b78df feat(ui): Live player add/remove based on sse updates 2026-06-17 13:24:23 +00:00
FoxxMD 6b93e839fa fix(atproto): fix not throwing applogin exception 2026-06-17 01:56:55 +00:00
FoxxMD 7e6040ce7b fix(ui): Fix accessing components before db is ready 2026-06-17 01:46:45 +00:00
FoxxMD d4b2057c41 fix: fix nullish access of dbcomponent during testing 2026-06-17 01:44:26 +00:00
FoxxMD bc04603fb4 fix: closest match needs id to update updatedAt 2026-06-17 01:42:37 +00:00
FoxxMD ba2a23e039 fix: Fix app migrations table name inconsistency 2026-06-17 01:24:06 +00:00
FoxxMD 6bfe203a34 fix(atproto): Fix not throwing on applogin error 2026-06-17 01:08:40 +00:00
FoxxMD 8c6f34ebc7 fix: Guard against undefined synced reason 2026-06-17 01:05:48 +00:00
FoxxMD 18e20123c2 fix(ui): fix z-index on logs panel 2026-06-16 22:07:54 +00:00
FoxxMD dc44f6c921 feat(ui): Implement fetchable logs with filtering 2026-06-16 20:46:51 +00:00
FoxxMD a19fd42a5b feat(ui): Implement float logs 2026-06-16 20:15:17 +00:00
FoxxMD 206fd62f70 feat(ui): Splitter logs 2026-06-16 19:30:12 +00:00
FoxxMD 6123503ee8 feat(ui): Implement logs component 2026-06-16 18:12:54 +00:00
FoxxMD b751c6f5f9 feat(ui): Implement side nav 2026-06-16 15:33:55 +00:00
FoxxMD d1ef09ba0d feat(ui): Implement header 2026-06-15 21:08:29 +00:00
FoxxMD d073642203 feat(ui): Implement live updates with sse 2026-06-15 19:17:50 +00:00
FoxxMD 9bb48fbf13 POC player live update 2026-06-15 17:17:08 +00:00
FoxxMD bf8f1175ce feat(ui): Implement static component list in app entrypoint 2026-06-15 14:43:13 +00:00
FoxxMD 4bfcc40571 poc multi page 2026-06-12 17:57:54 +00:00
FoxxMD 892d035d7a feat(ui): Implement component summary list 2026-06-12 16:03:07 +00:00
FoxxMD d4019e6dea feat(ui): Implement Player and api fixtures 2026-06-11 20:48:10 +00:00
FoxxMD 3ce94f443a feat(ui): Implement component overview and api fixtures 2026-06-11 18:55:11 +00:00
FoxxMD b863a9773a better optimization of activity details 2026-06-11 00:18:46 +00:00
FoxxMD 489d7d12cb improvements to play list and story 2026-06-11 00:18:36 +00:00
FoxxMD 63c7ea7809 fea(ui): Implement tanstack-query for ActivityDetails and mock in storybook 2026-06-10 20:44:51 +00:00
FoxxMD a6a019e9fd activity ui improvements 2026-06-10 19:45:13 +00:00
FoxxMD dbd1c81c90 update timelline component
* make activity optional with fallback loading render
* fix storybook default args with constant
2026-06-10 19:06:13 +00:00
FoxxMD 2b42b40c0a update storybook 2026-06-10 19:05:01 +00:00
FoxxMD 4230674c2c some ui fixes 2026-06-10 17:36:53 +00:00
FoxxMD b0955270ba refactor: Remove indexed prop from PlayMeta
* Remove indexed any prop so meta is stricter (better catching bugs)
* Implement mergeable generic with meta where we do need extra prop
* Fix bad usage of meta now that indexed prop is gone
2026-06-10 01:54:35 +00:00
FoxxMD a194f752bb refactor: Remove lifecycleless types 2026-06-10 01:54:35 +00:00
FoxxMD 4180ae0c6b feat(migrations): Implement play lifecycle location migration 2026-06-10 01:54:35 +00:00
FoxxMD 3227541f34 feat(db): Add unix timestamp to database bakup file and fix test for detecting it 2026-06-10 01:54:35 +00:00
FoxxMD e949da3830 refactor: Move all lifecycle properties to top-level Play
And remove lifecycle from meta
2026-06-10 01:54:35 +00:00
FoxxMD e13d86b1d0 feat: Add more identying data to lifecycle step 2026-06-10 01:54:35 +00:00
FoxxMD f7577c43e0 refactor: Move scrobble to top level of play object 2026-06-10 01:54:35 +00:00
FoxxMD f3531f41ec feat: Add creation timestamps to lifecycle data 2026-06-10 01:54:35 +00:00
FoxxMD d9d37c68fc Implement api fixtures 2026-06-10 01:54:35 +00:00
FoxxMD 3bb887763f refactor: Move original play data to top-level property in play
* Simplifies lifecycle
* Simplifies play generation for testing
2026-06-10 01:54:35 +00:00
FoxxMD c65504ee9f feat(ui): Define initial API contract 2026-06-10 01:54:35 +00:00
FoxxMD 373fdb0dc2 fix(ui): Gaurd against undefined code prop 2026-06-10 01:54:35 +00:00
FoxxMD 825d0da532 fix(ui): Gaurd againast missing duration 2026-06-10 01:54:35 +00:00
FoxxMD 2efdada4c0 fix(musicbrainz): Fix default assignment of weights in stage config
Should not assign a default value so that stage defaults are not overriden in stage config when no explicit config is given.

Fixes #627
2026-06-10 01:41:41 +00:00
Matt Foxx 01afe9006a Merge pull request #626 from FoxxMD/lzImport
fix(listenbrainz endpoint): `import` handling and error responses
2026-06-09 11:48:24 -04:00
FoxxMD da9edb2116 feat(lzendpoint): Improved error handling
* Return error response when no sources found by request, or none configured
* try-catch and return well-formatted error response if source handling fails
2026-06-09 13:42:54 +00:00
FoxxMD d5c886d718 fix(listenbrainz): Handle 'import' listen_type with multiple scrobbles
#625
2026-06-09 13:28:44 +00:00
FoxxMD e99351407e fix(musicbrainz): Fix order of release priority list
Should be reversed since we use index as score, so "first" in the list should have the highest index/score

#624
2026-06-08 15:38:55 +00:00
Matt Foxx d9184fa350 Merge pull request #623 from FoxxMD/mpdArtistAssignment
fix(mpd): Assign empty array if artists not defined
2026-06-07 22:22:38 -04:00
Matt Foxx e4af1e3f18 Merge pull request #619 from FoxxMD/atprotoHistory
feat: Implement historical scrobble querying for atproto clients
2026-06-07 11:51:33 -04:00
FoxxMD 9ca4f3fa62 fix(mpd): Assign empty array if artists not defined
#621
2026-06-07 15:47:43 +00:00
FoxxMD 20e3f22f3f docs(azuracast): Add clearer guidance on station configuration and access 2026-06-05 19:55:14 +00:00
FoxxMD bca9e195c9 feat(scrobblers): Detect sync gap and resolve sync status if filled 2026-06-05 19:10:29 +00:00
FoxxMD 8b1765cb03 fix: Fix missing type in prometheus stats 2026-06-05 15:21:34 +00:00
FoxxMD 0605cf1fd5 feat: Implement compaction prometheus statistics 2026-06-05 15:05:53 +00:00
FoxxMD 07d6487875 feat: Implement database play prometheus stats
* Total plays by component/state
* Total historical plays by client
2026-06-05 14:54:29 +00:00
FoxxMD 6222aa5899 fix(rocksky): Do not persist scrobble payloads as historical plays 2026-06-05 13:33:04 +00:00
FoxxMD 47f61b491e fix: Fix tables used in historical play repository 2026-06-05 13:23:59 +00:00
FoxxMD 42818c51c1 feat(rocksky): Update configuration and document new token 2026-06-04 20:44:24 +00:00
FoxxMD a0fee4184e chore(rocksky): Bump version to get new exports 2026-06-04 20:23:26 +00:00
FoxxMD 470a70f951 feat(rocksky): Implement historical scrobbles 2026-06-04 20:07:08 +00:00
FoxxMD 2d2b3a6d48 feat(rocksky): Implement native sdk 2026-06-04 19:18:14 +00:00
FoxxMD 6f40dd387e chore(atproto): Remove unused @atproto packages 2026-06-04 17:58:12 +00:00
FoxxMD 2445336ec0 refactor(atproto): Remove unused oauth implementation
Oauth will need a larger, ground-up rewrite if/when its eventually done. Remove the old, unusued implementation for now to reduce complexity.
2026-06-04 17:54:41 +00:00
FoxxMD 41d181e02f refactor: Replace @atproto with @atcute for abstract/app api implementation 2026-06-04 17:50:40 +00:00
FoxxMD ff158c99cd refactor: Move some stateless functions out of atproto class 2026-06-04 16:05:58 +00:00
FoxxMD c1cdcd016a refactor: Move more atproto-generic function into abstract api base 2026-06-04 15:37:01 +00:00
FoxxMD fcb9092f8a refactor: Rename bluesky to atproto, classes 2026-06-04 15:14:45 +00:00
FoxxMD ba2bd0cc8a refactor(teal): Refactor atproto api implementation out of tight teal coupling 2026-06-04 15:12:37 +00:00
FoxxMD f224ce6b6a refactor: Rename bluesky to atproto 2026-06-04 14:51:49 +00:00
FoxxMD c74678e9ef feat(scrobbler): Improve historical scrobbler hydration
* Refactor post init behavior into promise so we can fallback to recent sync if full hydration fails
  * This way user can still compare real time scrobble activity without full hydration
* Simplify sync status usage
  * Log reason based on presence of sync error
  * Always try to do full hydration if not synced
* Fix missing logging for hydration error
* Add MS-scrobbled scrobbles to historical database
2026-06-04 13:53:08 +00:00
FoxxMD 1b1770ff29 chore(release): Bump version for release 2026-06-03 19:20:22 +00:00
Matt Foxx 41d3a63c42 Merge pull request #615 from FoxxMD/artistCreditCleanup
fix: Add missing artist credit transforms
2026-06-03 15:07:46 -04:00
FoxxMD 52240aa13b feat(teal): Implement historical scrobble searching for existing scrobbles and auto hydrate, or sync recent, on init 2026-06-03 19:06:26 +00:00
FoxxMD 95d7d276d7 refactor(teal): Generate and use real lexicon definitions for tealfm implementation
Removes hand-rolled interfaces and generates interfaces+validation for official lexicon definitions
2026-06-03 18:14:20 +00:00
FoxxMD 18c520ce2f feat(tealfm): Stream repo CAR to monitor download progress 2026-06-03 16:33:40 +00:00
FoxxMD fb65cb222a fix: fix rebase conflicts 2026-06-03 16:33:33 +00:00
FoxxMD c4f028fd2c feat(tealfm): Implement historical scrobble hydration 2026-06-03 16:33:24 +00:00
FoxxMD b85452f5c6 feat: Implement historical scrobble client abstract class 2026-06-03 16:32:16 +00:00
FoxxMD 6546ee3db2 Add package for atproto repo car handling 2026-06-03 16:29:57 +00:00
FoxxMD f0e798c052 fix: Add missing artist credit transforms
Fixes #613
2026-06-03 13:24:32 +00:00
FoxxMD 44424f2570 fix: more logging and arg fixes after re-generating lock file 2026-06-03 13:24:10 +00:00
FoxxMD 72df520300 chore: re-generate lock file
* Caused by npm/cli#6301
* will make packaging for nix easier NixOS/nixpkgs#519119
2026-06-03 12:54:31 +00:00
FoxxMD 25c783051d fix: Fix argument order for logger labels 2026-06-03 12:50:16 +00:00
FoxxMD 264bc31af6 Merge branch 'storybookRefresh' 2026-06-02 14:11:04 +00:00
FoxxMD e0bdb8fe3c docs: Fix typo in jriver docs 2026-06-02 12:52:09 +00:00
FoxxMD 4d7c93f7dc fix(maloja): Fix source maloja envs
#610
2026-06-01 22:32:15 +00:00
FoxxMD dd54075c52 docs: Fix bad config example for id on aio in 0.14.0 upgrade 2026-06-01 19:37:02 +00:00
FoxxMD ff08b6327d docs: Comment out required upgrade callout, for now
Unnessary information since there is nothing > 0.14.0. May confuse some users. Can uncomment after 0.14.1 or later.
2026-06-01 19:27:09 +00:00
FoxxMD 76392a9668 chore(release): Bump version for release 2026-06-01 18:45:20 +00:00
FoxxMD f47faabf7c docs: Remove example of caching for transform best practices
Example is already in the main cache configuration docs. Consolidate/remove the transform example so there is only one authorative example in the docs.
2026-06-01 17:30:56 +00:00
FoxxMD 120e280fc8 docs: More upgrade guidance for 0.14.0 2026-06-01 15:22:38 +00:00
FoxxMD 6ae80fd881 feat(docs): Update colors for regular important admonition 2026-06-01 14:51:11 +00:00
FoxxMD be70e7074a fix(docs): Fix css for important admonition details important type 2026-06-01 14:35:02 +00:00
Matt Foxx 65841a8c6b Merge pull request #609 from HiiJax/fixMbIdCaps
fix(tealfm): trackMbid -> trackMbId
2026-06-01 08:40:14 -04:00
Jax 82374fc806 fix(tealfm): trackMbid -> trackMbId 2026-05-31 20:02:09 -04:00
FoxxMD 004400d7ea refactor: Replace jsondiffpatch-react with @pierre/diffs
Good ridance.
2026-05-30 20:02:20 +00:00
FoxxMD d54e169a14 fix for transformstep date 2026-05-30 19:05:36 +00:00
FoxxMD 31c35647dd feat(ui): Update artist rendering to use credits 2026-05-30 19:05:36 +00:00
FoxxMD 204ee159d6 fix(ui): Update JSX namespace to fix missing ambient in react 19 2026-05-30 19:05:36 +00:00
FoxxMD 4fe1f8ced9 chore(release): Bump for beta release 2026-05-30 15:12:25 +00:00
Matt Foxx c26e6f77f5 Merge pull request #608 from FoxxMD/nowPlayingGranular
refactor: Now Playing rewrite to handle realtime client updates
2026-05-30 10:59:40 -04:00
FoxxMD e2b59900e5 fix(listenbrainz): Fix artist credit mapping for minimum track data 2026-05-30 14:59:18 +00:00
FoxxMD c9cde11871 fix(tealfm): Missing function call to statusExpiresSoon 2026-05-29 14:45:42 +00:00
FoxxMD ea0c75ccf3 chore: Simplify gitignore pattern for db files 2026-05-29 14:40:57 +00:00
FoxxMD 13a64bd63a refactor(now playing): Improve now playing to generically handle clients that have real-time updates
* Break out player status checks and thresholds into separate functions
* Consolidate all should update functionality into shouldUpdatePlayingNow for easier testing
* Introduce realtime flag for teal/discord and check if clearing is possible/desired
  * Remove equivalent functionality in individual clients
* (tealfm): Correctly set max threshold using track duration
* (tealfm): Skip clearing update if close to/after expiration time
* (tealfm): Increase min threshold to avoid spamming pds updates for pauses/skip track
2026-05-29 14:40:45 +00:00
FoxxMD c3b23271d4 fix(now playing): Fix acessing undefined play when source player state is reported but without any data
598#issuecomment-4566553387
2026-05-28 17:48:05 +00:00
FoxxMD d047c600c2 feat(tealfm): Update lexicon defs with new mbid uri format
teal-fm/teal#102
2026-05-28 16:13:06 +00:00
FoxxMD b6cae4a538 fix(jellyfin): Handle string or object data for album props when parsing to play
#598#issuecomment-4564522030
2026-05-28 15:08:14 +00:00
FoxxMD 1eed48456c feat: Make artist name to credit function more robust
* artistNameToCredit can now handle partial ArtistCredit object
* Re-use artistNameToCredit for mapping in artistNameToCredits
2026-05-28 15:07:19 +00:00
FoxxMD 20834e812e feat: case-insensitive proxy handles spread/rest 2026-05-28 14:44:02 +00:00
FoxxMD d1e5b11598 chore(release): Bump version for release 2026-05-28 13:05:12 +00:00
FoxxMD c7af346daf docs(lastfm): Document 2 week scrobble window
#606
2026-05-28 12:31:28 +00:00
Matt Foxx 3975edeaf3 Merge pull request #603 from HiiJax/tealfmStatus
feat(tealfm): Add now playing support with fm.teal.alpha.actor.status
2026-05-27 20:43:01 -04:00
Jax 119c8d7fd7 fix(tealfm): avoid nested ternary operators in status expiry 2026-05-27 20:30:25 -04:00
Jax 3a96c28b2d fix(tealfm): status clearing, expiry, and state detection per review 2026-05-27 18:04:29 -04:00
Matt Foxx 6905853aa3 Merge pull request #604 from FoxxMD/listenRangeGuard
fix: Guard against old/bad cached listen range data
2026-05-27 15:52:17 -04:00
FoxxMD 3f1a952e44 fix: Guard against old/bad cached listen range data
#601
2026-05-27 19:25:17 +00:00
FoxxMD 405ee4f6d7 fix(scrobbler): Fix try-catch block so queued scrobble is always added, even if existing queued check fails 2026-05-27 19:24:21 +00:00
FoxxMD 34e0eaefd8 fix(scrobbler): Add migrated play to queue
* Utilize existing methods for added queued/dead scrobbled when migrating from cache
  * Using queueScrobble fixes migration not creating queued state
  * Using addDeadLetterScrobble reduces duplicated code in migration
* Drop listenranges to avoid bad cache data issue (potentially)
2026-05-27 19:19:24 +00:00
FoxxMD 6d4b617409 fix: Add missing sort order to failed/scrobbled api calls
#598#issuecomment-4490136917
2026-05-27 18:19:45 +00:00
Matt Foxx 4195bde081 Merge pull request #600 from nabijaczleweli/master
docs: u{p => t}most typo
2026-05-27 08:28:41 -04:00
Jax 925f3b916d feat(tealfm): Add now playing support with fm.teal.alpha.actor.status 2026-05-26 05:20:31 -04:00
наб 770621ba41 docs: u{p => t}most typo 2026-05-22 20:10:45 +02:00
FoxxMD a00e5d0dbc feat(database): Track component last ready and last active at 2026-05-15 17:21:22 +00:00
FoxxMD 0d6d803df3 feat(database): Implement database structures needed for historical plays 2026-05-15 17:03:48 +00:00
FoxxMD 396e4b922e docs: Overhaul kitchensink 2026-05-15 15:14:29 +00:00
FoxxMD 14ecb57210 fix(docs): Fix missing comma in discord config example 2026-05-15 15:14:12 +00:00
FoxxMD 14369836f6 feat(docs): Add more descriptive error logging for config file parsing 2026-05-15 15:13:52 +00:00
FoxxMD 70e5aaaad5 docs: update kitchensink with ids 2026-05-15 13:09:42 +00:00
FoxxMD 366e668c13 fix(docs): Fix json syntax in sample configs 2026-05-14 20:38:12 +00:00
FoxxMD d0e842ede3 docs: Update ENV configs with ID and name 2026-05-14 20:33:34 +00:00
FoxxMD a0c1f71162 docs: Update examples with id 2026-05-14 20:05:29 +00:00
FoxxMD 7ef159448e feat: Improved retention policy
* Add ENVs for compacting
* Default compacting all data types
* Add rention guidance to docs
2026-05-14 19:48:04 +00:00
FoxxMD b4e6ce6e78 chore(release): Bump the version beta 1 release 2026-05-14 17:40:37 +00:00
Matt Foxx abe75869b1 Merge pull request #588 from FoxxMD/database
0.14.0
2026-05-14 13:35:16 -04:00
FoxxMD 246e9ad7c5 chore(release): Bump version for release 2026-05-06 12:12:52 +00:00
507 changed files with 28116 additions and 11732 deletions
+2 -1
View File
@@ -31,4 +31,5 @@ tmp-*
.env
*.env
.storybook
.stories
.stories
**/lexicons/**/*.json
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
run: |
set +e
export NODE_ENV=production
timeout --preserve-status 10s node node_modules/.bin/tsx src/backend/index.ts
timeout --preserve-status 10s node src/backend/index.ts
exitcode="$?"
if [[ "$exitcode" -eq 143 ]] || [[ "$exitcode" -eq 137 ]]; then
echo "App stayed up long enough and exited with expected status"
+7 -5
View File
@@ -153,8 +153,10 @@ tmp-*
storybook-static
*.db
*.db.*
*.db-*lib
*.db
*.db.*
*.db-*lib
*.db*
**/lexicons/**/*.json
bun.lock
.DS_Store
**/.DS_Store
-1
View File
@@ -1,7 +1,6 @@
{
"reporter": "dot",
"extension": "ts",
"import": "tsx/esm",
"spec": "./src/backend/tests/**/*.test.ts",
"file": [
"./src/backend/tests/setup.ts"
+2
View File
@@ -12,12 +12,14 @@ export default defineMain({
// "@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs",
'storybook-addon-remix-react-router'
// "@storybook/addon-onboarding"
],
features: {
// https://github.com/storybookjs/storybook/discussions/33279
sidebarOnboardingChecklist: false,
},
staticDirs: ['../public'],
"framework": "@storybook/react-vite",
viteFinal: async (config) => {
return mergeConfig(config, {
+1 -1
View File
@@ -2,5 +2,5 @@ import { addons } from 'storybook/manager-api';
import { themes } from 'storybook/theming';
addons.setConfig({
theme: themes.dark,
theme: themes.normal,
});
+10 -1
View File
@@ -3,8 +3,17 @@ import addonA11y from "@storybook/addon-a11y";
import { definePreview } from "@storybook/react-vite";
import { themes } from 'storybook/theming';
import { initialize, mswLoader, getWorker } from 'msw-storybook-addon';
// https://github.com/mswjs/msw-storybook-addon/issues/82#issuecomment-3894302575
try {
getWorker();
} catch (_) {
initialize();
}
export default definePreview({
loaders: [mswLoader],
parameters: {
controls: {
matchers: {
@@ -20,7 +29,7 @@ export default definePreview({
test: 'todo'
},
docs: {
theme: themes.dark,
theme: themes.normal,
},
},
+5 -20
View File
@@ -20,7 +20,7 @@
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/index.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"runtimeExecutable": "node",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
@@ -35,7 +35,7 @@
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/index.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"runtimeExecutable": "node",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"env": {
@@ -48,28 +48,13 @@
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "tsx",
"type": "node",
"request": "launch",
// Debug current file in VSCode
"program": "${file}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
},
{
"name": "schema",
"type": "node",
"request": "launch",
// Debug app in VSCode
"program": "${workspaceFolder}/src/backend/utils/SchemaStaticUtil.ts",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"runtimeExecutable": "node",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
@@ -96,7 +81,7 @@
"--recursive",
"${workspaceFolder}/src/backend/tests/**/*.test.ts"
],
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"runtimeExecutable": "node",
"internalConsoleOptions": "openOnSessionStart",
"name": "Mocha Tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
@@ -120,7 +105,7 @@
"--config", "${workspaceRoot}/.mocharc.json",
"${file}"
],
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
"runtimeExecutable": "node",
"internalConsoleOptions": "openOnSessionStart",
"name": "Mocha Test on File",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
+4 -3
View File
@@ -1,5 +1,4 @@
{
"mochaExplorer.require": "tsx/esm",
"mochaExplorer.timeout": 1200000,
"mochaExplorer.exit": true,
"search.exclude": {
@@ -11,7 +10,9 @@
"docsite/**"
],
"files.associations": {
"*.css": "tailwindcss"
"*.css": "tailwindcss",
"**/config/*.json": "jsonc"
},
"tailwindCSS.experimental.configFile": "src/client/index.css"
"tailwindCSS.experimental.configFile": "src/client/index.css",
}
+3
View File
@@ -56,6 +56,7 @@ RUN npm install -g concurrently
ARG data_dir=/config
VOLUME $data_dir
ENV CONFIG_DIR=$data_dir
ENV DATA_DIR=$data_dir
COPY docker/root /
@@ -118,6 +119,8 @@ RUN npm ci --omit=dev --no-audit \
&& npm uninstall ts-json-schema-generator \
&& npm cache clean --force \
&& chown -R abc:abc node_modules \
## peerDependency of validbot and can(?) be removed for production
&& rm -r node_modules/typescript \
&& npx @usex/prune-mod -w \
&& rm -rf /root/.cache
+1
View File
@@ -12,6 +12,7 @@ alt="multi-scrobbler logo" width="180" height="180">
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
* [Apple Music](https://docs.multi-scrobbler.app/configuration/sources/applemusic/?configType=file)
* [Azuracast](https://docs.multi-scrobbler.app/configuration/sources/azuracast)
* [Deezer](https://docs.multi-scrobbler.app/configuration/sources/deezer)
* [Google Cast (Chromecast)](https://docs.multi-scrobbler.app/configuration/sources/google-cast)
+29
View File
@@ -0,0 +1,29 @@
[
{
"id": "myAppleMusic",
"name": "My Apple Music",
"enable": true,
"clients": [],
"data": {
// either key or token needs to be provided
"key": {
"id": "2HPSNJZ88N",
"teamId": "SN6YASW8G4",
"p8": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEG...-----END PRIVATE KEY-----"
},
"token": "your-jwt-token-here",
"mediaUserToken": "your-media-user-token-here",
// optional: Origin header for API requests (required when using a browser token)
"origin": "https://music.apple.com",
// optional: polling interval in seconds (default 60)
"interval": 60
},
"options": {
"logDiff": true,
"recoverUnchangedTopHistory": true,
"normalizeAlbum": true
}
}
]
+2 -1
View File
@@ -2,7 +2,8 @@
{
"type": "azuracast",
"enable": true,
"name": "azura",
"name": "Cool Station",
"id": "myAzura",
"data": {
"url": "ws://192.168.0.101",
"station": "my-station-name",
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyCast",
"name": "My Cast",
"id": "myCast",
"data": {
"blacklistedDevices": [],
"whitelistedDevices": [],
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "DeezerARL",
"name": "Family Account",
"id": "myDeezerId",
"enable": true,
"clients": [],
"data": {
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"name": "MS",
"name": "Desktop",
"enable": true,
"id": "myDiscordLocal",
"data": {
"applicationId": "8190211179716453570",
"ipcLocations": ["192.168.0.105:6655"]
+9 -9
View File
@@ -1,11 +1,11 @@
[
{
"name": "MS",
"enable": true,
"data": {
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
"applicationId": "8190211179716453570"
}
{
"name": "Discord",
"enable": true,
"id": "myDiscordRemote",
"data": {
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
"applicationId": "8190211179716453570"
}
]
}
]
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"name": "myLfm",
"name": "LFM For App",
"enable": true,
"id": "myLfmApp",
"data": {
"slug": "mySlug"
}
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "myLz",
"name": "LZ for Navidrome",
"id": "myLzNavidrome",
"enable": true,
"data": {
"token": "myToken"
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"name": "myIcy",
"name": "Station 1",
"enable": true,
"id": "myIceStation1",
"data": {
"url": "http://myIcecast.com/radio"
}
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"name": "MyJellyfin",
"name": "Cool JF Server",
"enable": true,
"id": "myJellyfin",
"clients": [],
"data": {
"url": "http://localhost:8096",
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyJriver",
"name": "My JRiver Server",
"id": "myRiver",
"enable": true,
"data": {
"url": "0.0.0.0",
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyKodi",
"name": "My Kodi Server",
"id": "myKodi",
"enable": true,
"data": {
"url": "http://localhost:8080/jsonrpc",
+5 -3
View File
@@ -1,6 +1,7 @@
[
{
"name": "koito-client",
{
"name": "Koito Client A",
"id": "myKoitoClient",
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
@@ -9,7 +10,8 @@
}
},
{
"name": "koito-source",
"name": "Koito Source B",
"id": "myKoitoSource",
"configureAs": "source",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
+4 -2
View File
@@ -1,6 +1,7 @@
[
{
"name": "myLastFmClient",
"name": "Foxx LFM Client",
"id": "myLastFmClient",
"enable": true,
"configureAs": "client",
"data": {
@@ -10,7 +11,8 @@
}
},
{
"name": "myLastFmSource",
"name": "Foxx LFM Source",
"id": "myLastFmSource",
"enable": true,
"configureAs": "source",
"data": {
+4 -2
View File
@@ -1,6 +1,7 @@
[
{
"name": "myLibreFmClient",
"name": "Foxx Libre Client",
"id": "myLibreFmClient",
"enable": true,
"configureAs": "client",
"data": {
@@ -10,7 +11,8 @@
}
},
{
"name": "myLibreFmSource",
"name": "Foxx Libre Source",
"id": "myLibreFmSource",
"enable": true,
"configureAs": "source",
"data": {
+4 -2
View File
@@ -1,6 +1,7 @@
[
{
"name": "brainzClient",
"name": "Foxx LZ Client",
"id": "brainzClient",
"enable": true,
"configureAs": "client",
"data": {
@@ -10,7 +11,8 @@
}
},
{
"name": "brainzSource",
"name": "Foxx LZ Source",
"id": "brainzSource",
"enable": true,
"configureAs": "source",
"data": {
+4 -2
View File
@@ -1,6 +1,7 @@
[
{
"name": "myMaloja-client",
"name": "Maloja Client",
"id": "myMalojaClient",
"enable": true,
"configureAs": "client",
"data": {
@@ -9,7 +10,8 @@
}
},
{
"name": "myMaloja-source",
"name": "Maloja Source",
"id": "myMalojaSource",
"enable": true,
"configureAs": "source",
"data": {
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyMopidy",
"name": "Mopidy Desktop",
"id": "MyMopidy",
"enable": true,
"data": {
"url": "localhost",
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"enable": true,
"name": "MyMPD",
"id": "myMPD",
"name": "Desktop",
"data": {
"url": "192.168.0.100:6600",
"password": "MY_PASSWORD"
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "ubuntu",
"name": "Desktop",
"id": "myMprisUbuntu",
"enable": true,
"data": {
"whitelist": ["vlc", "mpd"],
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "myYamaha",
"name": "Livingroom Reciever",
"id": "myYamaha",
"enable": true,
"data": {
"url": "192.168.0.101"
+2 -1
View File
@@ -2,7 +2,8 @@
{
"type": "musikcube",
"enable": true,
"name": "musikcube",
"name": "Desktop",
"id": "myMusikcube",
"data": {
"url": "ws://localhost:7905",
"password": "MY_PASSWORD"
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyPlexApi",
"name": "Foxx Plex",
"id": "MyPlexApi",
"enable": true,
"clients": [],
"data": {
+6 -4
View File
@@ -1,17 +1,19 @@
[
{
"name": "myRockskyClient",
"id": "myRockskyClient",
"name": "FoxxMD Rocksky Client",
"configureAs": "client",
"data": {
"key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6cGxjOng0NjVwZmtyeW5zaDZ...",
"handle": "alice.bsky.social"
}
},
{
"name": "myRockskySource",
"id": "myRockskySource",
"name": "FoxxMD Rocksky Source",
"configureAs": "source",
"data": {
"key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6cGxjOng0NjVwZmtyeW5zaDZ...",
"handle": "alice.bsky.social"
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"enable": true,
"name": "MySonos",
"id": "MySonos",
"name": "Boom 2",
"data": {
"host": "192.168.0.150",
"devicesAllow": ["move","beam"],
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MySpotify",
"id": "MySpotify",
"name": "Foxx Spotify",
"enable": true,
"clients": [],
"data": {
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MySubsonic",
"id": "mySubsonic",
"name": "Airsonic Server",
"enable": true,
"data": {
"url": "http://localhost:4040/airsonic",
+4 -2
View File
@@ -1,6 +1,7 @@
[
{
"name": "myTeal",
"id": "myTealClient",
"name": "Foxx Teal Client",
"configureAs": "client",
"data": {
"identifier": "alice.bsky.social",
@@ -8,7 +9,8 @@
}
},
{
"name": "myTealSource",
"id": "myTealSource",
"name": "Foxx Teal Source",
"configureAs": "source",
"data": {
"identifier": "alice.bsky.social",
+2 -1
View File
@@ -1,7 +1,8 @@
[
{
"enable": true,
"name": "MyVlc",
"id": "myVlc",
"name": "Desktop",
"data": {
"url": "localhost:8080",
"password": "changeme"
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyWebScrobbler",
"id": "MyWebScrobbler",
"name": "Firefox Laptop",
"data": {
"slug": "MyOptionalSlug",
"whitelist": [],
+1
View File
@@ -1,5 +1,6 @@
[
{
"id": "myYandex",
"name": "Yandex Music",
"type": "ymbridge",
"data": {
+2 -1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyYTMusic",
"id": "myYtMusic",
"name": "Foxx YT Music",
"enable": true,
"clients": [],
"data": {
@@ -1,33 +1,19 @@
#!/usr/bin/with-contenv bash
# used https://github.com/linuxserver/docker-plex as a template
POPULATE_EXAMPLES=false
echo "-------------------------------------"
echo -e "Setting up multi-scrobbler config directory based on CONFIG_DIR env: ${CONFIG_DIR}\n"
echo -e "Verifying multi-scrobbler directory permissions based on CONFIG_DIR env: ${CONFIG_DIR} and DATA_DIR env: ${DATA_DIR}\n"
# make config folder if it does not exist
if [ ! -d "${CONFIG_DIR}" ]; then
echo "Directory does not exist! Creating..."
POPULATE_EXAMPLES=true
mkdir -p "${CONFIG_DIR}"
else
if [ "$(ls -A ${CONFIG_DIR})" ]; then
echo "Directory is not empty, not creating examples."
else
POPULATE_EXAMPLES=true
fi
if [ -d "${CONFIG_DIR}" ]; then
echo "chown'ing config directory to ensure correct permissions."
chown -R abc:abc "${CONFIG_DIR}"
echo "Done!"
fi
# add example configs
if [ "$POPULATE_EXAMPLES" = true ]; then
echo "Directory is empty, adding examples..."
cp -r /app/config/. "${CONFIG_DIR}"/
if [ -d "${DATA_DIR}" ]; then
echo "chown'ing data directory to ensure correct permissions."
chown -R abc:abc "${DATA_DIR}"
echo "Done!"
fi
# permissions
echo "chown'ing directory to ensure correct permissions."
chown -R abc:abc "${CONFIG_DIR}"
echo "Done!"
echo -e "-------------------------------------\n"
@@ -7,4 +7,4 @@ echo -e "\nmulti-scrobbler is starting!"
export NODE_ENV=production
cd /app || exit
exec \
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/node_modules/.bin/tsx /app/src/backend/index.ts
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/src/backend/index.ts
@@ -0,0 +1,177 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs groupId="configType" queryString>
<TabItem value="file" label="File">
Using [File Config](/configuration/?configType=file#configuration-types)
```json5 title="CONFIG_DIR/jellyin.json"
[
{
"id": "fredJf",
"name": "Freds Jellyfin",
"clients": ["fredLFM"],
"data": {
"url": "https://jellyfin.fred.com",
"user": "Fred",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
},
{
"id": "maryJf",
"name": "Marys Jellyfin",
"clients": ["maryLFM"],
"data": {
"url": "https://jellyfin.mary.com",
"user": "Mary",
"apiKey": "26fe9c5dc6374542adc4f4d5e8352838"
}
}
]
```
```json5 title="CONFIG_DIR/plex.json"
[
{
"id": "foxxPlex",
"name": "Foxx Plex",
"clients": ["foxxKoi"],
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400"
}
}
]
```
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"id": "maryLFM",
"name": "Marys Lastfm",
"configureAs": "client",
"data": {
"apiKey": "maryApiKey",
"secret": "marySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
},
{
"id": "fredLFM",
"name": "Freds Lastfm",
"configureAs": "client",
"data": {
"apiKey": "fredApiKey",
"secret": "fredSecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
```
```json5 title="CONFIG_DIR/koito.json"
[
{
"id": "foxxKoi",
"name": "Foxx Koito",
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
}
]
```
</TabItem>
<TabItem value="aio" label="AIO">
Using [AIO Config](/configuration/?configType=aio#configuration-types)
```json5 title="CONFIG_DIR/config.json"
{
"sources": [
{
"type": "jellyfin",
"id": "fredJf",
"name": "Freds Jellyfin",
"clients": [
"fredLFM"
],
"data": {
"url": "https://jellyfin.fred.com",
"user": "Fred",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
}
},
{
"type": "jellyfin",
"id": "maryJf",
"name": "Marys Jellyfin",
"clients": [
"maryLFM"
],
"data": {
"url": "https://jellyfin.mary.com",
"user": "Mary",
"apiKey": "26fe9c5dc6374542adc4f4d5e8352838"
}
},
{
"type": "plex",
"id": "foxxPlex",
"name": "Foxx Plex",
"clients": [
"foxxKoi"
],
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": [
"foxx"
]
}
}
],
"clients": [
{
"type": "lastfm",
"id": "maryLFM",
"name": "Marys Lastfm",
"configureAs": "client",
"data": {
"apiKey": "maryApiKey",
"secret": "marySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
},
{
"type": "lastfm",
"id": "fredLFM",
"name": "Freds Lastfm",
"configureAs": "client",
"data": {
"apiKey": "fredApiKey",
"secret": "fredSecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
},
{
"type": "koito",
"id": "foxxKoi",
"name": "Foxx Koito",
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
}
]
}
```
</TabItem>
</Tabs>
@@ -0,0 +1,196 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
Using [ENV Config](/configuration/?configType=env#configuration-types)
```yaml title="compose.yaml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# spotify
- SPOTIFY_ID=foxxSpotify
- SPOTIFY_CLIENT_ID=H2UvGv8okrFlv2OUoc1lAtK7Xe5EGDgs
- SPOTIFY_CLIENT_SECRET=HvoFE5Ce2hdblNE6vaFBEx6dRVcDdo43
- SPOTIFY_REDIRECT_URI=https://multiscrobbler.example.com/callback
# navidrome listenbrainz endpoint
- LZE_ID=myLzNavidrome
- LZE_TOKEN=myToken
# plex
- PLEX_ID=foxxPlex
- PLEX_URL=192.168.0.233:32400
- PLEX_TOKEN=JtRnwQWD__XMJF8sT3jc
- PLEX_USERS_ALLOW=foxx
- PLEX_LIBRARIES_ALLOW=oldies
# lastfm
- LASTFM_ID=myLFM
- LASTFM_API_KEY=myApiKey
- LASTFM_SECRET=mySecret
- LASTFM_REDIRECT_URI=https://multiscrobbler.example.com/lastfm/callback
# koito
- KOITO_ID=foxxKoi
- KOITO_TOKEN=029b081ba-9156-4pe7-88e5-3be671f5ea2b
- KOITO_USER=admin
- KOITO_URL=http://192.168.0.100:4110
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
</TabItem>
<TabItem value="file" label="File">
Using [File Config](/configuration/?configType=file#configuration-types)
```json5 title="CONFIG_DIR/spotify.json"
[
{
"id": "foxxSpotify",
"name": "Foxx Spotify",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback",
"interval": 60
}
}
]
```
```json5 title="CONFIG_DIR/endpointlz.json"
[
{
"id": "myLzNavidrome",
"name": "LZ for Navidrome",
"data": {
"token": "myToken"
}
}
]
```
```json5 title="CONFIG_DIR/plex.json"
[
{
"id": "foxxPlex",
"name": "Foxx Plex",
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["foxx"],
"librariesAllow": ["oldies"],
}
}
]
```
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"id": "myLFM",
"name": "Old Lastfm",
"configureAs": "client",
"data": {
"apiKey": "myApiKey",
"secret": "mySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
```
```json5 title="CONFIG_DIR/koito.json"
[
{
"id": "foxxKoi",
"name": "Foxx Koito",
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
}
]
```
</TabItem>
<TabItem value="aio" label="AIO">
Using [AIO Config](/configuration/?configType=aio#configuration-types)
```json5 title="CONFIG_DIR/config.json"
{
"sources": [
{
"type": "spotify",
"id": "foxxSpotify",
"name": "Foxx Spotify",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback",
"interval": 60
}
},
{
"type": "endpointlz",
"name": "LZ for Navidrome",
"id": "myLzNavidrome",
"data": {
"token": "myToken"
}
},
{
"type": "plex",
"id": "foxxPlex",
"name": "Foxx Plex",
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["foxx"],
"librariesAllow": ["oldies"]
}
}
],
"clients": [
{
"type": "lastfm",
"id": "myLFM",
"name": "Old Lastfm",
"configureAs": "client",
"data": {
"apiKey": "myApiKey",
"secret": "mySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
},
{
"type": "koito",
"id": "foxxKoi",
"name": "Foxx Koito",
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
}
]
}
```
</TabItem>
</Tabs>
@@ -0,0 +1,226 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
Using [ENV Config](/configuration/?configType=env#configuration-types)
```yaml title="compose.yaml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# required setup for musicbrainz
- MB_CONTACT=contact@mydomain.com # a real email
# searches with sensible release sorting with the searchOrder
# isrc => basic => artist (native mode)
- MB_PRESETS=default,sensible,native
# spotify
- SPOTIFY_ID=foxxSpotify
- SPOTIFY_CLIENT_ID=H2UvGv8okrFlv2OUoc1lAtK7Xe5EGDgs
- SPOTIFY_CLIENT_SECRET=HvoFE5Ce2hdblNE6vaFBEx6dRVcDdo43
- SPOTIFY_REDIRECT_URI=https://multiscrobbler.example.com/callback
# navidrome listenbrainz endpoint
- LZE_ID=myLzNavidrome
- LZE_TOKEN=myToken
# applies musicbrainz Stage to preTransform of navidrome source
- LZE_TRANSFORMS=musicbrainz
# lastfm
- LASTFM_ID=myLFM
- LASTFM_API_KEY=myApiKey
- LASTFM_SECRET=mySecret
- LASTFM_REDIRECT_URI=https://multiscrobbler.example.com/lastfm/callback
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
</TabItem>
<TabItem value="file" label="File">
Using [File Config](/configuration/?configType=file#configuration-types)
For non-ENV config, [Musicbrainz Transformer](/configuration/transforms/musicbrainz/#api-setup) must be setup using the [AIO Config](/configuration/?configType=aio#configuration-types) `config.json` file. This configuration uses the [sensible defaults](/configuration/transforms/musicbrainz/#sensible-default) in the `defaults` property. This is a suggestion and can be removed if not desired.
```json5 title="CONFIG_DIR/config.json"
{
"transformers": [
{
"type": "musicbrainz",
"name": "MyMB",
"data": {
"apis": [
{
"contact": "contact@mydomain.com"
}
]
},
"defaults": {
"releaseStatusPriority": ["official"],
"releaseGroupPrimaryTypePriority": [
"album",
"single",
"ep"
],
"releaseCountryPriority": ["XW"],
"searchArtistMethod": "native",
"searchOrder": [
"isrc",
"basic",
"artist"
]
}
}
]
}
```
```json5 title="CONFIG_DIR/spotify.json"
[
{
"id": "foxxSpotify",
"name": "Foxx Spotify",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback",
"interval": 60
}
}
]
```
```json5 title="CONFIG_DIR/endpointlz.json"
[
{
"id": "myLzNavidrome",
"name": "LZ for Navidrome",
"data": {
"token": "myToken"
},
"options": {
"playTransform": {
"preCompare": [
{
"type": "musicbrainz",
"name": "MyMB"
}
]
}
}
}
]
```
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"id": "myLFM",
"name": "Old Lastfm",
"configureAs": "client",
"data": {
"apiKey": "myApiKey",
"secret": "mySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
```
</TabItem>
<TabItem value="aio" label="AIO">
Using [AIO Config](/configuration/?configType=aio#configuration-types)
This configuration uses the [sensible defaults](/configuration/transforms/musicbrainz/#sensible-default) in the `defaults` property. This is a suggestion and can be removed if not desired.
```json5 title="CONFIG_DIR/config.json"
{
"transformers": [
{
"type": "musicbrainz",
"name": "MyMB",
"data": {
"apis": [
{
"contact": "contact@mydomain.com"
}
]
},
"defaults": {
"releaseStatusPriority": ["official"],
"releaseGroupPrimaryTypePriority": [
"album",
"single",
"ep"
],
"releaseCountryPriority": ["XW"],
"searchArtistMethod": "native",
"searchOrder": [
"isrc",
"basic",
"artist"
]
}
}
],
"sources": [
{
"type": "spotify",
"id": "foxxSpotify",
"name": "Foxx Spotify",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback",
"interval": 60
}
},
{
"type": "endpointlz",
"name": "LZ for Navidrome",
"id": "myLzNavidrome",
"data": {
"token": "myToken"
},
"options": {
"playTransform": {
"preCompare": [
{
"type": "musicbrainz",
"name": "MyMB"
}
]
}
}
}
],
"clients": [
{
"type": "lastfm",
"id": "myLFM",
"name": "Old Lastfm",
"configureAs": "client",
"data": {
"apiKey": "myApiKey",
"secret": "mySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
}
```
</TabItem>
</Tabs>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

@@ -22,7 +22,7 @@ Check the [main Discord docs page](/configuration/clients/discord) for a compari
<DetailsAdmo type="note" summary="How MS Uses Discord API">
Multi-scrobbler does its upmost to implement, to-spec, [Gateway API communication](https://docs.discord.com/developers/events/gateway) and respect usage. It requires no [permissions](https://docs.discord.com/developers/events/gateway#gateway-intents) and reads no data other than monitoring what other user sessions are active.
Multi-scrobbler does its utmost to implement, to-spec, [Gateway API communication](https://docs.discord.com/developers/events/gateway) and respect usage. It requires no [permissions](https://docs.discord.com/developers/events/gateway#gateway-intents) and reads no data other than monitoring what other user sessions are active.
Aside from the on-paper violation of using a user token programmatically (on your behalf), there is no misuse of the API to achieve rich presence.
@@ -54,7 +54,9 @@ For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLO
<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>
@@ -16,6 +16,20 @@ import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
:::
<DetailsAdmo type="warning" summary="Limits for Old Scrobbles">
Last.fm will not accept scrobbles that are timestamped as **older than 2 weeks** from the current time.
This is not officially documented but has been confirmed by multiple people/mods in the last.fm support forums: [[1]](https://support.last.fm/t/importing-listening-history-spotify-apple-music-and-itunes-wmp/1424/3) [[2]](https://support.last.fm/t/suggestion-allow-last-fm-pro-members-to-use-the-api-to-submit-scrobbles-older-than-2-weeks/69190/2) [[3]](https://support.last.fm/t/how-to-scrobble-complete-spotify-history/44558)
Multi-scrobbler can queue scrobbles for you if the Last.fm service is unavailable, or if there is an issue with a specific scrobble. However, you are responsible for making sure these scrobbles are eventually sent to Last.fm (by retrying in the UI or retarting Last.fm client in UI) before the two week window has passed or else they will not be scrobblable *at all* unless the timestamp is changed.
A future update to Multi-scrobbler will enable editing of unsent scrobble data and modifying the timestamp from the UI.
If you have issues with specific scrobbles not being sent and retries/restarts do not fix it, please make sure to [create a new issue](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=01-bug-report.yml) in a timely manner so that it can be resolved before the window has passed.
</DetailsAdmo>
You will need a [Last.fm](https://www.last.fm) account to use the Last.fm API.
After creating your account, [register for an API account here.](https://www.last.fm/api/account/create)
@@ -51,8 +65,10 @@ The Redirect URL can be explicitly specified in the configuration below, or by u
<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>
@@ -15,9 +15,9 @@ import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
:::
To use the official [Libre.fm](https://libre.fm/) instance you will need an account. Or, you can run your own Libre.fm instance [using the legacy source code](https://github.com/foocorp/gnu-fm).
To use the official [Libre.fm](https://libre.fm/) website you will need an account. Or, you can run your own GNU FM instance [using the source code](https://github.com/foocorp/gnu-fm).
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is currently needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
### Callback/Redirect URL
@@ -56,10 +56,12 @@ https://my.cool.libre.com/2.0/
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client name="librefm">
| Environmental Variable | Required? | Default | Description |
| ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
| `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/` |
</Config>
| `LIBREFM_NAME` | No | | A vanity name different than ID |
</Config>
@@ -25,7 +25,9 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
<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>
@@ -38,8 +38,10 @@ 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_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
| 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>
+27 -14
View File
@@ -13,7 +13,7 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
:::
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol and your your [**Atmosphere Account**](https://atmosphereaccount.com/).
<details>
@@ -23,13 +23,13 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
</details>
<DetailsAdmo type="warning" summary="Your Data Is Public">
<DetailsAdmo type="important" summary="Your Data Is Public">
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
</DetailsAdmo>
<DetailsAdmo type="warning" summary="No Scrobbling Guarantee">
<DetailsAdmo type="important" summary="No Scrobbling Guarantee">
Rocksky **does not guarantee** that every scrobble you send will be persisted. Rocksky's design goal is to store scrobbles with **metdata-complete, 3rd party verifiable track information** so that it can be a metadata-rich discovery platform, rather than an exhaustive scrobble data source-of-truth.
@@ -41,22 +41,16 @@ If you need your scrobbles to be 100% captured then you should scrobble to an *a
## Setup
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
You will need an [**Atmosphere Account**](https://atmosphereaccount.com/) to use Rocksky.
:::note
Usage is adapted from [Rocksky docs](https://docs.rocksky.app/migrating-to-rocksky-scrobble-api-957839m0) on [docs.rocksky.app](https://docs.rocksky.app)
:::
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Bluesky account
* Then, navigate to the [RockSky developer dashboard](https://rocksky.app/apikeys) and obtain an API Key
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Atmosphere Account
* Then, navigate to the [Access Tokens](https://rocksky.app/access-tokens) in the Rocksky settings dropdown, and create a new token
## Configuration
### Handle
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
The handle used with multi-scrobbler should be your **full** [**Atmosphere Account**](https://atmosphereaccount.com/) handle, including TLD.
* For regular Bluesky account this will be like: `alice.bsky.social`
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
@@ -64,9 +58,28 @@ The handle used with multi-scrobbler should be your **full** ATProto handle, inc
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
<DetailsAdmo type="warning" summary="API Applications Deprecated">
In < 0.14.1, Multi-Scrobber used Rocksky's API Appication "API Key" for communication with a Listenbrainz-compatible API from Rocksky.
Going forward, this should be replaced with the official guidance in the [Setup](#setup) section above: use an **Access Token**. This uses the native Rocksky API which has more functionality.
If your configuration is using either
* (File/AIO) `'key': ...`
* (ENV) `ROCKSKY_KEY`
you should redo the [Setup](#setup) section and modify your configuration to use an Access Token instead.
Keys are now **deprecated** and functionality for them will be removed in a future version of Multi-Scrobbler.
</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_KEY` | Yes | | API Key obtained from Rocksky developer dashboard |
| `ROCKSKY_TOKEN` | Yes | | Access Token obtained from Rocksky |
| `ROCKSKY_NAME` | No | | A vanity name different than ID |
</Config>
+12 -3
View File
@@ -16,7 +16,7 @@ To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (So
:::
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
[**teal.fm**](https://teal.fm/) is a social discovery and browsing app for scrobbles stored in your [**Atmosphere Account**](https://atmosphereaccount.com/).
<details>
@@ -46,6 +46,13 @@ To view your teal.fm scrobble data you can:
## Setup
:::note
Multi-scrobbler will work with *any* Atmosphere Account, not just Bluesky. See [Configuration](#configuration) below for provider-agnostic config.
:::
#### Bluesky
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
@@ -57,10 +64,10 @@ To view your teal.fm scrobble data you can:
### Identifier
The ATPRoto identifier used with multi-scrobbler should be either:
The ATProto identifier used with multi-scrobbler should be either:
* A valid [DID](https://atproto.com/specs/did#at-protocol-did-identifier-syntax), starting with `did:plc:...` or `did:web:...`
* Your **full** ATProto handle, including TLD
* Your **full** [Atmosphere Account](https://atmosphereaccount.com/) ATProto handle, including TLD
If using a handle:
@@ -73,6 +80,8 @@ If your identifier does not look like a DID and does not include a TLD then mult
<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>
@@ -440,6 +440,273 @@ Example
</TabItem>
</Tabs>
## Database
Multi-scrobbler depends on a SQLite database (`ms.db`) that is created on first run and stored in the [`CONFIG_DIR`](/installation/?dockerSetting=storage#recommended-settings). When upgrading Multi-scrobbler version, if there are any required database changes than this database is [automatically backed up and migrated.](/updating#database)
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.
### Retention
<DetailsAdmo status="note" summary="How much storage does the database use?">
The amount of data stored for each Play can widely vary based on a few factors:
* how much data the Source service exposes
* how many clients each Source is scrobble to (Plays are duplicated for each Client a Source sends a scrobble to)
* if you are using any [Transforms](/configuration/transforms) the diff of each step is stored, along with any external request/response data (like [Musicbrainz queries](/configuration/transforms/musicbrainz))
The [MS repository contains a benchmark](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/database/drizzle.test.ts#L551) to measure an average database size in a few common scenarios.
<Tabs groupId="dbSize">
<TabItem label="Minimal" value="minimal">
Assuming your Sources send a minimal amount of data or you have compacted all plays:
| Play Count | DB Size |
| ---------- | -------- |
| 100 | `160kb` |
| 1000 | `1MB` |
| 10k | `10.2MB` |
</TabItem>
<TabItem label="With Original Input" value="input">
Assuming your Sources have a non-trivial amount of input data (like Spotify or Listenbrainz) that is not compacted:
| Play Count | DB Size |
| ---------- | -------- |
| 100 | `356kb` |
| 1000 | `3MB` |
| 10k | `29.6MB` |
</TabItem>
<TabItem label="All Debug Deta" value="all">
Assuming your Sources/Clients:
* have a non-trivial amount of input data (like Spotify or Listenbrainz)
* and has many [Transforms](/configuration/transforms) steps that include requests
* nothing is compacted
| Play Count | DB Size |
| ---------- | -------- |
| 100 | `684kb` |
| 1000 | `6.28MB` |
| 10k | `61.3MB` |
</TabItem>
</Tabs>
</DetailsAdmo>
A retention policy can be configured to delete Plays, or unused debug data, from the database after a certain amount of time. If no configuration is provided then a default policy is used that should be reasonable for most users.
<Tabs groupId="policy" queryString>
<TabItem label="Compaction" value="compaction">
The **Compaction** Retention Policy is used to delete different types of debug data from your stored Plays.
This is a useful way to reduce used storage space when you are not having problems with your Plays, or iterating on a configuration, that requires referencing all this extra data.
There are two types of data that can be compacted (deleted from the Play):
* `input` - this is the untouched data retrieved by Multi-scrobbler, from a Source, and used to generate a Play/scrobble. This can be used to reconstruct and replay a Play, when used from troubleshooting or reporting an issue
* `transform` - this is all of the steps generated by [transforms](/configuration/transforms), the diff of the Play resulting from the step, and any request/responses used to complete the step
:::note[Defaults]
When no Compact configuration is provided, Multi-scrobbler uses this policy:
* Compact (delete) `input` and `transform` data on all Plays after 3 days
:::
#### Configuring Compaction Policy
<details>
<summary>Details</summary>
Each value in the configuration properties below can be either
* a number of seconds EX `3600` = 10 minutes
* a unit of a common duration with the pattern `X unit` EX
* `30 minutes`
* `5 hours`
* `2 days`
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
* `COMPACT_PROPERTIES` - which properties to compact
* `RETENTION_COMPACT_AFTER` - Default to use for all Plays
* `RETENTION_COMPACT_COMPLETED_AFTER` - Compact only completed Plays after...
* `RETENTION_COMPACT_FAILED_AFTER` - Compact only failed Plays after...
* `RETENTION_COMPACT_DUPED_AFTER` - Compact only duped/discard Plays after...
Example
```ini
# only delete input when compacting
COMPACT_PROPERTIES=input
# compact all plays after 3 days
RETENTION_COMPACT_AFTER=3 days
# specifically compact completed plays after 30 minutes
RETENTION_COMPACT_COMPLETED_AFTER=30 minutes
```
</TabItem>
<TabItem value="aio" label="AIO">
Compacting all Play types and deleting both input and transform:
```json title="config.json"
{
"database": {
"retention": {
"compactAfter": "3 days",
"compact": [
"input",
"transform"
]
}
}
}
```
* Delete only input during compacting
* Compact all after 3 days except completed which compacts after 30 minutes
```json title="config.json"
{
"database": {
"retention": {
"compactAfter": {
"completed": "30 minutes",
"duped": "3 days",
"failed": "3 days"
},
"compact": [
"input"
]
}
}
}
```
</TabItem>
</Tabs>
</details>
</TabItem>
<TabItem label="Deletion" value="deletion">
The **Deletion** Retention Policy is used to delete different types of stored Plays from Multi-scrobbler database.
**This does not delete Plays from your Clients.** It's only deleting the "in-flight" data MS used to create the scrobble that was eventually sent to your clients.
:::warning[Plays Should be Ephemeral]
**Multi-Scrobbler is not designed to store Plays/Scrobbles indefinitely.**
It should scale fine for thousands of scrobbles but it not meant to store 10's of thousands of scrobbles forever. It is not a scrobbler server.
You **should** set a reasonable deletion policy so that MS stores less than 1000 scrobbles at a time, ideally less.
:::
:::note[Defaults]
When no Deletion policy configuration is provided, Multi-scrobbler uses this policy:
* Delete all Plays after 7 days
:::
#### Configuring Deletion Policy
<details>
<summary>Details</summary>
Each value in the configuration properties below can be either
* a number of seconds EX `3600` = 10 minutes
* a unit of a common duration with the pattern `X unit` EX
* `30 minutes`
* `5 hours`
* `2 days`
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
* `RETENTION_DELETE_AFTER` - Default to use for all Plays
* `RETENTION_DELETE_COMPLETED_AFTER` - Delete only completed Plays after...
* `RETENTION_DELETE_FAILED_AFTER` - Delete only failed Plays after...
* `RETENTION_DELETE_DUPED_AFTER` - Delete only duped/discard Plays after...
Example
```ini
# delete all plays after 3 days
RETENTION_DELETE_AFTER=3 days
# specifically, delete completed plays after 30 minutes
RETENTIOND_DELETE_COMPLETED_AFTER=30 minutes
```
</TabItem>
<TabItem value="aio" label="AIO">
Deleting all Play types after 3 days:
```json title="config.json"
{
"database": {
"retention": {
"deleteAfter": "3 days"
}
}
}
```
* Delete all after 3 days except completed which are deleted after 30 minutes
```json title="config.json"
{
"database": {
"retention": {
"deleteAfter": {
"completed": "30 minutes",
"duped": "3 days",
"failed": "3 days"
},
}
}
}
```
</TabItem>
</Tabs>
</details>
</TabItem>
</Tabs>
## Debug Mode
+75 -299
View File
@@ -1,317 +1,93 @@
---
sidebar_position: 5
title: Kitchen Sink
description: Kitchen Sink Example
description: Kitchen Sink Examples
---
# Example Complex Scenario
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import MultiUsecase from "@site/src/components/snippets/_scenario-config-multi.mdx"
import MultiPersonsServices from "@site/docs/configuration/_kitchensink_configs/_multi-persons-services.mdx"
import MultiSourcesClients from "@site/docs/configuration/_kitchensink_configs/_multi-sources-clients.mdx"
import Musicbrainz from "@site/docs/configuration/_kitchensink_configs/_musicbrainz.mdx"
import SingleUsecase from "@site/src/components/snippets/_scenario-config-single.mdx"
Scenario:
## What's This?
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
* Each person has their own [Maloja](/configuration/clients/maloja) server
* Each person has their own Spotify account
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
* You have your own [Youtube Music](/configuration/sources/youtube-music) account you want to scrobble from
* Mary has her own [Last.fm](/configuration/clients/lastfm) account she also wants to scrobble to
* Fred has his own [Spotify](/configuration/sources/spotify) application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
* Fred has a Plex server and wants to scrobble everything he plays
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
* Fred also has his own [Jellyfin server](/configuration/sources/jellyfin) and wants to scrobble everything he plays
* You have an android music app that can scrobble to a [custom listenbrainz server](/configuration/sources/listenbrainz-endpoint)
On this page you find contrived, complex usecases for multi-scrobbler that provide examples of equivalent [configuration types](/configuration#configuration-types) to fulfill that usecase.
### All-in-one Config
These example are useful for:
```json5 title="CONFIG_DIR/config.json"
{
"sourceDefaults": {
"maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config
"maxRequestRetries": 1, // optional, default # of http request retries a source can make before error is thrown. can be overridden by property in individual config
"retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config
},
"clientDefaults": {
"maxRequestRetries": 1, // optional, default # of http request retries a client can make before error is thrown. can be overridden by property in individual config
"retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config
},
"sources": [
{
"type": "spotify",
"name": "foxxSpot",
"clients": ["foxxMaloja"],
"data": {
"clientId": "foxxSpotifyAppId",
"clientSecret": "foxxSpotifyAppSecret",
},
"options": {
"maxRequestRetries": 2, // override default max retries because spotify can...spotty
}
},
{
"type": "spotify",
"name": "marySpot",
"clients": ["maryMaloja"],
"data": {
"clientId": "foxxSpotifyAppId", // only need one application, it can be used by all users of this multi-scrobbler instance
"clientSecret": "foxxSpotifyAppSecret",
}
},
{
"type": "spotify",
"name": "fredSpot",
"clients": ["fredMaloja"],
"data": {
"accessToken": "fredsToken",
"refreshToken": "fredsRefreshToken",
"interval": 120, // he also wants a slower check interval because his application already has heavy api usage
}
},
{
"type": "plex",
"name": "fredPlex",
"clients": ["fredMaloja"],
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["fredUser"]
}
},
{
"type": "plex",
"name": "maryPlex",
"clients": ["maryMaloja"],
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["maryUser"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
"librariesAllow": ["podcasts"]
}
},
{
"type": "plex",
"name": "partyPlex",
// omitting clients (or making it empty) will make this Source scrobble to all Clients
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"librariesAllow": ["party"]
}
},
{
"type": "jellyfin",
"name": "FredJelly",
// omitting clients (or making it empty) will make this Source scrobble to all Clients
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
"usersAllow": ["fredUser"]
}
},
{
"type": "subsonic",
"name": "foxxAirsonic",
"clients": ["foxxMaloja"],
"data": {
"user": "foxx",
"password": "foxxPassword",
"url": "https://airsonic.foxx.example"
}
},
{
"type": "ytmusic",
"name": "foxxYoutube",
"clients": ["foxxMaloja"],
"data": {
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
}
},
{
"type": "endpointlz",
"name": "listenbrainzfoxx",
"clients": ["foxxMaloja"],
"data": {
"token": "myToken"
}
}
],
"clients": [
{
"type": "maloja",
"name": "foxxMaloja",
"data": {
"url": "https://maloja.foxx.example",
"apiKey": "foxxApiKey"
}
},
{
"type": "maloja",
"name": "fredMaloja",
"data": {
"url": "https://maloja.fred.example",
"apiKey": "fredApiKey"
}
},
{
"type": "maloja",
"name": "maryMaloja",
"data": {
"url": "https://maloja.mary.example",
"apiKey": "maryApiKey"
}
},
{
"type": "lastfm",
"name": "maryLFM",
"data": {
"apiKey": "maryApiKey",
"secret": "marySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
}
* getting an idea of what you can do with Multi-scrobbler
* using these examples as a starting point for your own configuration
* providing an example of the same configuration across different [configuration types](/configuration#configuration-types)
```
:::tip
### Separate JSON files
More common, straightforward scenarios can be found in the [**quickstart**](/quickstart) guide.
```json5 title="CONFIG_DIR/spotify.json"
[
{
// may omit 'type' property since app knows this is file is for spotify configs
"name": "foxxSpot",
"clients": ["foxxMaloja"],
"data": {
"clientId": "foxxSpotifyAppId",
"clientSecret": "foxxSpotifyAppSecret"
}
},
{
"name": "marySpot",
"clients": ["maryMaloja"],
"data": {
"clientId": "foxxSpotifyAppId",
"clientSecret": "foxxSpotifyAppSecret"
}
},
{
"name": "fredSpot",
"clients": ["fredMaloja"],
"data": {
"accessToken": "fredsToken",
"refreshToken": "fredsRefreshToken",
"interval": 120
}
},
]
```
:::
```json5 title="CONFIG_DIR/plex.json"
[
{
"name": "fredPlex",
"clients": ["fredMaloja"],
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["fredUser"]
}
},
{
"name": "maryPlex",
"clients": ["maryMaloja"],
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"usersallow": ["maryUser"],
"librariesAllow": ["podcasts"]
}
},
{
"name": "partyPlex",
"data": {
"token": "JtRnwQWD__XMJF8sT3jc",
"url": "192.168.0.233:32400",
"librariesAllow": ["party"]
}
}
]
```
## Examples
```json5 title="CONFIG_DIR/jellyfin.json"
[
{
"name": "FredJelly",
"data": {
"url": "http://localhost:8096",
"user": "FoxxMD",
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
"usersAllow": ["fredUser"]
}
}
]
```
### Multiple Sources to Multiple Clients {#multi-sources-clients}
```json5 title="CONFIG_DIR/ytmusic.json"
[
{
"name": "foxxYoutube",
"clients": ["foxxMaloja"],
"data": {
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
}
}
]
```
```json5 title="CONFIG_DIR/endpointlz.json"
[
{
"name": "listenbrainzfoxx",
"clients": ["foxxMaloja"],
"data": {
"token": "myToken",
}
}
]
```
You are the only user of Multi-scrobbler. You have many Sources you listen to and want to scrobble them to multiple services.
```json5 title="CONFIG_DIR/maloja.json"
[
{
"name": "foxxMaloja",
"data": {
"url": "https://maloja.foxx.example",
"apiKey": "foxxApiKey"
}
},
{
"name": "fredMaloja",
"data": {
"url": "https://maloja.fred.example",
"apiKey": "fredApiKey"
}
},
{
"name": "maryMaloja",
"data": {
"url": "https://maloja.mary.example",
"apiKey": "maryApiKey"
}
}
]
```
* You want to **scrobble from...**
* A [Spotify](/configuration/sources/spotify) account
* A Navidrome instance that you want to use with the built-in [listenbrainz](/configuration/sources/listenbrainz-endpoint) scrobble function
* A shared [Plex](/configuration/sources/plex) instance that you want to scrobble only from a specific library named `oldies`
* You want to **scrobble to...**
* Your decades-old [Last.fm](/configuration/clients/lastfm) account
* A [Koito](/configuration/clients/koito) server for backup
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"name": "maryLFM",
"data": {
"apiKey": "maryApiKey",
"secret": "marySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
```
<details>
<summary>Configuration</summary>
<MultiSourcesClients/>
</details>
### Scrobble for Multiple People to Multiple Clients {#multi-persons-services}
<MultiUsecase/>
You want to scrobble plays for yourself (Foxx), Fred, and Mary:
* The three of you want to **scrobble from...**
* Fred and Mary each have their own [Jellyfin servers](/configuration/sources/jellyfin)
* You have a [Plex](/configuration/sources/plex) server
* The three of you want to **scrobble to...**
* Fred and Mary each have their own [Last.fm](/configuration/clients/lastfm) accounts
* You have a [Koito](/configuration/clients/koito) server
<details>
<summary>Configuration</summary>
<MultiPersonsServices/>
</details>
### Use Musicbrainz to Fix Scrobbles from Navidrome
You have multiple Sources of music but your personal, on-filesystem music collection is not well organized.
You want to use the [Musicbrainz Transformer](/configuration/transforms/musicbrainz) to fix any scrobbles that use Navidrome, where your personal collection is located, so that the scrobble artists/album/title are all standardized.
* You want to **scrobble from...**
* A [Spotify](/configuration/sources/spotify) account
* A Navidrome instance that you want to use with the built-in [listenbrainz](/configuration/sources/listenbrainz-endpoint) scrobble function
* Plays from this Source should be fixed by Musicbrainz before going to your scrobble Clients
* You want to **scrobble to...**
* A [Last.fm](/configuration/clients/lastfm) account
<details>
<summary>Configuration</summary>
<Musicbrainz/>
</details>
@@ -0,0 +1,262 @@
---
title: Apple Music
toc_min_heading_level: 2
toc_max_heading_level: 5
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import AppleMusicConfig from '!!raw-loader!@site/../config/applemusic.json.example';
This Source **monitors your Apple Music listening history** via the official Apple Music API and scrobbles new activity to your configured [Clients](/configuration/clients).
Because of how the Apple Music API works, Multi-Scrobbler (MS) requires two different tokens to function: a **Media User Token** (identifies your personal account) and an **Authentication Token** (authorizes API access).
### 1. Getting a Media User Token
The `mediaUserToken` is required to access your recently played history. You can extract it directly from the Apple Music web app:
1. Visit [music.apple.com](https://music.apple.com) in your browser and log in.
2. Open your browser's Developer Tools (usually `F12` or <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>).
3. Navigate to the **Application** tab (Chrome/Edge) or **Storage** tab (Safari/Firefox).
4. Expand **Cookies** in the left sidebar and select `https://music.apple.com`.
5. Find the cookie named `media-user-token` and copy its value (it typically starts with `0.`).
:::caution[Token Expiry]
The `mediaUserToken` will eventually expire. If Multi-Scrobbler begins throwing authentication errors in the logs, simply repeat these steps to obtain and configure a fresh token.
:::
### 2. Authentication
To authorize API access, Multi-Scrobbler requires an Authentication Token (JWT). Because the official Apple Music API is designed for developers, there are two ways to provide this token depending on your situation:
* **Browser Token (Free & Most Common):** Best for 99% of users. You can easily extract a temporary token from the Apple Music web player without needing an Apple Developer account.
* **MusicKit Key (Requires Paid Apple Developer Account):** If you happen to be enrolled in the paid Apple Developer Program ($99/year), you can provide a private key to automatically generate permanent tokens.
Choose your preferred method below:
<Tabs groupId="appleMusicAuth" queryString>
<TabItem value="token" label="Browser Token (Free)">
Since most users don't have a paid developer account, extracting the token from the web player is the standard approach.
Behind the scenes, the Apple Music website makes a lot of background requests, which can make finding the right token confusing. To filter out the noise and grab the exact token we need, follow the steps for your browser below:
<details>
<summary>Chrome, Edge & Firefox</summary>
1. Visit [music.apple.com](https://music.apple.com) and log in.
2. Open Developer Tools (`F12` or `Ctrl+Shift+I` / <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd>) and navigate to the **Network** tab.
3. In the filter box at the top left, paste exactly: `https://amp-api.music.apple.com/v1/me/account` (**Label 1**).
4. Click the **Fetch/XHR** filter button to hide irrelevant requests (**Label 2**).
5. Reload the page (**Label 3**).
6. Under the "Name" column, click the request starting with `account?meta=` (**Label 4**).
7. In the panel that opens, scroll down to the **Request Headers** section.
8. Find the `authorization` header. Copy the long string of text **after** the `Bearer ` prefix (**Label 5**).
![Chrome Network Tab Instructions](../applemusic_chrome_devtools.png)
</details>
<details>
<summary>Safari</summary>
Before you can extract the token in Safari, you must enable developer tools.
{/* This weird formatting is because ⌘ is usually incredibly tiny */}
1. Open Safari Settings (<kbd>⌘</kbd> + <kbd>,</kbd>), navigate to the **Advanced** tab, and check **"Show features for web developers"** at the very bottom.
![Safari Advanced Settings](../applemusic_safari_settings.png)
2. Visit [music.apple.com](https://music.apple.com) and log in.
3. Open the Web Inspector (<kbd>⌘</kbd>+<kbd>⌥</kbd>+<kbd>I</kbd> or Develop > Show Web Inspector) and navigate to the **Network** tab.
4. In the filter bar on the left side of the inspector, paste exactly: `https://amp-api.music.apple.com/v1/me/account` (**Label 1**).
5. Reload the page (**Label 2**).
6. Click the `account` request that appears in the Name list (**Label 3**).
7. In the details sidebar, look under the **Request** section for the `Authorization` header. Copy the long string of text **after** the `Bearer ` prefix (**Label 4**).
![Safari Network Tab Instructions](../applemusic_safari_devtools.png)
</details>
<br />
Once you have your token, add it to your configuration file along with the origin header:
```json title="applemusic.json"
{
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIm...",
"mediaUserToken": "your-media-user-token-here",
"origin": "https://music.apple.com"
}
}
```
:::info[Origin Header Required]
When using a JWT extracted from the browser, Apple requires the request to match the domain it was issued to. You **must** include the `origin` field in your config as shown above.
:::
*Note: Browser-generated JWTs are valid for a maximum of 35 days and must be updated manually when they expire.*
</TabItem>
<TabItem value="key" label="MusicKit Key (Paid Account)">
:::warning[Apple Developer Account Required]
To generate JWTs automatically, you must be enrolled in the paid [Apple Developer Program](https://developer.apple.com/programs/) ($99/year) and create a MusicKit key. If you don't have an account, use the **Browser Token (Free)** tab instead.
:::
To generate JWTs automatically and avoid manual token refreshes, you need an Apple Music API key:
1. Go to the [Apple Developer portal](https://developer.apple.com/account/resources/authkeys/list) and create a **MusicKit** key.
2. Download the `.p8` file — this is your private key.
3. Note your **Key ID** and **Team ID** (found in your Apple Developer account Membership details).
Add these details to your config:
```json title="applemusic.json"
{
"data": {
"key": {
"id": "2HPSNJZ88N",
"teamId": "SN6YASW8G4",
"p8": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEG...-----END PRIVATE KEY-----"
},
"mediaUserToken": "your-media-user-token-here"
}
}
```
:::tip
When pasting the contents of your `.p8` file into JSON, make sure to replace physical line breaks with `\n` so it remains a valid, single-line JSON string.
:::
</TabItem>
</Tabs>
---
### How Multi-Scrobbler handles Apple Music quirks
#### Timestamp Estimation
The Apple Music API **does not provide timestamps** for when tracks were played. Multi-Scrobbler estimates play times by taking the current time and subtracting track durations backwards:
* The most recent track is assumed to have finished playing **now**.
* Each older track is estimated to have played `duration` seconds before the previous one.
*For the most accurate scrobble timestamps, it is highly recommended to keep the polling interval (`APPLEMUSIC_INTERVAL`) low (the default is 60 seconds).*
#### Duplicate Play Recovery
The Apple Music history API strictly deduplicates tracks. Instead of showing the same song multiple times in a row, it simply bumps the re-played track back to the #1 spot and removes the older entry.
For example, if you listen to **Song A** → **Song B**, your history is `[Song B, Song A]`. If you then listen to **Song A** again, the API returns `[Song A, Song B]` (Song A was bumped to the top).
If Multi-Scrobbler polled when the top track was Song A, and polls again after it's bumped back to the top, the top track appears functionally unchanged. This usually confuses standard scrobblers into ignoring the update entirely. By default, MS correctly detects this "top-rebound" pattern, extracts the intermediate tracks that were squeezed in (Song B), and scrobbles Song A again as a re-listen.
*Note: Because of this strict deduplication, **"pure loops"** (listening to Song A continuously on repeat without playing anything else in between) cannot be tracked. The history list never changes shape, so MS has no way of knowing it was played again.*
If you ever experience false positives (tracks being scrobbled that you didn't actually re-listen to), you can disable this recovery behavior with `"recoverUnchangedTopHistory": false` in your config options or by setting the `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY=false` env var.
#### Album name normalization
The Apple Music API appends ` - EP` or ` - Single` to the album name for EPs and singles. These suffixes are *not* part of the official names of the album and can cause issues when trying to scrobble or match metadata.
By default, Multi-Scrobbler strips these suffixes out so only the official name is used.
If you do not want this behavior it can be disabled with ENV `APPLEMUSIC_NORMALIZE_ALBUM=false` or File/AIO option `"normalizeAlbum": false`.
<DetailsAdmo type="tip" summary="Stripping Suffix with User Stage">
If you still want this stripping functionality but with more control over how it is applied you can use a [User Stage](/configuration/transforms/user).
Here is an example of creating a User Stage to only strip suffixes for a specific scrobble client:
<details>
<summary>Example</summary>
```json5 title="config.json"
{
// ...
"transformers": [
{
"type": "user",
"name": "AppleAlbumStip",
"defaults": {
"album": [
"/ - (EP|Single)$/i"
]
}
}
]
}
```
```json5 title="applemusic.json"
{
{
"id": "myAppleMusic",
"name": "My Apple Music",
"data": {
// ...
},
// highlight-start
"options": {
"normalizeAlbum": false
}
// highlight-end
}
}
```
```json5 title="lastfm.json"
[
{
"name": "Foxx LFM Client",
"id": "myLastFmClient",
"enable": true,
"configureAs": "client",
"data": {
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/lastfm/callback"
},
// highlight-start
"options": {
"playTransform": {
"preCompare": [
{
"type": "user",
"name": "AppleAlbumStip",
}
]
}
}
// highlight-end
}
]
```
</details>
</DetailsAdmo>
## 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. |
</Config>
@@ -16,9 +16,9 @@ Azuracast may not correctly report track length or position. If this is the case
:::
The Azuracast server should have **Use High-Performance Now Playing Updates** enabled in _Administration -> System Settings_
The Azuracast server should have **Use [High-Performance Now Playing Updates](https://www.azuracast.com/docs/developers/now-playing-data/#high-performance-updates)** enabled in _Administration -> System Settings_
#### URL
### URL
The URL used by MS to connect to Azuracast has the syntax:
@@ -47,13 +47,77 @@ If either is configured then MS will automatically scrobble based on when these
If neither is configured automatic scrobble defaults to **off** and you should either manually initiate it or explicitly set the default behavior.
### Station
#### Station Identifier
The station identifier multi-scrobbler uses (ENV `AZURA_STATION` or `station` in File/AIO config) needs to be the **URL Stub** or **shortcode** of the Azuracast station, **not** its display name. Use of the methods below to find an apprioriate station identifier.
<Tabs groupId="stationId" queryString>
<TabItem value="stub" label="URL Stub">
The **URL Stub** is the value used in the URL to access the station EX
```
https://azura.mydomain.com/public/my-cool-station
^^^^ url stub ^^
```
Use `my-cool-station` as the station identifier.
</TabItem>
<TabItem value="shortcode" label="Shortcode">
The **shortcode** can be found in the raw JSON response for your station. Navigate to this URL in your browser:
```
https://azura.mydomain.com/api/stations
```
Find your station in the returned data (by `name`) and then locate the value of the `shortcode` property in that object.
<details>
<summary>Example</summary>
```json
[
{
"id": 1,
"name": "My Cool Station",
"shortcode": "my-cool-station",
"description": "Re-broadcasts of my cool music",
"frontend": "icecast",
"backend": "liquidsoap",
...
```
Use `my-cool-station` as the station identifier.
</details>
</TabItem>
</Tabs>
#### Station Access
Multi-Scrobbler does not use authentication to communicate with Azuracast. Your station will likely need some form of **Public Pages** in order for you to monitor it.
In your station's **Edit Station Settings** -> **Public Pages** make sure that at least one of these is enabled:
* Enable Public Pages
* Enable Public APIs
:::note
For users who aren't administrators of Azuracast: if you can access the station using `https://azura.mydomain.com/public/xxxxxx` then it is public.
:::
## Configuration
<Config config="AzuracastSourceConfig" fileContent={AzuracastConfig} name="azuracast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
| `AZ_URL` | Yes | | Azuracast URL *without station name* |
| `AZ_STATION` | Yes | | The station name shown on the public page |
| `AZURA_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 |
</Config>
@@ -176,10 +176,12 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-----------------------------------------|-----------------------------------|
| ---------------------- | --------- | --------------------------------------- | --------------------------------- |
| `DEEZER_ID` | Yes | | A unique ID |
| `DEEZER_CLIENT_ID` | Yes | | Your **Application ID** |
| `DEEZER_CLIENT_SECRET` | Yes | | Your **Secret Key** |
| `DEEZER_REDIRECT_URI` | No | `http://localhost:9078/deezer/callback` | URI must end in `deezer/callback` |
| `DEEZER_NAME` | No | | A vanity name different than ID |
</TabItem>
<TabItem value="file" label="File">
@@ -149,11 +149,13 @@ 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_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 |
| 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 |
</Config>
@@ -64,7 +64,9 @@ 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_URL` | Yes | | Icecast URL |
| 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 |
</Config>
@@ -54,6 +54,7 @@ Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for
<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)** |
@@ -66,4 +67,5 @@ Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for
| `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 |
</Config>
@@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import JriverConfig from '!!raw-loader!@site/../config/jriver.json.example';
In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:
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:
* Tools -> Options -> Media Network
* Check `Use Media Network to share this library...`
@@ -50,9 +50,11 @@ 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 |
|------------------------|----------|---------------------------------|------------------------------------------------|
| 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 |
</Config>
+7 -5
View File
@@ -51,9 +51,11 @@ 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_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
| KODI_USERNAME | No | | The username set |
| KODI_PASSWORD | No | | The password set |
| 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 |
</Config>
@@ -27,7 +27,9 @@ See the [Koito (Client)](/configuration/clients/koito) configuration for general
<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>
@@ -41,6 +41,8 @@ http://localhost:9078/api/lastfm/mySlug
<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 |
</Config>
@@ -26,8 +26,10 @@ To configure this Source use the same registration instructions and configuratio
<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>
@@ -25,10 +25,12 @@ To configure this Source use the same registration instructions and configuratio
<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>
@@ -111,8 +111,10 @@ To troubleshoot any errors, and assuming you are using Home Assistant, view the
<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 |
</Config>
@@ -28,7 +28,9 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
<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>
@@ -22,8 +22,10 @@ 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_URL` | Yes | | Base URL of your installation |
| `SOURCE_MALOJA_API_KEY` | Yes | | Api Key |
| 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>
+6 -4
View File
@@ -23,8 +23,10 @@ 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_URL` | No | `localhost:6600` | |
| `MPD_PASSWORD` | No | | |
| 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 |
</Config>
+3 -1
View File
@@ -72,8 +72,10 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d
<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 |
</Config>
@@ -32,7 +32,9 @@ If no URL is provided to MS it will try to use `ws://localhost:7905`
<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 |
</Config>
@@ -196,6 +196,7 @@ To configure MS to use Musicbrainz with your Plex metadata use one of the method
<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 |
@@ -204,4 +205,5 @@ To configure MS to use Musicbrainz with your Plex metadata use one of the method
| `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 |
</Config>
@@ -35,6 +35,8 @@ See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration f
<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>
@@ -92,9 +92,11 @@ You **must** define the IP address of at least one Sonos/accessory in order for
<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 |
</Config>
@@ -15,6 +15,7 @@ A **Source** is a data source that contains information about tracks you are pla
| Name | Networking | Scrobble SOT | [Multi Device/User](#multi-deviceuser) | [Scrobble Destination](#limiting-scrobble-destination) | [Thresholds](#scrobble-thresholds) | [Should Scrobble](#should-scrobble-behavior) |
| :---------------------------------------------------------------------- | :------------------------------------------------------- | :-------------------------------------------------- | :------------------------------------- | :----------------------------------------------------- | :--------------------------------- | :------------------------------------------- |
| [Apple Music](/configuration/sources/applemusic) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
| [Azuracast](/configuration/sources/azuracast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ✅ |
| [Deezer](/configuration/sources/deezer) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ✅ | ✅ | ❌ | ❌ |
| [Google Cast (Chromecast)](/configuration/sources/google-cast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
@@ -111,9 +111,11 @@ 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_CLIENT_ID` | Yes | | |
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
| `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` |
| 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 |
</Config>
@@ -32,8 +32,10 @@ Use the optional `usersAllow` property with **File** or **AIO** configuration to
<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 |
</Config>
@@ -106,6 +106,8 @@ Allow scrobbles from plyr.fm and unknown services:
<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>
+6 -4
View File
@@ -99,8 +99,10 @@ 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_URL` | No | `localhost:8080` | |
| `VLC_PASSWORD` | Yes | | |
| 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 |
</Config>
@@ -88,8 +88,10 @@ This affects **only** MS's behavior and does not affect the general connector be
<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 |
</Config>
@@ -31,7 +31,9 @@ EX
## Configuration
<Config config="MusicCastSourceConfig" fileContent={JsonConfig} name="musiccast">
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | :----------------------- |
| `MCAST_URL` | Yes | | The Musiccast device URL |
| 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 |
</Config>
@@ -74,6 +74,8 @@ If you have issues specifically with the MS Source please mention [**Druidblack*
<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 |
</Config>
@@ -179,11 +179,13 @@ Only one of these methods needs to be used.
## Configuration
<Config config="YTMusicSourceConfig" fileContent={JsonConfig} name="ytmusic">
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|-----------------------------------------------|
| 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 |
</Config>
@@ -593,39 +593,6 @@ MS uses [caching](/configuration/#caching) to reduce the number of API calls nee
**If you are using any Transform stages you should configure [secondary caching](/configuration#secondary-caching)** to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS.
<details>
<summary>Example</summary>
Add valkey service to your [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker) and configure MS to use it for metadata:
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
# ...
environment:
# ...
// highlight-start
- CACHE_METADATA=valkey
- CACHE_METADATA_CONN=redis://valkey:6379
// highlight-end
# ...
// highlight-start
valkey:
image: valkey/valkey
volumes:
- valkeydata:/data
volumes:
valkeydata:
driver: local
// highlight-end
```
</details>
## Examples
See **Examples** sections in specific Stage docs (also in the sidebar):
+15
View File
@@ -3,6 +3,8 @@ slug: /
title: Overview
sidebar_position: 1
---
import Carousel from '@site/src/components/Carousel/NextCarousel';
import BrowserOnly from '@docusaurus/BrowserOnly';
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
@@ -13,6 +15,7 @@ sidebar_position: 1
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
* Supports monitoring activity from many [**Sources**](/configuration/sources)
* [Apple Music](/configuration/sources/applemusic)
* [Azuracast](/configuration/sources/azuracast)
* [Deezer](/configuration/sources/deezer)
* [Google Cast (Chromecast)](/configuration/sources/google-cast)
@@ -64,6 +67,18 @@ A dockerized app that monitors your music listening activity from *everywhere* a
[**Quick Start Guide**](/quickstart)
<DetailsAdmo type="tip" summary="New UI Preview">
A **full rewrite** of the UI is now available for preview. The new UI modernizes Multi-Scrobbler and gives you in depth insight into how your scrobbles are handled end-to-end.
<Carousel/>
To try out the stable-release preview navigate to `http://yourMSDomain:9078/next`
Or to try out the **latest preview snapshot** see the docker image pinned [in this comment.](https://github.com/FoxxMD/multi-scrobbler/issues/500#issuecomment-4917525794)
</DetailsAdmo>
<img src={require('/img/status-ui.png').default} width="800"/>
**Why should I use this over a browser extension and/or mobile app scrobbler?**
+46 -1
View File
@@ -42,7 +42,7 @@ Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
<Tabs groupId="dockerSetting" queryString>
<TabItem value="storage" label="Storage">
You **should** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
You **should** bind a host directory into the container for storing configurations, credentials, database, and other files. Otherwise, these will be lost when the container is updated.
```yaml title="docker-compose.yml"
services:
@@ -54,6 +54,24 @@ services:
// highlight-end
```
Optionally, use a separate directory for data (database, cache, etc...) by mounting an additional directory and setting the `DATA_DIR` ENV to the directory that should be used inside the container:
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
environment:
# ...
// highlight-start
- DATA_DIR=/msData
// highlight-end
volumes:
- "./config:/config" # used for config files
// highlight-start
- "./my/host/folder:/msData" # used for data files
// highlight-end
```
</TabItem>
<TabItem value="networking" label="Networking">
If you are using a [bridge network](https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/) (default docker setup) you must map a port to the container in order to access the dashboard and use MS with some sources (Webscrobbler, LFM/LZ Endpoints). The default container port is `9078`.
@@ -231,6 +249,33 @@ npm run docs:install && npm run build
npm run start
```
#### Config And Data Directories
The directories MS uses for **Configuration** (`config.json`, `jellyfin.json`, etc...) and **Data** (database, cache, etc...) can be specified by environmental variables passed directly to `npm`/`node`, or set in an `.env` file in the working directory.
When parsing these ENVs MS will resolve relative directories but *not* bash expansions like `~` for home. Examples:
* `CONFIG_DIR=/my/absolute/path` => sets directory to use for config files
* `DATA_DIR=./relative/data` => sets directory relative to working directory for data files
If you do not set these environmental variables then MS will attempt to use OS-standardized directories:
<Tabs>
<TabItem value="linux" label="Linux">
* Config => `$XDG_CONFIG_HOME/multi-scrobbler` or `~/.config/multi-scrobbler`
* Data => `$XDG_DATA_HOME/multi-scrobbler` or `~/.local/share/multi-scrobbler`
</TabItem>
<TabItem value="mac" label="MacOS">
* Config => `~/Library/Preferences/multi-scrobbler`
* Data => `~/Library/Application Support/multi-scrobbler`
</TabItem>
<TabItem value="win" label="Windows">
* Config => `%APPDATA%\multi-scrobbler\Config`
* Data => `%LOCALAPPDATA%\multi-scrobbler\Data`
</TabItem>
</Tabs>
#### Rollup build error
During building if you encounter an error like: `Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.`
+8
View File
@@ -54,6 +54,7 @@ services:
- JELLYFIN_URL=192.168.0.110:8096 # URL to Jellyfin UI
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b # Api Key created in previous step
- JELLYFIN_USER=MyUser # Your username in Jellyfin
- JELLYFIN_ID=myJellyin
```
</TabItem>
<TabItem value="spotify" label="Spotify">
@@ -68,6 +69,7 @@ services:
- SPOTIFY_CLIENT_ID=yourClientId
- SPOTIFY_CLIENT_SECRET=yourClientSecret
- SPOTIFY_REDIRECT_URI=https://multi-scrobbler.mydomain.com/callback
- SPOTIFY_ID=mySpotify
```
Later, after [starting multi-scrobbler](#start-multi-scrobbler), visit the dashboard at `http://192.168.0.100:9078` and click **(Re)authenticate** on the Spotify card to authorize multi-scrobbler to use your account. Monitoring will begin automatically after authorization is complete.
@@ -83,6 +85,7 @@ services:
```yaml title="~/msData/docker-compose.yml"
- LZE_TOKEN=myCoolToken
- LZE_ID=navidromeLZ
```
The token can be anything. [(Re)start multi-scrobbler](#start-multi-scrobbler) after making this change.
@@ -105,6 +108,7 @@ services:
```yaml title="~/msData/docker-compose.yml"
- PLEX_URL=http://192.168.0.101:32400
- PLEX_TOKEN=6c0ihz8j7__f5r18oa0zm8
- PLEX_ID=myPlex
# optional, restrict scrobbling to a list of comma-delimited users
#- PLEX_USERS_ALLOW=myPlexUsername
```
@@ -132,6 +136,7 @@ services:
- SOURCE_LASTFM_API_KEY=MY_API_KEY_PLACEDHOLDER
- SOURCE_LASTFM_SECRET=MY_SECRET_PLACEHOLDER
- SOURCE_LASTFM_REDIRECT_URI=http://192.168.0.100:9078/lastfm/callback
- SOURCE_LASTFM_ID=myLFMSource
```
Later, after [starting multi-scrobbler](#start-multi-scrobbler), visit the dashboard at `http://192.168.0.100:9078` and click **(Re)authenticate** on the **Last.fm (Source)** card to authorize multi-scrobbler to use your account. Monitoring will begin automatically after authorization is complete.
@@ -251,6 +256,7 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
- KOITO_TOKEN=myToken
- KOITO_USER=myUser
- KOITO_URL=myURL
- KOITO_ID=myKoito
```
:::tip
@@ -274,6 +280,7 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
```yaml title="~/msData/docker-compose.yml"
- LASTFM_API_KEY=myApiKey
- LASTFM_SECRET=myApiSecret
- LASTFM_ID=myLfmClient
```
</TabItem>
<TabItem value="maloja" label="Maloja">
@@ -306,6 +313,7 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
```yaml title="~/msData/docker-compose.yml"
- MALOJA_URL=http://192.168.0.100:42010
- MALOJA_API_KEY=myApiKey
- MALOJA_ID=myMaloja
```
</TabItem>
</Tabs>
+64 -24
View File
@@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
import RequiredUpgrade from "@site/src/components/snippets/_upgrade-required.mdx"
<RequiredUpgrade before='< 0.14.0' current='0.14.0' old='0.13.2' />
{/* <RequiredUpgrade before='< 0.14.0' current='0.14.0' old='0.13.2' /> */}
## Source/Client IDs
@@ -75,32 +75,34 @@ Add an `id` to the top-level for each Source/Client configuration, next to `data
Add an `id` to the top-level for each Source/Client configuration, next to `data`:
```json title="koito.json"
[
{
"name": "koito-source",
"configureAs": "source",
"type": "koito",
// highlight-start
"id": "myKoitoID",
// highlight-end
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
```json title="config.json"
{
"sources": [
{
"name": "koito-source",
"configureAs": "source",
"type": "koito",
// highlight-start
"id": "myKoitoID",
// highlight-end
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "admin",
"url": "http://192.168.0.100:4110"
}
}
}
]
]
}
```
</TabItem>
</Tabs>
:::note[Default ID]
:::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.
**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 name as the ID in order to keep Plays/Scobbles associated with the same config.
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.
:::
@@ -108,13 +110,19 @@ If you decide to add an ID later you must use the name as the ID in order to kee
:::tip
Before upgrading, if your MS dashboard shows 0 queued/failed for all [Scrobble Clients](/configuration/clients) then you can **ignore this step.**
Before upgrading, if your MS dashboard shows 0 queued/failed for all [Scrobble Clients](/configuration/clients) then you can **ignore this section.**
:::
:::important
The cached scrobbles for a Client/Source were based on its **name**. If you plan on changing the name of a Client/Source with this upgrade you should do it **after** this migration has run or else MS will not be able to find any associated cached scrobbles.
:::
MS `0.14.0` introduces a database for persisting Plays/Scrobbles, queues, and other data. Prior to `0.14.0`, queued/failed scrobbles were stored in a cache **file** inside your [`CONFIG_DIR`](/installation/?dockerSetting=storage#recommended-settings) named `ms-scrobble.cache`.
When a [Client](/configuration/clients) is created and **enabled** in `0.14.0` it will attempt to automatically migrate scrobbles from this cache into the database.
When a [Client](/configuration/clients) is created and **enabled** in `0.14.0` it will attempt to **automatically migrate scrobbles** from this cache into the database.
* You can see progress/results for this process by filtering logs for `[Cached Scrobble Migration]`
* If the process fails MS will attempt to migrate failed scrobbles, again, the next time it is started
@@ -140,7 +148,7 @@ In `0.14.0` [Cache](/configuration#caching) has been simplified with much of the
This only applies to users who have `cache` in their [AIO Config](/configuration/?configType=aio#configuration-types) `config.json` or are using `CACHE_*` [ENV Config](/configuration/?configType=env#configuration-types)
If you do not have any of the above [Cache](/configuration#caching) configuration defined then you can **ignore this step.**
If you do not have any of the above [Cache](/configuration#caching) configuration defined then you can **ignore this section.**
:::
@@ -210,9 +218,41 @@ Remove the `connection` property from the `auth` object. If using `file` the ent
</TabItem>
</Tabs>
### Renamed ENV Keys
## Renamed ENV Keys
In an effort to standard ENV prefixes some ENV keys have been renamed:
* `LZENDPOINT_ENABLE` => `LZE_ENABLE`
* `LFMENDPOINT_ENABLE` => `LFM_ENABLE`
* `LFMENDPOINT_ENABLE` => `LFM_ENABLE`
## (Optional) ENV Client/Source Names
In the MS Dashboard, prior to 0.14.0, UI Client/Sources configured with [ENV Config](/configuration/?configType=env#configuration-types) always had generated names like `unnamed-koito`.
ENV Config Clients/Sources can now be configured with a name by using the `_NAME` suffix, example:
```yaml title="compose.yaml"
services:
multi-scrobbler:
# ...
environment:
- KOTIO_TOKEN=...
# ...
- KOITO_ID=myKoitoID
// highlight-start
- KOITO_NAME=Koito A
// highlight-end
- JELLYFIN_URL=...
# ..
- JELLYFIN_ID=myJellyfinID
// highlight-start
- JELLYFIN_NAME=Foxx Jelly
// highlight-end
```
:::important
If you have **cached scrobbles** make sure to upgrade and run [Cached Scrobble Migration](#cached-scrobble-migration) **before** adding names to Client/Sources.
:::
+30
View File
@@ -22,6 +22,8 @@
"clsx": "^2.0.0",
"docusaurus-plugin-sass": "^0.2.6",
"docusaurus-theme-github-codeblock": "^2.0.2",
"embla-carousel": "^9.0.0-rc02",
"embla-carousel-react": "^9.0.0-rc02",
"json5": "^2.2.3",
"micromark-extension-directive": "^3.0.1",
"modern-react-json-editor": "^1.0.2",
@@ -10863,6 +10865,34 @@
"integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==",
"license": "ISC"
},
"node_modules/embla-carousel": {
"version": "9.0.0-rc02",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-9.0.0-rc02.tgz",
"integrity": "sha512-GOQBX6v5cNk/ptfuHUoHhW9rvl5KeSGsBoppaUqtihu2kDmQAyzvnH/95r3TaSScFSJTj68V9AM6E5nq3jUVcQ==",
"license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "9.0.0-rc02",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-9.0.0-rc02.tgz",
"integrity": "sha512-DpcD0kTv6L31KvBcrx5d57E6T2rP/YceTGAdq7llkpKFdfM1of8YotHVEQTNIU3I9o5mjE/QxLi1ndJmm1DikA==",
"license": "MIT",
"dependencies": {
"embla-carousel": "9.0.0-rc02",
"embla-carousel-reactive-utils": "9.0.0-rc02"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/embla-carousel-reactive-utils": {
"version": "9.0.0-rc02",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-9.0.0-rc02.tgz",
"integrity": "sha512-H1c+yUbcVST9eJoGU+05L1vSDyBs0hf/Gf1OadHYIJ3asBfyvn6oaGCs2wXq1D8/f09y0wwCxeRxT9UZU/TxJA==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "9.0.0-rc02"
}
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+2
View File
@@ -30,6 +30,8 @@
"clsx": "^2.0.0",
"docusaurus-plugin-sass": "^0.2.6",
"docusaurus-theme-github-codeblock": "^2.0.2",
"embla-carousel": "^9.0.0-rc02",
"embla-carousel-react": "^9.0.0-rc02",
"json5": "^2.2.3",
"micromark-extension-directive": "^3.0.1",
"modern-react-json-editor": "^1.0.2",
@@ -0,0 +1,96 @@
import React, {
ComponentPropsWithRef,
useCallback,
useEffect,
useState
} from 'react'
import { EmblaCarouselType } from 'embla-carousel'
type UsePrevNextButtonsType = {
prevBtnDisabled: boolean
nextBtnDisabled: boolean
onPrevButtonClick: () => void
onNextButtonClick: () => void
}
export const usePrevNextButtons = (
emblaApi: EmblaCarouselType | undefined
): UsePrevNextButtonsType => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
const onPrevButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.goToPrev()
}, [emblaApi])
const onNextButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.goToNext()
}, [emblaApi])
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setPrevBtnDisabled(!emblaApi.canGoToPrev())
setNextBtnDisabled(!emblaApi.canGoToNext())
}, [])
useEffect(() => {
if (!emblaApi) return
onSelect(emblaApi)
emblaApi.on('reinit', onSelect).on('select', onSelect)
}, [emblaApi, onSelect])
return {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick
}
}
type PropType = ComponentPropsWithRef<'button'>
export const PrevButton = (props: PropType) => {
const { children, disabled, ...restProps } = props
return (
<button
className={'embla__button embla__button--prev'.concat(
disabled ? ' embla__button--disabled' : ''
)}
type="button"
{...restProps}
>
<svg className="embla__button__svg" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0 13.785 13.804 13.785 36.238 0 50.034L201.22 266l204.442 204.61c13.785 13.805 13.785 36.239 0 50.044-13.793 13.796-36.208 13.796-50.002 0a5994246.277 5994246.277 0 0 0-229.332-229.454 35.065 35.065 0 0 1-10.326-25.126c0-9.2 3.393-18.26 10.326-25.2C172.192 194.973 332.731 34.31 355.66 11.354Z"
/>
</svg>
{children}
</button>
)
}
export const NextButton = (props: PropType) => {
const { children, disabled, ...restProps } = props
return (
<button
className={'embla__button embla__button--next'.concat(
disabled ? ' embla__button--disabled' : ''
)}
type="button"
{...restProps}
>
<svg className="embla__button__svg" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M176.34 520.646c-13.793 13.805-36.208 13.805-50.001 0-13.785-13.804-13.785-36.238 0-50.034L330.78 266 126.34 61.391c-13.785-13.805-13.785-36.239 0-50.044 13.793-13.796 36.208-13.796 50.002 0 22.928 22.947 206.395 206.507 229.332 229.454a35.065 35.065 0 0 1 10.326 25.126c0 9.2-3.393 18.26-10.326 25.2-45.865 45.901-206.404 206.564-229.332 229.52Z"
/>
</svg>
{children}
</button>
)
}
@@ -0,0 +1,158 @@
:root[data-theme="light"] {
--text-body: rgb(54, 49, 61);
--text-comment: rgb(99, 94, 105);
--text-high-contrast: rgb(49, 49, 49);
--text-medium-contrast: rgb(99, 94, 105);
--text-low-contrast: rgb(116, 109, 118);
--detail-high-contrast: rgb(192, 192, 192);
--detail-medium-contrast: rgb(234, 234, 234);
--detail-low-contrast: rgb(240, 240, 242);
--text-body-rgb-value: 54, 49, 61;
--text-comment-rgb-value: 99, 94, 105;
--text-high-contrast-rgb-value: 49, 49, 49;
--text-medium-contrast-rgb-value: 99, 94, 105;
--text-low-contrast-rgb-value: 116, 109, 118;
--detail-high-contrast-rgb-value: 192, 192, 192;
--detail-medium-contrast-rgb-value: 234, 234, 234;
--detail-low-contrast-rgb-value: 240, 240, 242;
}
:root[data-theme="dark"] {
--text-body: rgb(222, 222, 222);
--text-comment: rgb(170, 170, 170);
--text-high-contrast: rgb(230, 230, 230);
--text-medium-contrast: rgb(202, 202, 202);
--text-low-contrast: rgb(170, 170, 170);
--detail-high-contrast: rgb(101, 101, 101);
--detail-medium-contrast: rgb(25, 25, 25);
--detail-low-contrast: rgb(21, 21, 21);
--text-body-rgb-value: 222, 222, 222;
--text-comment-rgb-value: 170, 170, 170;
--text-high-contrast-rgb-value: 230, 230, 230;
--text-medium-contrast-rgb-value: 202, 202, 202;
--text-low-contrast-rgb-value: 170, 170, 170;
--detail-high-contrast-rgb-value: 101, 101, 101;
--detail-medium-contrast-rgb-value: 25, 25, 25;
--detail-low-contrast-rgb-value: 21, 21, 21;
}
.embla {
max-width: 48rem;
margin: auto;
--slide-height: 33rem;
--slide-spacing: 1rem;
--slide-size: 100%;
margin-bottom: 1rem;
}
.embla__viewport {
overflow: hidden;
}
.embla__container {
display: flex;
touch-action: pan-y pinch-zoom;
margin-left: calc(var(--slide-spacing) * -1);
}
.embla__slide {
transform: translate3d(0, 0, 0);
flex: 0 0 var(--slide-size);
min-width: 0;
padding-left: var(--slide-spacing);
}
.embla__slide__container {
border: 0.15rem solid transparent;
border-radius: 1.2rem;
display: flex;
align-items: normal;
justify-content: center;
height: var(--slide-height);
user-select: none;
overflow:hidden;
flex-wrap: wrap;
}
.imageCaption {
font-style: italic;
font-size: 0.8em;
text-wrap-style: balance;
text-align: center;
}
.embla__controls {
display: grid;
grid-template-columns: auto 1fr;
justify-content: space-between;
gap: 1.2rem;
// margin-top: 1.8rem;
}
.embla__buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
align-items: center;
}
.embla__button {
-webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5);
-webkit-appearance: none;
appearance: none;
background-color: transparent;
touch-action: manipulation;
display: inline-flex;
text-decoration: none;
cursor: pointer;
border: 0;
padding: 0;
margin: 0;
border: 0.2rem solid var(--detail-medium-contrast);
width: 2.5rem;
height: 2.5rem;
z-index: 1;
border-radius: 50%;
color: var(--text-body);
display: flex;
align-items: center;
justify-content: center;
transform: rotate(0deg);
}
.embla__button--disabled {
color: var(--detail-high-contrast);
}
.embla__button__svg {
width: 35%;
height: 35%;
}
.embla__selected-snap-display {
justify-self: flex-end;
align-self: center;
color: var(--text-low-contrast);
font-weight: 600;
}
.embla__slide:nth-child(1) {
flex: 0 0 90%;
}
.embla__slide:nth-child(2) {
flex: 0 0 60%;
}
.embla__slide:nth-child(3) {
flex: 0 0 75%;
}
.embla__slide:nth-child(4) {
flex: 0 0 80%;
}
.embla__slide:nth-child(5) {
flex: 0 0 100%;
}
.embla__slide:nth-child(6) {
flex: 0 0 100%;
}

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