Example is already in the main cache configuration docs. Consolidate/remove the transform example so there is only one authorative example in the docs.
* 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
* 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)
* Use ErrorLike or ErrorIsh for error utilities, rather than requiring object be an Error instance
* Render error summary on dead scrobble page and have copy action include all error data
* serialize errors when sending through api
* fix error assignment for new dead scrobbles
* Update hast-util-to-jsx-runtime with npm override
* Re-export JSX as namespace for remaining packages with issues
May be addressed by future package updates or upgrading to docusaurus 3.10.x
* Remove ListenProgress specific serialization tests and function since its not needed anymore
* postfix tmp dirs with test name hints for easier debugging
* try to prevent filecache from leaving files behind
* re-use dead scrobble removal method for clearing all
* temp fix for rendering queue errors in frontend
* properly decrement queue based on dead scrobble queue status
* display total dead on status card
* Refactor should play methods to return result and reason, rather than always logging reason
* Combine reasons when logging to reduce log statements
* Demote "no update" logging to TRACE, promot "update" logging to VERBOSE
* Use new updatedAt column to determine if plays should be deleted
* Update plays on already discovered to keep them from being delted
* Fix various bugs and missed awaits in retention cleanup function
multiplatform is still useful for memory players but is not useful for discovery and just complicates code paths.
there is no reason to discover based on platform as the Source is the smallest UoW anyway. If a play should be discovered (or not) it shouldn't matter what platform is was played on, it's all the same Source
* Don't keep sticky now playing if the player has gone stale/orphaned
* When multiple players are present, prefer those that are not stale/orphaned
#480
* Use and return only plain PlayProgress on player state finish
* Removes the need to serialize/deserialize ListenProgress class and compare/persist toad scheduler or timeout functions
* Potentially fixes timeout comparison stack error #569
* Helper script to build a static font
* Move all image rendering code into own folder
* Simplify generated text to description OR title
* Use text-wrap balance to make it look prettier
* Add tagline and increase font sizes to fill card dimensions
This document outlines the Contributor Covenant Code of Conduct, including our pledge, standards, enforcement responsibilities, and guidelines for community behavior.
* Use filtered, processable scrobbles in for-loop to avoid any weird in-place array splice issues when iterating
* Add more logging with labels/ids to make logging more readable
* Move dead scrobble removal function closer blocks where it should happen
* Re-use lifecycle steps/patch to build diffs
* Break out step generation into own function to simplify logic
* Cache steps instead of play to avoid returning a cached play when data is the same #495
* Replace patch object/patching logic with json-diff-ts as its more robust and actually maintained
* Reduces need to stringify everywhere due to lack of cloning in jsonpatchdiff
Some items on Plex store the MBID in the `guid` field in the `Metadata`
response, not in the `Guid` field. This change checks the `guid` field
as a fallback if the `Guid` field is not set.
* Check queue before adding so that previously cached or re-discovered plays from a source are not added multiple times to the queue (should only be unique plays)
* Add timestamp heuristic to dupe matching -- if score is *close* to matching and timestamp is *exact* then nudge it into a match
* Use p-retry to standard retry behavior
* Simplify/remove other own-rolled retry logic
* Fix superagent request instantiation
* Requests are mutable, cannot be retried once executed. Need to make a new request for each retry
Use a limit/maxFetch upper limit to return a bulk of scrobbles by count, rather than timerange, so that backlogged scrobbles are less likely to cause many fetches on first startup
* Remove design bias towards "recent" scrobbles with limited timerange and complex shouldRefresh logic
* Replace with logic to fetch scrobbles from timeranges that are relevant to queued/dead scrobbles
* timeranges are grouped by closely timestamped queued scrobbles
* timeranges are cached based on staleAfter
Koito functionality has diverged enough that it doesn't make sense to provide partial support via LZ components.
This also makes timerange pagination easier.
* All logger use trace as minimum level
* Force app log stream to trace minimum so we always show all possible logs in dashboard
* Add trace level to dashboard
* Make dashboard start in debug logs for readability
* Set auth failure on 4000 from server (invalid client id)
* Better logging for connection issues
* Don't fail on connect so we can keep retrying on MS activity
* Handle connect and auth separately so it can be restarted by scrobble heartbeat
* make ms-initiated reconnect use manual close/open since isosockets won't
* better error handling for auth/connection init stages
* send heartbeats even if not authed
* Use pocike icon for placeholder art
* Use MS logo for small art with link to docs
* Use web link as track url, if available
* Use MB link as track or artist url
* Add as dev dependency with npm script so it can be run anywhere project is installed
* Update config with newer template based on scopesorted
* Add more scopes and give them pretty emojis
The old jellyfin config had optional options which kept the new jf options required from *always* being required. Removing the old config revealed this bug.
Fixes#469
* Store Source input and original play
* Store transformer hook results as diff patches alongside stage-specific arbitrary input data
* Use jsondiffpatch console format to log stage diffs
* Store scrobble payload data
Device Name/GroupName is more likely to exist and less likely to cause issues than upnpclass and other things we are looking for to verify its a valid track.
User can block the erroring device to "fix" bad behavior.
* Use invariant hash for seen state logging
* Removes dependence on individual properties being available
* Drop player state if an existing existing player has the same play, rather than dropping by existing group
* Enable logging empty player for more debugging info
try to get device summary with for loop
* Use dynamic import for schema based on environment
* If production use pre-generated json to avoid loading vega generator -- big memory footprint reduction
* If not production use vega so schema is always up to date
Source that are also clients did not have ENV config due to early design choices that are now irrelevant.
Implement ENV config for these source to make config easier for everyone.
* feat(librefm): Implement librefm
* feat(librefm): Make api key/secret optional
* feat(librefm): Add LIBREFM_ENABLE env to allow enabling librefm with no other options
* feat(ui): Show client/source hint for all ambiguous components
* Adds `fields` and `id` presets to give ENV users more flexibility
* BREAKING: Removes searchOrder from `sensible` so users must now use `default` with presets (other than `sensible`) to get default searchOrder methods added
* Cache full transform hooks and omit logging when cache hit
* Big logging reduction for sources that return all plays (teal, lastfm, etc.)
* Implement new error type for stages to discriminate between expected prerequisite failures and true exceptions
* Used to determine logging level and if hook should be cached
* search order and type of searches now determined by `searchOrder` instead of multiple `fallback*` options with hardcoded order
* Deprecate `fallback*` options
* Deprecate `fallbackArtistSearch` for `searchArtistMethod`
* Refactor ENV presets to use new options
* Parse irsc and track number from spotify data
* Add isrc and track number to listenbrainz submission
* Correct spotify ids for listenbrainz submission
#418
* Merge causes if a simple error exists in the stack, reduces stack logging noise
* Log top level message and print error instead of duplicating message in another error
* Fix adding transform history for failed-continue stage
* Implement multiple endpoints for API client with round robin load balancing
* Implement caching responses
* Add transformer options for search triggers
* Implement transformer checks based on search triggers and score
* Aggressively start Now Playing update (based on reported) but aggressibely end, based on stale/orphaned, so updates are early but not often
* Refactor update thresholds to use scrobble client behavior
* LFM and LZ use track duration as "time visible" for Now Playing, don't re-update during that time period
* Add thresholds to logging for easier troubleshooting
#395
* Use vite tailwind plugin instead of postcss
* Update tailwind so it actually works with plugin
* Remove postcss
* Merge vite config into inline vite-express config
* Use typeson for stringified data marshalling
* Use clone for in-memory serializing to preserve dayjs
* Refactor cache backend init into indepedent, testable functions
* Add `false` as cache provider type to allow disabling secondary cache
* Two-tier caching
* primary is always in-memory LRU
* secondary supports file and valkey
* Different caches for metadata and scrobbles
* no file support for metadata (too big)
Maybe recenttracks/track does not exist for totally new accounts, though it should.
Guard against this and log response if something unexpected happens so we can troubleshoot
#353
* Move API code into own class for reusability with future Source
* Remove Maloja V2 compatibility
* Throw on connection if server version is too old or unknown
As of April 2025 Spotify no longer allows insecure redirect URIs which complicates initial Spotify setup.
* Improve spotify App creation instructions
* Add several approaches to creating a valid redirect URI
* Remove Spotify as the default Source example in docs due to complexity of Redirect URI requirements
Closes#330
* Use MS Player to mark Play as a repeat if session repeat is detected
* Remove FUZZY temporal accuracy as acceptable for detecting duplicate if candidate play is marked as repeat
* Add queue for aggregating Now Playing updates
* Filter play based on 1. source priority and 2. player stickiness or deterministic order
* Use toad scheduler to processing NP queue
* Define if scrobble client can update Now Playing based on class property, use this to filter now playing update
* Use thresholds to determine when/if Now Playing should be updated
* Set LastfmScrobbler as supported for Now Playing
* Allow non-positional Sources to still report position (for display only)
* Refactor Azuracast to be non-positional due to reported position not always being accurate (azuracast approximates track length)
* Rename from manualListening to Should Scrobble to better describe functionality
* Move check to Source scrobble function to avoid short circuting Source player for no reason
* Add to Azuracast and make functionality more generic
Clearing pre-stale listening range prevents any future resume (player becomes non-stale) from using old listening range to scrobble. Represents a physical "break" between listening sessions for the same song
Missing arg to pass to getPlayedObject to ensure the timestamp used for playDate is the same as normally discovered tracks. Without the arg two tracks played with the second being scrobbled on stale would cause the latter to have the same TS as the former
fixes#254
Replaces `build-push-action` buildx QEMU building for multiple `arch` with a multi-runner matrix that builds each image on native runners and combines digests before pushing to registries.
* Use arm public preview runner
* Refactor publish image workflow to use multi-runner
* Refactor PR workflow to use multi-runner
* Update to latest node 20.x minor version
* Remove `--verbose` and enable `--no-audit` for npm install to reduce install time
* These were left over from debugging ARM builds from Feb 2024
* Do play cleanup during polling for new plays so that we can discover tracks from abandoned players
* Fixes missed scrobbles from Plex API when Source stops returning a session due to player queue end #223
* Diff type should always be prepend since we are checking for new tracks
* Warn and log if another type is detected
* Warn if more than one track diffed for discovery
* Refactor and and make YTM history parsing testable
#227
* Fixes#241 by using the track artist as the primary artist when album artist is present
* Bug is reported in LukeHagar/plexjs#27 but will need be patched at runtime for the foreeable future due to npm packages not being published since September LukeHagar/plexjs#26
* try-catch and bubble up depending on usage
* replace poll/monitor code on startup with scheduler task usage
* better notification messages with truncated cause chain
* Notifications are not mission critical so its okay if they fail on init because service outage may be transient. Always init notification services and always attempt to push (we catch failures anyway)
* Parse more info from notification URL and test simple reachability of host/port to make troubleshooting easier
Fixes#215
* Patch generator to generate definition titles until vega/ts-json-schema-generator#2127 is resolved
* Add launch config for debugging schema generation
* Disallow topRef generation so docusarus json viewer works
* Write to docsite static assets on generation
Youtube TV seems no longer have scope for reading history or account details?
Cookie may work and custom oauth seems most stable based on reporting from LuanRT/YouTube.js#803
* Identify when session is for the local user (no user is logged in)
* Add Local User as default allowed when no usersAllow settings
* Allow user config to allow/block local user
* Update plex docs
Fixes#218
* Add session key dervied from source data to Play and PlayerState
* Discriminate states based on a session key, if possible
* Log which states are being dropped based on session key (plex)
* Add session key to important logging (new player play, in UI for player info)
* Introduce positional and non-positional data structures
* Refactor state player into types of each
* Refactor listen ranges and progress into types of each
* Implment (internal) real-time player and base positional player seeked/repeat on real-time drift instead of just reported position
* Refactor tests to use (emulated) real-time components
Plex only updates player position every 15 seconds (of played track) so player state needs to be adapted to not detect this as a pause or seek.
* Allow per-source drift allowed before triggering seek
* Allow per-source pause detection
Was not passing stopped state due to undefined play but need to do this to let last play actually be scrobbled.
Refactor jellyfin session validation and types for MemorySource to correctly allow processing player state with no plays
* Default disallow any activity not from a library of collectionType 'music'
* Allow user config for allow/block by library name
* Allow user config for allowing additional library collection types
* Update docs and example config
SDK types say AlbumArtists etc. should be defined but #200 shows that's not always the case, for some reason.
Check for undefined value on playback data that requires additional transformation to prevent errors.
Fixes#200
Fixes a lot of headaches:
* vega is more up to date and extensible
* no more hacky regex for reference names
* complete control over parsing and formatting (const as enum)
* does not duplicate typescript as dependency
* should properly throw errors on schema generation instead of just logging to console
* move schema static files into programmatic creation instead of package scripts
* fix types being removed in prod which caused schema generation errors
Not enough to include it in both dependencies and devDependencies -- it needs to be NOT in devDependencies to prevent `npm ci --omit=dev` from purging it.
* Add suite for testing client/source parsing and validation
* Additionally, use example configs for two-birds-one-stone checking they are valid
* Refactor sources init as separate step from component building
* Use runtime-generated schemas
* Ensures validated schemas always match code
* Can use schemas for individual source/clients without having to generate files
* catch invididual validation errors and log instead of crashing
* refactor validation error output into chained error instead of as individual log statements to improve clarity
* Swtch to devcontainer for node 18.x and remove terminal/launch settings for version (removes nvm dependency in container)
* Update eslint config to use proper config for include/ignore files
* Add spec for mocha and mocha test runner extension + settings
* Fix documentation and jellyfin example to match code
* Add tests for jellyfin api config parsing and valid activities
* Fix jellyin api ENV naming to match code
* Fix config parsing missing lowercase transform
Fixes#196
* All required files in the same file by using .actrc
* Include example .secrets and .env
* Update README with usage
* Refactor actTest to make docker build steps optional with ENV
* Add refreshMinInterval to prevent hammering upstream services
* Refactor refresh logic to be simpler and account for backlogged tracks
* (test): Improve play generation utils for testing
* (test): Refactor testing for upstream scrobble refreshing to use more actual scrobbler class behavior
* Move file parsing to own function
* Add some typings to file parsing to help with development (typings are bad)
* Defensively check structure of formdata for nested arrays since Plex seems to waffle on how it sends data (fixes#191)
* Add logging for all files found to help with future end-user debugging
express-static prefers directories over files when using the extensions
option, meaning an extensionless route will always route to the
directory instead of the file (expressjs/serve-static#138).
* Ensure pre-release tags don't also get latest tag
* Disable alpine builds
* Only increases testing and build complexity and there has been no feedback from users about needing it
* Aligns with "normal" default branch usage better, allows dependabot to actually run
* Change image publishing to push tagged images as 'latest' and master images as `edge`
* Implement new jellyfin source using JF typescript api client library for communication
* Use either API Key or username/password
* Implement real-time scrobble monitoring
* Implement feature parity filters from old jellyfin source (users) and add devices filters
* Refactor documentation for new configuration with api and add migration steps for webhook users
* Add deprecation warning to jellyfin webhook source
* Move recentScrobbles and generic refresh logic into abstract class
* New abstract function for scrobbler children to implement to get scrobbler-specific play lists on refresh
* Implement shouldRefereshScrobble with logging for better insight into why or why not a scrobbler refreshed upstream scrobbles
Since docs are now generated alongside builds and hosted on the app server there is no longer a barrier to using docusaurus docs as the main documentation presentation. Any user looking for docs can use the GH Pages hosted site or a "versioned" docs site hosted on their MS app. Due to this we can fully commit to docusaurus and take advantage of MDX layouts to simplify docs.
* Replace env/file configs headings in configuration with MDX tabs to make reading easier
* Remove superfluous TOCs
* Replace internal links in repo readme with links to docsite
* setup.sh takes care of building generated sources for project/docsite and building flatpak app
* Modify flatpak manifest to include two generated sources files and build docsite files
* Update flatpak docs with script usage and generated sources changes, move to docusauraus doc site
* Add npm scripts for installing/building docusaurus from main project
* Set base url based on ENV
* Build docsite in docker image
* Add link to doc in dashboard and link to github
* Update nodejs install instructions to include doc build command
* Update GH pages workflow to use correct base url
* Add hint page to main app when docs are not built
* Use lower headers so docusaurs sidebar TOC is more accurate
* Specify header levels for config/FAQ to make sidebar more concise
* Wrap inline TOC in details block -- so docusaurs users don't have to see it twice but its still available to github viewers
Sporadic DNS issues with alpine have caused a number of issues #160#126#134
Use debian as default since it has been proven to be stable. The image size increase is worth squashing these issues.
* Use superdiff to diff PlayObject lists and detect changes as well as append/prepend scenarios
* Replace YTM recently played logic with list diffing, only accept prepend-validated lists
* On non-prepend scenarios replace existing recently played and log human readable diff
* Patch youtube-music-ts-api to use updated cookies from response and provide a callback on update
* Implement currentCreds/build init data and read from MS-updated creds if available
* Write to currentCreds when ytm-ts-api invokes auth update callback and optionally log what parts changed based on config options
* Update Vite dep
* Use inlineViteConfig for vite-express middleware and detect correct base string
* Refactor vite.config.ts to detect correct base string
* Move code into testable function
* Fix port erasure when url uses standard port but it is explicitly specified
* Simplify building default url and fix using undefined mainPort variable
* Fix port erasure when https protocol is explicitly specified
* Refactor localUrl usage as URL instead of string
* Implement pathname joining on URL to make handling trailing/leading slashes take place in nodejs lib
* Implements tests for URL building/manipulation
* Increase default backlog limits to maximum pagination supported by each source
* Add scrobbleBacklogCount config property so user can explicitly set # of tracks to backlog
* Move some debug => verbose for non-repeating logs that are relevant to state, notifications, and scrobbling
* Move some verbose => debug that were repeating and making verbose level noisy
If a user does not need the dashboard or has security concerns about ingress and cannot control their hosting environment this enables reducing attack surface.
Closes#150
Error Cause (https://github.com/tc39/proposal-error-cause) was finalized and introduced in ES2022 so we don't need to ponyfill this anymore. Still using the helpful helper functions thought.
* Refactor SOT to be a type (may have more in the future)
* Set player stale/orphan interval based on SOT type
* Add note in log when initializing MemorySource which does not use player as SOT
* Add tooltip to UI player when it is not SOT
Migrating to @spotify/spotify-web-api-ts-sdk requires that the credentials we provide have valid expires_in/expires data and grant type. Make this migration easier by writing that data now so users upgrading later most likely do not need to re-authenticate.
* Actually remove device if it fails after X retries
* Properly close controllers/platform/client to prevent uncaught errors from castv2
* Move manually configured devices to device discovery function so they can be reconnected on heartbeat (if previously removed)
* Implement interfaces for response shape and getters for data
* Parse version data from response and use ping endpoint to return info on connection test
* Fix throwing auth/connection errors to include actual cause along with more human-friendly parsed error info
* Build error causes based on known error response types and expected maloja error body
* Only retry requests if error is network-related
* Set upstream error as NOT showstopping if we find expected error structure and status is less than 500
* Better maloja error parsing and logging
* Switch to lower-level mdns library for more control during debugging
* Use avahi-browse with host avahi-daemon to circumvent musl (alpline) issues with mdns resolution https://johnsiu.com/blog/alpine-linux/
* Add transportId shortname to logger label for better differentiation of "per session" apps
* Fix handling of MS player deletion when pruning apps by checking for other valid apps before deletion
* Refactor credit parsing into two stages: joiner separation and credits parsing
* Break credit parsing into wrapped vs. non-wrapped for simpler regexes
* Implement suffix matching after credits
* Add tests for wrapped vs. non-wrapped credits and with suffixes
* Break up initialize into build data => check connection => check auth
* Implement stub methods in AbstractSource with default return for steps
* Refactor existing initialize overrides into steps for each source
* Move (re)start to same location on status card as client and change verbage to be the same
* Fix api behavior to use correct poll function so that backlog is triggered
* Consolidate playerDelete event emitter to deletePlayer function
* Consolidate logging for player deletion to deletePlayer function
* Add removal reason when transferring player state for single-platform sources
The assumption is that single platform sources (like spotify) can only play from one platform at a time. If they are smart (like Spotify) the "new" platform picks up where the old platform left off WRT playing music. This commit implements the same behavior for MS player state by transferring existing state from the existing (previous) platform to the newly discovered platform.
* Track datetime player "completes" play at
* Add scrobble datetime SOC metadata
* Log SOC used when printing play datetime
* Use hinted SOC scrobble datetime when comparing existing scrobbles
* Use hinted SOC scrobble datetime when building scrobbler client scrobble payload
* Hint Spotify backlog plays use END play date for scrobble SOC
* Refactor using 'close' boolean to 'match' granularity
* Makes using granularity for future logic easier
* Easier logging for granularity in summary
* Remove intermediate temporal functions in classes for DRY and so we can use comparison results
* Add Time Detail to match breakdown for more visibility during logging
* Fix undefined token inclusion caused by token lists with differing lengths
* Always compare longer to shorter string so sameness parameter order is invariant
* Add comments to make logic easier to understand
* Add tests to test new functionality
* Move logging for init/auth into client instead of builder class
* Move startup from builder class to main backend and use initScrobbleMonitoring() instead of calling init/auth individually
Paves the way for manual restarting #114
If player is calculated as playing and tracks have positional data then start and end positions can be fudged to completion if jitter is small enough
* At beginning of new track if position is within 3 seconds of start of track fudge to 1 second
* At end of old track if position is within 3 seconds of end track fudge to completion
This can be achieved with high confidence due to smart polling near end of tracks added in 1d84f07a40
If player has an absolute position and play has duration we can determine if polling interval will occur after play would end. In this scenario we can shorten interval to before play ends to get more accurate data about play state close to end of track and report more accurate play time.
* Wake up timestamp was based on ts AFTER polling finished + interval. This means real-time interval between polling drifted by the amount of time it takes to execute polling loop. Fixed by getting ts at beginning of loop instead of at end.
* Implement interval function that can be overriding by child classes
* Format interval to make floats less ugly
* Implement abstract playToClientPayload function to make future testing easier
* Refactor play object to include album artist in data
* Refactor scrobble client payloads to include album artist, if applicable
* Discriminate between proper errors (permissions) and network errors
* Fix return value and playbackState flag set during post-auth hook
* Improve error logging using cause and upstream error
* Fix undefined destructure when device is not present in playingNow payload
* Implement type/guard for node network error
* Refactor auth in sources & clients to flag if error is network or api based
* Retry auth on heartbeat if auth failure was caused by network
* Implement initial tests for testing client auth
* Fix tests hanging due to running async functions
* Store logs by levels, fixes issue where rarer warn/info are pushed out of buffer by frequent debug/verbose
* Use FixedSizeList instead of homegrown buffer
* Implement API and ui controls for setting log limit
* Implement backend api endpoints for dead letter CRUD
* Include dead letter queue count in client status data
* Implement dead letter client view
* Partially working retry/remove actions for client (request is OK, RTK not working for update yet)
* Fix repeat end position percent test (should be 85% not 15%)
* Fix current play/range not being reset correctly on repeat
* Add reportedTS param to facilitate testing
* Use running/idle for scrobble client to indicate processing status
* Change polling sources status to polling/idle to better reflect behavior
* Add status indicator color to statuses
* Delay scrobble based on last scrobble attempt TS
* rename abstract scrobble func to doScrobble
* Implement scrobble() in abstract client and track last scrobble attempt time, delay based on arg or client property
* This way first scrobble isn't delayed and we only delay if subsequent scrobble attempts are close in time
* Make scrobble processing sleep time on empty queue configurable
* Implemented logic for processing and removing scrobbles from dead letter queue
* Only retry scrobbles under X retries based on config or argument
* Add removal-only function (for later use with UI)
* Move scrobble checking into client (out of client manager) for single responsibility
* Implement scrobble queue/deadletter
* Less chance of duplicate processing since sources can't scrobble concurrently
* Better throttling since multiple sources can't scrobble concurrently
* When scrobbling fails we can retry on delay just like polling and *keep* the failed scrobbles for later retry rather than dropping them
* Deadletter (to be fully implemented) for scrobbles that fail multiple times or are signalled as not upstream api issues by client
Wait until player gets a new play or becomes stale before trying to discover the (previous) play so that we store a more accurate amount of time the user actually listened to that play
When looking for dups and finding a fuzzy timestamp match, close title, and mismatch (but some matched) multiple artist -- we give a bonus to artist score since its likely a dup.
* Add text/* wildcard so any text-prefixed content-type is valid (Jellyfin should always be sending json anyway)
* Add logging WARN when body is empty
Fixes#101
* QoL improvement -- Instead of needing to rename a config file so it is not read by MS a user can now use the 'enable' property in all source/client configs to enable or disable loading the config
* Added property to all config examples
* Use lastfm 'nowPlaying' property in user listens for MS player implementation
* Do not use MS player as source of truth -- continue to use lfm listen history
Will not be used by listenbrainz source for actual scrobbling so make logs less noisy by suppressing what would normally be logging on newely added tracks from player
* Allows users to define a base URL to be used for default redirect URI generation
* Convenience mostly for docker users so they do not have to explicitly define redirect uri for each source/client
* Use in UI start up logging hints
files in node_module can have wierd ownership based on how a package owner published the package. If this occurs a rootless container build system could potentially have issues with subui/gid mapping for non-standard (high number) ids.
During image build recursively chown all node_modules files to normalize ownership and avoid this problem.
Fixes#96
* Due to increased scrobbled thresholds in 590610022d we need to pol more frequently to make sure more of the track is accurately monitored
* Decreased default from 30 to 10 seconds
* Decreased max interval from 60 to 30 seconds
* Better use of header
* Show name on separate line
* Only show client/source if ambiguous
* Move status into right side of header
* Reduce padding/margin on card grid
* Reduce padding on card headers and bodies
* Improve log experience
* Reduce log font size and convert to mono
* Use proper log message parsing to remove dangerous html set
* Check for stale/orphaned/dead in independent loop so they are not dependent on getting source updates to update
* Only emit events if player state has updated
* Create and implement api interface for player state data
* Serialize memory source players to object indexed by platform id
* Refactor event emitters to separate event data better from event metadata
* Update source slice when player update event is broadcast
label:Please check existing knowledge before opening an issue
options:
- label:I have [read the Help docs](https://foxxmd.github.io/multi-scrobbler/help/)
required:true
- label:I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required:true
- label:I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required:true
- type:markdown
attributes:
value:|
If this a how-do-I question, troublehooting, or generally **not** a bug/error with multi-scrobbler please consider [**starting a discussion**](https://github.com/FoxxMD/multi-scrobbler/discussions) instead of opening an issue.
- type:textarea
id:what-happened
attributes:
label:Describe the Bug
description:Describe what you expected to happen and what happened instead. Please provide as much detail as possible.
placeholder:The app should have scrobbled from Spotify but instead it crashed with an error about undefined properties, etc...
validations:
required:true
- type:checkboxes
attributes:
label:What configuration types are you using for multi-scrobbler? Check all that apply.
description:What platform are you running multi-scrobbler on?
options:
- Docker
- Local (NodeJS)
- Flatpak
default:0
validations:
required:true
- type:textarea
attributes:
label:Versions
description:What version of multi-scrobbler and issue-related sources/clients are running?
placeholder:|
- multi-scrobbler: 0.8.3 on docker
- maloja 3.1.4
- jellyfin 10.8.9
validations:
required:true
- type:textarea
id:logs
attributes:
label:Logs
description:|
Please copy and paste any/all relevant from the DEBUG level log output.
Even if your issue does not seem to be shown in the logs you should still add as much detail as possible, a _lack_ of issues in logs may be a good hint during debugging.
Your logs will be automatically formatted into code. Do not use fenced code blocks (backticks).
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/configuration#debug-mode).
If relevent, please copy and paste all [**Debug Data**](https://foxxmd.github.io/multi-scrobbler/help#copy-play-debug-data) related to the Plays you are having issues with.
Your data will be automatically formatted into code. Do not use fenced code blocks (backticks) in the text area below.
description:Suggest a new feature or functionality
title:"feat: "
labels:["enhancement"]
body:
- type:checkboxes
attributes:
label:Please check existing knowledge before opening an issue
options:
- label:I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required:true
- label:I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required:true
- type:textarea
attributes:
label:Describe the new functionality
description:Please describe the feature or functionality you want multi-scrobbler to have. Use as much detail as possible about the exact behavior you wish to see.
validations:
required:true
- type:textarea
attributes:
label:Configuration Example
description:|
If the functionality involves changes to configuration files please provide an example here
render:json5
- type:checkboxes
attributes:
label:Will you help implement it?
options:
- label:I am willing to contribute code towards completing this feature
- label:I am willing to help test preview builds for this feature
description:Request a new Source or Client to be implemented
title:"feat: "
labels:["enhancement"]
body:
- type:checkboxes
attributes:
label:Please check existing knowledge before opening an issue
options:
- label:I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
required:true
- label:I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
required:true
- type:dropdown
attributes:
label:What Type?
description:Is this a Source or a Client?
options:
- Source
- Client
default:1
validations:
required:true
- type:input
attributes:
label:Name
description:"What is the name of the new service/software?"
placeholder:"My Cool Player"
validations:
required:true
- type:input
attributes:
label:Website
description:"Website for the service/software. Preferably a github repo or other source-code homepage"
validations:
required:true
- type:dropdown
attributes:
label:API Available?
description:Does the service have an API or other protocol that multi-scrobbler can use to communicate? **Without this the likelyhood of implementation is very low.**
options:
- 'Yes'
- 'No'
default:1
validations:
required:true
- type:dropdown
attributes:
label:Free To Use?
description:Is this service/software freely available or does it require purchasing/subscription?
options:
- 'Yes'
- 'No'
default:1
validations:
required:true
- type:dropdown
attributes:
label:Willing to finance development?
description:If the software is **non-free** are you will to provide a paid account/subscription to multi-scrobbler developer's in order to facilitate development/testing?
options:
- 'Yes'
- 'No'
validations:
required:false
- type:checkboxes
attributes:
label:Will you help implement it?
options:
- label:I am willing to contribute code towards completing this feature
- label:I am willing to help test preview builds for this feature
- type:textarea
attributes:
label:Additional Context
description:Add any other context about the new Source/Client here
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
```
Copy and paste as much log data as possible related to this issue here.
```
**Versions (please complete the following information):**
Provide version information for any related sources/clients.
Need to have credentials in a [`.secrets` file](https://nektosact.com/usage/index.html#secrets), copy and rename [`.secrets.example`](./secrets.example) to `.secrets`, then fill out blank fields. Required for docker/metadata-action to read...something. Fails with `Parameter token or opts.auth is required` if they are not supplied.
An ENV file can also be made by copy and renaming [`.env.example`](./env.example). Set `NO_DOCKER_BUILD=true` if you only want to test APP_VERSION and docker tags output.
If running a full docker build for multi-runner workflows you will need to create an [artifact server](https://github.com/nektos/act/issues/329#issuecomment-1905955589) for ACT to work:
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#with-bake (https://github.com/docker/build-push-action/issues/671#issuecomment-1609106171)
# with example from here https://github.com/crazy-max/docker-linguist/blob/master/.github/workflows/build.yml that uses caching
#
# may have to use this to enable pushing to multiple registries? https://github.com/docker/build-push-action/discussions/1067#discussioncomment-8677682
"line":"[2024-12-26 01:58:48.261 -0500] DEBUG : [App] [Sources] [Jellyfin - unnamed] Last activity was at 01:58:48-05:00 | Next check in 10.00s | No new tracks discovered",
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime &&echo$TZ > /etc/timezone
RUNecho"Node: $(node -v)\nNPM: $(npm -v)"
RUN npm install -g concurrently
ARGdata_dir=/config
VOLUME$data_dir
ENVCONFIG_DIR=$data_dir
COPY docker/root/ /
RUN npm install -g patch-package
COPY docker/root /
WORKDIR/app
FROMbaseasbuild
FROMbaseASbuild
# would very much like to use YARN for dependency management but its got show-stopping bad design for prod dependencies
# https://github.com/yarnpkg/yarn/issues/6323 -- always downloads devDependencies -- and im not about to migrate to v2 since alpine doesn't support it yet
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://docs.multi-scrobbler.app/configuration#webhook-configurations), [healthcheck endpoints](https://docs.multi-scrobbler.app/configuration#health-endpoint), or [prometheus metrics](https://docs.multi-scrobbler.app/configuration/#prometheus).
* Supports [Now Playing](https://docs.multi-scrobbler.app/configuration/clients#now-playing) for scrobble Clients
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
*Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
*Modify data before scrobbling with [regular expression or search patterns](https://docs.multi-scrobbler.app/configuration/transforms)
* Install using [Docker images for x86/ARM](https://docs.multi-scrobbler.app/installation#docker) or [locally with NodeJS](https://docs.multi-scrobbler.app/installation#nodejs)
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -42,17 +73,15 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
<img src="/assets/status-ui.jpg" width="800">
Yes! You can use [Last.fm as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
You set up [configurations](https://docs.multi-scrobbler.app/configuration) for one or more [**Sources**](https://docs.multi-scrobbler.app/configuration/sources) and one or more [**Clients**](https://docs.multi-scrobbler.app/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
### Source
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
A [**Source**](https://docs.multi-scrobbler.app/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
A [**Client**](https://docs.multi-scrobbler.app/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Quick Start
[See the **Quick Start Guide**](https://docs.multi-scrobbler.app/quickstart)
## Installation
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
[See the **Installation** documentation](https://docs.multi-scrobbler.app/installation)
## Configuration
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
[See the **Configuration** documentation](https://docs.multi-scrobbler.app/configuration)
## Usage
@@ -90,7 +123,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
## Help/FAQ
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
Having issues with connections or configuration? Check the [FAQ](https://docs.multi-scrobbler.app/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](https://docs.multi-scrobbler.app/development/dev-common)
console.log(`${importPathPrefix} Import looks like an alias ${dirExists?'(dir exists, file does not)':'(dir does not exist)'} but import types does specify alias`);
console.warn(`${importPathPrefix}${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
#- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#- SPOTIFY_CLIENT_ID=
#- SPOTIFY_CLIENT_SECRET=
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
#- MALOJA_URL=http://maloja:42010
#- MALOJA_API_KEY=
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
# set if using a source/client with redirect URI that you have not explicitly set and MS is NOT running on the same machine that you will view the dashboard from
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
#- BASE_URL=http://MyHostIP:9078
volumes:
- "./config:/config"
#networks:
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
ports:
- 9078:9078# first port is the HOST port multi-scrobbler will serve UI and API on
- TZ=Etc/GMT# Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#- SPOTIFY_CLIENT_ID=
#- SPOTIFY_CLIENT_SECRET=
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
#- JELLYFIN_APIKEY=
#- JELLYFIN_URL=
#- JELLYFIN_USER=
#- MALOJA_URL=http://maloja:42010
#- MALOJA_API_KEY=
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
# set if using a source/client with redirect URI that you have not explicitly set and MS is NOT running on the same machine that you will view the dashboard from
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
#- BASE_URL=http://MyHostIP:9078
volumes:
- /path_on_host/multi-scrobbler-config:/config
- "./config:/config"
#networks:
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
These three [sources](/#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
These sources are **ingress-based** which means that multi-scrobbler waits for the Webscrobbler extension or app using Listenbrainz Endpoint to contact multi-scrobbler, as opposed to multi-scrobbler contacting them.
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:
```
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] Received request from a new remote address: ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59)
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) Received valid data from server examplePlex for the first time.
2023-02-22T10:55:56-05:00 warn : [Plex Request ] Received valid Plex webhook payload but no Plex sources are configured
[2025-07-10 12:32:21.417 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] Received request from a new remote address: ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0)
[2025-07-10 12:32:21.418 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0) Received data for API URL slug '(none)' for the first time.
```
It also logs if a server tries to connect to a URL that it does not recognize:
```
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.100 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
```
**So, if you do not see either of these in your logs then Plex/Tautulli/Jellyfin is not able to connect to your multi-scrobbler instance at all.**
**So, if you do not see either of these in your logs then the Source is not able to connect to your multi-scrobbler instance at all.**
This is not something multi-scrobbler can fix and means you have an issue in your network.
### Troubleshooting
#### Troubleshooting
Check or try all these steps before submitting an issue:
#### Turn on Debug Logging
##### Turn on Debug Logging
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `LOG_LEVEL=debug`:
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `DEBUG_MODE=true`. Then, check the output for any additional information.
* using node `LOG_LEVEL=debug ... node src/index.js`
* using docker `docker run -e LOG_LEVEL=debug ... foxxmd/multi-scrobbler`
##### Check Host name and URL
Check the output for any additional information.
The URLs examples in the [configuration](/configuration#base-url) documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
#### Check Host name and URL
EX `http://localhost:9078/api/webscrobbler` -> `http://192.168.0.140:9078/api/webscrobbler`
The URLs examples in the [configuration](configuration/configuration.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
#### Check Firewall and Port Forwarding
##### Check Firewall and Port Forwarding
If the machine multi-scrobbler is running on has a firewall ensure that port **9078** is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine.
#### Check Source Service Logs
##### Check Source Service Logs
Plex/Tautulli/Jellyfin all have logs that will log if they cannot connect to multi-scrobbler. Check these for further information.
###### Webscrobbler
##### Plex
See [Debugging the extension](https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension) to get logs which should have information about failed requests.
Settings -> Manage -> Console
##### Tautulli
Check the command-line output of the application or docker logs.
##### Jellyfin
Administration -> Dashboard -> Advanced -> Logs
## Jellyfin has warnings about undefined or missing data
Make sure you have
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
You can verify the payload sent from the webhook by modifying your jellyfin configuration to include `logPayload: true` which will output the raw payload to DEBUG level logging:
```json
[
{
"name":"MyJellyfin",
"clients":[],
"data":{
"users":["FoxxMD"],
"options":{
"logPayload":true
}
}
}
]
```
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
## Spotify/Deezer/LastFM won't authenticate
### Spotify/Deezer/LastFM won't authenticate
Ensure any **client id** or **secrets** are correct in your configuration.
@@ -109,9 +61,46 @@ If multi-scrobbler is not running on the same machine your browser is on then th
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
# Configuration Issues
### I get errors with `ECONNRESET` `ETIMEDOUT` `ENOTFOUND` `EAI_AGAIN`
## Config could not be parsed
These are errors indicating a network issue is occuring between you and the Source/Client.
*`ECONNRESET` means the upstream service unexpectedly closed the connection
*`ETIMEDOUT` means the upstream service did not respond to a request MS
*`ENOTFOUND` means that MS could not find a DNS record for the domain of the upstream service
*`EAI_AGAIN` means that a DNS server did not respond to MS's request to lookup the record for a domain
`ECONNRESET` and timeout issues can occur when you have a poor connection or if the upstream service is overloaded or is generally not behaving well. This is a common occurrence for Listenbrainz/Musicbrainz related requests.
Please check the [Upstream Services Status Page](https://status.multi-scrobbler.app) to independently verify if the service is misbehaving. If the service is up and has good uptime then the issue may be in your network.
`ENOTFOUND` and `EAI_AGAIN` and generally caused by DNS caching (stale records) or misbehaving DNS servers. Though this can happen at the upstream service's end, it is *usually* an issue in your network. If you run a Pihole, AdGaurd, or other DNS solution try flushing the cache for the mishaving domain and check that your DNS application is working correctly.
### Youtube Music fails after some time
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
```
Error: Could not send the specified request to browse. Status code: 401
```
then YTM has invalidated your authentication.
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
#### When using OAuth Client Authentication
Refresh your authentication by using the **(Re)authenticate** link from MS's web dashboard.
#### When using Cookies Authentication
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth) and then restart MS to potentially resolve the problem.
## Configuration Issues
### Config could not be parsed
If you see something like this in your logs:
@@ -123,14 +112,130 @@ If you see something like this in your logs:
It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like [JSONLint](https://jsonlint.com/) to find out where errors you have and fix them.
# Scrobbling Issues
## Scrobbling Issues
## Last.fm does not scrobble tracks with multiple artists correctly
### How do I prevent duplicate scrobbles?
Multi-scrobbler has a [robust duplicate scrobble detection system](/configuration/duplicates#how-duplicates-are-detected) that should handle this for you in the majority of usecases. If you have a [simple setup without any circular dependencies](/configuration/duplicates#valid-configs) then you are already protected against duplicates.
If your scenario is more complex or you have a service(s) acting as **both** a **Client** and **Source** you should refer to the [**Duplicate Scrobble Guidance**](/configuration/duplicates#valid-configs) page to determine if you can avoid a configuration that may result in duplicate scrobbles.
### Last.fm does not scrobble tracks with multiple artists correctly
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
## Jellyfin does not scrobble tracks with multiple artists correctly
### Google Cast track information is missing/incorrect or MS player has weird times
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
To diagnose these issues you [**must enable payload logging**](/configuration/sources/google-cast#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
### Google Cast device does not track media
It is likely the app playing on the cast device is incorrectly reporting the media type as **not music**.
MS logs will tell you what type the media is reported as with lines like:
```
My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track
```
Refer to [Allow Unknown Media Type](/configuration/sources/google-cast#allow-unknown-media-type) section to fix this
```
My Artist - Example Track has 'movie' media type so will not track
```
Refer to [Force Media Tracking](/configuration/sources/google-cast#forcing-media-tracking) section to fix this
### VLC is not scrobbling fields correctly
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](/configuration/sources/vlc#vlc-information-reporting)
### Youtube Music misses or duplicates scrobbles
<details>
<summary>How MS detects YTM history</summary>
In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs **a source of truth.** For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).
To compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.
Duplicate scrobbles can also occur if the change between two checks is technically consistent. For instance, if you listen to a track twice in some period, separated by other music, YTM will sometimes "remove" the track from the earlier time (further down in your history) and "re-add" it at the top of the history.
</details>
#### Reporting YTM scrobble issues
If you experience scrobble detection issues you can help improve MS's YTM heureistic by providing thorough feedback as [an issue.](https://github.com/FoxxMD/multi-scrobbler/issues/new?assignees=&labels=bug&projects=&template=01-bug-report.yml&title=bug%3A+) **Please do the following to provide the most useful report:**
##### Turn on Change Detection
In your YTM configuration (`ytmusic.json`) add `logDiff` under `options` like this:
```json
{
"type":"ytmusic",
"name":"MyYTM",
"data":{...},
"options":{
"logDiff":true
}
}
```
or set either ENVs:
*`YTM_LOG_DIFF=true`
* [`DEBUG_MODE=true`](/configuration#debug-mode)
This will cause MS to log YTM history changes similar to this:
```
[Ytmusic - MyYTM] Changes from last seen list:
1. (tuhe1CpHRxY) KNOWER - I’m The President --- undefined => Moved - Originally at 6
2. (Mtg8V6Xa2nc) Vulfpeck - Romanian Drinking Song --- Schvitz => Moved - Originally at 1
3. (rxbCaiyYSXM) Nightmares On Wax - You Wish --- In A Space Outta Sound => Moved - Originally at 2
4. (tMt_YXr90AM) Gorillaz - O Green World --- undefined => Moved - Originally at 3
...
```
Which are essential to troubleshooting this behavior.
##### Turn on File Logging
Optionally, if your logs are busy due to many other sources, you can log **only** YTM logs to a file. Turn this on with the `logToFile` config option:
```json
{
"type":"ytmusic",
"name":"MyYTM",
"data":{...},
"options":{
"logDiff":true,
"logToFile":true
}
}
```
This will cause MS to write YTM logs to a file in your `logs` folder named `ytmusic-MyYTM.log` (based on your source name).
##### Provide Detail and Context
Provide a detailed account of how you were using YTM when the issue occurred, including things like:
* the platform listening on (desktop, mobile, 3rd party client, etc...)
* any changes in platform
* > I switched from desktop to listening on my phone...
* how you were listening to music
* > I was playing an album start to finish
* > I listened to two songs in a row, then browsed for a new song in library by artist, then went back to a song in the queue...
Explain the expected behavior (it should have scrobbled songs x, y, then z) and what actually happened (it scrobbled songs x, then y, then x again, then z)
Provide ALL logs from the time when the issue occurred including logs from BEFORE (ideally 2-3 minutes of logs) and AFTER the issue.
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.
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.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.