Compare commits

...
783 Commits
Author SHA1 Message Date
FoxxMD bc517e0cb0 Merge branch 'develop' 2024-03-26 09:37:03 -04:00
FoxxMD 41900b0048 chore: Bump version for release 2024-03-26 09:36:37 -04:00
FoxxMD 9ea71b931c fix(flatpak): Fix missing package.json needed to run as esm module 2024-03-26 09:04:31 -04:00
FoxxMD 64f222b5b7 fix: Add binary alias(?) to pass appstream guidelines
flathub/io.github.foxxmd.multiscrobbler#14
2024-03-25 09:49:01 -04:00
FoxxMD 6c27406803 fix: Fix colored output being applied to file logging 2024-03-21 17:08:14 -04:00
FoxxMD 2f51e7ad99 feat: Enable colorizing docker log output 2024-03-21 16:29:33 -04:00
FoxxMD d86dd63236 fix: Fix log name to match existing logs names 2024-03-21 15:50:43 -04:00
FoxxMD f794133915 fix: Fix missing parent logger 2024-03-21 15:33:06 -04:00
FoxxMD c7d2e7acc2 feat(ui): Use ansi-to-react for log coloring
Vastly simplifies log formatting on the dashboard
2024-03-21 15:33:06 -04:00
FoxxMD 2dde3840c6 refactor(logging): Replace winston with @foxxmd/logging 2024-03-21 15:33:06 -04:00
FoxxMD 4675847fec fix(flatpak): Reduce summary length 2024-03-11 16:14:45 -04:00
FoxxMD 830d1a2243 fix(flatpak): Add missing developer name
https://github.com/flathub/io.github.foxxmd.multiscrobbler/pull/13#issuecomment-1949888692
2024-03-06 10:10:13 -05:00
FoxxMD 8a43f0ded5 test arm64 building for debian docker image using updated node version #126
Using latest LTS (20) seems to have fixed TLS timeout/connection reset issue seen when trying to `npm ci` on LTS 18
2024-02-19 15:32:24 -05:00
FoxxMD af24e0ffbe chore(docker): Remove unused dependencies 2024-02-19 15:09:55 -05:00
FoxxMD d3a70bf2c1 fix: Add comment for future typed linting 2024-02-19 13:24:53 -05:00
FoxxMD a62fe705f9 fix: Fix lint errors 2024-02-19 13:02:28 -05:00
FoxxMD 1c6407238c feat: Add initial linting config 2024-02-19 12:27:34 -05:00
FoxxMD ec897b185c fix: Fix logger used for tests to always be 'noop'
Cleans up logging output during tests
2024-02-19 10:50:10 -05:00
FoxxMD 69e15b3797 fix: Refactor static arrow function class fields to normal functions #143
Something about using arrow function form for static class methods causes node debugger to crash.

Closes #143
2024-02-19 10:38:59 -05:00
FoxxMD dae4af070f chore: Bump mocha version 2024-02-15 13:01:22 -05:00
FoxxMD beaf0f3bfd chore(!): Upgrade Node to LTS for --import support
So that we can use tsx with official --import rather than requiring (old-style) which doesn't work with string-sameness esm for some reason

https://github.com/mochajs/mocha/issues/5002
https://github.com/mochajs/mocha-examples/pull/76
2024-02-15 12:40:02 -05:00
FoxxMD 8057982184 chore: Upgrade string-sameness
* Removes dependency on unmaintained dice coeffecient library
* New version is esm-only library
2024-02-15 12:27:59 -05:00
FoxxMD 9a5a808780 fix: Remove alt single quote when normalizing strings 2024-02-15 12:05:50 -05:00
FoxxMD d2d5f4a705 chore: Remove more unused packages 2024-02-15 10:10:01 -05:00
FoxxMD 4aa875660f chore: Remove unused es6-error package 2024-02-15 09:25:32 -05:00
FoxxMD 5ab8f00aeb Fix microsecond conversion 2024-02-15 09:23:26 -05:00
FoxxMD 1e1b5f56ce refactor: Replace dbus-next with dbus-ts #142 2024-02-14 16:32:38 -05:00
FoxxMD 49b9ee96f3 fix: Fix ts schema generation import issue
Changing tsconfig resolution caused generation to generate new (bad?) import lines. Replacing with regex from https://github.com/YousefED/typescript-json-schema/issues/582 returned ref/definitions to normal
2024-02-14 09:47:00 -05:00
FoxxMD aef7716634 fix(webscrobbler): Fix missing wildcard to match slug route #137
Closes #137
2024-02-14 09:45:14 -05:00
FoxxMD 57f2e1d500 feat: Log more URL when route is unknown 2024-02-14 09:44:47 -05:00
FoxxMD 41c696992a feat: Add legacy fallback options for subsonic communication #136
* Add config options for using legacy authentication and ignoring TLS cert errors
* Fallback to legacy auth if subsonic error code 41 is detected
2024-02-13 15:59:17 -05:00
FoxxMD f231d4e1e4 ci: Implement test checks for PRs and image publishing 2024-02-08 13:07:13 -05:00
FoxxMD 0efbe41b7e test: Use tsx through mocha config 2024-02-08 12:14:11 -05:00
FoxxMD be9675bb10 fix: Handle when no existing scrobble match with score greater than 0 #130
https://github.com/FoxxMD/multi-scrobbler/issues/130#issuecomment-1934452275
2024-02-08 11:51:00 -05:00
FoxxMD 4e45d35925 test: Add test that handles no existing scrobble match with score greater than 0 #130
Detects track string error thrown reported via https://github.com/FoxxMD/multi-scrobbler/issues/130#issuecomment-1934452275
2024-02-08 11:47:58 -05:00
FoxxMD a5e293554c Merge branch 'develop' 2024-02-06 09:28:35 -05:00
FoxxMD c76d4822fe docs: Fix which host debian image is available for 2024-02-06 09:28:22 -05:00
FoxxMD 0f8fd5cc0e chore: Forgot to bump version in lock file 2024-02-06 09:28:02 -05:00
FoxxMD 8b27890254 Merge branch 'develop' 2024-02-05 11:53:18 -05:00
FoxxMD 8747c8375e chore: Bump version 2024-02-05 11:52:47 -05:00
FoxxMD 50f27c110c Merge branch 'develop' 2024-02-05 11:47:40 -05:00
FoxxMD c1f8997c01 feat: Implement remaining recently played endpoints 2024-02-05 11:40:06 -05:00
FoxxMD 12ed767961 ci: Bump action versions to fix node deprecations 2024-02-05 10:47:58 -05:00
FoxxMD 335808217b docs: Add debian docker image network workaround to docs #126 #134 2024-02-05 10:38:15 -05:00
FoxxMD b5bb7024fc docs: Add workarounds for rollup issues and flatpak disclaimer 2024-02-05 10:28:40 -05:00
FoxxMD ac2806dc66 fix(frontend): Fix scss division deprecation warnings 2024-02-05 10:14:11 -05:00
FoxxMD 9d149dab49 Try to use direct node binary distribution #126 2024-02-02 12:55:58 -05:00
FoxxMD 451404c0f9 Disable arm64 debian builds and document issues in dockerfile #126 2024-02-02 11:05:09 -05:00
FoxxMD a3f1f077ce Revert fancy default negation
Doesn't work
2024-02-01 12:18:30 -05:00
FoxxMD dcd13833ae Ignore secrets files 2024-02-01 12:16:03 -05:00
FoxxMD 78b732b75c Update metadata-action version 2024-02-01 11:55:57 -05:00
FoxxMD 3152933937 chore: Add matrix for building debian flavor tag 2024-02-01 11:54:40 -05:00
FoxxMD 6c6126bb27 chore: make container image build tags agnostic to default branch name 2024-02-01 11:48:15 -05:00
FoxxMD ad13b7687d feat: Implement debian-based image #126 2024-02-01 11:39:05 -05:00
FoxxMD 633e366e82 feat: Improved logging for last.fm api network errors #115 2024-01-31 15:52:25 -05:00
FoxxMD 69b1786859 fix: Add patch-package as postinstall script so packages actually get patched 2024-01-31 14:09:31 -05:00
FoxxMD 41839a961a chore: Ignore apple cert extensions 2024-01-31 14:09:06 -05:00
FoxxMD ccb50c64bb fix(lastfm): Patch lfm library to actually implement a request timeout #134 2024-01-31 13:15:20 -05:00
FoxxMD d88ac1b424 feat(source): Start implementing recent from API 2024-01-31 12:36:35 -05:00
FoxxMD c807611e55 fix: Revert debug statement 2024-01-31 11:51:20 -05:00
FoxxMD 155ec1403e feat(sources): Improve Source of Truth usage and presentation to user #134
* 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
2024-01-31 11:46:22 -05:00
FoxxMD ecde657c3e feat(lastfm): Implement nowPlaying lfm api call 2024-01-31 10:01:12 -05:00
FoxxMD 6aa8374f25 feat: Add debug logging for user api requests 2024-01-31 09:30:58 -05:00
FoxxMD c2f01473fd fix(scrobbler): Fix erasing non-english characters #121 2024-01-30 14:47:19 -05:00
FoxxMD 41e607c995 refactor(scrobbler): Use simpler method for handling queued scrobbles #130 2024-01-30 13:40:52 -05:00
FoxxMD fee754f15f feat(chrome): Used improved library 2024-01-30 13:24:09 -05:00
FoxxMD d62d18232d chore: Remove some unused packages 2024-01-30 10:30:33 -05:00
FoxxMD a1acab0fed Merge branch 'develop' 2024-01-29 10:35:27 -05:00
FoxxMD d39a7099c7 docs(cast): Add disclaimer about status of source support 2024-01-29 10:34:13 -05:00
FoxxMD 7b2c4f99f3 chore: Bump version for release 2024-01-29 10:33:57 -05:00
FoxxMD b941ff08dc Merge branch 'develop' 2024-01-29 10:25:06 -05:00
FoxxMD 210a74c75f chore: Bump version for release 2024-01-29 10:24:50 -05:00
FoxxMD eafce71cd0 chore(flatpak): Update flatpak release version 2024-01-29 10:24:08 -05:00
FoxxMD d5777b501d chore(flatpak): Update flatpak manifest to work with new app structure 2024-01-29 10:21:44 -05:00
FoxxMD dd60e5eb21 fix(flatpak): Fix missing destination when building locally 2024-01-29 09:49:33 -05:00
FoxxMD c57dbf23e1 chore(maloja): Deprecate support for versions below 3.0.0 2024-01-26 10:31:23 -05:00
FoxxMD cb52adb4de feat: Log when heartbeat tasks start 2024-01-26 10:17:17 -05:00
FoxxMD a75b0fc549 feat(chrome): Reduce device rejection logging noise
Change device rejection log to verbose if we've seen the blacklisted device before
2024-01-26 10:12:20 -05:00
FoxxMD 1a546b72d2 docs(chrome): Add more google cast configuration docs and update TOCs 2024-01-26 09:55:50 -05:00
FoxxMD fd13d268d0 fix(chrome): Typo 2024-01-25 16:47:10 -05:00
FoxxMD 5837fc11a5 fix: Better handling of uncaught exceptions and rejections 2024-01-25 10:54:44 -05:00
FoxxMD efdbbe40e8 fix(chromecast): Remove debugging interval 2024-01-25 09:52:39 -05:00
FoxxMD 5103cf1c12 feat(chromecast): Catch buffering media 2024-01-25 09:50:23 -05:00
FoxxMD e855a3de7d feat(chromecast): Try to connect all discovered interfaces in the event one fails 2024-01-25 09:43:15 -05:00
FoxxMD b90e38d595 fix(chromecast): Fix black/white app name variable assign 2024-01-25 09:15:53 -05:00
FoxxMD 1864f39be0 fix(chromecast): Fix unknown play tracking 2024-01-25 09:11:44 -05:00
FoxxMD 89e80b6f8b fix(chromecast): Debounce avahi discovery 2024-01-24 14:47:15 -05:00
FoxxMD 564295ee64 feat(spotify): Prepare for future spotify lib migration by writing completing credential data
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.
2024-01-23 15:10:33 -05:00
FoxxMD 61c7f26007 fix: Fix where retries are checked 2024-01-23 11:37:51 -05:00
FoxxMD 54b9ece5b6 fix: Fix when to check connection
Since all sources can now control if they need to check connection or not do not restrict to just those that can poll
2024-01-23 11:27:54 -05:00
FoxxMD 335136561a fix(chromecast): Better handling of client offline events
* 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)
2024-01-23 10:45:12 -05:00
FoxxMD 56f1cacd07 fix: Fix return values for source init startup 2024-01-23 09:41:40 -05:00
FoxxMD 558e358d0b fix: Fix default import usage 2024-01-22 13:25:31 -05:00
FoxxMD 290e6eecc4 fix: Fix import missing extension 2024-01-22 13:25:17 -05:00
FoxxMD f512f55782 feat(deezer): Fix credentials check during build data stage 2024-01-22 13:21:00 -05:00
FoxxMD ad6eba58d2 feat: improve data init stage log wording 2024-01-22 13:13:29 -05:00
FoxxMD b675aee8ff fix: Fix status text based on initialization OK values 2024-01-22 13:10:12 -05:00
FoxxMD c29ea9c89c fix: Fix status indicator color based on status text 2024-01-22 13:09:43 -05:00
FoxxMD 50016a31a8 fix: Fix isReady logic 2024-01-22 13:09:32 -05:00
FoxxMD 1cecffc30f feat(subsonic): Improve error/response parsing and report errors correcetly on startup #129
* 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
2024-01-22 12:00:53 -05:00
FoxxMD 833273c19d feat: Improve discovery for network exceptions in error causes 2024-01-22 12:00:50 -05:00
FoxxMD 35603d0caf chore: update ChromecastSource to comply with new initialization flow 2024-01-22 12:00:47 -05:00
FoxxMD de1377afdd fix: actually throw auth failure so initialization fails correctly 2024-01-22 12:00:43 -05:00
FoxxMD a22ddd10cc Merge branch 'develop' into sourceInitializeRefactor
# Conflicts:
#	src/backend/scrobblers/LastfmScrobbler.ts
#	src/backend/sources/DeezerSource.ts
#	src/backend/sources/MopidySource.ts
#	src/backend/sources/SpotifySource.ts
#	src/backend/sources/SubsonicSource.ts
#	src/backend/tasks/heartbeatSources.ts
2024-01-22 11:59:59 -05:00
FoxxMD 280add7107 Prepare for reverse proxy usage #125
Not working correctly yet but no BC for regular, non suburl, usage
2024-01-19 11:40:15 -05:00
FoxxMD b34abcab56 Update index.html 2024-01-19 11:05:34 -05:00
FoxxMD 788b26949e Fix uncaught Vite startup errors 2024-01-19 11:00:09 -05:00
FoxxMD c11da3df3d Working dockerfile 2024-01-18 16:59:45 -05:00
FoxxMD 0d68fea0eb Working app version indicator 2024-01-18 16:41:35 -05:00
FoxxMD c98672bcef Simplify port usage 2024-01-18 15:23:56 -05:00
FoxxMD bb0c079757 Cleanup package.json 2024-01-18 15:23:47 -05:00
FoxxMD 891144546e Simplify (and fix) test tooling for new esm stuff 2024-01-18 15:06:25 -05:00
FoxxMD 86611d764d Cleanup 2024-01-18 14:36:28 -05:00
FoxxMD e604268004 It works! 2024-01-18 14:18:08 -05:00
FoxxMD 396c3da277 Convert all relative backend endings to js 2024-01-18 13:58:31 -05:00
FoxxMD 0591b90089 somewhat working esm conversion 2024-01-18 13:15:06 -05:00
FoxxMD 6763be53ac Use vite-express to migrate from CRA
TODO convert backend to esmodule?
2024-01-18 11:52:13 -05:00
FoxxMD aee23f7550 fix(chrome): Remove device if it fails to refresh after 6 retries 2024-01-17 10:11:53 -05:00
FoxxMD 57b5528d43 docs(chrome): Update device connection instructions 2024-01-16 11:40:19 -05:00
FoxxMD bd8cf18a1a feat(chrome): refactor mdns discovery to run on source heartbeat as well as startup 2024-01-16 11:29:43 -05:00
FoxxMD 84acdd901a refactor: Move mdns discovery into own functions
May be useful for other sources...
2024-01-16 11:10:49 -05:00
FoxxMD e213a95996 fix(chrome): Refactor error handling for client and mdns libraries to catch errors properly 2024-01-16 09:52:41 -05:00
FoxxMD 78cd2eecf9 feat(chrome): Allow manual device connections 2024-01-15 16:11:33 -05:00
FoxxMD ebfeea1087 feat(chrome): Implement user filtering media by type 2024-01-15 16:02:10 -05:00
FoxxMD ddca623792 Fix error causing schema to generate 2024-01-15 16:01:36 -05:00
FoxxMD f0da35d115 docs: Add local source reference to flatpak manifest 2024-01-15 14:18:29 -05:00
FoxxMD 10eb355f92 chore(chromecast): Switch to scoped chromecast-client to fix flatpak source generation limitation 2024-01-15 14:18:14 -05:00
FoxxMD 85702c534a feat(maloja): Improve error handling
* 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
2024-01-15 12:45:01 -05:00
FoxxMD e2dfe317a9 feat(chrome): Improve mdns discovery for docker
* 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/
2024-01-12 17:00:05 -05:00
FoxxMD 7008ae8672 feat(chrome): Handle applications with transportId context
* 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
2024-01-12 13:26:53 -05:00
FoxxMD 10d9fa3581 feat(chrome): Improved app logging
Use child winston logger for better readability
2024-01-12 12:58:14 -05:00
FoxxMD 2b85927739 feat(chrome): Don't use bad media data as a play
* Check media data for missing artist or track and skip play if either is missing
* Remove player if bad data for 60 seconds
2024-01-12 12:31:35 -05:00
FoxxMD 8c402238ed feat(chrome): Add user debugging 2024-01-12 11:38:52 -05:00
FoxxMD 758770d278 refactor(chrome): Use forked chromecast-client lib
* Instead of patching using forked lib with validation fixes and improvements
* Print raw data if validation error is found
2024-01-12 11:12:21 -05:00
FoxxMD ee7ebb159b fix(chrome): Fix ENV names 2024-01-12 10:14:31 -05:00
FoxxMD f47a66060f refactor(chrome): Proper package patch for optional/nullish validation fixes
https://github.com/dantaylor3/chromecast-client/pull/14
2024-01-12 10:14:17 -05:00
FoxxMD 8929a630ed docs: chromecast docs and schema 2024-01-11 15:25:06 -05:00
FoxxMD 077f5d7547 feat: Improve chromecast handling 2024-01-11 14:51:52 -05:00
FoxxMD 369ad42946 feat: Add initial Chromecast source 2024-01-10 16:43:34 -05:00
FoxxMD 4bd996eb46 Merge branch 'develop' 2024-01-10 08:45:47 -05:00
FoxxMD a6ddc2b9f5 chore: Bump version for release 2024-01-10 08:45:10 -05:00
FoxxMD afb2e76b2d feat: Improve track credits parsing to recognize post-feat suffixes
* 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
2024-01-09 11:50:42 -05:00
FoxxMD 47812546e3 Merge branch 'playCompletedContext' into develop 2024-01-08 14:07:02 -05:00
FoxxMD 5ab6b85b30 refactor(source): Implement more granular initialization
* 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
2024-01-05 13:19:34 -05:00
FoxxMD 9709d6348a refactor(source): Start consolidating isReady behavior 2024-01-05 11:15:30 -05:00
FoxxMD 10fea9b23d feat(source): Improve (re)start behavior and ui
* 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
2024-01-05 11:06:09 -05:00
FoxxMD 051dd5d128 feat(player): Improved player deletion behavior
* 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
2024-01-04 11:10:39 -05:00
FoxxMD 369e18b1c2 feat(player): Handle state transfer 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.
2024-01-04 10:57:03 -05:00
FoxxMD fd49d17de4 Remove unused import 2024-01-04 08:49:14 -05:00
FoxxMD e0f034fa99 feat: Implement play completion context #121
* 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
2024-01-03 12:42:27 -05:00
Matt Foxx 80bc629ac2 Merge pull request #123 from Bujiraso/issue-122
Correct json-schema.app links
2024-01-02 20:54:39 -05:00
Bujiraso 1a90c23e76 fix: correct json-schema.app links
* Add missing 'backend' folder
  * Change 'develop' branch to 'master'
2024-01-02 18:41:44 -04:00
FoxxMD 48ddd4272f feat(scrobbler): Improve log readability for duplicate checking 2024-01-02 15:38:13 -05:00
FoxxMD ea408ac7a4 feat(scrobbler): Use play data to extract non-joined track name when comparing for duplicate check 2024-01-02 15:25:38 -05:00
FoxxMD 79c48af3e9 chore(tests): Fix TS compile warning on test for bad data 2024-01-02 15:24:24 -05:00
FoxxMD 12c2c0a132 feat(scrobbler): Change fuzzy diff threshold to be inclusive 2024-01-02 14:46:33 -05:00
FoxxMD 78abfc2168 refactor: Temporal comparison result data improvements #121
* 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
2024-01-02 13:55:53 -05:00
FoxxMD 0f721b94c2 refactor: Move Play time comparison code into own file 2024-01-02 13:02:44 -05:00
FoxxMD f126bf0ec0 refactor: Improve string comparison robustness
* 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
2024-01-02 12:38:24 -05:00
FoxxMD 064a435c79 Simplify debug mode usage for testing 2024-01-02 12:34:19 -05:00
FoxxMD c0e5bb872a fix(lastfm): Fix handling of non-string mbid string values #114 2024-01-02 11:09:51 -05:00
FoxxMD e9561b5783 fix(jellyfin): Split musicbrainz mbid artist identifier semicolon #114
Jellyfin can apparently return multiple artist mbids but does so in one string deliminated by a semicolon
2023-12-28 10:07:11 -05:00
FoxxMD 77c3a2f0c7 fix(listenbrainz): Treat 400 responses as non-showstopping #114 2023-12-28 09:56:25 -05:00
FoxxMD db4e64dc2a Merge remote-tracking branch 'origin/develop' into develop 2023-12-27 11:55:45 -05:00
FoxxMD fcc05a8b19 feat(scrobblers): Implement user-initiated (re)start mechanism #114
* Add api endpoint for restarting client
* Implement (re)start link on client card
2023-12-27 11:50:57 -05:00
FoxxMD c37d94e71b refactor(scrobblers): Consolidate scrobbler client startup surface area
* 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
2023-12-27 11:02:15 -05:00
FoxxMD aa59bc4607 Fix service documentation link #119 2023-12-20 13:31:40 -05:00
FoxxMD a993df4f0b docs: Add contributing guidelines and PR template 2023-12-12 10:01:58 -05:00
Matt Foxx 7e83e0f085 Merge pull request #118 from brunohpaiva/develop
fix(spotify): undefined player state data when in private session
2023-12-12 09:40:17 -05:00
Bruno Henrique Paiva 5b2449adb0 fix(spotify): undefined player state data when in private session 2023-12-12 11:37:47 -03:00
FoxxMD 5811912745 feat(webscrobbler): Log the best-guess URL for requests using configured slug 2023-12-04 11:13:20 -05:00
FoxxMD b7d6e9e497 docs: Update schema for logging enums 2023-12-04 11:06:14 -05:00
FoxxMD 5fd0cc3fe4 docs(webscrobbler): Move slug into data #111 2023-12-04 11:06:02 -05:00
FoxxMD 6e2e6f4492 fix(webscrobbler): Correct ENV names to match those documented #111 2023-12-04 11:02:19 -05:00
FoxxMD ed6f7cfe5f docs(webscrobbler): Add where to find connector list #112 2023-12-04 10:47:27 -05:00
FoxxMD 6b17babd7a fix(mopidy): Fix album artist parsing #110
Should be using mapped artist string name rather than model but cannot test due to mopidy being difficult. Hopefully this works?
2023-11-30 11:15:49 -05:00
FoxxMD c77e4355fc fix(player): Fix listen range default value 2023-11-30 11:14:52 -05:00
FoxxMD 25de65f41f chore: bump package-lock version 2023-11-30 11:13:59 -05:00
FoxxMD af037ee215 Merge branch 'develop' 2023-11-29 11:41:17 -05:00
FoxxMD 39c4764b40 chore: Bump releases in flatpak 2023-11-29 11:40:59 -05:00
FoxxMD ec28d2aeb5 Merge branch 'develop' 2023-11-29 11:22:22 -05:00
FoxxMD baf8635587 chore: Bump version for release 2023-11-29 11:22:07 -05:00
FoxxMD a3839a28a2 feat(scrobbler): Use listenedFor when checking for existing scrobbles
* Parse listenedFor from maloja response data
* Compare listenedFor against playdate diff when comparing fuzzy times for existing scrobbles
2023-11-28 11:43:45 -05:00
FoxxMD 4247f545c5 feat(player): Pad scrobble listen duration when confidence is high #108
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
2023-11-16 13:24:26 -05:00
FoxxMD c54c3ff5de docs(spotify): Add note about duration accuracy when using Spotify's Automix feature 2023-11-16 12:38:45 -05:00
FoxxMD 1d84f07a40 feat(source): Smart interval calculation based on track position and end time #108
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.
2023-11-16 12:29:55 -05:00
FoxxMD b47c80dc11 fix(source): Fix polling interval drift
* 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
2023-11-16 12:27:03 -05:00
FoxxMD 75e2b2b337 fix(player): Correctly output value of play timestamps for player based on undefined or not #107
Fixes #107
2023-11-15 10:35:00 -05:00
FoxxMD 08ca7fff35 feat: Add app version output
Output to logs and display in front end next to header
2023-11-13 12:25:07 -05:00
FoxxMD 3140df28b7 non working version display 2023-11-13 11:50:05 -05:00
FoxxMD 14546b8d74 fix(lastfm): Fix api error when listening history is private by sending session key #105
Thanks @FozzieHi for doing the legwork on finding the solution for this

Closes #105
2023-11-13 10:40:06 -05:00
FoxxMD 341e896eca feat: Implement album artist for all sources #104 2023-11-07 11:44:36 -05:00
FoxxMD b3b4636e7f feat: Implement album artist and scrobble client payload function
* 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
2023-11-07 10:54:14 -05:00
FoxxMD 43f5aeb5dc fix(plex): Use album artist if present and remove "Various" artist #104 2023-11-06 12:41:05 -05:00
FoxxMD d38aabf7b8 chore(flatpak): Update metainfo and sources 2023-11-03 15:58:37 -04:00
FoxxMD c64b8d16e6 fix(spotify): Improve api error handling #103
* 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
2023-11-02 11:20:42 -04:00
FoxxMD 1938fc3a27 fix: Catch errors during source backlog processing #103 2023-11-02 11:16:54 -04:00
FoxxMD 36181396c7 refactor: don't stringify error when using upstream options 2023-11-02 11:16:26 -04:00
FoxxMD bb76a3b214 fix: test auth correctly on auth flow completion 2023-11-02 11:16:02 -04:00
FoxxMD 339bb7fcd2 refactor: Simplify network error detection during auth test 2023-11-02 10:07:02 -04:00
FoxxMD 3a25057d05 fix(maloja): Fix missing return value for auth test 2023-11-02 09:49:03 -04:00
FoxxMD 14ab6032f5 feat: Refactor auth testing to differentiate between network issues and actual auth errors #102
* 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
2023-11-01 15:15:09 -04:00
FoxxMD 3e1f785437 Add pony-cause find by function helper 2023-11-01 15:09:06 -04:00
FoxxMD 91bb9d6089 feat: Allow disabling console and file logging through ENV
Used internally for controlling logging when testing
2023-11-01 15:08:26 -04:00
FoxxMD 9cf373e9f4 chore: Update string-sameness version 2023-11-01 09:39:37 -04:00
FoxxMD 9ec22e9000 Merge branch 'develop' 2023-10-24 09:20:25 -04:00
FoxxMD 9616860e23 chore: Bump version for release 2023-10-24 09:20:04 -04:00
FoxxMD 0f6638dfdf docs: Add queued scrobble feature highlight 2023-10-24 08:56:14 -04:00
FoxxMD fe9f49779c feat: Implement queued scrobbles display
* Show number of queued scrobbles for clients in ui
* Implement live updates to queue count
2023-10-19 13:40:43 -04:00
FoxxMD 3aab6a207b feat: Implement live client/source status updates 2023-10-19 11:53:48 -04:00
FoxxMD 49e9fd4b3b feat: Implement live updates for dead letter and scrobble counts 2023-10-19 11:41:06 -04:00
FoxxMD 0143ae62aa feat: Implement retry/remove all dead letter scrobbles
* Implement API endpoints
* Implement front-end logic and actions on dead letter page
2023-10-19 11:03:04 -04:00
FoxxMD 2882f65b69 refactor: Reduce noisy logging at INFO level 2023-10-19 10:36:57 -04:00
FoxxMD 62e9f7f620 feat: Log storage improvements and limit controls
* 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
2023-10-13 14:28:24 -04:00
FoxxMD 57f2ebf2b5 feat: Add dead letter error to data 2023-10-13 12:12:46 -04:00
FoxxMD 884134cab9 Simplify dead letter data clear
Just reset state on GET pending instead of using effect cleanup in react component
2023-10-13 11:38:38 -04:00
FoxxMD 0c90de78ab Implement full-fat deadletter reducer
couldn't figure out a better way to handle this with just the api endpoint code
2023-10-13 11:34:22 -04:00
FoxxMD 1c5530ee2a fix: deadletter endpoint responses
Send empty response so RTK Query correctly parses response
2023-10-13 11:33:47 -04:00
FoxxMD fbb67a8fe6 feat: Implement dead letter endpoints
* 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)
2023-10-11 12:38:43 -04:00
FoxxMD c8c5f5dcb7 docs(webscrobbler): Add disclaimer about hostname for firefox 2023-10-06 09:44:29 -04:00
FoxxMD eb449eeca6 feat: Implement WebScrobbler source 2023-10-05 13:58:20 -04:00
FoxxMD 7cfdb10813 chore: Update flatpak commit 2023-10-05 10:10:58 -04:00
FoxxMD be599b5562 Merge branch 'develop'
# Conflicts:
#	flatpak/io.github.foxxmd.multiscrobbler.metainfo.xml
2023-10-04 10:05:50 -04:00
FoxxMD 07d97a4937 chore: Bump version numbers 2023-10-04 10:05:08 -04:00
FoxxMD 7037d6f833 Merge branch 'develop' 2023-10-04 09:13:21 -04:00
FoxxMD 2deca322e7 fix(ui): For non-polling sources use last activity date for status
So that status changes if player is updated instead of waiting for track to be discovered
2023-10-03 14:16:07 -04:00
FoxxMD ab84cb396d docs(jellyfin): Add request header workaround for jellyfin #101 2023-10-03 14:08:07 -04:00
FoxxMD bb63c61ca8 docs: Clarify subsonic api compatibility 2023-10-03 14:00:57 -04:00
FoxxMD f4256d1084 docs: Update dashboard screenshot 2023-10-03 13:56:27 -04:00
FoxxMD fc87b86f52 test(player): Add player tests 2023-10-03 12:36:00 -04:00
FoxxMD 5dff617ed6 fix(player): Fix some bugs and add param for easier testing
* 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
2023-10-03 12:35:48 -04:00
FoxxMD f2a171c596 refactor(player): Move listen progress/range functionality into own class (SRP) 2023-10-03 11:06:44 -04:00
FoxxMD 336970c1f7 feat(ui): Improve status display
* 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
2023-10-03 10:15:18 -04:00
FoxxMD d04bc038d4 feat(ui): Implement status indicator 2023-10-03 10:13:45 -04:00
FoxxMD 1ab12355f3 feat(player): Implement seek and repeat detection
* Start new listening ranges if player has seeked forward/backwards
* Treat track repeats as new plays
2023-10-02 17:22:59 -04:00
FoxxMD fb5d9fb7b0 feat(player): Log when play fails discovery on player change 2023-10-02 13:54:55 -04:00
FoxxMD 9aae57f966 test(scrobble): Add tests for scrobble processing queue and existing comparisons 2023-10-02 13:39:33 -04:00
FoxxMD fefc82c521 feat(scrobble): Improve existing scrobble comparison logic
* Use existing time comparison function with "close" matching
* Fix wording and return value
2023-10-02 13:39:14 -04:00
FoxxMD 43ebdcb10c refactor(scrobble): Refactor scrobble delay and scrobble processing sleep behavior
* 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
2023-10-02 13:38:08 -04:00
FoxxMD adfb166320 feat(scrobble): Implement heartbeat task for scrobblers
Task does scrobble processing restart and dead letter queue processing
2023-09-29 15:07:44 -04:00
FoxxMD 1c54ae6465 feat(scrobble): Implement dead letter queue processing
* 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)
2023-09-29 15:07:08 -04:00
FoxxMD 9309f7d8cf feat(scrobble): Initial scrobble queue implementation
* 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
2023-09-29 14:15:32 -04:00
FoxxMD 8717fad08a feat(source): Control backlogging via config #100 2023-09-29 11:28:32 -04:00
FoxxMD 983b4c7417 feat(maloja): Add listened duration to scrobble payload 2023-09-28 15:23:14 -04:00
FoxxMD a663e9af14 feat(player): Defer play discovery so listenedFor is accurate
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
2023-09-28 15:22:22 -04:00
FoxxMD 249a606bf5 feat(scrobble): Implement artist dup bonus scoring heuristic
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.
2023-09-28 12:19:11 -04:00
FoxxMD 0ec8d8814d fix(scrobble): Fix not using reference duration for fuzzy comparison 2023-09-28 11:25:33 -04:00
FoxxMD f0cee72f43 test(scrobble): Improve test durability by using mixed duration sources 2023-09-28 11:01:07 -04:00
FoxxMD a82aafa6b8 test(scrobble): Add test for duplicate when exact title/time 2023-09-28 10:47:07 -04:00
FoxxMD 3b624205ad fix: Fix missing destructure default value 2023-09-28 10:35:38 -04:00
FoxxMD c6d38e40d0 refactor(scrobble): Adjust dup score weights 2023-09-28 10:31:45 -04:00
FoxxMD 1ca7834feb test(scrobble): Add more tests for similar-but-unique scrobble detection 2023-09-28 10:31:06 -04:00
FoxxMD 77c9977d27 Use string highScore instead of weighted since cosine isn't messing things up anymore 2023-09-28 10:29:34 -04:00
FoxxMD 521130562d feat(scrobble): Use new string approach for artists 2023-09-28 09:55:09 -04:00
FoxxMD 9512ca7e84 feat(scrobble): Use new string approach for track
And move weights to constants
2023-09-27 16:01:09 -04:00
FoxxMD 159901f79e refactor(tests): Move jellyfin/listenbrainz test into own folders 2023-09-27 15:44:22 -04:00
FoxxMD 9758568ded feat: Implement new string comparison logic 2023-09-27 15:43:44 -04:00
FoxxMD a953f83d07 test(scrobble): Add more tests 2023-09-27 14:14:49 -04:00
FoxxMD 2bbe2e6ba0 refactor(scrobble): Print score breakdown in different statement
Is more readable
2023-09-27 14:14:34 -04:00
FoxxMD ec4ab8c078 test(scrobble): Implement basic scrobble tests 2023-09-27 13:20:03 -04:00
FoxxMD 7885c4ca9c fix(scrobble): Fix wrong artists used for comparison 2023-09-27 13:19:25 -04:00
FoxxMD 8371ec3a01 fix(scrobble): Fix config destructing default value 2023-09-27 13:18:58 -04:00
FoxxMD 43cbadcd2d refactor(scrobble): Clean up base design a little
* Make alreadyScrobbled abstract function
* Make sorting recentScrobbles automatic by refactoring into a getter/setter
2023-09-27 13:18:20 -04:00
FoxxMD 6e1e2c3534 Add verify example for body parser 2023-09-26 15:38:04 -04:00
FoxxMD a46389e745 Remove unused import 2023-09-26 14:08:02 -04:00
FoxxMD ce5f8d5fca fix(jellyfin): Enable parsing requests with more content-types and add logging on invalid types
* 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
2023-09-26 14:03:03 -04:00
FoxxMD bdbbce4cb0 feat(jellyfin): Initial testing suite 2023-09-26 11:39:34 -04:00
FoxxMD 034cb321ef fix(flatpak): Add missing release node 2023-09-21 09:12:07 -04:00
FoxxMD ac2d453aa2 feat(config): Implement property to control whether a source/client is used
* 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
2023-09-20 11:31:15 -04:00
FoxxMD 860d2d953f fix(youtube): Use updated yt music library #98 2023-09-19 09:22:22 -04:00
FoxxMD a486102747 feat(lastfm): Add player support
* 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
2023-09-18 15:21:25 -04:00
FoxxMD 735264cb2b feat(listenbrainz): Add player support #74
* Use listenbrainz 'playing-now' endpoint for MS player implementation
* Do not use MS player as source of truth -- continue to use LZ listen history
2023-09-18 14:58:18 -04:00
FoxxMD 7dece447df feat(player): Add sourceOfTruth variable to control logging and future use
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
2023-09-18 14:56:23 -04:00
FoxxMD 3fb5d6aae9 fix(ui): Remove duration percent display when duration is zero 2023-09-18 14:40:22 -04:00
FoxxMD d5dfc33fce fix(deezer): Fix deezer passport generation 2023-09-14 12:51:16 -04:00
FoxxMD 6dc40e50ad feat: Implement Base URL
* 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
2023-09-14 12:38:20 -04:00
FoxxMD b8ebd8ca7e chore: Ensure production mode for webpack without relying on env 2023-09-14 10:34:07 -04:00
FoxxMD e9eb7d357f fix*(scrobblers): Test auth when client completes auth callback 2023-09-14 10:07:44 -04:00
FoxxMD 3f2cb8ac0c fix: Pass localUrl to scrobble clients for use by lastfm 2023-09-14 10:07:12 -04:00
FoxxMD 8e36c27ec9 fix(backend): Fix how port is determined and used for localUrl 2023-09-14 10:06:42 -04:00
FoxxMD fa90a3b821 fix: Load variables from .env for backend
Was being used for frontend (cra) but not applied to backend
2023-09-14 10:05:24 -04:00
FoxxMD 2d2319fa27 fix(ui): Fix wrong route for client auth #97 2023-09-14 09:08:01 -04:00
FoxxMD ba161b7b92 chore: Remove unused dashboard backend endpoint 2023-09-14 09:07:36 -04:00
FoxxMD d438c8b256 fix(docker): Normalize ownership of node_modules files
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
2023-09-13 14:55:04 -04:00
FoxxMD bbb4555c0e chore(docker)!: Bump lsio base image version to 3.18
* Updates alpine 3.17 -> 3.18
  * Main reason is to potentially fix DNS issues related to musl #88
    * https://github.com/nodejs/node/issues/15780#issuecomment-334496907 and https://bell-sw.com/blog/how-to-deal-with-alpine-dns-issues/
    * Resolved in musl 1.2.4
      * https://gitlab.alpinelinux.org/alpine/tsc/-/issues/43
      * https://www.openwall.com/lists/musl/2023/05/02/1
  * Bumps npm 9.1.2 -> 9.6.6

New/changed ENVs used by LSIO may require removing "stored" ENVs if using portainer
2023-09-13 13:21:44 -04:00
FoxxMD 6e8ea96f32 fix: Fix js endings 2023-09-13 12:19:58 -04:00
FoxxMD b06de77f42 Merge branch 'player' into develop 2023-09-13 12:13:17 -04:00
FoxxMD 3aa8279914 Merge branch 'develop' 2023-09-13 12:02:51 -04:00
FoxxMD a64724e114 chore: Bump version for release 2023-09-13 12:02:38 -04:00
FoxxMD 052234a3d9 Merge branch 'develop' 2023-09-13 12:02:04 -04:00
FoxxMD aa2c565afd fix(ui): Log line parsing missing line terminator capture 2023-09-13 11:59:27 -04:00
FoxxMD d0fd225d99 chore: Use constant for default retry multiplier 2023-09-13 11:40:29 -04:00
FoxxMD c40acdd7c7 feat!: Decrease default polling interval
* 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
2023-09-13 11:36:22 -04:00
FoxxMD 590610022d feat!: Set default scrobble thresholds to standard values #73
https://github.com/krateng/maloja/blob/master/API.md#scrobbling-guideline
https://www.last.fm/api/scrobbling

The defaults are 50% of the track or 4 minutes.
2023-09-13 10:49:54 -04:00
FoxxMD f7dae32f50 feat(maloja): Generate web links for scrobbled tracks 2023-09-12 16:01:32 -04:00
FoxxMD 3cbbc0837f feat(ui): Expand displayed plays
* Implement API for fetching client recently scrobbled
* Reduce text noise by combining track stats and link for recent
2023-09-12 15:46:49 -04:00
FoxxMD 0197e70a1a feat(ui): Remove unnecessary wording for track stats 2023-09-12 14:58:20 -04:00
FoxxMD 40a7d9e398 feat(ui): Make player dates human friendly 2023-09-12 14:57:41 -04:00
FoxxMD c7b2dcbbbe fix(ui): Use normal match parameters
groups are undefined probably due to minification?
2023-09-12 12:11:39 -04:00
FoxxMD a524e18820 feat(ui): More ui compaction
* 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
2023-09-12 12:03:49 -04:00
FoxxMD 81894d691b feat(ui): Compact ui a bit
* 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
2023-09-12 11:07:39 -04:00
FoxxMD 0fb192dd41 refactor(ui): Prevent nodemon from restarting on tailwind config change 2023-09-12 11:05:56 -04:00
FoxxMD 3f608c6c8e fix(listenbrainz): Artist parsing joiner phrases must begin with non-word character
Prevents artists with joiner phrases in their name (EX 'Daft Punk' contains 'ft') from erroneously being split

Fixes #94
2023-09-12 09:29:04 -04:00
FoxxMD dcff9ee73d feat(player state): Implement player status and listened duration in UI 2023-09-11 13:21:24 -04:00
FoxxMD f1cd66d3e8 feat(player state): Preserve position when player is not in stopped status
Makes for a nicer UI experience to see where the player was paused
2023-09-11 13:20:58 -04:00
FoxxMD d95fef09a3 refactor(player state): Remove old play logic
Recent plays are dead. Long live recent plays.
2023-09-11 12:38:10 -04:00
FoxxMD d1060e724e feat(ui): Remove player when dead 2023-09-11 12:36:10 -04:00
FoxxMD a7faaaac86 refactor(player state): Refactor handling of non-updated players
* 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
2023-09-11 12:35:41 -04:00
FoxxMD 3762f95bae fix(player state): Fix stale/dead logic checks
* Stale should not reset to stale when status is orphaned
* Dead check should be in seconds, not minutes
2023-09-11 12:34:09 -04:00
FoxxMD b9f04deb28 Always emit player state 2023-09-08 16:35:20 -04:00
FoxxMD 9cc820c76e feat: More player polish and additional info pane 2023-09-08 16:33:23 -04:00
FoxxMD f7195fb40c feat: Rework source api to include player states
* 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
2023-09-08 14:47:53 -04:00
FoxxMD 9c0f029992 feat: POC player
Based on https://codepen.io/keithpickering/pen/wjYJKM
2023-09-08 13:40:43 -04:00
FoxxMD f370b8ad1e fix: Update heartbeat job to 20 minutes
1 minute was for debugging...
2023-09-05 11:44:22 -04:00
FoxxMD 7c962ec43a fix: Fix more import endings 2023-09-05 11:19:13 -04:00
FoxxMD 2013a6692e fix: Fix import ending 2023-09-05 11:18:15 -04:00
FoxxMD 4a5eb91f16 feat: Schedule heartbeat task to restart sources that should be polling
Addresses #5 and #65 -- in default config use-cases where:

 * request/polling retry attempts are low
 * source was initially OK (auth OK, initial communication OK)
 * and there is a non-auth related *upstream* communication issue

 there is no fallback path for restarting source polling after upstream has normalized.

 This feat implements a scheduled heartbeat task that attempts to restart polling every 20 minutes if this scenario is valid so that a good source config will recover without user intervention.
2023-09-05 11:11:10 -04:00
FoxxMD a606801bb0 Merge branch 'develop' 2023-08-31 10:58:48 -04:00
FoxxMD db395e40e8 chore: Bump for release 2023-08-31 10:58:30 -04:00
FoxxMD 1c498a2c71 Merge branch 'develop' 2023-08-31 10:55:10 -04:00
FoxxMD e4a647d425 fix(jellyfin): Attempt to fix route redirect #92
Should not be appending to route but replacing...see if leading slash fixes things
2023-08-31 09:29:11 -04:00
FoxxMD c1a91ab610 fix: Fix route redirect 2023-08-31 09:28:19 -04:00
FoxxMD 093007a72b Merge branch 'develop' 2023-08-29 09:46:25 -04:00
FoxxMD cb5cdb09f7 chore: Bump versions for release 2023-08-29 09:45:54 -04:00
FoxxMD 8f2e1ad7e7 refactor: Update flatpak build
* Update flatpak build instructions to use build
* Add run command to readme
* Update default config to keep web port backwards compatible
* Fix flatpak application name in run command in installation docs
2023-08-29 09:35:49 -04:00
FoxxMD ed6bcd60b3 refactor: Remove unused react packages and fix CRA babel plugin warning 2023-08-28 10:39:03 -04:00
FoxxMD edbe131f1b fix: Fix tsconfig and out locations for generated schema 2023-08-24 12:10:49 -04:00
FoxxMD 881e537a89 fix: ts libcheck prevents mocha from causing errors while generating schema
https://stackoverflow.com/a/65568463/1469797
2023-08-24 12:10:31 -04:00
FoxxMD abb286f4a8 fix: Fix js extensions 2023-08-24 11:53:42 -04:00
FoxxMD 04ffc0a589 refactor: Restructure server directory
* Rename to backend to better reflect what it is
* Move server functionality out of entry file and into own server subdirectory
* Rename 'apis' folder to 'vendor' to reflect usage and move into common
* Rename 'clients' folder to 'scrobblers' reflect usage and prevent ambiguity with client CRA folder
2023-08-24 11:12:27 -04:00
FoxxMD 4cccdeeff0 chore: Clean up site metadata and update logo
* Changed logo color to be easy to view in both dark/light modes (better color in general, imo)
* Changed and cleaned up head/meta/manifest info and logos for docs and frontend
2023-08-24 10:26:45 -04:00
FoxxMD 2295cf4756 docs: Update example configs 2023-08-24 09:40:59 -04:00
FoxxMD dcd63ff093 feat: Add more IP info and docker hint at server startup 2023-08-23 12:33:32 -04:00
FoxxMD c4bf076d0a Use string-sameness package 2023-08-23 10:31:39 -04:00
FoxxMD 5b32602aca Update yt music package and add missing leven package 2023-08-22 16:29:52 -04:00
FoxxMD 82167ab5ec fix(spotify): Correctly catch 401 errors on refresh token failure 2023-08-22 15:29:19 -04:00
FoxxMD aac963a21c feat: Improve LZ play parsing to pass test suite 2023-08-22 14:36:25 -04:00
FoxxMD fc84d846b2 feat: Implement test skeleton and basic testing for LZ listen parsing 2023-08-22 12:52:49 -04:00
FoxxMD 7674593bfe fix: Use correct data for clients slice 2023-08-22 09:32:41 -04:00
FoxxMD 500dc809bf Even more js extension removals... 2023-08-21 16:48:02 -04:00
FoxxMD b4e9c52301 More js extension removals 2023-08-21 16:43:17 -04:00
FoxxMD 6efac246ff Remove erroneous js extensions 2023-08-21 16:31:37 -04:00
FoxxMD bec841e833 Implement live events infra with POC for tracks discovered updates 2023-08-21 16:21:30 -04:00
FoxxMD 25ebfbcccf refactor: Migrate from react-query to RTK 2023-08-21 14:56:03 -04:00
FoxxMD 74c2bed572 Increase number of log lines in buffer 2023-08-21 11:28:59 -04:00
FoxxMD a1672dfa45 Implement log level filtering 2023-08-21 11:24:16 -04:00
FoxxMD abe494e2b0 Remove socket.io 2023-08-18 14:47:20 -04:00
FoxxMD fabcfc1377 Put dev packages in correct dependency location 2023-08-18 14:39:57 -04:00
FoxxMD 9f89507ce4 Switch back to NPM because of yarn prod dependency issue 2023-08-18 14:32:56 -04:00
FoxxMD f277557e83 Codeshift relative imports on server to not use extension
Makes debugging work in webstorm
2023-08-18 12:54:21 -04:00
FoxxMD 59dfdf1400 Implement codeshift for transforming import extensions 2023-08-18 12:53:45 -04:00
FoxxMD 322b25db00 Add 404 route 2023-08-18 09:26:34 -04:00
FoxxMD a2b424de93 Add empty render for recently played tracks 2023-08-18 09:24:05 -04:00
FoxxMD 09e6e5e534 Add reauth/initialze links for client 2023-08-18 09:22:25 -04:00
FoxxMD 5450c8ddf1 Add nodemon config to prevent auth json file writes from causing server to restart 2023-08-17 16:06:10 -04:00
FoxxMD 99e772e9f6 Add source auth flow link to source card 2023-08-17 14:32:42 -04:00
FoxxMD 69fb3f1dbd Need to use full source maps and remove '.js' extensions to get webstorm to fully use debugger 2023-08-17 13:57:35 -04:00
FoxxMD e9c4f9f9bb fix(web): Implement log prefetch for dashboard 2023-08-17 13:40:30 -04:00
FoxxMD fa6e46f453 fix(sources): Fix polling bug that caused infinite loop
* If user tried to start polling while polling was already occuring it caused an infinite while loop
* Add more signals for stopping polling for a restart use case
2023-08-17 13:19:02 -04:00
FoxxMD 2d6bafecaf Don't refetch status query on window focus 2023-08-17 13:17:58 -04:00
FoxxMD 7dffc3def7 Don't recompile tsx (front end only) 2023-08-17 13:17:38 -04:00
FoxxMD 922f4be14f refactor(web): Move server routes into own files and setup with one function (reorg) 2023-08-17 12:11:07 -04:00
FoxxMD e7bab87e6e refactor(web): Implement recently played 2023-08-16 16:08:57 -04:00
FoxxMD dd67501337 Add proxy setup to git 2023-08-16 15:02:45 -04:00
FoxxMD 4f60f2fa9a Prevent browser from auto opening on client start 2023-08-16 15:01:37 -04:00
FoxxMD 7cb215be52 refactor: Re-organize server/client structure so everything exists on same port
Used https://spin.atomicobject.com/2020/08/17/cra-express-share-code as a base for refactoring
2023-08-16 14:47:21 -04:00
FoxxMD e2b472cf44 Some progress towards recent page 2023-08-15 14:28:56 -04:00
FoxxMD 64c46ac87f Update yarn lock and nvm 2023-08-15 12:47:51 -04:00
FoxxMD 334b782ca3 Merge branch 'webClient' into next
# Conflicts:
#	package-lock.json
#	src/common/infrastructure/Atomic.ts
2023-08-15 12:44:25 -04:00
FoxxMD 9e214243c9 fix(jellyfin): Disregard time difference direction and always check absolute diff #87
May allow more UserDataSaved scrobbles to occur but gives up any form of disallowing future scrobbles from bad timestamps
2023-08-15 12:16:11 -04:00
FoxxMD 77bf84fef4 feat(jellyfin): Simplify tz correct and detect/correct ~future~ listens #87 2023-08-11 12:28:43 -04:00
FoxxMD cde6046ffb feat(jellyfin): Timezone trickery to sus out weird timezone reporting from jellyfin #87 2023-08-10 13:30:27 -04:00
FoxxMD af4ac98357 feat(logging): Expand temporal logging comparison functionality so we can analyze results
* Result and criteria are returned from methods
* Logging method to summarize results
* Log results for jellyfin if using debug mode or explicit config option
2023-08-10 11:44:27 -04:00
FoxxMD 6f66813db0 feat(logging): Add debug mode shortcut property
Allows enabling all debugging for properties that aren't explicitly set
2023-08-10 10:45:27 -04:00
FoxxMD f42fef85b7 feat(config): Add a common 'options' property for all sources and implement logPlayerState
* Will eventually migrate all non-source specific properties to this new property to simplify data config
* Add logPlayerState to allow configurable player state output to DEBUG logging
2023-08-10 10:14:45 -04:00
FoxxMD 400f448108 fix(jellyfin): Use new player state for checking for existing track 2023-08-10 09:58:52 -04:00
FoxxMD 4659735c12 feat: Use listen ranges for play comparisons 2023-08-10 09:58:31 -04:00
FoxxMD c08fe0e54b fix: State Player range fixes and logging
* include all listen ranges when played object output
* include incoming play details in logging when starting new play
2023-08-10 09:58:16 -04:00
FoxxMD 37d4a1c81a feat: POC player state tracking #74
* Implements generic Player state tracking when using MemorySource
  * State tracking will replace processRecentPlays and all its data structures
* Tracks reported player position OR real-time listened when position not available
* Records listened sessions for more exhaustive "listen to" duration and eventually comparing incoming data playedAt for dups
* Uses own tracked data to calculate most likely player state (stopped/started/paused) as well as health (stale/orphaned from no data)
* Tracks player's reported state, if provided
2023-08-09 14:35:29 -04:00
FoxxMD 857971517f Implement basic player state 2023-08-08 17:26:28 -04:00
FoxxMD 51d16c9026 Fix timezone offset detection #87
Check forward and backward offsets and use absolute difference
2023-08-08 15:47:05 -04:00
FoxxMD 6b4fdf53eb Improve Jellyfin UserDataSaved event handling and prevent double reporting due to incorrect timestamp #87
* Move UserDataSaved heuristics into scrobble method implementation with more descriptive logging
* Add more aggressive UserDataSaved filtering by discarding play if its found in memory tracking, regardless of play date
* Attempt to correct bad UTC offset for second UserDataSaved event
2023-08-07 12:33:07 -04:00
FoxxMD 09dd5b7c5f Implement heuristics to detect offline jellyfin plays #87
* Refactor candidate/discovered Play maps to use tuple id instead of string so user can be compared later
* Add option to check all discovered platform plays (since jellyfin UserDataSaved doesn't include device id in payload)
* Check for UserDataSaved notification with correct reason as well as sanity check last played date (since jellyfin will save as played regardless of duration played)
* Bypass play tracking
2023-08-04 15:56:13 -04:00
FoxxMD c182767e5e Implement most read-only functionality for dashboard in CRA
* Layout and skeleton for dashboard
* Client/source cards with initial data
* Logs using SSE
2023-08-03 14:50:03 -04:00
FoxxMD efa93a1529 Redirect non-view server routes to path with /api prefix 2023-08-02 15:18:20 -04:00
FoxxMD 6efd1e54a8 POC CRA with existing express server 2023-08-02 14:18:02 -04:00
FoxxMD 231bd03d9a chore: Add docsite folders to dockerignore 2023-08-02 11:07:21 -04:00
FoxxMD 38c096b4d6 fix(youtube): Use youtube-music-ts-api fork while waiting for upstream PR merge #86 2023-08-02 10:08:47 -04:00
FoxxMD 68ca275673 fix(listenbrainz): Check for delimiters in user-submitted values before splitting #90 2023-08-02 09:49:28 -04:00
FoxxMD de05612873 fix(listenbrainz): Normalize and parse artist names from user-submitted artist/track info #90
Try to parse common patterns (ft/feat/featuring etc) from both artist and track information
2023-08-01 17:51:57 -04:00
FoxxMD bcb0c7471b fix(listenbrainz): Normalize artist names before mapping to scrobble payload #90 2023-07-27 10:13:18 -04:00
FoxxMD f856f79e51 Merge branch 'develop' 2023-07-17 15:41:00 -04:00
FoxxMD fbea08dded chore: Bump version 2023-07-17 15:40:44 -04:00
FoxxMD dc52376395 docs: Use external links for config examples
Docusaurus treats the files as assets and forces them to download when relative links. Instead, use external links to github so the user can stay in browser
2023-07-17 15:39:24 -04:00
FoxxMD 1e2bc0ca85 Merge branch 'docsite' into develop 2023-07-14 11:27:56 -04:00
FoxxMD 7849d6098a docs: Fix docker heading 2023-07-13 16:29:55 -04:00
FoxxMD 0b101b758e feat(tautulli): Support empty user to enable source with all users #84 2023-07-13 16:13:16 -04:00
FoxxMD 756ff0e0c1 chore(github): Update trigger branch for docs deploy to master 2023-07-13 16:12:31 -04:00
FoxxMD 2ddeca7d29 Use working directory without cd command 2023-07-13 15:58:12 -04:00
FoxxMD 1de934e30a Use working directory 2023-07-13 15:57:26 -04:00
FoxxMD 0409c526ee Assume we are still in docsite dir? 2023-07-13 15:54:25 -04:00
FoxxMD a7c340188f Fix run step typo 2023-07-13 15:52:42 -04:00
FoxxMD 5dc54849cc chore(github): Add action for build/deploy docusaurus docsite 2023-07-13 15:51:20 -04:00
FoxxMD 058fbee0c8 Migrate all docs to docsite 2023-07-13 15:40:48 -04:00
FoxxMD daea76eef3 feat: Implement somewhat-working docusaurus site 2023-07-13 15:08:00 -04:00
FoxxMD b81b1d94a3 docs: Update docker registry options 2023-07-13 11:21:27 -04:00
FoxxMD 226b7c741c feat: Implement filter logging controls
For jellyfin/plex/tautulli -- `logFilterFailure` option in config allows controlling if and what log LEVEL user/server filter failures are logged. #85

* Updated schema
* Updated examples
* Also added expected/found detailed logging for filters to jellyfin #84
2023-07-13 11:07:12 -04:00
FoxxMD e452bd761c chore(github): Publish to GHCR
Also pin actions at versions instead of commits
2023-07-13 09:58:07 -04:00
FoxxMD 19c3d97a46 chore(docker): Remove armv7 buildx platform
It's legacy and not supported in most places! Don't need to build for this anymore.
2023-07-13 09:46:22 -04:00
FoxxMD 64164b28de fix(plex): More config data normalization
* Split user value on comma to align with what config describes as acceptable (comma-delim)
* Trim found values to fix any whitespace typos
* On disallowed event logging add expected/found values to help with debugging

Fixes #84
2023-07-13 09:42:21 -04:00
FoxxMD b03cbe7a0a Merge branch 'develop' 2023-06-12 14:20:09 -04:00
FoxxMD 91bd702deb docs: Add multiple artist scrobbling limitation to FAQ 2023-06-12 12:23:49 -04:00
Matt Foxx ede22e10e9 Merge pull request #82 from Whhoesj/lastfm-fix 2023-06-12 12:10:05 -04:00
Wouter Habets c648ad4ae3 fix(lastfm): only scrobble primary artist to last.fm
When a track contains multiple artist, only the first artist should be send to Last.fm since it doesn't support multiple artists per track.
Previous behaviour was to combine all artists in a single string.

Issue: #81
2023-06-12 17:56:37 +02:00
FoxxMD a0fbd1129a chore: Add git-cliff template 2023-06-07 10:01:30 -04:00
FoxxMD b90ea03924 Merge branch 'develop' 2023-06-07 09:23:51 -04:00
FoxxMD 88efe2ad87 Bump versions for release 2023-06-07 09:23:40 -04:00
FoxxMD 39f933418e fix(ntfy): Include authorization object correctly in publish request data #80 2023-05-30 10:32:20 -04:00
FoxxMD cc8c7d84e2 fix(kodi): Fix hardcoded string #79
Probably leftover from debugging, oops
2023-05-25 12:18:29 -04:00
FoxxMD 6d9783c5f5 fix(tautulli): More response formatting safeguards #77
* Log warning if payload is empty
* Destructure request body with default empty object
2023-05-25 11:46:00 -04:00
FoxxMD c10d46b774 fix: Passing wrong argument to tautulli payload formatter #77 2023-05-24 11:47:29 -04:00
FoxxMD a5417277a8 feat: Use pre/post auth hooks for polling start to simplify logic
* Removes need to override poll() in child class and better defines when logic should be performed
* Fixes app crash when spotify credentials are present but invalid #76
2023-05-24 11:04:51 -04:00
FoxxMD 5a1c931513 fix: Re-initialize auth state after receiving auth callback
So we don't have to restart after authenticating in order to start polling

#76
2023-05-24 10:45:07 -04:00
FoxxMD 09f4503593 fix: Handle null/non-string values when truncating
#76
2023-05-19 12:25:22 -04:00
FoxxMD 045cae66f5 fix: Respect maloja response status and throw on error
Don't assume track scrobble correctly. Check body status prop and throw on error or log warnings if they are present

Maybe fixes #57
2023-04-18 10:51:25 -04:00
FoxxMD e67ef5fb0f fix: try to limit number of plays returned by deezer 2023-04-03 16:10:58 -04:00
FoxxMD 1677c0021f Update maloja track parsing in preparation for Maloja 3.2
Album structure changes
2023-04-03 15:59:29 -04:00
FoxxMD 3400e747a8 Fix listenbrainz example
Thanks @captainnapalm
2023-04-03 15:46:43 -04:00
FoxxMD c08ff4843c Bump flatpak build source 2023-03-27 10:02:49 -04:00
FoxxMD 5e905eae17 Merge branch 'develop' 2023-03-27 09:40:42 -04:00
FoxxMD 7ebe73ccb3 Update flatpak install instructions (official!) 2023-03-27 09:40:20 -04:00
FoxxMD 574911cfa0 Update flatpak instructions 2023-03-24 09:34:15 -04:00
FoxxMD 91a84dc4f4 Use files from repo rather than including separately to keep more things upstreams
https://github.com/flathub/flathub/pull/4002#pullrequestreview-1355412003
2023-03-23 16:09:18 -04:00
FoxxMD 9a578e54a8 Fix app id and other issues from review in flathub PR
https://github.com/flathub/flathub/pull/4002
2023-03-23 16:03:43 -04:00
FoxxMD 1a8d5eded3 Update metadata 2023-03-23 15:21:48 -04:00
FoxxMD 94765057db oops typo on icon name 2023-03-23 12:36:54 -04:00
FoxxMD 86c639ad2c Add actual MS icon
Commissioned from @mkalunivsky https://www.fiverr.com/mkalunivsky
2023-03-23 12:35:00 -04:00
FoxxMD f3045debb3 Scope dbus access to mpris 2023-03-23 12:16:28 -04:00
FoxxMD ffc826f04d Log message if running as foreground app with flatpak 2023-03-23 12:00:48 -04:00
FoxxMD e70a4bbf1c Update flatpak build with proper desktop integration
* Fix appstream (metainfo) missing properties and correct component types
* Add .desktop file that specifies terminal use
* Add placeholder icon
2023-03-23 11:56:53 -04:00
FoxxMD a9eca51bfa Implement user-defined scrobble thresholds #73
Add a scrobbleThresholds object with 'duration' and 'percent' values to all sources and sourceDefaults. If both are defined then first passing condition trigger scrobble.

* duration, in seconds, tests how long MS has been tracking the play (since discovery)
* percent, only used if source provides track duration, tests time MS has been tracking play (since discovery) is some % of track duration
2023-03-22 14:55:38 -04:00
FoxxMD de4a0aecd5 Add .flatpak-builder ignore in flatpak folder 2023-03-22 12:00:52 -04:00
FoxxMD f097fac8ea docs: Add systemd usage and add preview flatpak install instructions 2023-03-22 11:39:51 -04:00
FoxxMD a20b35f7cd More build improvements and provide config defaults to flatpak config dir
* Remove npm dev dependencies after app install/build
* Delete typescript files after build
* Provide a default config with file logging disabled and copy examples to config folder if empty
2023-03-22 10:32:29 -04:00
FoxxMD 2d51a0e159 Don't create rotating log file on init logger
We don't know if use wants files or there may not be permissions necessary...wait until we try to parse logging from config in appLogger
2023-03-22 10:31:04 -04:00
FoxxMD e524077242 Update flatpak build instructions to use npm 2023-03-21 16:54:23 -04:00
FoxxMD a5b5fe8d2a Use npm package-lock and update winston dependencies
* Update winston-duplex to remove hard winston dependency
* Use scoped @foxxmd/winston instead of git commit
  * Replace 'winston' with '@foxxmd/winston' in application
2023-03-21 16:53:42 -04:00
FoxxMD ddb3e0e02d Move into own folder and cleanup/instructions
Only blocking issue is needing to use yarn instead of npm
2023-03-21 11:09:25 -04:00
FoxxMD ec97ec428a Update permissions 2023-03-21 09:15:13 -04:00
FoxxMD 49b834af5d working config folder 2023-03-21 09:15:13 -04:00
FoxxMD 6329aaf6a9 Revert to git source for winston to fix typings problem 2023-03-21 09:15:12 -04:00
FoxxMD fd6e5ab084 Working example 2023-03-21 09:15:12 -04:00
FoxxMD 1dbb1b9ba3 Add network permission 2023-03-21 09:15:12 -04:00
FoxxMD fc65a63d43 Use yarn instead of npm 2023-03-21 09:15:12 -04:00
FoxxMD 4d1ddde57b NON working flatpak manifest
See comments in manifest

use npm published @foxxmd winston package
2023-03-21 09:15:12 -04:00
FoxxMD 7d14fb5341 Modify ioc and index so we can get port from config 2023-03-21 09:05:16 -04:00
FoxxMD e310b3aec8 Add PORT to aio config 2023-03-21 09:05:16 -04:00
FoxxMD 056457d028 Fix readJson logging usage 2023-03-21 09:05:16 -04:00
FoxxMD 73183e760e feat: Improve robustness of MPRIS data parsing
* Log warnings when any specific mpris info for a player isn't able to be parsed rather than failing entire poll attempt
* Fallback to undefined tracking progress if position cannot be parsed
2023-03-21 09:04:41 -04:00
FoxxMD 3fff729d32 Scrobble to Listenbrainz with MBID metadata if present
* Add play object data "meta" with a brainz property and common mbid tags
* Parse MBID data from jellyfin webhook
* Add MBID data from play object to 'additional_info' when scrobbling to listenbrainz

Closes #72
2023-03-20 22:37:54 -04:00
FoxxMD 19e199bd58 Pre-emptive adding flatpak folders to gitignore 2023-03-20 22:31:27 -04:00
FoxxMD 7b0f5801a1 Merge branch 'develop' 2023-03-20 11:06:38 -04:00
FoxxMD 4dbea91a5e docs: some words 2023-03-15 12:28:44 -04:00
FoxxMD 548356aaa4 feat: Implement Kodi as a Source 2023-03-15 12:26:35 -04:00
FoxxMD 8b3f4c636c Merge branch 'develop' 2023-03-15 09:13:20 -04:00
FoxxMD 1b0212f3bb feat: Improve polling retry strategy
* Increase default to 5 for all sources
* Use true exponential backoff for determining delay between polling
2023-03-15 09:06:26 -04:00
FoxxMD 14e9ec0b45 refactor: Remove superfluous 'Client' from scrobbler label
Already know what it is since it is prefixed by [Scrobblers] label
2023-03-15 08:30:18 -04:00
FoxxMD dab113ab76 docs: Missing jriver TOC entry 2023-03-13 14:01:40 -04:00
FoxxMD ad729769ec Implement JRiver Source #26 2023-03-13 13:59:24 -04:00
FoxxMD bb4ae549a3 Remove superfluous spotify label 2023-03-13 10:13:47 -04:00
FoxxMD 0786642a38 Increase default poll retries for Spotify
This seems to be a pain point so increasing ought to help.
2023-03-13 10:09:22 -04:00
FoxxMD 77d5f5884e Use error with cause for spotify api errors 2023-03-13 10:00:23 -04:00
FoxxMD 7a0a23b7fd Merge branch 'develop' 2023-03-10 15:12:53 -05:00
FoxxMD 1207a5eec1 docs: Update wording 2023-03-10 14:58:27 -05:00
FoxxMD 48a0ecad41 feat: Implement Listenbrainz as a client #16 2023-03-10 14:38:07 -05:00
FoxxMD f792feeb6c fix: re-add accidentally delete lastfm docs 2023-03-10 14:28:50 -05:00
FoxxMD d6b682a23a fix: Do not prematurely return no existing scrobble when no recent scrobbles exist
don't want to assume we won't find an existing just because there are no scrobbles from the client itself -- we may have found it from recent recorded scrobbles sent
2023-03-10 14:26:42 -05:00
FoxxMD d77d547c15 fix: fix always adding plays to newly discovered 2023-03-10 14:25:05 -05:00
FoxxMD 0dc828fa85 fix: time frame should be valid if there were no recent scrobbles retrieved 2023-03-10 14:08:48 -05:00
FoxxMD bd00a5f414 feat: Improve error and json logging
* Properly handle ErrorWithCause stacks
* Output json when log meta exists
2023-03-10 12:41:07 -05:00
FoxxMD f587ad4bbc feat: Use fixed length array to store scrobbled plays
May reduce memory usage as we don't store all scrobbles ever
2023-03-10 11:58:57 -05:00
FoxxMD b8f2b1e3f8 feat: Increase number of recent scrobbles to fetch 2023-03-10 11:38:24 -05:00
FoxxMD bf3b81f1cd feat(listenbrainz): Add timeout for listens response 2023-03-10 11:34:41 -05:00
FoxxMD 9375a6fce4 feat: Improve listenbrainz scrobbler source of truth behavior
Use updated listenbrainz api methods and scrobbled play method
2023-03-10 11:24:15 -05:00
FoxxMD ca148f490e refactor: Make scrobbled data transform responsibility of concrete scrobbler class 2023-03-10 11:23:15 -05:00
FoxxMD 9ab0468913 feat: Improve listenbrainz api
* The response track data is different than submission data so make sure we are accurately parsing that data to PlayObjects
* Add separate method for getting listens as recently played (play objects) and refactor getUserListens to return raw payload
2023-03-10 11:22:21 -05:00
FoxxMD 99a73a80db feat: Implement Listenbrainz source 2023-03-10 09:54:20 -05:00
FoxxMD 80ba6e6c79 remove commented out code 2023-03-09 14:33:16 -05:00
FoxxMD 7a4f0f0d3f fix: Fix mopidy response handling when no track is being played 2023-03-09 13:12:31 -05:00
FoxxMD a9a6c6ae15 feat: Improve scrobble client debug logging during scrobble loop
Use client logger for cleaner labels
2023-03-09 13:09:26 -05:00
FoxxMD c5190fc2a6 feat: Improve logging
* use child loggers instead of creating new loggers for each part of the app
  * cleans up and automates nested labels in log output
  * fixes "event emitters may be leaky" node warning
* add ability to specify log levels per output type (console, ui, file) using aioconfig
* improve UI logging experience
  * use more color-blind friendly colors (borrowed from context-mod)
  * add verbose log level output
  * increase log buffer and store all logs, then filter at render time, instead of discarding based on min log level so that we always have logs to display
  * fix level selection in ui to work for all levels
2023-03-09 13:02:46 -05:00
FoxxMD 93b543e1a4 Fix mopidy json schema url 2023-03-08 15:21:10 -05:00
FoxxMD 4d77ca9c11 Add missing mopidy example 2023-03-08 15:18:24 -05:00
FoxxMD 03cf3d1e3c feat: Implement Mopidy source #24 2023-03-08 15:16:37 -05:00
FoxxMD d2134f3467 fix: fix reversing referenced array 2023-03-08 09:57:34 -05:00
FoxxMD 97fe411ac0 feat: improved last activity date tracking 2023-03-07 14:22:27 -05:00
FoxxMD ff953b4535 fix: oops left in debug statement 2023-03-07 12:31:27 -05:00
FoxxMD b68fb9a9f9 fix(youtube): Fix bad index when checking for repeat tracks 2023-03-07 12:20:02 -05:00
FoxxMD ad1895d6e1 fix: Add fallback comparison when sorting plays by playDate 2023-03-07 12:19:29 -05:00
FoxxMD 51b4ea3b16 Refactor: Consolidate formatPlayObj options 2023-03-07 11:51:36 -05:00
FoxxMD 3d86a38a2e Refactor: Create a Source of Truth for track discover by sources and consolidate/simplify scrobble behavior
* Add a 'recentDiscoveredPlays' class property for abstract source
  * recentDiscoveredPlays is platform aware and configured based on source 'multiPlatform' class property
* Consolidate scrobble behavior into abstract class
  * Check for existing discovered track before scrobbling
  * Add to discovered if not existing and increment discovered tracks counter
* Decouple sources from clients by using event emitter when a newly discovered track should be scrobbled
* Use discovered tracks for UI display instead of api data
2023-03-07 11:27:25 -05:00
FoxxMD 605aad1df3 Refactor: Consolidate scrobble client existing play check into abstract client
* Move main logic into class methods that can be overridden
* Move title, artist, and date checks into class methods that can be overridden
2023-03-07 10:36:05 -05:00
FoxxMD 3d2e3534ff Refactor: Move play date comparison into util 2023-03-07 10:34:20 -05:00
FoxxMD 2a65ffe6c4 Merge branch 'statefulSOT' into experimental 2023-03-06 13:20:37 -05:00
FoxxMD e3e3e03249 Merge branch 'spotifyConnect' into experimental
# Conflicts:
#	src/sources/AbstractSource.ts
#	src/sources/SpotifySource.ts
#	src/sources/SubsonicSource.ts
2023-03-06 13:20:30 -05:00
FoxxMD 6bd8642c91 Initial pass at IOC 2023-03-06 13:13:25 -05:00
FoxxMD 40ae842f82 Add fixed-size-list
To be used for storing a fixed length array of scrobbled plays for clients and a discovered tracks for sources
2023-03-06 11:12:11 -05:00
FoxxMD a2ab1aa49e feat: Add polling options to config schema and make polling more aggressive
Since spotify now uses currently playing with memory we need to poll more aggressively to make sure we catch tracks as they are played. Additionally need to decrease max polling interval so as not to miss activity.
2023-03-03 12:32:16 -05:00
FoxxMD 6d3cbf32df feat(spotify): Refactor spotify Source as Memory Source #51
* Use recently-played to scrobble backlogged tracks
* Use currently-playing or current-state to implement spotify as a memory source in order to track plays made from spotify connect devices
2023-03-02 15:29:31 -05:00
FoxxMD 7a8138773c feat(spotify): Implement currently playing retrieval 2023-03-02 13:38:56 -05:00
FoxxMD 5ae79deca4 feat: Implement MPRIS Source 2023-03-02 12:57:29 -05:00
FoxxMD 285f593a0f Add dbus-next dependency 2023-03-02 10:29:46 -05:00
FoxxMD e0ae621b62 feat: Implement more context awareness for memory sources to handle multi-platform/device plays
* Track candidate plays separately based on device/platform/user metadata
* Perform additional check for playback progress if metadata exists

Fixes #67
2023-02-28 12:34:09 -05:00
FoxxMD 259d4dabc8 feat: Parse device id metadata from sources to be used for multi-player awareness #67 2023-02-28 11:12:15 -05:00
FoxxMD f7b03d9622 refactor: Rename sourceId to trackId to be clearer in what it actually is 2023-02-28 10:14:06 -05:00
FoxxMD bd188a8ca7 refactor: Remove unused trackLength property 2023-02-28 09:58:56 -05:00
FoxxMD 28910a9332 Merge branch 'develop' 2023-02-27 11:13:28 -05:00
FoxxMD ee38252e1a docs: Add webhook and healthcheck endpoint documentation 2023-02-27 10:58:39 -05:00
FoxxMD 6f3595db31 feat: Add initialization and auth tests for notifiers 2023-02-27 10:58:31 -05:00
FoxxMD c5c0cd9cd2 refactor: Improve title and messages for webhook notifications 2023-02-27 10:09:25 -05:00
FoxxMD f5da32d233 fix: YT Music error throwing 2023-02-27 09:35:44 -05:00
FoxxMD 315869dafe fix: Update auth check when starting polling
Use general requiresAuth instead of only requiresAuthInteraction
2023-02-27 09:35:29 -05:00
FoxxMD 330802e29b fix: Default import for gotify library 2023-02-27 09:34:52 -05:00
FoxxMD 3d5e988ef0 Suppress error 2023-02-24 15:31:43 -05:00
FoxxMD 9a2b4b3d1e fix: Maybe fix for missing lastfm credentials configdir base 2023-02-24 15:27:16 -05:00
FoxxMD 4a1ebcdbc6 fix: Fix webhook missing initializer value 2023-02-24 13:57:08 -05:00
FoxxMD bc66fdb2b6 fix: Stop api calls if authentication failed 2023-02-24 13:56:52 -05:00
FoxxMD 6c2b0557c6 feat: Implement health endpoint #66
* HTTP Status as primary indicator with messages in json response
* No parameters in request aggregates all client/source statuses. Otherwise can use 'type' or 'name' parameters to restrict client/sources to aggregate
2023-02-24 13:51:59 -05:00
FoxxMD c24dc41811 feat: Implement webhook-based status notifications #66
* Implemented notifications for Gotify and Ntfy
* Notifications pushed on: polling started, polling retry, polling stopped on error, scrobble client scrobble failure
2023-02-24 13:19:03 -05:00
FoxxMD 0a41d32084 fix: Fix config deconstruct for polling data 2023-02-24 11:13:58 -05:00
FoxxMD b7aa087b74 feat: Add debug logging of jellyfin webhook payload with docs 2023-02-24 11:04:47 -05:00
FoxxMD 1f511d964d feat: Improve jellyfin logging
* Refactor initial connection info to be more verbose and only log once
* Refactor valid event check logging order of operations and make logging more verbose
2023-02-24 10:48:40 -05:00
FoxxMD 14eabecb7d Update google headers image 2023-02-23 12:57:11 -05:00
FoxxMD 8e31aa431d docs: Add youtube music docs and example 2023-02-23 12:54:48 -05:00
FoxxMD e47a636c2d feat: Add friendly auth error and hints for YT authentication test 2023-02-23 12:27:37 -05:00
FoxxMD bdd14b16de feat: Implement YT recent played UI 2023-02-23 12:16:44 -05:00
FoxxMD 8cc5aacb9a feat: Implement working YT Music source #34 2023-02-23 12:07:06 -05:00
FoxxMD d8e861dd54 feat: Add YTMusic api implementation and basic source 2023-02-22 16:35:09 -05:00
FoxxMD e369f9eb21 Merge branch 'develop' 2023-02-22 12:55:23 -05:00
FoxxMD ce6246e479 docs: Add FAQ to bug report template 2023-02-22 12:55:14 -05:00
FoxxMD 0c58cc6687 Merge branch 'develop' 2023-02-22 12:09:40 -05:00
FoxxMD 19687b725d fix: Fix logging empty ingress notifications 2023-02-22 12:00:13 -05:00
FoxxMD 02dee5fb6c docs: Add bug report template 2023-02-22 11:54:13 -05:00
FoxxMD e08a1ad514 fix: fix typo 2023-02-22 11:46:01 -05:00
FoxxMD 865f1064f5 docs: Add FAQ 2023-02-22 11:43:57 -05:00
FoxxMD 2b798e5a69 feat: Implement initial request logging for all sources that use ingress instead of polling
In order to make basic troubleshooting easier all ingress-based sources (plex, tautulli, jellyfin) now log initial connections and basic request validation checks:

* Request logging includes remote address and user agent
* Log initial requests before any middleware/body parsing
* Include method for checking if request is valid at a low-level (IE checking request verb)
* Include source-specific methods for checking if request payload is valid
2023-02-22 11:04:50 -05:00
FoxxMD da4d2ecade docs: Add additional properties to tautulli payload 2023-02-22 11:01:24 -05:00
FoxxMD 89540d8b76 docs: Add jellyfin example to kitchen sink and fix inconsistencies #55 2023-02-21 12:54:40 -05:00
FoxxMD 931709d277 fix: typo 2023-02-21 12:50:11 -05:00
FoxxMD 1b199750bd docs: Simplify and correct example configs
* Keep examples as valid json by removing comments and fixing trailing commas #55
* Add README pointing to configuration docs
2023-02-21 12:49:20 -05:00
FoxxMD 4b1fa710f7 docs: Rename json-based to file-based for consistent terminology 2023-02-21 12:47:56 -05:00
FoxxMD 360b6f4cf0 docs: Add links to schema explorer for json configs 2023-02-21 12:27:55 -05:00
FoxxMD e960438968 feat: Revert specific config requirement to be a top-level array
Wasn't really necessary and would cause breaking change for existing configs.
2023-02-21 12:27:27 -05:00
FoxxMD 93230c45c7 docs: Add examples and more descriptions to json schema 2023-02-21 11:52:27 -05:00
FoxxMD a3fd30c427 fix: Update project TS and node configuration in order to correctly generate json schema
* Output to commonjs using TS (remove type:module from package.json)
* Update tsconfig config
* Downgrade formidable to fix ES module import error
* Remove js assertions and use separate json schema for aio client/source validation to keep MS log output cleaner
2023-02-21 11:05:33 -05:00
FoxxMD c9bac6f7d2 fix: Use separate interface for maloja config data with inherited properties 2023-02-21 11:02:30 -05:00
FoxxMD 06c0b64ee5 fix: Correct plex data types 2023-02-21 11:01:28 -05:00
FoxxMD 70a1181ea1 chore: Add missing passport-deezer typings 2023-02-21 11:00:56 -05:00
FoxxMD be9938fe95 missed some wording 2023-02-20 11:32:47 -05:00
FoxxMD 27acfa9489 docs: Simplify oauth config by removing unused access/refresh token
Although it was nice to include the option no one is providing their own tokens from a separate flow completed elsewhere. It simplifies readability and usage in MS to remove these and always generate out own.
2023-02-20 11:31:24 -05:00
FoxxMD fc26125f80 fix anchor 2023-02-20 11:19:33 -05:00
FoxxMD 193a5c1e4d docs: Rewrite docs to include more examples and clearer usage 2023-02-20 11:18:01 -05:00
FoxxMD 700722627a feat: Allow parsing json files as json5
So users can use example json files with comments as-is
2023-02-20 09:42:16 -05:00
FoxxMD 1f6996d881 chore: Update superagent version 2023-02-14 11:35:44 -05:00
FoxxMD 1cbad9ccfd refactor: Use node from alpine packages instead of building from source
Using LTS either way but building from source makes image much larger and takes forever on build runners (github)
2023-02-14 11:23:05 -05:00
FoxxMD 04c9de648d refactor: Update s6-overlay usage for ms config and start up
Refactor s6 usage based on most recent lsio images
2023-02-14 10:44:31 -05:00
FoxxMD 2b60a0f76f fix: Fix ENV usage for jellyfin #61
Correct property names when parsing from ENV
2023-02-10 13:53:09 -05:00
FoxxMD 176ff80172 Fix docker example folder mapping 2023-02-10 12:46:20 -05:00
FoxxMD e2d150ce15 feat: add docker-compose with instructions #55 2023-02-10 12:44:22 -05:00
FoxxMD 636004a1bb Refactor: Update dockerfile and simplify configuration #55
* Update to alpine 3.17 and node 18 to match the project
* Use LSIO base to enable ui/guid usage through env
* Use build stages to reduce image size and speed up build
* Refactor build to use typescript
* Simplify config directory by using root-level /config as default
* Add bash script on startup to copy example configs if a new config folder is detected
2023-02-10 12:12:57 -05:00
FoxxMD 2258bb36b9 Refactor: Simplify logs location by using config directory #55 2023-02-10 12:09:20 -05:00
FoxxMD 0583a59177 Regenerate json schema files 2023-02-10 12:08:37 -05:00
FoxxMD fbbcfc5a57 Downgrade socket.io to avoid ESM module issue 2023-02-10 11:09:43 -05:00
FoxxMD 317da71ec3 Ignore .bak files 2023-02-10 10:18:15 -05:00
FoxxMD 22bbcfe3e5 Remove unused subsonic library 2023-02-10 10:18:05 -05:00
FoxxMD de3832f3ce More TS fixes 2023-02-09 16:09:52 -05:00
FoxxMD 04b07d6bfb Update abstract source config usage 2023-02-09 15:18:31 -05:00
FoxxMD fdf00d8acc Update deezer config usage 2023-02-09 15:18:09 -05:00
FoxxMD 3d2296858f Update subsonic source config usage 2023-02-09 15:15:24 -05:00
FoxxMD b1390cd74a Update spotify client typings 2023-02-09 15:05:14 -05:00
FoxxMD 216d92cad7 Add recentlyPlayed option interface 2023-02-09 15:04:59 -05:00
FoxxMD dcde41f89a Fix spotify source credential handling 2023-02-09 14:48:27 -05:00
FoxxMD 1e9518ac54 Fix spotify source credential handling 2023-02-09 14:48:20 -05:00
FoxxMD ac2aa8297c Remove ts-migrate 2023-02-09 14:48:12 -05:00
FoxxMD 8611798051 it works! 2023-02-09 13:51:06 -05:00
FoxxMD a659897d37 refactor: Update project to ESM
* Update relative import extension
* Remove (bad?) TSconfig extends presets
* Fix default non-relative imports
2023-02-09 12:50:37 -05:00
FoxxMD b4303fa81c refactor: add typings everywhere they are needed 2023-02-09 12:22:55 -05:00
FoxxMDandts-migrate 8f0a75a81b [ts-migrate][.] Run TS Migrate
Co-authored-by: ts-migrate <>
2023-02-08 12:56:09 -05:00
FoxxMDandts-migrate e006476b52 [ts-migrate][.] Rename files from JS/JSX to TS/TSX
Co-authored-by: ts-migrate <>
2023-02-08 12:56:04 -05:00
FoxxMDandts-migrate bb7bfc4a6e [ts-migrate][.] Init tsconfig.json file
Co-authored-by: ts-migrate <>
2023-02-08 12:56:04 -05:00
FoxxMDandts-migrate 03c3f6b6a2 [ts-migrate][.] Init tsconfig.json file
Co-authored-by: ts-migrate <>
2023-02-08 12:55:08 -05:00
FoxxMDandts-migrate 900f3c2471 [ts-migrate][.] Init tsconfig.json file
Co-authored-by: ts-migrate <>
2023-02-08 12:53:42 -05:00
FoxxMDandts-migrate cd86acdcf0 [ts-migrate][.] Init tsconfig.json file
Co-authored-by: ts-migrate <>
2023-02-08 12:53:13 -05:00
FoxxMD abf338f71a refactor: Move application files into src folder
* In preparation for documentation site and building from TS isolate app files to own folder
* Fix log/config/views folder default location
2023-02-08 12:46:46 -05:00
FoxxMD 91bb0cba45 Update to note 18 and add tsconfig 2023-02-08 12:30:39 -05:00
FoxxMD 8499e3197d feat: Add unknown route logging 2023-02-08 11:53:37 -05:00
FoxxMD bfc3568b00 feat: Add more debug logging for memory source play object tracking 2023-02-08 11:13:35 -05:00
FoxxMD 26613f8b68 docs: Update jellyfin webhook install instructions 2023-02-08 11:13:03 -05:00
FoxxMD 0b2d4b5f89 feat: Add more information to jellyfin play object and logging
* Standardize runtime stamp parsing
* Add playback position for future use
* Add source version and log first-seen server logging to jellyfin source
2023-02-08 11:12:44 -05:00
FoxxMD 6561ec9404 fix: Fix retrieving incorrect source for spotify callback when sources are unnamed
MS incorrectly chooses the first unnamed source (based on built source order) when handling spotify callback. Fixed by specify source type when retrieving source. Fixes #63
2023-02-03 11:04:49 -05:00
FoxxMD 517df8a3ad chore: remove quotes from nvmrc 2023-02-03 11:02:33 -05:00
Matt Foxx 4a0cdc0e87 Merge pull request #62 from samdoshi/lastfm-docs 2023-01-21 17:48:27 -05:00
Sam Doshi a54bc99925 add note about privacy setting for Last.fm sources 2023-01-21 15:28:21 +00:00
Matt Foxx eeae98360f Merge pull request #56 from CPU-Blanc/bugfixing
Fix: Jellyfin source
2022-12-29 08:58:08 -05:00
CPU_Blanc c5a938d4eb Fix Jellyfin source 2022-11-29 02:01:20 +00:00
FoxxMD c5b7dbd50f fix: Improve scrobble client init flow and retries
* Refactor init state to have an interim value and use getter/setter to control it
* Replace usage of 'ready' class field on maloja client with serverIsHealthy (make it client specific)
* Add some missing functions in abstract client class
* If client is not already initialized (or trying to initialize) then try to initialize client on scrobble attempt, maybe fixes #44
2022-06-16 11:06:06 -04:00
FoxxMD 26cd478c6f fix(plex): Properly handle requests without data 2022-06-15 12:01:38 -04:00
FoxxMD 670805f18a feat(plex): Improve request handling and debug info
* Replace multer with formidable as it is more up-to-date and has more granular options for tracking progress and parsing data
* Add debug logging for the lifecycle of a plex request
* throws errors if any part of plex request lifecycle is unexpected
2022-06-15 11:51:58 -04:00
Matt Foxx 8b851d2220 Merge pull request #49 from northys/tautulli_readme_duration
docs: tautulli payload duration => duration_sec [fixes #48]
2022-04-28 16:24:36 -04:00
Jiri Travnicek 7a1279b02c docs: tautulli payload duration => duration_sec [fixes #48] 2022-04-28 22:11:42 +02:00
FoxxMD 3f2a49dedf Merge remote-tracking branch 'origin/develop' into develop 2022-04-28 10:25:27 -04:00
FoxxMD d0f792c107 feat: Add scrobble client request data to logging
* Output request payload as part of log message on error
* Always output request payload (raw) to debug
* Should help with debugging #48
2022-04-28 10:25:19 -04:00
Matt Foxx ff9172001f Merge pull request #46 from northys/build_arm
build arm [closes #45]
2022-04-26 23:14:10 -04:00
Jiri Travnicek 0b1772b30d build arm [closes #45] 2022-04-26 20:50:28 +02:00
FoxxMD 64b77cab6b fix(maloja): Use length as track length property for all maloja versions
#42
2022-04-15 12:19:15 -04:00
FoxxMD 8aecd9dbce fix(maloja): Use a different data shape for new scrobbles on maloja >= 3.0.3
Addresses #42
2022-04-15 09:44:43 -04:00
FoxxMD 0bdd0fd801 fix(ui): Provide defaults to express-session to prevent deprecation message
Closes #41
2022-04-14 09:59:49 -04:00
FoxxMD 0ede2cd96b feat(maloja): Support maloja >= 3.0.0
* Use server version to determine shape of scrobble data when formatting play object
* Refactor adding new scrobble to recent list to use correct shape based on server version
2022-04-14 09:45:44 -04:00
FoxxMD 8219dcecd7 refactor(maloja): Handle maloja server version use cases for auth/readiness 2021-12-17 10:51:42 -05:00
FoxxMD 42a5fdf9a1 Don't invoke scrobble function if source has no plays
If source does not have new OR existing plays to check against clients there is no reason to call scrobble function
2021-12-14 11:06:21 -05:00
FoxxMD 9bc6f44af4 Implement readiness check for maloja client
* Use rebuild status and health reported from maloja api endpoint to determine if server is ready to scrobble/query
* Use readiness as prerequisite for auth test (can be removed if krateng/maloja#92 is merged)
2021-12-14 11:05:22 -05:00
FoxxMD ccb31c5866 Implement ready state check for scrobble clients
Default state is calculated from initialized and auth status. Allows an additional check for client readiness independent of communication (initialized) status and auth status.
2021-12-14 11:03:03 -05:00
FoxxMD 30f9ebf900 Add note about authentication flow on deezer config 2021-10-20 14:30:56 -04:00
FoxxMD 9b4eb2d383 Implement Deezer source
Closes #35
2021-10-20 14:23:41 -04:00
FoxxMD 6e2f765e69 fix(lastfm): Fix mishandled properties in scrobble request causing ignored track
Removing undefined properties from request data for track scrobble fixes an issue where last.fm ignores the track and returns error code 1

Fixes #33
2021-10-05 12:06:50 -04:00
FoxxMD 57fd88556d Better detection when last.fm ignores a scrobble
* Use ignored count to determine if warning is logged
* Let user know if no error message included
* Include link to error code reference in last.fm api docs
2021-09-29 12:42:22 -04:00
FoxxMD 034f5b217e Fix missing auth variable for status render 2021-08-26 10:23:07 -04:00
FoxxMD a5112e0bbb Add github action to push docker image 2021-08-26 10:22:54 -04:00
FoxxMD f6db5fa14e Add streaming indicator 2021-07-14 10:35:43 -04:00
FoxxMD d7de41d642 Improve log streaming
* Emit individual log statements in winston formatter
* Emit different event when logs should be cleared
* Use window global to keep track of sort order when appending on emit
2021-07-14 10:16:51 -04:00
Matt Foxx ca0d1a4f90 Merge pull request #30 from christophernewton/master
feat: removed fs and made the options rely more on sockets
2021-07-14 09:42:08 -04:00
Chris Newton 46fc9143b4 feat: removed fs and made the options rely more on sockets 2021-07-14 13:58:45 +10:00
Matt Foxx 0800cff6fc Merge pull request #29 from christophernewton/master
Added Websockets
2021-07-13 10:00:13 -04:00
Chris Newton 3106b56dee feat: fixed issue with trailing commas 2021-07-13 14:43:35 +10:00
Chris Newton 08b1ac04bb feat: added websocket for streaming logs 2021-07-13 14:20:20 +10:00
Chris Newton 1a55cc482c feat: cleaned up formatting 2021-07-13 11:53:18 +10:00
Matt Foxx 73aa7ad890 Merge pull request #28 from christophernewton/master
Small QOL updates
2021-07-12 13:47:38 -04:00
Chris Newton 95db3da58f Merge branch 'FoxxMD:master' into master 2021-07-12 14:14:46 +10:00
Chris Newton 31926e6ad9 feat: added better check for log colors, added favicon, fixed mobile viewport issue 2021-07-12 14:13:51 +10:00
FoxxMD 71a8ad2418 Fix typo on users specification for jellyfin source setup 2021-07-05 10:43:44 -04:00
FoxxMD 49bd1c8836 Refactor initialization and authentication stages for client/source building
* Break up into different functions and add properties to concrete classes signalling its capabilities
* More detailed logging based on which fails/succeeds
* Simplify client/source init/auth step (no more need for case switch specifics other than creating object)
2021-04-14 16:22:05 -04:00
FoxxMD fc3dd62858 Refactor source building and initialization to log failures but continue
* Instead of stopping the whole application for one misconfigured config just log the issue and continue with any valid/working configs.
* Add better logging for type.json issues (add index)
* Add deprecation warnings for single-user mode structures in type.json (to be removed in 0.4)
2021-04-14 11:28:55 -04:00
FoxxMD ff114a0ae5 Refactor client building and initialization to log failures but continue
* Instead of stopping the whole application for one misconfigured config just log the issue and continue with any valid/working configs.
* Add better logging for type.json issues (add index)
* Add deprecation warnings for single-user mode structures in type.json (to be removed in 0.4)
2021-04-14 11:14:57 -04:00
FoxxMD 8bfcb6cd9a Showcase last.fm as a source in readme 2021-04-14 10:19:23 -04:00
FoxxMD 8154e30939 Add last.fm as source to readme 2021-04-13 17:05:46 -04:00
FoxxMD 506440825f Implement Last.fm as a source
For #23
2021-04-13 17:04:40 -04:00
FoxxMD 646723fcf8 Add ui screenshot to readme 2021-03-17 10:34:51 -04:00
FoxxMD a560a49aac Fix typo 2021-03-16 15:44:38 -04:00
FoxxMD de763d68f3 Add Jellyfin source #15
* Implement Jellyfin source similar to Plex, but using MemorySource
* Add configuration docs and json example
2021-03-16 15:36:51 -04:00
FoxxMD f731c25332 Return new plays when memory source adds them to stateful
Will make it easier for non-polling sources to see what is new from state
2021-03-16 15:33:41 -04:00
FoxxMD f738eb92e8 Merge branch 'develop' into feat-jellyfin 2021-03-16 14:39:26 -04:00
FoxxMD b9f4f43d30 Refactor subsonic source to use MemorySource
* Should stabilize subsonic as a source and prevents duplicate scrobbles
* Reduce polling interval to 10 seconds for more accurate recently played data and restrict max backoff time to 30 seconds
2021-03-16 14:38:59 -04:00
FoxxMD e42f00604e Refactor polling source backoff approach
Use time-based approach rather than check count approach so active period is not sensitive to interval value
2021-03-16 14:37:15 -04:00
FoxxMD 74eed98b9b Fix filter method 2021-03-16 14:25:55 -04:00
FoxxMD cef6f5864a Implement "in memory" recently played tracking for sources that don't support a source of truth
If sources don't support a scrobble action or don't return a sane "recently played" data source we need to keep track of these plays ourselves. Do it in memory based on currently playing and return that as source "source of truth" for recently played.
2021-03-16 14:01:17 -04:00
FoxxMD d5e816b1d3 Use a persistent datetime based on source instantiation or last track play as client refresh check threshold
We don't need to refresh clients on every source poll, only when first creating a source or when a new track play is discovered.
2021-03-16 13:59:43 -04:00
FoxxMD dcc4201019 Improve parsing of last.fm track responses and handling recent tracks
* Include some metadata (mbid, url, nowplaying)
* Check for more artist properties since responses seem inconsistent now
* Check for timestamp to handle nowplaying use case
* Handle invalid scrobbles from last.fm on recents refresh WRT nowplaying and missing timestamps #22
2021-03-16 10:57:44 -04:00
FoxxMD 9c093a8455 POC jellyfin implementation #15
* Hopefully can get artist from webhook payload in the future so we can actually use this
* Add basic instructions for using Webhook plugin in configuration docs
2021-03-12 11:24:33 -05:00
FoxxMD 9a56a3ee4d Merge branch 'develop' into feat-jellyfin
# Conflicts:
#	index.js
2021-03-12 10:24:54 -05:00
Matt Foxx f2b0714dea Merge pull request #21 from christophernewton/master
Feature: Fix padding on client box
2021-03-02 09:19:19 -05:00
Chris Newton 0a1357acc7 feat: fix padding on client boxes 2021-03-02 08:54:47 +11:00
Matt Foxx 403af711eb Merge pull request #20 from christophernewton/master
Frontend update
2021-03-01 16:36:07 -05:00
Chris Newton 31cd72cd15 feat: fixed typo in debug output, added active state for links 2021-03-02 08:24:32 +11:00
Chris Newton 55afe876e0 feat: fixed typo in readme for docker 2021-02-27 14:31:41 +11:00
Chris Newton 49fadd8c9a feat: added styles, dark mode to frontend 2021-02-27 14:30:39 +11:00
Chris Newton 58157ddfee wip: updating styles for front end 2021-02-26 16:54:08 +11:00
FoxxMD 2a9cd87848 Update package properties 2021-02-25 09:20:37 -05:00
FoxxMD 7b31285f89 Fix missing throw error from polling try-catch
Without it the polling continues forever on loop! oops
2021-02-19 09:14:10 -05:00
FoxxMD 89db858289 Fix import error for Errors from spotify-web-api-node 2021-02-19 09:13:16 -05:00
FoxxMD 2fec6aff6e Implement request and polling retries
* Hierarchical retries and delay options for sources and clients (override general config => individual config)
* Logging for retry attempts
* Respect Retry-After header on responses if present
2021-02-18 15:01:21 -05:00
FoxxMD 9beadfaf0f Bump dependencies to fix spotify-web-api-node crash
spotify-web-api-node#340
2021-02-18 10:04:12 -05:00
FoxxMD dd7e971e71 Fix how scrobbled tracks are returned to source
Returns tracks array should only show that was a track was scrobbled or not (by existing in the array) -- and only be in the array once.
2021-01-12 09:47:47 -05:00
FoxxMD 1aee74bfc1 Fix scrobbled tracks statistic for scrobble clients 2021-01-11 10:11:40 -05:00
FoxxMD 123c171d07 Fix case insensitive check 2021-01-05 17:10:18 -05:00
FoxxMD 764b490cfd Fix includes usage 2021-01-05 10:20:35 -05:00
FoxxMD aec3398edb Return a proper response on lastfm cb 2021-01-04 17:21:52 -05:00
FoxxMD e552820b4d Add missing session property for lastfm config 2021-01-04 17:07:54 -05:00
FoxxMD 1bc8edb07c Update documentation for last.fm
* Add last.fm configuration docs, kitchen sink, and json example
* Update readme
2021-01-04 17:02:36 -05:00
FoxxMD 085e61db5c Refactor app for lastfm client and multiple clients (!)
* Pass config dir to client handler
* Rename auth routes to be source/client specific
* Pass client info to status page to enable displaying client stats and actions (auth)
* Handle auth callback from lastfm
2021-01-04 16:35:38 -05:00
FoxxMD 1df6a8d6f7 Add lastfm to scrobble clients handler with some refactoring
* Refactor scrobble clients to be more granular on error handling for scrobble call
* use initialized param on clients to additionally check if they should be used
* Pass config dir to constructor so we can use it any in the class (for lastfm)
* Use error property to determine if we she keep trying to scrobble plays after caught error
2021-01-04 16:34:13 -05:00
FoxxMD d0e1e83ebd Implement Last.fm scrobble client
* Authentication is user-interaction required with saved session file
* Use initialized to signal auth is done and client is ready to scrobble/get tracks
* Add some retry attempts based on error returned from api
2021-01-04 16:27:23 -05:00
FoxxMD e0a2ada59b Add lastfm node client 2021-01-04 16:25:30 -05:00
FoxxMD 855d3f6144 Jellyfin initial code 2021-01-04 12:06:45 -05:00
FoxxMD 0508551dc7 Add airsonic advanced guidance to docs #10 2020-12-09 11:02:37 -05:00
FoxxMD f4a310cda9 Fix variable typo 2020-12-09 11:00:32 -05:00
FoxxMD 6ac8938644 Add subsonic docs #10 2020-12-09 10:51:02 -05:00
FoxxMD f41b3eea55 Implement subsonic ENV based config 2020-12-09 10:38:53 -05:00
FoxxMD db34d92329 Relax time thresholds for finding existing tracks when source is from Subsonic
Due to inaccuracy of subsonic DT need to relax threshold from 10 seconds (or same) to 60 seconds since granularity is only down to a minute #10
2020-12-09 10:27:51 -05:00
FoxxMD a60a0f89ad Filter out recently played subsonic tracks so that only tracks played for a minute or more are valid #10 2020-12-09 10:26:55 -05:00
FoxxMD 1de9c5cdc1 Refactor polling behavior
* Use plain ol async because it gets the job done and i don't need no fancy generator/yield. Can wait until loop is done to detect signal to stop polling
* Refactor polling into AbstractSource, remove duplicated code for Spotify/Subsonic sources
* Add 'canPoll' property to Sources
* Stagger polling invocation on app start so log messages don't get jumbled
* Refactor auth/poll/recent endpoints to be generic (based on source properties)
* Refactor status page to show functionality based on canAuth/canPoll rather than type
2020-12-08 16:49:10 -05:00
FoxxMD 299e37e32a Implement subsonic polling and scrobbling 2020-12-08 15:36:57 -05:00
FoxxMD 4921f963a8 Implement basic Subsonic source with api wrapper and authentication #10 2020-12-08 14:52:51 -05:00
FoxxMD 22cc300733 More doc updates
* Format example json comments to be cleaner to read
* Rewrite intro to better reflect new functionality
2020-12-08 10:05:56 -05:00
FoxxMD 46ee5b89c6 Add a kitchen sink example 2020-12-08 09:34:35 -05:00
FoxxMD e660b8f835 Merge branch 'multiUser' into develop 2020-12-07 16:39:16 -05:00
Matt Foxx 0050d114ce Merge pull request #14 from FoxxMD/add-license-1
Add license
2020-12-07 16:38:59 -05:00
FoxxMD e7ed5d9381 Simplify initializing message for plex/tautulli 2020-12-07 16:36:37 -05:00
FoxxMD a1848c0a44 Fix config example 2020-12-07 16:33:31 -05:00
FoxxMD 49827c80bc Implement better non-authed spotify html on status page 2020-12-07 16:26:01 -05:00
FoxxMD 1c6427b8a7 Fix some markdown issues 2020-12-07 16:12:08 -05:00
FoxxMD ce35b67f37 Rewrite docs #13
* Refactor examples to all be multi-user structured
* Move configuration into its own file
* Separate config approaches into env/json approaches with guidance on which one to use
* Add more comments to example json
* Simplify main readme and provide a more opinionated, minimal example there
2020-12-07 16:05:47 -05:00
FoxxMD 4c0ff1c9ce Fix client naming so it matches sources (for unnamed) 2020-12-07 14:28:00 -05:00
FoxxMD f29e224631 More debug logging for checking existing 2020-12-07 14:19:38 -05:00
FoxxMD d4bdab1f8d Correctly destructure objects when checking for existing submitted 2020-12-07 13:42:29 -05:00
FoxxMD 8f1808e24f Always rename unnamed sources
Derp
2020-12-07 13:21:33 -05:00
FoxxMD 1e5a427a3d Use identifier to make filtering debug statement more descriptive 2020-12-07 13:18:32 -05:00
FoxxMD b8ba0fe224 Add identifier property to abstract source 2020-12-07 13:18:07 -05:00
FoxxMD 058473a595 Add deprecated source configs with warning message 2020-12-07 13:13:59 -05:00
FoxxMD 5696d5598b Refactor source building to only warn about dup names if they are also the same source
Also allow additional configs to be passed before building starts
2020-12-07 13:13:37 -05:00
FoxxMD 937f22e643 Refactor Maloja existing scrobble check to be MUCH more robust
* Refactor comparison methodology to use weighted scoring system (psuedo-fuzzy) instead of all-or-nothing if statements
* Use reference scrobbles (playObjs we have submitted while app is running) to check for existing
* Make title cleaning more robust by removing parenthesis and all "feat" strings
* Add artists matching
* Rewrite debugging info to show score/breakdowns and add more granular debugging options (show on match/no match)
2020-12-07 12:44:48 -05:00
FoxxMD ec6b952110 Implement more generic scrobble client functionality
* Add scrobbledPlayObjs to so we can keep track of formatted playObjs we *know* we have scrobbled (for more accurate comparisons elsewhere)
* Filter scrobbledPlayObjs based on oldest returned recent scrobble from client
* Add instanced generic formatPlayObj for use in other methods
* Add generic source title cleaning function
* Add generic function to search for scrobbled playObj from known submitted playObjs
2020-12-07 12:41:12 -05:00
FoxxMD bbb0adf448 Add source id for Spotify 2020-12-07 12:38:55 -05:00
FoxxMD 67a35cb81a More granular track building functionality
* Change time to use @ to be more compact
* Add artist and track to include options
2020-12-07 12:38:42 -05:00
FoxxMD 44b40cc35f Implement playObj comparison function
Check by source/sourceId, artists, and track
2020-12-07 12:38:01 -05:00
Matt Foxx 8eb4289b18 Create LICENSE 2020-12-06 21:51:20 -05:00
FoxxMD ee614e1a61 Big ol rewrite for multi user
* Refactor source building into a new class to hold all sources
* ScrobbleSources supports single and multi user config variant parsing (based on client parsing)
* Refactor client/source abstract classes to accept a type and name -- logger is based off of both
* Refactor config file parsing into ScrobbleSources
* Refactor status page to use array of source data instead of hardcoding
* Status page now uses flex css to layout sources instead of hardcoding
* Refactor spotify endpoints to require a name from querystring in order to determine which source to work on (middleware with this)
* Refactor ScrobbleClients to use optional scrobble filter name, passed by configured source
* Implement library and server configuration params for plex/tautulli to help with multi-user
* Make library, server, and user checks on plex/tautulli case insensitive and include more debug information
* Consolidate validEvent checks for plex/tautulli
2020-12-04 18:47:05 -05:00
FoxxMD e0a274a314 Update scrobble client to integrate config name into logging label #13 2020-12-04 15:22:33 -05:00
FoxxMD 71c26f1964 Refactor client configuration parsing to support multiple users
* Add sane defaults for single-user mode client configs (naming)
* Refactor client building into a two-stage process of 1) parsing and checking for valid config 2) validating config per client
* Refactor client ENVs config into separate config (don't mix/overwrite env and json configs)
* Check for unique names
* Implement more descriptive and thorough config structure validation including hinting at config source (location)

Relates to #13
2020-12-04 15:09:58 -05:00
FoxxMD 2c83364b9b Merge remote-tracking branch 'origin/master' 2020-12-04 09:34:32 -05:00
FoxxMD 20da4bd0b8 Fix artist concat for spotify recently played 2020-12-03 15:56:49 -05:00
FoxxMD 307d52fbf0 Improve existing scrobble checking for Maloja
* Strip source track of feat and [artist] since Maloja does this as well
* Compare position-agnostic and de-duped tokens from source/scrobble titles for a better match
2020-12-03 15:41:12 -05:00
FoxxMD 101e2c8657 Refactor handling of artists values
* Refactor playObj artist (string) => artists (array) so we have more info to work with and less ambiguity in naming
* Use forward slash as artist deliminator for Maloja scrobbling because it parses better
* Track building the artists uses forward slash for less ambiguity as well
2020-12-03 14:50:57 -05:00
FoxxMD 8bdeca9089 Add spotify backlog activity disclaimer 2020-12-03 11:13:55 -05:00
FoxxMD 5eb82f0da2 Implement recently played Spotify tracks view
Format returned tracks with padding and "X from now" time for easier reading
2020-12-03 11:10:41 -05:00
FoxxMD 8c31b65525 Implement a bunch of formatting options for building track strings
* Implement func for finding longest string length (for use with padding plays)
* Implement func for truncating string based on a fixed length (for use with padding plays)
* Implement play string building transformers for all parts of the string for finer control
* Implement "X from now" time formatting
2020-12-03 11:08:46 -05:00
FoxxMD f0d439a785 Refactor spotifyApi usage to make token refresh functionality more reusable
* Move all spotifyApi invocation into a wrapped function that handles token refresh so we can just call the api from anywhere without having to worry about re-authenticating
* Implement "get recent played" function using new wrapper and also support formatting results to playObjs before returning
* add open.spotify.com web url to playObj meta if it exists
2020-12-03 11:06:29 -05:00
Matt Foxx 29cdf34ef8 Merge pull request #7 from FoxxMD/develop
Develop
2020-11-27 19:11:27 -05:00
FoxxMD 5a1fb1c08e Attempt to fix use-case when maloja history is empty and provide more logging for not scrobbling
* Fix an empty maloja history always causing oldest scrobble to now() time, preventing time frame from ever being valid
* Add debug logging to timeframe and existing scrobble checks for new tracks
2020-11-25 18:49:23 -05:00
FoxxMD 66dcdc30d2 Add argument for including extra info when building a track string 2020-11-25 18:46:05 -05:00
FoxxMD f74940800b Fix bad return type for existing scrobble
Should only be returning undefined or object
2020-11-25 17:49:16 -05:00
FoxxMD 8a46890b20 Refactor/improve error handling for configuration in spotify and maloja
* Implement api wrapping for maloja and handle formatting error
* Implement testing maloja connection to make sure configuration is valid (check server info and test endpoint)
* Provide better defaults for maloja scrobbles list when empty  (maybe fixes #5)
* Better formatting for maloja scrobble api calls
* Better handling of maloja and spotify configuration issues during initialization (And logging for it)
2020-11-25 14:09:56 -05:00
FoxxMD a49212ea24 Refactor app to use read json handling
Also fix spotify auth request when config is invalid
2020-11-25 14:07:11 -05:00
FoxxMD ce63a9b775 Refactor json reading to handle not found and parsing errors 2020-11-25 14:06:07 -05:00
FoxxMD eb1ee5cfed More try-catch blocks for scrobble clients 2020-11-25 09:46:35 -05:00
FoxxMD 0be4fbb8c5 Move try-catch for main function into async execution 2020-11-25 09:33:45 -05:00
FoxxMD 3437519ef4 Implement error stack logging
And replace users CWD directory with placeholder to make it easier to debugging in public (Github)
2020-11-25 09:33:19 -05:00
FoxxMD 02791d7aba Fix dockerhub links 2020-11-24 15:47:45 -05:00
FoxxMD f8eeebe29f Fix splat arg usage for now
Should be able to just throw arguments at it but for some reason it needs to be in an object for now?
2020-11-24 15:06:10 -05:00
FoxxMD aa95604cc3 Use splat args for media identification on plex/tautulli debug 2020-11-24 14:56:25 -05:00
FoxxMD f981f9e207 Implement splat serialization 2020-11-24 14:56:13 -05:00
FoxxMD eff9cfbdb8 Refactor logging to simplify usage and clean up actual logged statement structure
* Move winston format combining and final formatter function into utils
* Use individual loggers for every "area" they are needed by using winston.loggers
* Create a default logger with final transport options
* util convenience function for creating a new labelled format combo
* util convenience function for created new logger with labelled formatter
* remove label usage everywhere! so much cleaner
* remove logger passing everywhere and replace with winston get call for logger we want (from util function)
2020-11-24 14:12:17 -05:00
FoxxMD 37b4aca6be Move spotify auth code handling into SpotifySource
Just cleaning up index
2020-11-24 12:35:05 -05:00
FoxxMD 77ffcae41b Pass name to plex/tautulli constructor so initialized shows correct label 2020-11-24 12:33:38 -05:00
FoxxMD e50623c9d4 Fix module import 2020-11-24 12:27:09 -05:00
FoxxMD 82f89b51ea Simplify plex/tautulli codebase
* TautulliSource extends Plex class, just changes name and valid event check
* Standardize log levels and verbiage for valid event check
2020-11-24 12:13:23 -05:00
FoxxMD 5b4a53121d Don't log debug for already scrobbled tracks with diff due to noise
Too much logging since we are now checking tracks on every spotify polling interval. Really only need to log if we decide its *not* a dupe because of track diffs
2020-11-24 11:29:20 -05:00
FoxxMD 7c0014b590 Refactor source and client play object handling to be more date independent
* Map all tracks from source/clients immediately to playObjs to reduce cognitive load
* Move responsibility for source and "newFromSource" meta from source -> scrobble clients -- to source -> playObj formatting so playObjs hold all their own meta
* Always immediately sort source/client track lists by playDate ascending for less cognitive load
* Relax valid time frame for maloja client to be based on oldest returned scrobble (since we are always returning most recent scrobbles)
* Refactor debugging for already scrobbled so all diff statements are lumped at the end and formatted
* Simplify spotify source fetching since meta is handled by play objects now
* Fix new track recognition for spotify source by using sorted playObjs instead of relying on order spotify returns tracks
2020-11-24 11:26:47 -05:00
FoxxMD b22d96bbec Implement function for sorting by playDate ascending 2020-11-24 11:21:28 -05:00
FoxxMD f2396c7955 Update plex/tautulli usage for new scrobble function on clients 2020-11-23 17:07:00 -05:00
FoxxMD e4e398ac99 Implement scrobbling backlogged tracks
Check all returned tracked from recent spotify plays against scrobbles from clients to see if any have not been scrobbled. This helps compensate for when Spotify has backlogged their own recent plays (plays being "added" to recent response long after they were actually played)

* Check time frame for returned scrobbles from client before checking if a source track has been scrobbled to make sure only source tracks played within valid time frame are scrobbled
* Remove debug logging for recent tracks checks for now -- since all tracks are now checked
* Use different verbiage in logging when track is from backlog vs. new from source client
2020-11-23 17:02:48 -05:00
FoxxMD 433e077eba Fix backoff interval not reset on new track 2020-11-23 15:46:24 -05:00
FoxxMD 51339cf74d Remove extra level specification outside of log config
Oops forgot this one
2020-11-23 15:24:24 -05:00
FoxxMD eb5ded164c Log to debug when spotify polling doesn't find any new tracks
Also show the most recent track spotify returns
2020-11-23 14:27:06 -05:00
FoxxMD c06f7b4695 Implement basic logging config updates and info
* Show current level/sort/limit for logging on status page
* Render log config values as links and implement endpoint to update them

Now a user can switch to debug level to check more detailed info without restarting the app
2020-11-23 13:47:20 -05:00
FoxxMD 65a211df8b Fix destructuring default object when no spotify config is present 2020-11-23 09:40:52 -05:00
FoxxMD 0efeeafb8c Fix it again 2020-11-18 20:40:41 -05:00
FoxxMD c8408e8e00 Fix user comparison
I'm tired
2020-11-18 20:40:20 -05:00
FoxxMD 4ebe9427a4 Fix async oopsie 2020-11-18 19:55:37 -05:00
FoxxMD ce366dbb7b Fix typo 2020-11-18 19:43:54 -05:00
FoxxMD 5dbebf662b More readme cleanup 2020-11-18 19:42:41 -05:00
FoxxMD c398c3f97f Add config example and instructions in readme 2020-11-18 19:36:07 -05:00
FoxxMD 554327d8d0 Implement plex/tautulli source user restriction 2020-11-18 19:29:05 -05:00
FoxxMD fc80953094 Implement Plex webhook source 2020-11-18 17:26:40 -05:00
FoxxMD 4c2ec559d8 More readme cleanup 2020-11-18 16:52:09 -05:00
FoxxMD 1e1977a07c Change name 2020-11-18 16:39:56 -05:00
FoxxMD 2b0cb894d8 Add Tautulli instructions 2020-11-18 16:34:06 -05:00
FoxxMD cfd128a779 Refactor spotify polling to check for yield error 2020-11-18 16:29:22 -05:00
FoxxMD 658fd9f5ec return yield value as caught error in generator so we can handle inline 2020-11-18 16:29:08 -05:00
FoxxMD 444aa80b9e Refactor to a more class-based structure and decouple spotify from app "running" state
* Refactor spotify config and api building into a Source class
* Move spotify polling loop into Source class and use generator to handle one-run only restriction (Closes #2)
* Signal polling status using promise finally/catch
* Implement formatPlayObj static method to standardize play data
* Implement Tautulli source class
* Refactor client scrobbling and building/config into ScrobbleClients class
* Implement Tautulli endpoint for scrobbling from notification agent webhook
* Add Tautulli information to status page
* Log number of tracks discovered by each source on status page
2020-11-18 15:53:13 -05:00
FoxxMD ce9e09c3a6 Add examples and update readme 2020-11-16 11:55:09 -05:00
FoxxMD 7b45ad6e9e More cleanup on readme 2020-11-16 11:31:48 -05:00
FoxxMD 44ea06020b Use whole numbers for scrobble time diffs for better readability 2020-11-16 11:30:56 -05:00
FoxxMD 89ea2e5ee1 Add badges to readme 2020-11-16 11:29:46 -05:00
FoxxMD 9fb915067e Use max param for returning maloja scrobbles
Still keep since/to in case user is running an older server version

krateng/maloja#53
2020-11-16 11:14:06 -05:00
FoxxMD 55ca8762f2 Improve datetime handling
* Switch to dayjs from date-fns
* Format timestamps and play time to local timezone
2020-11-16 11:12:06 -05:00
FoxxMD ca8589891a Implement index route to display polling status and buffered log data
* (Crude) detection of running poll loop
* Show auth url if not detected (from initialization)
* Output last 50 buffered log statements in descending order

Closes #4
2020-11-16 10:58:03 -05:00
FoxxMD 5383fb8280 Implement back off behavior if listening behavior is idle 2020-11-15 00:03:10 -05:00
FoxxMD 569778d69f Fix path 2020-11-14 20:38:17 -05:00
FoxxMD 9fd769513d Add some docker env var documentation 2020-11-14 20:36:55 -05:00
FoxxMD aa782d3bf7 Move spotify creds file into CONFIG_DIR
* Easier to keep track of all files in one folder in code
* Keeping in CONFIG_DIR means creds file will persist after docker container rebuild on image update
2020-11-14 20:31:14 -05:00
FoxxMD b0fd0c6959 Refactor client config building so that a json file is not required 2020-11-14 20:19:25 -05:00
FoxxMD f05ddd2e3e Improve documentation 2020-11-14 19:44:29 -05:00
FoxxMD 63b73a5ea4 Simplify configuration options and improve configuration documentation
* Removed individual config file location vars in favor of using just CONFIG_DIR or json objects to reduce complexity
* Justified json example comments and removed unused variables
* Tables for env variable documentation in readme (and remove unused)
* Implement LOG_DIR docker volume
* Implement LOG_LEVEL environment for setting global log level
2020-11-14 19:36:02 -05:00
290 changed files with 62416 additions and 1201 deletions
+16 -1
View File
@@ -4,6 +4,21 @@ npm-debug.log
Dockerfile
.dockerignore
.gitignore
.github
.git
spotifyCreds.json
*.log
.idea
config/*.json
config/*.bak
config/*.p8
/docs
/logs
.flatpak-builder
flatpak/generated-sources.json
flatpak/.flatpak-builder
docsite/build
docsite/node_modules
docsite/.docusaurus
docsite/.cache-loader
build
*.secrets
+40
View File
@@ -0,0 +1,40 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
```
Copy and paste as much log data as possible related to this issue here.
```
**Versions (please complete the following information):**
Provide version information for any related sources/clients.
- multi-scrobbler: [e.g. 0.4.0 on docker]
- maloja [e.g. 3.1.4]
- jellyfin [e.g. 10.8.9]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
## Checklist before requesting a review
- [ ] **I am opening this PR for the [`develop` branch](https://github.com/FoxxMD/multi-scrobbler/tree/develop) and NOT `master`.**
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## Describe your changes
## Issue number and link, if applicable
+42
View File
@@ -0,0 +1,42 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- master
# Review gh actions docs if you want to further define triggers, paths, etc
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
jobs:
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
working-directory: ./docsite
- name: Build website
run: npm run build
working-directory: ./docsite
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
publish_dir: ./docsite/build
# The following lines assign commit authorship to the official
# GH-Actions bot for deploys to `gh-pages` branch:
# https://github.com/actions/checkout/issues/13#issuecomment-724415212
# The GH actions bot is used by default if you didn't specify the two fields.
# You can swap them out with your own user credentials.
user_name: github-actions[bot]
user_email: 41898282+github-actions[bot]@users.noreply.github.com
+102
View File
@@ -0,0 +1,102 @@
name: Publish Docker image to Dockerhub
on:
push:
branches:
- 'master'
- 'develop'
tags:
- '*.*.*'
# don't trigger if just updating docs
paths-ignore:
- '**.md'
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Install dev dependencies
run: npm ci
- name: Build Backend
run: 'npm run build:backend'
- name: Test Backend
run: npm run test
push_to_registry:
name: Build and push container images
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./Dockerfile
suffix: ''
platforms: 'linux/amd64,linux/arm64'
- dockerfile: ./debian.Dockerfile
suffix: '-debian'
# can't build arm64 due to a TLS issue when running npm install??
# https://github.com/FoxxMD/multi-scrobbler/issues/126
platforms: 'linux/amd64,linux/arm64'
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
foxxmd/multi-scrobbler
ghcr.io/foxxmd/multi-scrobbler
# generate Docker tags based on the following events/attributes
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
tags: |
type=raw,value=latest,enable={{is_default_branch}},suffix=${{ matrix.suffix }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }},suffix=${{ matrix.suffix }}
type=semver,pattern={{version}},suffix=${{ matrix.suffix }}
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platforms }}
+24
View File
@@ -0,0 +1,24 @@
name: Publish Docker image to Dockerhub
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Install dev dependencies
run: npm ci
- name: Build Backend
run: 'npm run build:backend'
- name: Test Backend
run: npm run test
+14 -2
View File
@@ -1,7 +1,7 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
@@ -117,6 +117,18 @@ dist
.yarn/install-state.gz
.pnp.*
*.json
config/*.json
*.txt
.idea/
src/**/**.js
src/**/**.js.map
*.bak
*.p8
.flatpak-builder
flatpak/generated-sources.json
*.secrets
build
!setupProxy.js
+5
View File
@@ -0,0 +1,5 @@
{
"reporter": "dot",
"extension": "ts",
"import": "tsx/esm"
}
+1 -1
View File
@@ -1 +1 @@
"lts/fermium"
v18.19.1
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": [
"node_modules/",
"**/src/backend/common/schema/**",
"**/src/backend/tests/**",
"register.js",
"**/src/server/**/*.d.ts",
"**/src/client/**"
],
"include": [
"**/src/backend/**/*.ts",
"**/src/**/backend/*.js",
"**/src/**/backend/*.js.map"
],
"extension": [
".ts"
],
"reporter": [
"text-summary",
"html"
],
"report-dir": "./coverage"
}
+8
View File
@@ -0,0 +1,8 @@
# Creating a Pull Request
Please follow these guidelines when contributing code to this repository:
* The PR **must be for the [`develop` branch.](https://github.com/FoxxMD/multi-scrobbler/tree/develop)** The `master` branch is for releases only.
* Use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format when creating commits.
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `develop`.
* Ensure that if your code is covered by [an existing test](./src/backend/tests) that you have updated the test accordingly
+67 -14
View File
@@ -1,26 +1,79 @@
FROM node:fermium-alpine3.10
FROM lsiobase/alpine:3.18 as base
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node
ENV TZ=Etc/GMT
WORKDIR /home/node/app
RUN \
echo "**** install build packages ****" && \
apk add --no-cache \
avahi \
avahi-tools \
nodejs \
npm \
&& \
echo "**** cleanup ****" && \
rm -rf \
/root/.cache \
/tmp/*
COPY package*.json ./
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
USER node
ARG data_dir=/config
VOLUME $data_dir
ENV CONFIG_DIR=$data_dir
RUN npm install
COPY docker/root/ /
COPY --chown=node:node . .
WORKDIR /app
ENV NPM_CONFIG_LOGLEVEL debug
FROM base as build
ARG config_dir=/home/node/config
RUN mkdir -p $config_dir
VOLUME $config_dir
ENV CONFIG_DIR=$config_dir
# 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
# copy dep/TS config and install dev dependencies
#COPY --chown=abc:abc package.json tsconfig.json yarn.lock ./
COPY --chown=abc:abc package*.json tsconfig.json ./
COPY --chown=abc:abc patches ./patches
RUN npm ci \
&& chown -R root:root node_modules
#RUN yarn install
COPY --chown=abc:abc . /app
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run build && rm -rf node_modules
#RUN yarn run build && rm -rf node_modules
FROM base as app
#COPY --chown=abc:abc package.json yarn.lock ./
COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc patches ./patches
COPY --from=build --chown=abc:abc /app/dist /app/dist
COPY --from=build --chown=abc:abc /app/src /app/src
COPY --from=base /usr/local/bin /usr/local/bin
COPY --from=base /usr/local/lib /usr/local/lib
ENV NODE_ENV=production
ENV IS_DOCKER=true
ENV COLORED_STD=true
#
#RUN yarn global add patch-package \
# && yarn install --production=true \
# && yarn global remove patch-package \
# && yarn cache clean --mirror \
# && chown abc:abc node_modules \
# && rm -rf node_modules/ts-node \
# && rm -rf node_modules/typescript
RUN npm ci --omit=dev \
&& npm cache clean --force \
&& chown -R abc:abc node_modules \
&& rm -rf node_modules/@types
ARG webPort=9078
ENV PORT=$webPort
EXPOSE $PORT
CMD [ "node", "index.js" ]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 FoxxMD
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+81 -65
View File
@@ -1,83 +1,99 @@
# spotify-scrobbler
# multi-scrobbler
A single-user, javascript app to scrobble your recent plays to [Maloja](https://github.com/krateng/maloja) (and other clients, eventually)
[![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)](https://github.com/FoxxMD/multi-scrobbler/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)](https://hub.docker.com/r/foxxmd/multi-scrobbler)
<img src="/assets/icon.png" align="right"
alt="multi-scrobbler logo" width="180" height="180">
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
* Supports scrobbling from many **Sources**
* [Spotify](/docsite/docs/configuration/configuration.md#spotify)
* [Plex](/docsite/docs/configuration/configuration.md#plex) or [Tautulli](/docsite/docs/configuration/configuration.md#tautulli)
* [Subsonic-compatible APIs](/docsite/docs/configuration/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
* [WebScrobbler](/docsite/docs/configuration/configuration.md#webscrobbler)
* [Youtube Music](/docsite/docs/configuration/configuration.md#youtube-music)
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm-source)
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz--source-)
* [Deezer](/docsite/docs/configuration/configuration.md#deezer)
* [MPRIS (Linux Desktop)](/docsite/docs/configuration/configuration.md#mpris)
* [Mopidy](/docsite/docs/configuration/configuration.md#mopidy)
* [JRiver](/docsite/docs/configuration/configuration.md#jriver)
* [Kodi](/docsite/docs/configuration/configuration.md#kodi)
* [Google Cast (Chromecast)](/docsite/docs/configuration/configuration.md#google-cast--chromecast-)
* Supports scrobbling to many **Clients**
* [Maloja](/docsite/docs/configuration/configuration.md#maloja)
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm)
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
* Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
**Why should I use this over a browser extension and/or mobile app scrobbler?**
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
* **Open-source** -- Get peace of mind knowing exactly how your personal data is being handled.
* **Track your activity regardless of where you listen** -- Scrobble from many Sources to one Client with ease and without duplicating tracks.
* **Manage scrobbling for others** -- Scrobble for your friends and family without any setup on their part. Easily silo sources to specific clients to keep plays separate.
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
<img src="/assets/status-ui.png" width="800">
## How Does multi-scrobbler (MS) Work?
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
### Source
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
Source configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Source.
* An optional list of Client names that the Source should scrobble to. If omitted the Source also scrobbles to all configured Clients.
### Client
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Installation
Clone this repository somewhere and then install from the working directory
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
```bash
npm install
```
## Configuration
## Setup/Configuration
All configuration is done through json files or environment variables. Reference the [examples in the config folder](https://github.com/FoxxMD/spotify-scrobbler/tree/master/config) more detailed explanations and structure.
**A property from a json config will override the environmental variable.**
### General
[JSON config example](https://github.com/FoxxMD/spotify-scrobbler/blob/master/config/config.json.example)
Environment Variables
* CONFIG_DIR - Default `./config` - Sets configuration directory to look for all other configuration files (if they are not specified)
* CONFIG_PATH - Default `CONFIG_DIR/config.json`
* LOG_PATH - Default `true` - If `false` no logs will be written. If `string` will be the directory logs are written to
* PORT - Default 9078 - Port to run web server on (for authentication callbacks)
### Spotify
[Spotify config example](https://github.com/FoxxMD/spotify-scrobbler/blob/master/config/spotify.json.example)
Environment Variables
* SPOTIFY_CONFIG_PATH - Optional, defaults to `CONFIG_DIR/spotify.json`
* SPOTIFY_CLIENT_ID - **Required**
* SPOTIFY_CLIENT_SECRET - **Required**
* SPOTIFY_ACCESS_TOKEN - Optional if client/secret provided
* SPOTIFY_REFRESH_TOKEN - Optional if client/secret provided
* SPOTIFY_REDIRECT_URI - Optional, default is `http://localhost:{port}/callback`
The app will automatically obtain new access/refresh token if needed and possible. These will override values from configuration.
### Maloja
[Maloja config example](https://github.com/FoxxMD/spotify-scrobbler/blob/master/config/maloja.json.example)
Environment Variables
* MALOJA_CONFIG_PATH - Optional, defaults to `CONFIG_DIR/maloja.json`
* MALOJA_URL - **Required** - Base Url of your Maloja installation
* MALOJA_API_KEY - **Required** - Api Key for scrobbling
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
## Usage
Output is provided to stdout/stderr as well as file if specified in configuration.
On first startup you may need to authroize Spotify by visiting a callback URL. The default url to open is:
A status page with statistics, recent logs, and some runtime configuration options can be found at
```
https://localhost:9078/authSpotify
http://localhost:9078
```
Output is also provided to stdout/stderr as well as file if specified in configuration.
### Running Directly
On first startup you may need to authorize Spotify and/or Last.fm by visiting the callback URL (which can also be accessed from the status page). Visit the status page above to find the applicable link to trigger this.
```
node index.js
```
## Help/FAQ
### Docker
[Docker repository](https://hub.docker.com/repository/docker/foxxmd/spotify-scrobbler)
```
foxxmd/spotify-scrobbler:latest
```
Minimal configuration requires you to bind a host directory for the configuration directory in the container:
```
docker run ... -v /path/on/host/config:/home/node/config ...
```
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
## License
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<style>
.cls-1 {
fill: #c34a36;
}
</style>
</defs>
<path class="cls-1" d="M76.94,65.49c.87-7.85-5.27-14.51-12.94-14.51-4.56,0-8.57,2.36-10.9,5.91h-4.55c2.36-5.11,7.17-8.87,12.95-9.71V20c0-11.05-8.95-20-20-20H5C2.24,0,0,2.24,0,5V123c0,2.76,2.24,5,5,5H41.5c11.05,0,20-8.95,20-20v-27.17c-5.78-.84-10.59-4.6-12.95-9.72h4.55c2.59,3.96,7.27,6.43,12.46,5.82,6.08-.71,10.71-5.37,11.39-11.45ZM15.3,94.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Zm11.12,14.55c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm37.58-39.53c-2.28,0-4.23-1.41-5.02-3.41h-21.43v28.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v28.39h21.43c.93-2.33,3.42-3.86,6.18-3.29,2.05,.42,3.7,2.08,4.13,4.12,.73,3.5-1.92,6.58-5.29,6.58Z"/>
<path class="cls-1" d="M123,0h-36.5c-11.05,0-20,8.95-20,20v27.18c7.55,1.12,13.5,7.2,14.4,14.82h9.55v-28.28c0-1,.68-1.92,1.66-2.08,1.25-.2,2.34,.76,2.34,1.97v60.67c0,1-.68,1.92-1.66,2.08-1.25,.2-2.34-.76-2.34-1.97v-28.39h-9.55c-.9,7.62-6.84,13.7-14.4,14.82v27.18c0,11.05,8.95,20,20,20h36.5c2.76,0,5-2.24,5-5V5c0-2.76-2.24-5-5-5Zm-17.42,108.94c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm11.12-14.55c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

-76
View File
@@ -1,76 +0,0 @@
import ScrobbleClient from "./ScrobbleClient.js";
import request from 'superagent';
import format from 'date-fns/format/index.js';
export default class MalojaScrobbler extends ScrobbleClient {
name = 'Maloja';
refreshScrobbles = async () => {
const {url} = this.config;
const resp = await request.get(`${url}/apis/mlj_1/scrobbles?since=${format(new Date(), 'yyyy/MM/dd')}&to=${format(new Date(), 'yyyy/MM/dd')}`)
this.recentScrobbles = resp.body.list.slice(0, 10);
this.lastScrobbleCheck = new Date();
}
alreadyScrobbled = (title, playDate, duration) => {
const playUnix = playDate.getTime() / 1000;
const lowerTitle = title.toLocaleLowerCase();
return this.recentScrobbles.some((x) => {
const {time: scrobbleTime, title: scrobbleTitle} = x;
const lowerScrobbleTitle = scrobbleTitle.toLocaleLowerCase();
if (lowerTitle.includes(lowerScrobbleTitle) || lowerScrobbleTitle.includes(lowerTitle)) {
// check if scrobble time is same as play date (when the track finished playing AKA entered recent tracks)
let scrobblePlayDiff = Math.abs(playUnix - scrobbleTime);
if (scrobblePlayDiff < 10) {
this.logger.debug(`Scrobble with same name found and the play (finish time) vs. scrobble time diff was smaller than 10 seconds`, {label: this.name});
return true;
}
// also need to check that scrobble time isn't the BEGINNING of the track
let scrobblePlayStartDiff = Math.abs(playUnix - (scrobbleTime - duration));
if (scrobblePlayStartDiff < 10) {
this.logger.debug(`Scrobble with same name found and the play (start time) vs. scrobble time diff was smaller than 10 seconds`, {label: this.name});
return true;
}
this.logger.debug(`Scrobble with same name found but the start/finish times vs scrobble time diffs were too large to consider dups (Start Diff ${scrobblePlayStartDiff}s) (End Diff ${scrobblePlayDiff}s)`, {label: this.name});
return false;
}
return false;
})
}
scrobble = async (playObj) => {
const {url, apiKey} = this.config;
const {
track: {
artists = [],
name,
id,
external_urls: {
spotify,
} = {}
} = {},
played_at
} = playObj;
let artistString = artists.reduce((acc, curr) => acc.concat(curr.name), []).join(',');
const time = new Date(played_at);
try {
await request.post(`${url}/apis/mlj_1/newscrobble`)
.type('json')
.send({
artist: artistString,
title: name,
key: apiKey,
time: time.getTime() / 1000
});
this.logger.info('Scrobbled', {label: this.name});
} catch (e) {
this.logger.error('Error while scrobbling', {label: this.name});
this.logger.log(e);
}
return true;
}
}
-20
View File
@@ -1,20 +0,0 @@
export default class ScrobbleClient {
name;
recentScrobbles = [];
lastScrobbleCheck = new Date();
config;
logger;
constructor(logger, config = {}) {
this.logger = logger;
this.config = config;
}
scrobblesLastCheckedAt = () => {
return this.lastScrobbleCheck;
}
}
+67
View File
@@ -0,0 +1,67 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- *({{commit.scope}})* {{ commit.message | upper_first }}
{%- if commit.breaking %}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{%- endif -%}
{%- endfor -%}
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- *(No Category)* {{ commit.message | upper_first }}
{% if commit.breaking -%}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{% endif -%}
{% endif -%}
{% endfor -%}
{% endfor %}
"""
# remove the leading and trailing whitespaces from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# allow only conventional commits
# https://www.conventionalcommits.org
conventional_commits = true
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
+217
View File
@@ -0,0 +1,217 @@
import {API, Collection, FileInfo, Options} from 'jscodeshift';
import path from 'path';
import fs from 'fs';
export type TransformFrom = 'none' | 'any' | string;
export type TransformTo = 'none' | 'any' | string;
export type ImportTypes = 'any' | 'relative' | 'alias';
const base = process.cwd();
export default function transformer(
file: FileInfo,
{ jscodeshift: j }: API,
options: Options,
) {
const tf = options.transformFrom as string | undefined;
if(tf === undefined) {
throw new Error(`arg '--transformFrom' must be defined.`);
}
const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[];
const tfIsAny = transformFrom.includes('any');
const tt = options.transformTo as string | undefined;
if(tt === undefined) {
throw new Error(`arg '--transformTo' must be defined.`);
}
const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[];
if(transformTo.length > 1 && transformFrom.length !== transformTo.length) {
throw new Error('When more than one Transform To is specified then number of Transform From arguments must match');
}
const itypes = options.importTypes as string | undefined;
if(itypes === undefined) {
throw new Error(`arg '--importTypes' must be defined.`);
}
const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[];
const importsIsAny = importTypes.includes('any');
const fileDir = path.dirname(path.join(base, file.path));
const processingFilePath = file.path;
console.log(`Processing => ${processingFilePath}`);
let source: Collection<any>;
try {
source = j(file.source);
} catch (e) {
console.error(`Failed to parse ${processingFilePath}, will skip`);
console.error(e);
}
const imports = source.find(j.ImportDeclaration)
imports.forEach((x) => {
const importPath = x.value.source.value as string;
const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`;
const pathInfo = path.parse(importPath);
const hasNoExt = pathInfo.ext === '';
let filenameFromDir: string;
if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) {
const pathFull = path.join(fileDir, importPath);
const dir = path.dirname(pathFull);
const normalExt = pathInfo.ext.replace('.', '');
let isRelative: boolean | undefined;
let dirExists: boolean | undefined;
let fileExists: boolean | undefined;
try {
// is dir path real?
fs.realpathSync(dir);
dirExists = true;
} catch (e) {
isRelative = false;
dirExists = false;
}
if(isRelative === undefined) {
// if extension is none we need to check for any file in dir with this name
if(hasNoExt) {
const dirFiles = fs.readdirSync(dir)
filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name);
if(filenameFromDir === undefined) {
fileExists = false;
isRelative = false;
} else {
fileExists = true;
isRelative = true;
}
} else {
try {
// does a file exist?
fs.realpathSync(pathFull);
isRelative = true;
} catch (e) {
isRelative = false;
fileExists = false;
}
}
}
if(!importsIsAny) {
if(!isRelative && !importTypes.includes('alias')) {
console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`);
return;
} else if(isRelative && !importTypes.includes('relative')) {
console.log(`${pathFull} => Import is relative but import types does not specify relative`);
return;
}
}
// determine transformTo
// if there is only one TO then use it
let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined;
if(derivedTT === undefined) {
// otherwise we find the TO by using the same index as the matching FROM extension type
const tfIndex = transformFrom.findIndex((x) => {
if(x === 'any') {
return true;
}
if(x === 'none' && hasNoExt) {
return true;
}
if(x === normalExt) {
return true;
}
});
if(tfIndex === -1) {
console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`);
return;
}
derivedTT = transformTo[tfIndex];
}
let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`;
let transformedImport: string;
switch(derivedTT) {
case 'none':
if(hasNoExt) {
console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`);
return;
}
transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name);
break;
case 'any':
if(hasNoExt && filenameFromDir) {
transformedImport = filenameFromDir;
} else {
const otherFilename = fs.readdirSync(dir).find(x => {
const pinfo = path.parse(x);
return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext;
});
if(otherFilename === undefined) {
console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
return;
}
transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename);
transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})`
}
break;
default:
transformedImport = combineDirFile(pathInfo.dir, `${path.parse(filenameFromDir).name}.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`);
break;
}
console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`);
j(x).replaceWith(
j.importDeclaration(
x.node.specifiers,
j.stringLiteral(transformedImport)
)
);
} else {
console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`);
return;
}
});
/**
* Early exit condition
* -----
* It is often good practice to exit early and return the original source file
* if it does not contain code relevant to the codemod.
* See this page for more information:
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output
*/
// if (/* Some condition here */ true) {
// return file.source;
// }
/**
* Codemod logic goes here 👇
* -----
* This is where the core logic for your codemod will go,
* consider grouping specific actions into 'motions' and running them in sequence
*
* See this page for more information:
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions
*/
//source.findVariableDeclarators('foo').renameTo('bar');
/**
* Return your modified AST here 👇
* -----
* This is where your modified AST will be transformed back into a string
* and written back to the file.
*/
return source.toSource(options.printOptions);
}
const combineDirFile = (dir: string, file: string) => {
if(dir === '') {
return file;
}
return `${dir}${path.sep}${file}`;
}
+12
View File
@@ -0,0 +1,12 @@
These are **example configurations** for all Source/Client types and AIO config.
These can be used as-is by renaming them to `.json` and filling or replacing sample data.
For docker installations these examples are copied to your configuration directory on first-time use.
These are **NOT** exhaustive examples. You should consult the **configuration** documentation and the **schema explorer links** for each source/config type to see a complete list of options and descriptions for all properties.
Documentation at
* [internal docs](../docsite/docs/configuration/configuration.md)
* External Link: https://foxxmd.github.io/multi-scrobbler/docs/configuration
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MyCast",
"data": {
"blacklistedDevices": [],
"whitelistedDevices": [],
"blacklistedApps": [],
"whitelistedApps": []
}
}
]
+63 -15
View File
@@ -1,23 +1,71 @@
{
// string specifies a directory location to log to.
// boolean FALSE means do not log to file
// boolean TRUE means log to working directory
"logPath": true, // optional
"interval": 60, // optional, number of seconds to wait before checking spotify for new tracks
"port": 9078, // optional, port for server to start on
"spotify": 'object or string', // can specify full spotify config object here OR a string location of json file OR do not include to use config dir location
// array of client configurations
"debugMode": false,
"sourceDefaults": {
"maxPollRetries": 1,
"maxRequestRetries": 1,
"retryMultiplier": 1.5,
"scrobbleThresholds": {
"duration": 30,
"percent": 50
},
"options": {
"logPayload": false,
"logFilterFailure": "warn",
"logPlayerState": false
}
},
"clientDefaults": {
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
"sources": [
{
"type": "spotify",
"enable": true,
"clients": ["myConfig"],
"name": "mySpotifySource",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback"
}
}
],
"clients": [
{
"type": "maloja", // client name
"data": "/someLocation/aFile.json" // location to look for json configuration in
},
{
"type": "maloja", // client name
"type": "maloja",
"enable": true,
"name": "myConfig",
"data": {
// client configuration can also be an object
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
}
}
],
"webhooks": [
{
"name": "FirstGotifyServer",
"type": "gotify",
"url": "http://localhost:8070",
"token": "MyGotifyToken",
"priorities": {
"info": 5,
"warn": 7,
"error": 10
}
},
"maloja" // client can also be just the type if the configuration is located in the default configuration directory
{
"type": "ntfy",
"name": "MyNtfyFriendlyNameForLogs",
"url": "http://localhost:9991",
"topic": "MyMultiScrobblerTopic",
"username": "Optional",
"password": "Optional",
"priorities": {
"info": 3,
"warn": 4,
"error": 5
}
}
]
}
+13
View File
@@ -0,0 +1,13 @@
[
{
"name": "FoxxMDeezer",
"enable": true,
"clients": [],
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/deezer/callback",
"interval": 60
}
}
]
+15
View File
@@ -0,0 +1,15 @@
[
{
"name": "MyJellyfin",
"enable": true,
"clients": [],
"data": {
"users": ["FoxxMD"],
"servers": ["myServer","anotherServer"]
},
"options": {
"logPayload": false,
"logFilterFailure": "warn"
}
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MyJriver",
"enable": true,
"data": {
"url": "0.0.0.0",
"username": "auser",
"password": "apassword"
}
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MyKodi",
"enable": true,
"data": {
"url": "http://localhost:8080/jsonrpc",
"username": "myUser",
"password": "myPassword"
}
}
]
+12
View File
@@ -0,0 +1,12 @@
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "client",
"data": {
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "brainz",
"enable": true,
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
}
}
]
+10 -4
View File
@@ -1,4 +1,10 @@
{
"url": "https://domain.tld", // the base url of your maloja installation
"apiKey": "string" // your maloja api key
}
[
{
"name": "myMaloja",
"enable": true,
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
}
}
]
+12
View File
@@ -0,0 +1,12 @@
[
{
"name": "MyMopidy",
"enable": true,
"data": {
"url": "localhost",
"uriBlacklist": [],
"uriWhitelist": [],
"albumBlacklist": []
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "ubuntu",
"enable": true,
"data": {
"whitelist": ["vlc", "mpd"],
"blacklist": ["spotify"]
}
}
]
+15
View File
@@ -0,0 +1,15 @@
[
{
"name": "MyPlex",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+13 -6
View File
@@ -1,6 +1,13 @@
{
"clientId": "string", // spotify client id
"clientSecret": "string", // spotify client secret
"redirectUri": "http://localhost:9078/callback", // optional, spotify callback url. useful if this installation is behind a reverse proxy
"callbackPath": "callback" // optional, if redirectUri does not end with /callback then must specify path here
}
[
{
"name": "MySpotify",
"enable": true,
"clients": [],
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback",
"interval": 60
}
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MySubsonic",
"enable": true,
"data": {
"url": "http://localhost:4040/airsonic",
"user": "yourUser",
"password": "yourPassword"
}
}
]
+15
View File
@@ -0,0 +1,15 @@
[
{
"name": "MyTautuilli",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "MyWebScrobbler",
"data": {
"slug": null,
"whitelist": [],
"blacklist": []
}
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MyYTMusic",
"enable": true,
"clients": [],
"data": {
"cookie": "VISITOR_INFO1_LIVE=jMDXz2_L8rY; __Secure-3PAPISID=3AxsXpSXGqOInSDn1jEKn; DEVICE_INFO=ChxOekU0TmTBpjek5EWZ0G; YSC=7gZdl3Zdl3; SID=TwhNsaZRXYTAtXxzGyu6rZdpg2HvGROeW8J4Ym_FhkhoZMUYEQ.; __Secure-1PSID=TwhNOsaZRXYTyRBe4rxAtXRIKsIEtk_Qot2VLBNfHQrQ.; __Secure-3PSID=ZRXYTAtXRIKsIEtk_Qot2yRBerZdpg2HvvZRXYTAtXRIKsIEtk_Qot2yRBerkuZICFQ.; HSID=A1UMmELW79; SSID=AKhomOs; APISID=IlHHmuzkPdQzZZDhHn3; SAPISID=3AxsXpy0u75Qb/n1jEKn; __Secure-1PAPISID=3AxsXpQb/AkSDn1jEKn; LOGIN_INFO=AFmP6vFpyVCZZAIgDwbkhWMBBhluaIWAPP:QUQ314UW5NWMjNmd2ZUJnYnJsakdIMjZoaE5zVVMjNmd2ZZUiHRlb3ZlV3ZIcUVyRVIMjNmdjNmd2ZZUivYlNqX2ZNZUiHdUNFNFdaYmJIW1NkJRX3hqdlU2YnFESkFuSS1uTldnZVRmLXNjWFc5OUJuR3dTd3JsZGZYa2EtZFQ2a0k2Ry1KQQ==; PREF=volume=26; SIDCC=AFvI_94PxXwls-ndqpGfPgFX3FWj80y_94PxXwls-ndqfSh15sP; __Secure-1PSIDCC=AFvIBnUbRr96I96UCIp2U4T8HRVk2B0HfKzhzxwsiP; __Secure-3PSIDCC=AFvIB3bINuUN0ETDR9gO91wpwWIVmpGki3BxT3bINuUN0ETDR9gO91wCH",
"authUser": "0"
}
}
]
+112
View File
@@ -0,0 +1,112 @@
#FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy as base
FROM ghcr.io/linuxserver/baseimage-debian:bookworm as base
ENV TZ=Etc/GMT
ENV NODE_VERSION 20.11.1
# borrowing openssl header removal trick from offical docker-node
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
RUN \
ARCH= OPENSSL_ARCH= && dpkgArch="$(dpkg --print-architecture)" \
&& case "${dpkgArch##*-}" in \
amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \
ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \
s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \
arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \
armhf) ARCH='armv7l' OPENSSL_ARCH='linux-armv4';; \
i386) ARCH='x86' OPENSSL_ARCH='linux-elf';; \
*) echo "unsupported architecture"; exit 1 ;; \
esac && \
set -ex && \
echo "**** install build packages ****" && \
apt-get update && \
apt-get install --no-install-recommends -y \
#ca-certificates \
xz-utils \
curl && \
echo "**** Fetch and install node****" && \
# get node/npm directly from nodejs dist \
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L41
curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr --strip-components=1 --no-same-owner && \
rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
ln -s /usr/bin/node /usr/bin/nodejs && \
# curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
# apt-get install --no-install-recommends -y nodejs && \
npm update -g npm && \
echo "**** cleanup ****" && \
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L49
# Remove unused OpenSSL headers to save ~34MB
# (does not affect arm64 issue below)
find /usr/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; && \
apt-get purge --auto-remove -y perl xz-utils && \
apt-get autoclean && \
apt-get autoremove && \
rm -rf \
/config/.cache \
/root/cache \
/var/lib/apt/lists/* \
/var/tmp/* \
/tmp/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ARG data_dir=/config
VOLUME $data_dir
ENV CONFIG_DIR=$data_dir
COPY docker/root /
WORKDIR /app
FROM base as build
COPY --chown=abc:abc package*.json tsconfig.json ./
COPY --chown=abc:abc patches ./patches
# for debugging, so the build fails faster when timing out (arm64)
#RUN npm config set fetch-retries 1 && \
# npm config set fetch-retry-mintimeout 5000 && \
# npm config set fetch-retry-maxtimeout 5000
# https://www.npmjs.com/package/tls-test
# used to test that the OS supports downloading packages over HTTPS with TLS 1.2 enforced
# -- this always succeeds but a good sanity check
#RUN npm install -g https://tls-test.npmjs.com/tls-test-1.0.0.tgz
# This FAILS when building arm64 but not amd64 (and alpine-based Dockerfile has no issues building arm64)
# see https://github.com/FoxxMD/multi-scrobbler/issues/126
RUN npm ci \
--verbose \
# --no-audit \
&& chown -R root:root node_modules
# running with --no-audit does not have any affect even though the error only logs with prefix 'npm verb audit error'
COPY --chown=abc:abc . /app
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run build && rm -rf node_modules
FROM base as app
COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc patches ./patches
COPY --from=build --chown=abc:abc /app/dist /app/dist
COPY --from=build --chown=abc:abc /app/src /app/src
COPY --from=base /usr/bin /usr/bin
COPY --from=base /usr/lib /usr/lib
ENV NODE_ENV=production
ENV IS_DOCKER=true
RUN npm ci --omit=dev \
&& npm cache clean --force \
&& chown -R abc:abc node_modules \
&& rm -rf node_modules/@types \
/root/.cache
ARG webPort=9078
ENV PORT=$webPort
EXPOSE $PORT
+26
View File
@@ -0,0 +1,26 @@
version: '3.7'
services:
multi-scrobbler:
image: foxxmd/multi-scrobbler
container_name: multi-scrobbler
environment:
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#- SPOTIFY_CLIENT_ID=
#- SPOTIFY_CLIENT_SECRET=
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
#- 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
#networks:
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
ports:
- 9078:9078 # first port is the HOST port multi-scrobbler will serve UI and API on
restart: unless-stopped
+33
View File
@@ -0,0 +1,33 @@
#!/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"
# 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
fi
# add example configs
if [ "$POPULATE_EXAMPLES" = true ]; then
echo "Directory is empty, adding examples..."
cp -r /app/config/. "${CONFIG_DIR}"/
fi
# permissions
echo "chown'ing directory to ensure correct permissions."
chown -R abc:abc "${CONFIG_DIR}"
echo "Done!"
echo -e "-------------------------------------\n"
@@ -0,0 +1 @@
oneshot
@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-ms-config/run
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bash
# used https://github.com/linuxserver/docker-wikijs/blob/master/root/etc/s6-overlay/s6-rc.d/svc-wikijs/run as a template
# NODE_ARGS can be passed by ENV in docker command like "docker run foxxmd/multi-scrobbler -e NODE_ARGS=--optimize_for_size"
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
@@ -0,0 +1 @@
longrun
+20
View File
@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+41
View File
@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
+192
View File
@@ -0,0 +1,192 @@
<!-- TOC -->
* [Connection Issues](#connection-issues)
* [Plex/Tautulli/Jellyfin don't connect](#plextautullijellyfin-dont-connect)
* [Jellyfin has warnings about undefined or missing data](#jellyfin-has-warnings-about-undefined-or-missing-data)
* [Jellyfin has warnings about missing headers](#jellyfin-has-warnings-about-missing-headers)
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
* [Sporadic network problems on docker](#sporadic-network-problems-on-docker)
* [Configuration Issues](#configuration-issues)
* [Config could not be parsed](#config-could-not-be-parsed)
* [Scrobbling Issues](#scrobbling-issues)
* [Last.fm does not scrobble tracks with multiple artists correctly](#lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly)
* [Jellyfin does not scrobble tracks with multiple artists correctly](#jellyfin-does-not-scrobble-tracks-with-multiple-artists-correctly)
* [Google Cast track information is missing/incorrect or MS player has weird times](#google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times)
* [Google Cast device does not track media](#google-cast-device-does-not-track-media)
<!-- TOC -->
# Connection Issues
## Plex/Tautulli/Jellyfin don't connect
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.
multi-scrobbler will log information about any server that connects to it for these three 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
```
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
```
**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.**
This is not something multi-scrobbler can fix and means you have an issue in your network.
### Troubleshooting
Check or try all these steps before submitting an issue:
#### Turn on Debug Logging
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `LOG_LEVEL=debug`:
* using node `LOG_LEVEL=debug ... node src/index.js`
* using docker `docker run -e LOG_LEVEL=debug ... foxxmd/multi-scrobbler`
Check the output for any additional information.
#### Check Host name and URL
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
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
Plex/Tautulli/Jellyfin all have logs that will log if they cannot connect to multi-scrobbler. Check these for further information.
##### Plex
Settings -> Manage -> Console
##### Tautulli
Check the command-line output of the application or docker logs.
##### Jellyfin
Administration -> Dashboard -> Advanced -> Logs
## Jellyfin has warnings about undefined or missing data
Make sure you have
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
You can verify the payload sent from the webhook by modifying your jellyfin configuration to include `logPayload: true` which will output the raw payload to DEBUG level logging:
```json
[
{
"name": "MyJellyfin",
"clients": [],
"data": {
"users": ["FoxxMD"],
"options": {
"logPayload": true
}
}
}
]
```
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
## Jellyfin has warnings about missing headers
If you experience issues trying to scrobble with Jellyfin and find this in your MS logs
```
[API] Jellyfin is not sending a request with valid headers...
```
A workaround that may fix this:
* In Webhook settings:
* [In the webhook you have already configured...](configuration/configuration.md#jellyfin)
* Add Request Header...
* **Key:** `Content-Type`
* **Value:** `application/json`
* Then Save
## Spotify/Deezer/LastFM won't authenticate
Ensure any **client id** or **secrets** are correct in your configuration.
The callback/redirect URL for these services must be:
* the same address you would use to access the multi-scrobbler web interface
* the web-interface must be accessible from the browser you are completing authentication from.
If multi-scrobbler is not running on the same machine your browser is on then the default/example addresses (`http://localhost...`) **will not work.** You must determine the address you can reach the web interface at (such as `http://192.168.0.140:9078`) then use that in place of `localhost` in the callback URLs.
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
## Sporadic network problems on docker
If you encounter networking issues like:
* sporadic timeouts (`ETIMEDOUT`) without a pattern
* DNS errors (`EAI_AGAIN`) that do no occur consistently
* Failures to reach a host that was previously fine (`EHOSTUNREACH`)
there may be an issue with the underlying docker image OS (alpine) that may be solved by switching to a different image. Try switching to a `*-debian` variant tag (only available for ARM hosts) to see if this resolves your issue. IE `multi-scrobbler:latest-debian` or `multi-scrobbler:develop-debian`
# Configuration Issues
## Config could not be parsed
If you see something like this in your logs:
```
2023-02-19T10:05:42-06:00 warn : [App] App config file exists but could not be parsed!
2023-02-19T10:05:42-06:00 error : [App] Exited with uncaught error
2023-02-19T10:05:42-06:00 error : [App] Error: config.json could not be parsed
```
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
## Last.fm does not scrobble tracks with multiple artists correctly
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
## Jellyfin does not scrobble tracks with multiple artists correctly
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
## Google Cast track information is missing/incorrect or MS player has weird times
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
To diagnose these issues you [**must enable payload logging**](configuration/configuration.md#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
## 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/configuration.md#allow-unknown-media-type) section to fix this
```
My Artist - Example Track has 'movie' media type so will not track
```
Refer to [Force Media Tracking](configuration/configuration.md#forcing-media-tracking) section to fix this
@@ -0,0 +1,8 @@
{
"label": "Configuration",
"position": 2,
"link": {
"type": "generated-index",
"description": "Configuring Multi-Scrobbler and examples"
}
}
+986
View File
@@ -0,0 +1,986 @@
---
sidebar_position: 2
title: Overview
---
# Configuration
<!-- TOC -->
* [Overview](#overview)
* [ENV-Based Configuration](#env-based-configuration)
* [File-Based Configuration](#file-based-configuration)
* [All-in-One File Configuration](#all-in-one-file-configuration)
* [Specific File Configuration](#specific-file-configuration)
* [Application Options](#application-options)
* [Base URL](#base-url)
* [Source Configurations](#source-configurations)
* [Spotify](#spotify)
* [Plex](#plex)
* [Tautulli](#tautulli)
* [Subsonic](#subsonic)
* [Jellyfin](#jellyfin)
* [Last.fm (Source)](#lastfm--source-)
* [Listenbrainz (Source)](#listenbrainz--source-)
* [Deezer](#deezer)
* [Youtube Music](#youtube-music)
* [MPRIS](#mpris)
* [Mopidy](#mopidy)
* [JRiver](#jriver)
* [Kodi](#kodi)
* [WebScrobbler](#webscrobbler)
* [Multiple Users](#multiple-users)
* [Google Cast (Chromecast)](#google-cast--chromecast-)
* [Connecting Devices](#connecting-devices)
* [What Media Does MS Scrobble?](#what-media-does-ms-scrobble)
* [Cast Troubleshooting](#cast-troubleshooting)
* [Client Configurations](#client-configurations)
* [Maloja](#maloja)
* [Last.fm](#lastfm)
* [Listenbrainz](#listenbrainz)
* [Monitoring](#monitoring)
* [Webhook Configurations](#webhook-configurations)
* [Gotify](#gotify)
* [Ntfy](#ntfy)
* [Health Endpoint](#health-endpoint)
<!-- TOC -->
# Overview
[**Sources** and **Clients**](/#how-does-multi-scrobbler-ms-work) are configured using environmental (ENV) variables and/or json files.
**MS will parse configuration from both configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
TIP: Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
## ENV-Based Configuration
This is done by passing environmental variables and so does not require any files to run MS.
* Using a docker container EX `docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...`
* Using a local installations by exporting variables before running MS EX `SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret node index.js`
Use ENV-based configuration if:
* You are the only person for whom MS is scrobbling for
* You have a very simple setup for MS such as one scrobble [Client](/#client) and one [Source](/#source) IE Plex -> Maloja
## File-Based Configuration
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
* Local installation -> `PROJECT_DIR/config`
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.md#docker) for how to configure this correctly
Use File-based configuration if:
* You have many [Sources](/#source)
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
Funkwhale...
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
* You want only some **Sources** to scrobble to some **Clients** IE Fred's Spotify account scrobbles to Fred's Maloja
server, but not Mary's Maloja server
File-based configurations located in the `CONFIG_DIR` directory can be parsed from
* an **all-in-one** config file named `config.json` that contains information for all Sources and Clients and/or
* many **specific** files named based on the client/source to configure IE `plex.json` `spotify.json`
There are **example configurations** for all Source/Client types and AIO config located in the [/config](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
For docker installations these examples are copied to your configuration directory on first-time use.
There is also a [**kitchensink example**](kitchensink.md) that provides examples of using all sources/clients in a complex configuration.
### All-in-One File Configuration
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
Example directory structure:
```
/CONFIG_DIR
config.json
```
<details>
<summary>Config Example</summary>
```json5 title="config.json"
{
//...
"sources": [
{
"name": "myConfig",
"type": "spotify",
"clients": [
"myMalojaClient"
],
"data": {
"clientId": "anExample"
//...
}
}
],
"clients": [
{
"name": "myFirstMalojaClient",
"type": "maloja",
"data": {
"url": "http://myMalojaServer.example",
// ...
}
}
]
}
```
</details>
`config.json` can also be used to set default behavior for all sources/clients using `sourceDefaults` and `clientDefaults` properties.
See [config.json.example](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/config.json.example) for an annotated example or check out [the kitchen sink example](kitchensink.md).
### Specific File Configuration
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
Example directory structure:
```
/CONFIG_DIR
plex.json
spotify.json
maloja.json
```
<details>
<summary>Config Example</summary>
```json5
// in maloja.json
[
{
"name": "myFirstMalojaClient",
"data": {
"url": "http://myMalojaServer.example",
"apiKey": "myKey"
}
},
{
"name": "mySecondMalojaClient",
"data": {
"url": "http://my2ndMalojaServer.example",
"apiKey": "myKey"
}
}
]
```
</details>
See the [/config](https://github.com/FoxxMD/multi-scrobbler/blob/master/config) directory of this project for examples of each type of config file or reference specific files below.
# Application Options
These options affect multi-scrobbler's behavior and are not specific to any source/client.
#### Base URL
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](#spotify), [lastfm](#lastfm), and [deezer](#deezer) -- as well as some logging hints.
* Default => `http://localhost`
* Set with [ENV](#env-based-configuration) `BASE_URL` or `baseUrl` [all-in-one configuration](#all-in-one-file-configuration)
EX: Lastfm Redirect Url is `BASE_URL:PORT/lastfm/callback` -- Set `BASE_URL=http://192.168.0.101` => Redirect URL is `http://192.168.0.101:9078/lastfm/callback` (when no other redirectUri is specified for [lastfm configuration](#lastfm))
Useful when running with [docker](../installation/installation.md#docker) so that you do not need to specify redirect URLs for each configuration.
# Source Configurations
## [Spotify](https://www.spotify.com)
To access your Spotify history you must [register an application](https://developer.spotify.com/dashboard) to get a
Client ID/Secret. Make sure to also whitelist your redirect URI in the application settings.
**NOTE:** If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-is-Automix/ta-p/5257278) enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is "mixed" in your queue with a shorter play time than its actual length and [Spotify does not report this modified play time in its API.](https://community.spotify.com/t5/Spotify-for-Developers/Wrong-duration-ms-of-track-with-Automix/m-p/5429147) This **does not affect MS's ability to scrobble** from Spotify but it will affect the accuracy of the duration MS reports was played.
### ENV-Based
| 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` |
### File-Based
See [`spotify.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/spotify.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSpotifySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Plex](https://plex.tv)
Check the [instructions](plex.md) on how to setup a [webhooks](https://support.plex.tv/articles/115002267687-webhooks) to scrobble your plays.
### ENV-Based
| Environmental Variable | Required | Default | Description |
|------------------------|----------|---------|-------------------------------------------------|
| `PLEX_USER` | No | | The a comma-delimited list of usernames to scrobble tracks for. No usernames specified means all tracks by all users will be scrobbled. |
### File-Based
See [`plex.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/plex.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FPlexSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Tautulli](https://tautulli.com)
Check the [instructions](plex.md) on how to setup a notification agent.
### ENV-Based
| Environmental Variable | Required | Default | Description |
|------------------------|----------|---------|-------------------------------------------------|
| `TAUTULLI_USER` | No | | The a comma-delimited list of usernames to scrobble tracks for. No usernames specified means all tracks by all users will be scrobbled. |
### File-Based
See [`tautulli.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/tautulli.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FTautulliSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Subsonic](http://www.subsonic.org/)
Can use this source for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) and supports the [`getNowPlaying`](http://www.subsonic.org/pages/api.jsp#getNowPlaying) endpoint (such as [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
**Known Issues:**
* "Time played at" is somewhat inaccurate since the api only reports "played X minutes ago" so...
* All scrobble times are therefore "on the minute" and you may experience occasional duplicate scrobbles
* "played X minutes ago" sometimes is also not reported correctly
* Multiple artists are reported as one value and cannot be separated
* If using [Airsonic Advanced](https://github.com/airsonic-advanced/airsonic-advanced) the password used (under **Credentials**) must be **Decodable**
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|----------------------------------|----------------------------------------------------|
| `SUBSONIC_USER` | Yes | | |
| `SUBSONIC_PASSWORD` | Yes | | |
| `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server |
### File-Based
See [`subsonic.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/subsonic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSubSonicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Jellyfin](https://jellyfin.org/)
Must be using Jellyfin 10.7 or greater
* In the Jellyfin desktop web UI Navigate to -> Administration -> Dashboard -> Plugins -> Catalog
* Under Notifications -> **Webhook** -> Install, then restart your server
* Navigate back to -> Administration -> Dashboard -> Plugins -> My Plugins -> Webhook
* Click "..." -> Settings
* In Webhook settings:
* `Add Generic Destination`
* In the new `Generic` dropdown:
* Webhook Url: `http://localhost:9078/jellyfin`
* Notification Type: `Playback Progress`
* Item Type: `Songs`
* Check `Send All Properties`
* Save
If you see errors in the MS logs regarding `missing headers` when using Jellyfin [see this workaround.](../FAQ.md#jellyfin-has-warnings-about-missing-headers)
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|-------------------------------------------------------------------|
| `JELLYFIN_USER` | | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
| `JELLYFIN_SERVER` | | | Comma-separated list of Jellyfin server names to scrobble from |
### File-Based
See [`jellyfin.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/jellyfin.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FJellySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Last.fm (Source)](https://www.last.fm)
See the [Last.fm (Client)](#lastfm) setup for registration instructions. You may need to disable "Hide recent listening information" on your [privacy page](https://www.last.fm/settings/privacy) for this to work.
### ENV-Based
No support for ENV based for Last.fm as a client (only source)
### File-Based
See [`lastfm.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/lastfm.json.example), change `configureAs` to `source`. Or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Listenbrainz (Source)](https://listenbrainz.org)
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
**NOTE:** You cannot use ENV variables shown in the [Listenbrainz Client config](#listenbrainz) -- multi-scrobbler assumes Listenbrainz ENVs are always used for the **client** configuration. You must use the file-based config from below to setup Listenbrainz as a Source.
### File-Based
See [`listenbrainz.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
**Change `configureAs` to `source`**
## [Deezer](https://deezer.com/)
Create a new application at [Deezer Developers](https://developers.deezer.com/myapps)
* Application Domain must be the same as your multi-scrobbler domain. Default is `localhost:9078`
* Redirect URL must end in `deezer/callback`
* Default would be `http://localhost:9078/deezer/callback`
After application creation you should have credentials displayed in the "My Apps" dashboard. You will need:
* **Application ID**
* **Secret Key**
* **Redirect URL** (if not the default)
**If no access token is provided...**
After starting multi-scrobbler with credentials in-place open the dashboard (`http://localhost:9078`) and find your Deezer source. Click **(Re)authenticate and (re)start polling** to start the login process. After login is complete polling will begin automatically.
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|-----------------------------------------|----------------------------------------------------|
| `DEEZER_CLIENT_ID` | Yes | | Your **Application ID** |
| `DEEZER_CLIENT_SECRET` | Yes | | Your **Secret Key** |
| `DEEZER_REDIRECT_URI` | No | `http://localhost:9078/deezer/callback` | URI must end in `deezer/callback` |
### File-Based
See [`deezer.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/deezer.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FDeezerSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Youtube Music](https://music.youtube.com)
Credentials for YT Music are obtained from a browser request to https://music.youtube.com **once you are logged in.** [Specific requirements are here and summarized below:](https://github.com/nickp10/youtube-music-ts-api/blob/master/DOCUMENTATION.md#authenticate)
* Open a new tab
* Open the developer tools (Ctrl-Shift-I) and select the “Network” tab
* Go to https://music.youtube.com and ensure you are logged in
Then...
1. Find and select an authenticated POST request. The simplest way is to filter by /browse using the search bar of the developer tools. If you dont see the request, try scrolling down a bit or clicking on the library button in the top bar.
2. **Make sure **Headers** pane is selected and open
3. In the **Request Headers** section find and copy the **entire value** found after `Cookie:` and use this as the `cookie` value in your multi-scrobbler config
4. If present, in the **Request Headers** section find and copy the number found in `X-google-AuthUser` and use this as the value for `authUser` in your multi-scrobbler config
![Google Headers](google-header.jpg)
NOTES:
* YT Music authentication is "browser based" which means your credentials may expire after a (long?) period of time OR if you log out of https://music.youtube.com. In the event this happens just repeat the steps above to get new credentials.
* Communication to YT Music is **unofficial** and not supported or endorsed by Google. This means that **this integration may stop working at any time** if Google decides to change how YT Music works in the browser.
### File-Based
See [`ytmusic.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/ytmusic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FYTMusicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/)
MPRIS is a standard interface for communicating with Music Players on **linux operating systems.**
If you run Linux and have a notification tray that shows what media you are listening to, you likely have access to MPRIS.
![Notification Tray](mpris.jpg)
multi-scrobbler can listen to this interface and scrobble tracks played by **any media player** that communicates to the operating system with MPRIS.
**NOTE:** multi-scrobbler needs to be running as a [**Local Installation**](../installation/installation.md#local) in order to use MPRIS. This cannot be used from docker.
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|----------------------------------------------------------------------------------|
| MPRIS_ENABLE | No | | Use MPRIS as a Source (useful when you don't need any other options) |
| MPRIS_BLACKLIST | No | | Comma-delimited list of player names not to scrobble from |
| MPRIS_WHITELIST | No | | Comma-delimited list of players names to ONLY scrobble from. Overrides blacklist |
### File-Based
See [`mpris.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/mpris.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMPRISSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Mopidy](https://mopidy.com/)
Mopidy is a headless music server that supports playing music from many [standard and non-standard sources such as Pandora, Bandcamp, and Tunein.](https://mopidy.com/ext/)
multi-scrobbler can scrobble tracks played from any Mopidy backend source, regardless of where you listen to them.
### File-Based
See [`mopidy.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/mopidy.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMopidySourceConfig/%23%2Fdefinitions%2FMopidyData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
Configuration Options:
##### `url`
The URL used to connect to the Mopidy server. You MUST have [Mopidy-HTTP extension](https://mopidy.com/ext/http) enabled.
If no `url` is provided a default is used which assumes Mopidy is installed on the same server as multi-scrobbler: `ws://localhost:6680/mopidy/ws/`
Make sure the hostname and port number match what is found in the Mopidy configuration file `mopidy.conf`:
```
...
[http]
hostname = localhost
port = 6680
...
```
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
Part => Default Value
* Protocol => `ws://`
* Hostname => `localhost`
* Port => `6680`
* Path => `/mopidy/ws/`
<details>
<summary>URL Transform Examples</summary>
```json
{
"url": "mopidy.mydomain.com"
}
```
MS transforms this to: `ws://mopidy.mydomain.com:6680/mopidy/ws/`
```json
{
"url": "192.168.0.101:3456"
}
```
MS transforms this to: `ws://192.168.0.101:3456/mopidy/ws/`
```json
{
"url": "mopidy.mydomain.com:80/MOPWS"
}
```
MS transforms this to: `ws://mopidy.mydomain.com:80/MOPWS`
</details>
#### URI Blacklist/Whitelist
If you wish to disallow or only allow scrobbling from some sources played through Mopidy you can specify these using `uriBlacklist` or `uriWhitelist` in your config. multi-scrobbler will check the list to see if any string matches the START of the `uri` on a track. If whitelist is used then blacklist is ignored. All strings are case-insensitive.
EX:
```json
{
"uriBlacklist": ["soundcloud"]
}
```
Will prevent multi-scrobbler from scrobbling any Mopidy track that start with a `uri` like `soundcloud:song:MySong-1234`
#### Album Blacklist
For certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use "Soundcloud" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake Album name to this list. Multi-scrobbler will still scrobble the track, just without the bad data. All strings are case-insensitive.
EX:
```json
{
"albumBlacklist": ["SoundCloud", "Mixcloud"]
}
```
If a track would be scrobbled like `Album: Soundcloud, Track: My Cool Track, Artist: A Cool Artist`
then multi-scrobbler will instead scrobble `Track: My Cool Track, Artist: A Cool Artist`
## [JRiver](https://jriver.com/)
In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:
* Tools -> Options -> Media Network
* Check `Use Media Network to share this library...`
* If you have `Authentication` checked you will need to provide the **Username** and **Password** in the ENV/File configuration below.
#### URL
If you do not provide a URL then a default is used which assumes JRiver is installed on the same server as multi-scrobbler: `http://localhost:52199/MCWS/v1/`
* Make sure the port number matches what is found in `Advanced` section in the [Media Network](#jriver) options.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
Part => Default Value
* Protocol => `http://`
* Hostname => `localhost`
* Port => `52199`
* Path => `/MCWS/v1/`
<details>
<summary>URL Transform Examples</summary>
```json
{
"url": "jriver.mydomain.com"
}
```
MS transforms this to: `http://jriver.mydomain.com:52199/MCWS/v1/`
```json
{
"url": "192.168.0.101:3456"
}
```
MS transforms this to: `http://192.168.0.101:3456/MCWS/v1/`
```json
{
"url": "mydomain.com:80/jriverReverse/MCWS/v1/"
}
```
MS transforms this to: `http://mydomain.com:80/jriverReverse/MCWS/v1/`
</details>
### ENV-Based
| Environmental Variable | Required | Default | Description |
|------------------------|----------|---------------------------------|------------------------------------------------|
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
### File-Based
See [`jriver.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/jriver.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FJRiverSourceConfig/%23%2Fdefinitions%2FJRiverData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Kodi](https://kodi.tv/)
In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can can be in the Kodi GUI:
* Settings -> Services -> Control
* Check `Allow remote control via HTTP`
* Ensure you have a **Username** and **Password** set, you will need to provide them in the ENV/File configuration below.
#### URL
If you do not provide a URL then a default is used which assumes Kodi is installed on the same server as multi-scrobbler: `http://localhost:8080/jsonrpc`
* Make sure the port number matches what is found in **Port** in the [Control](#kodi) section mentioned above.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
Part => Default Value
* Protocol => `http://`
* Hostname => `localhost`
* Port => `8080`
* Path => `/jsonrpc`
<details>
<summary>URL Transform Examples</summary>
```json
{
"url": "kodi.mydomain.com"
}
```
MS transforms this to: `http://kodi.mydomain.com:8080/jsonrpc`
```json
{
"url": "192.168.0.101:3456"
}
```
MS transforms this to: `http://192.168.0.101:3456/jsonprc`
```json
{
"url": "mydomain.com:80/kodiReverse/jsonrpc"
}
```
MS transforms this to: `http://mydomain.com:80/kodiReverse/jsonrpc`
</details>
### ENV-Based
| 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 |
### File-Based
See [`kodi.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/kodi.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FKodiSourceConfig/%23%2Fdefinitions%2FKodiData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [WebScrobbler](https://web-scrobbler.com/)
After installing the extension open the preferences/settings for it:
* Under **Accounts**
* **Add Webhook**
* API URL: `http://localhost:9078/api/webscrobbler`
* Application name: `(whatever you want)`
Reload the extension after adding the webhook.
* **On Firefox** - Only FQNs (domain.tld), `localhost`, and `127.0.0.1` are supported for API URL due to [firefox requiring https](https://github.com/web-scrobbler/web-scrobbler/issues/4183#issuecomment-1749222006)
* **On Chromium-based Browsers** - Any domain will work for API URL
* All Other browsers are untested
#### Multiple Users
If you would like use multiple WebScrobbler sources they can be matched using a **slug** at the end of the **API URL.** This requires using [a file-based config.](#file-based-configuration)
Example:
In `webscrobbler.json`
```json
[
{
"name": "aUserWS",
"clients": [
"client1Maloja"
],
"data": {
"slug": "usera"
}
},
{
"name": "bUserWS",
"clients": [
"client2Maloja"
],
"data": {
"slug": "userb"
}
}
]
```
* To use `aUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/usera`
* To use `bUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/userb`
Note: `http://localhost:9078/api/webscrobbler` is matched with the first source that _that does not have a slug defined._
##### Connectors Black/Whitelist
MS can be configured to only scrobble, or NOT scrobble, from some WS connectors. Use the name of the website from the [supported websites](https://web-scrobbler.com/) or from the **Connectors** tab in the extension. Note that this **only** affects MS's behavior and does not affect the general connector behavior you have configured within the WebScrobbler extension.
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|---------|--------------------------------------------------------------------------|
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
| WS_WHITELIST | No | | Only scrobble from these WebScrobbler Connectors. Comma-delimited list |
| WS_BLACKLIST | No | | Do not scrobble from these WebScrobbler Connectors. Comma-delimited list |
### File-Based
See [`webscrobbler.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/webscrobbler.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FWebScrobblerSourceConfig/%23%2Fdefinitions%2FWebScrobblerData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
## [Google Cast (Chromecast)](https://www.google.com/chromecast/built-in/)
**NOTE:** Google Cast support is **experimental**. You may experience crashes and errors while using this Source. Please open an issue if you experience problems and include all information detailed in the issue template to help debug your issue.
If your media device can be **Cast** to using this button ![Chromecast Icon](https://upload.wikimedia.org/wikipedia/commons/2/26/Chromecast_cast_button_icon.svg) on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.
**Note:** This source relies on common, **basic** music data provided by the cast device which will always be less exhaustive than data parsed from full source integrations. If there is an existing [Source](#source-configurations) it is recommended to configure for it and blacklist the app on Google Cast, rather than relying solely on Google Cast for scrobbling.
### Connecting Devices
Cast devices can be manually configured using [File-based configuration](#file-based-14) OR automatically discovered using **mDNS.**
##### mDNS Discovery
The host machine running multi-scrobbler must be configured to allow [mDNS traffic on port 5353/UDP](https://book.hacktricks.xyz/network-services-pentesting/5353-udp-multicast-dns-mdns).
##### Linux
**Docker**
The host machine must have [avahi-daemon](https://avahi.org/) running to circumvent limitations with DNS resolution due to musl in Alpine. Most major linux distributions package avahi and many have it built-in. Once avahi is running you must pass D-Bus and the avahi daemon socket to your container like so:
```
docker run ... -v /var/run/dbus:/var/run/dbus -v /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket ... foxxmd/multi-scrobbler
```
**Flatpak/Nodejs**
No additional steps are required.
##### Windows
**Docker**
Unsupported at this time.
**Nodejs**
No additional steps are required.
### What Media Does MS Scrobble?
Cast devices report what type of media the current activity is [(see `metadata` property here)](https://developers.google.com/cast/docs/media/messages#MediaInformation). The reported type is dependent on the application playing the media to correctly report it, the cast device does not magically know what the media is. If an application does not report a type it is always classified as `unknown`.
**By default, MS will only track media that is reported as `MusicTrack`.**
#### Allow Unknown Media Type
Media with an Unknown (`Generic`) media type can be explicitly allowed by setting `"allowUnknownMedia": true` in the [file-based configuration.](#file-based-14) This can also be configured to only allow unknown media types for specific applications by using a list of application names like:
```json5
// in chromecast.json or config.json sources
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
// only allow unknown if app name contains any of these phrases
"allowUnknownMedia": ["smarttube", "default media receiver"]
},
}
]
```
#### Forcing Media Tracking
MS can be forced to track media from an application regardless of media type. This is useful if an application incorrectly reports a media type you are sure should be music. Set `"forceMediaRecognitionOn"` in the [file-based configuration.](#file-based-14) to a list of application names that should always be tracked like:
```json5
// in chromecast.json or config.json sources
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
// media from applications that contains these phrases will always be tracked, regardless of media type reported
"forceMediaRecognitionOn": ["smarttube", "default media receiver"]
},
}
]
```
### Cast Troubleshooting
Please include any/all logs with raw output if there are any errors encountered as this is critical to diagnosing issues.
To diagnose bad/incomplete track information or strange MS player behavior please turn on **payload logging** and include log output of the source running to help diagnose this issue:
```json5
// in chromecast.json or config.json sources
[
{
"name": "MyCast",
"type": "chromecast",
"data": {
//...
},
"options": {
"logPayload": true
}
}
]
```
### ENV-Based
Note: [Manually configuring cast device connections](#connecting-devices) is only available through [File-based config.](#file-based-14)
| 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 |
### File-Based
See [`chromecast.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/chromecast.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FChromecastSourceConfig/%23%2Fdefinitions%2FChromecastData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
# Client Configurations
## [Maloja](https://github.com/krateng/maloja)
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|---------|-------------------------------|
| `MALOJA_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
### File-Based
See [`maloja.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/maloja.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FMalojaClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
## [Last.fm](https://www.last.fm)
[Register for an API account here.](https://www.last.fm/api/account/create)
The Callback URL is actually specified by multi-scrobbler but to keep things consistent you should use
```
http://localhost:9078/lastfm/callback
```
or replace `localhost:9078` with your own base URL
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|-----------------------------------------|-------------------------------|
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
### File-Based
See [`lastfm.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/lastfm.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
## [Listenbrainz](https://listenbrainz.org)
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
### ENV-Based
| Environmental Variable | Required? | Default | Description |
|------------------------|-----------|-------------------------------|---------------------------------|
| LZ_TOKEN | Yes | | User token from your LZ profile |
| LZ_USER | Yes | | Your LZ username |
| LZ_URL | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
### File-Based
See [`listenbrainz.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
# Monitoring
multi-scrobbler supports some common webhooks and a healthcheck endpoint in order to monitor Sources and Clients for errors.
## Webhook Configurations
Webhooks will **push** a notification to your configured servers on these events:
* Source polling started
* Source polling retry
* Source polling stopped on error
* Scrobble client scrobble failure
Webhooks are configured in the main [config.json](#all-in-one-file-configuration) file under the `webhook` top-level property. Multiple webhooks may be configured for each webhook type. EX:
```json5
{
"sources": [
//...
],
"clients": [
//...
],
"webhooks": [
{
"name": "FirstGotifyServer",
"type": "gotify",
"url": "http://192.168.0.100:8070",
"token": "abcd"
},
{
"name": "SecondGotifyServer",
"type": "gotify",
//...
},
{
"name": "NtfyServerOne",
"type": "ntfy",
//...
},
//...
]
}
```
### [Gotify](https://gotify.net/)
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
EX
```json
{
"type": "gotify",
"name": "MyGotifyFriendlyNameForLogs",
"url": "http://192.168.0.100:8070",
"token": "AQZI58fA.rfSZbm",
"priorities": {
"info": 5,
"warn": 7,
"error": 10
}
}
```
### [Ntfy](https://ntfy.sh/)
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
EX
```json
{
"type": "ntfy",
"name": "MyNtfyFriendlyNameForLogs",
"url": "http://192.168.0.100:9991",
"topic": "RvOwKJ1XtIVMXGLR",
"username": "Optional",
"password": "Optional",
"priorities": {
"info": 3,
"warn": 4,
"error": 5
}
}
```
## Health Endpoint
An endpoint for monitoring the health of sources/clients is available at GET `http://YourMultiScrobblerDomain/health`
* Returns `200 OK` when **everything** is working or `500 Internal Server Error` if **anything** is not
* The plain url (`/health`) aggregates status of **all clients/sources** -- so any failing client/source will make status return 500
* Use query params `type` or `name` to restrict client/sources aggregated IE `/health?type=spotify` or `/health?name=MyMaloja`
* On 500 the response returns a JSON payload with `messages` array that describes any issues
* For any clients/sources that require authentication `/health` will return 500 if they are **not authenticated**
* For sources that poll (spotify, yt music, subsonic) `/health` will 500 if they are **not polling**
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

+293
View File
@@ -0,0 +1,293 @@
---
sidebar_position: 3
title: Kitchen Sink
---
# Example Config using all Possible Features
Scenario:
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
* Each person has their own Maloja server
* Each person has their own Spotify account
* You have your own Airsonic (subsonic) server you to scrobble from
* You have your own Youtube Music account you want to scrobble from
* Mary has her own Last.fm account she also wants to scrobble to
* Fred has his own Spotify application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
* Fred has a Plex server and wants to scrobble everything he plays
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
* The three of you have a shared library on Plex called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
* Fred also has his own Jellyfin server and wants to scrobble everything he plays
### All-in-one Config
Using just one config file located at `CONFIG_DIR/config.json`:
```json5
{
"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",
"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": {
"user": ["fred@email.com"]
}
},
{
"type": "plex",
"name": "maryPlex",
"clients": ["maryMaloja"],
"data": {
"user": ["mary@email.com"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
"libraries": ["podcasts"]
}
},
{
"type": "plex",
"name": "partyPlex",
// omitting clients (or making it empty) will make this Source scrobble to all Clients
"data": {
"libraries": ["party"],
}
},
{
"type": "jellyfin",
"name": "FredJelly",
// omitting clients (or making it empty) will make this Source scrobble to all Clients
"data": {
"user": ["fred@email.com"]
}
},
{
"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; ...",
"authUser": 1
}
},
],
"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"
}
}
]
}
```
### Separate JSON files
In `CONFIG_DIR/spotify.json`:
```json5
[
{
// 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
}
},
]
```
In `CONFIG_DIR/plex.json`
```json5
[
{
"name": "fredPlex",
"clients": ["fredMaloja"],
"data": {
"user": ["fred@email.com"]
}
},
{
"name": "maryPlex",
"clients": ["maryMaloja"],
"data": {
"user": ["mary@email.com"],
"libraries": ["podcasts"]
}
},
{
"name": "partyPlex",
"data": {
"libraries": ["party"]
}
}
]
```
In `CONFIG_DIR/jellyfin.json`
```json5
[
{
"name": "FredJelly",
"data": {
"user": ["fred@email.com"]
}
}
]
```
In `CONFIG_DIR/ytmusic.json`
```json5
[
{
"type": "ytmusic",
"name": "foxxYoutube",
"clients": ["foxxMaloja"],
"data": {
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
"authUser": 1
}
}
]
```
In `CONFIG_DIR/maloja.json`:
```json5
[
{
"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"
}
}
]
```
In `CONFIG_DIR/lastfm.json`:
```json5
[
{
"name": "maryLFM",
"data": {
"apiKey": "maryApiKey",
"secret": "marySecret",
"redirectUri": "http://localhost:9078/lastfm/callback"
}
}
]
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+73
View File
@@ -0,0 +1,73 @@
---
sidebar_position: 2
title: Plex/Tautulli
---
Tracks played on [Plex](https://plex.tv/) can be scrobbled either by:
* A [Tautulli](https://tautulli.com/) notification agent with a webhook.
* Using Plex [Webhooks](https://support.plex.tv/articles/115002267687-webhooks) (restricted to Plex Pass users)
# Using Tautulli
## Create a new Notification Agent
* Navigate to the **Notification Agents** page in **Settings**
* Click **Add a new notification agent**
* Select **Webhook**
## Configure the Agent
The below sections correspond with the tabs available in the notification agent configuration popup.
### Configuration
* Webhook URL -- `http://localhost:9078/tautulli` (substitute your domain if different than the default)
* Webhook Method -- POST
### Triggers
Select **Watched**
### Conditions
Refer to [Tautulli's documentation](https://github.com/Tautulli/Tautulli-Wiki/wiki/Custom-Notification-Conditions) if you need help here. It may be a good idea to restrict notifications to only one library (if you have a Music library, for instance)
**This app will only scrobble an item if `media_type` is a "track", which is the default for all music.**
### Data
Expand the **Watched** dropdown and add the following code block to the **JSON Data** text field:
```
{
"artist_name": "{artist_name}",
"track_name": "{track_name}",
"track_artist": "{track_artist}",
"album_name": "{album_name}",
"media_type": "{media_type}",
"title": "{title}",
"duration": "{duration_sec}",
"username": "{username}",
"server": "{server_name}",
"version": "{server_version}",
"library": "{library_name}",
"player": "{player}",
"device": "{device}",
"platform": "{platform}",
"action": "{action}",
"machine_id": "{machine_id}",
"session_key": "{session_key}"
}
```
**Click the Save button to finish.**
Your agent is now configured and ready to scrobble.
# Using Plex Webhooks
* Navigate to your **Account/Settings** and find the **Webhooks** page
* Click **Add Webhook**
* URL -- `http://localhost:9078/plex` (substitute your domain if different than the default)
* **Save Changes**
Plex is now configured to scrobble.
@@ -0,0 +1,8 @@
{
"label": "Installation",
"position": 1,
"link": {
"type": "generated-index",
"description": "Way to install Multi-Scrobbler"
}
}
+185
View File
@@ -0,0 +1,185 @@
---
sidebar_position: 1
title: 'Overview'
---
# Installation
# Local
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
## Nodejs
Clone this repository somewhere and then install from the working directory
```shell
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
nvm use # optional, to set correct Node version
npm install
npm run build
npm run start
```
#### Rollup build error
During building if you encounter an error like: `Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.`
Modify `overrides` in `package.json` to use `@rollup/wasm-node` as a drop-in replacement for rollup:
```json
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"
}
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.9.6"
}
}
```
See [this issue](https://github.com/FoxxMD/multi-scrobbler/issues/135#issuecomment-1927080260) for more detail.
### Usage Examples
* The web UI and API is served on port `9078`. This can be modified using the `PORT` environmental variable.
#### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
```shell
npm run start
```
#### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
```shell
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret MALOJA_URL="http://domain.tld" node src/index.js
```
## Flatpak
You must have [Flatpak](https://flatpak.org/) installed on your system.
```shell
flatpak install flathub io.github.foxxmd.multiscrobbler
```
**Note:** Flatpak users have experienced issues when using multi-scrobbler as a long-running process. Due to the relative difficulty in debugging issues with flatpak installations it is recommended:
* to use a [Docker](#docker) installation if possible or
* only if you need access to host-level resources like dbus for [MPRIS](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris) and cannot run a [nodejs](#nodejs) installation
### Usage Examples
#### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
The config directory for multi-scrobbler as a flatpak can be found under `/home/YourUser/.var/app/io.github.foxxmd.multiscrobbler/config`
```shell
flatpak run io.github.foxxmd.multiscrobbler
```
#### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
There are a few [options for running flatpak applications with temporary or permanent environmental variables.](https://ardasevinc.dev/launch-flatpak-apps-with-custom-args-and-environment-variables)
```shell
flatpak run --env=SPOTIFY_CLIENT_ID=yourId --envSPOTIFY_CLIENT_SECRET=yourSecret --env=MALOJA_URL="http://domain.tld" io.github.foxxmd.multiscrobbler
```
## Docker
Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
[Dockerhub](https://hub.docker.com/r/foxxmd/multi-scrobbler)
```
docker.io/foxxmd/multi-scrobbler:latest
```
[Github Packages](https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler)
```
ghcr.io/foxxmd/multi-scrobbler:latest
```
Or use the provided [docker-compose.yml](../../../docker-compose.yml) after modifying it to fit your configuration.
Recommended configuration steps for docker or docker-compose usage:
#### Storage
You **must** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
* [Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount): `-v /path/on/host/config:/config`
* [Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3): `- /path/on/host/config:/config`
#### Networking
If you are using a [bridge network](https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/) (default docker setup) you **must** map a port to the container in order to access the dashboard and use MS with some sources (Plex, Jellyfin).
The default container port is `9078`. To map container to host port:
* With [docker](https://docs.docker.com/engine/reference/commandline/run/#publish): `-p 9078:9078` (first port is the port on the host to use)
* With [docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1): `- "9078:9078"`
##### Base URL
Optionally, when
* using a [Source or Client](../configuration/configuration.md) that has a "Redirect URI" that you have not explicitly defined
* and
* using a bridge network or
* installing MS on a different machine than the one used to view the dashboard
set the [Base URL](../configuration/configuration.md#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
* With docker: `-e BASE_URL="http://hostMachineIP"` (first port is the port on the host to use)
* With docker-compose: [see comments in docker-compose.yml](../../../docker-compose.yml)
#### Other
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
### Linux Host
If you are
* using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
* running docker on MacOS or Windows
this **DOES NOT** apply to you.
If you are running Docker on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
To get the UID and GID for the current user run these commands from a terminal:
* `id -u` -- prints UID
* `id -g` -- prints GID
### Network Issues
If you encounter networking issues like:
* sporadic timeouts (`ETIMEDOUT`) without a pattern
* DNS errors (`EAI_AGAIN`) that do no occur consistently
* Failures to reach a host that was previously fine (`EHOSTUNREACH`)
there may be an issue with the underlying docker image OS (alpine) that may be solved by switching to a different image. Try switching to a `*-debian` variant tag (only available for x86/x64 hosts) to see if this resolves your issue. IE `multi-scrobbler:latest-debian` or `multi-scrobbler:develop-debian`
## Docker Usage Examples
If installing on a different machine make sure all redirect URIs are defined or that you have set a [Base URL](#base-url).
### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
```bash
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
```
### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
```bash
docker run -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
```
See the [docker-compose.yml](../../../docker-compose.yml) file for how to use with docker-compose.
+73
View File
@@ -0,0 +1,73 @@
---
sidebar_position: 2
title: 'As a Service'
---
If you have multi-scrobbler installed [locally](installation.md#local) you can enable it to run as a background service when you login.
Before running as a service you should run it at least once in the foreground to ensure it can start up correctly!
# [Systemd](https://systemd.io/)
Systemd is system and service manager that is used by [most popular linux distros](https://en.wikipedia.org/wiki/Systemd#Adoption) including [Ubuntu](https://wiki.ubuntu.com/SystemdForUpstartUsers), [Fedora](https://docs.fedoraproject.org/en-US/quick-docs/understanding-and-administering-systemd/), Pop!_OS, [Debian](https://wiki.debian.org/systemd), and [Arch](https://wiki.archlinux.org/title/systemd).
This setup will create a [user service](https://wiki.archlinux.org/title/systemd/User) that runs on login.
## Create A Unit File
Create a new service file for multi-scrobbler under your HOME config:
```console
mkdir -p ~/.config/systemd/user
touch ~/.config/systemd/user/multi-scrobbler.service
```
In a text editor add contents below to the file you created, `multi-scrobbler.service`:
```ini
[Unit]
Description=multi-scrobbler
After=network.target
[Service]
Type=simple
ExecStart=flatpak run io.github.multiscrobbler
Restart=no
[Install]
WantedBy=default.target
```
The above assumes you [installed multi-scrobbler using flatpak](installation.md#flatpak)
### Node.js Installs
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](installation.md#nodejs) you should modify the `[Service]`:
```ini
[Service]
Type=simple
WorkingDirectory=/path/to/multi-scrobbler/directory
ExecStart=node src/index.js
Restart=no
```
## Start the Service
Save the file then run:
```console
systemctl daemon-reload
systemctl --user enable multi-scrobbler.service
systemctl --user start multi-scrobbler.service
```
This will
* scan for new services and pickup our multi-scrobbler user service
* enable the service to run at login automatically
* start the service now
# Other Service Methods
Open a PR if you would like to document setting up multi-scrobbler for other service managers!
+159
View File
@@ -0,0 +1,159 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Multi-Scrobbler',
tagline: 'Scrobble all the things',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://foxxmd.github.io',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/multi-scrobbler',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'foxxmd', // Usually your GitHub org/user name.
projectName: 'multi-scrobbler', // Usually your repo name.
trailingSlash: false,
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
// blog: {
// showReadingTime: true,
// // Please change this to your repo.
// // Remove this to remove the "edit this page" links.
// editUrl:
// 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
// },
blog: false,
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themes: ["docusaurus-json-schema-plugin"],
plugins: [
[
require.resolve("@cmfcmf/docusaurus-search-local"),
{
indexBlog: false,
},
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
navbar: {
title: 'Multi-Scrobbler',
logo: {
alt: 'Logo',
src: 'img/icon.svg',
},
items: [
{
type: 'docSidebar',
sidebarId: 'tutorialSidebar',
position: 'left',
label: 'Docs',
},
{
to: 'playground',
position: 'left',
label: 'Config Playground',
},
{
href: 'https://github.com/foxxmd/multi-scrobbler',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
items: [
{
label: 'Overview',
to: '/',
},
{
label: 'Installation',
to: '/docs/installation',
},
{
label: 'Configuration',
to: '/docs/configuration',
},
],
},
/* {
title: 'Community',
items: [
{
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
},
{
label: 'Discord',
href: 'https://discordapp.com/invite/docusaurus',
},
{
label: 'Twitter',
href: 'https://twitter.com/docusaurus',
},
],
},*/
{
title: 'More',
items: [
{
label: 'GitHub',
href: 'https://github.com/foxxmd/multi-scrobbler',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Multi-Scrobbler. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;
+12912
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
{
"name": "my-website",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@cmfcmf/docusaurus-search-local": "^1.1.0",
"@docusaurus/core": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-json-schema-plugin": "^1.6.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=16.14"
}
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [
{type: 'autogenerated', dirName: '.'},
{
type: "link",
label: "Config Playground",
href: "/playground"
}
],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
module.exports = sidebars;
+44
View File
@@ -0,0 +1,44 @@
import React from "react"
import BrowserOnly from "@docusaurus/BrowserOnly"
import { useColorMode } from "@docusaurus/theme-common"
import JSONSchemaEditor from "@theme/JSONSchemaEditor"
// Context
import { usePlaygroundContext } from "@site/src/contexts/PlaygroundContext"
// Common stringify of the JSON
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t")
function JSONSchemaDataInner(): JSX.Element {
const {
state: { userSchema, editorRef, data: value },
updateState,
} = usePlaygroundContext()
const { colorMode } = useColorMode()
return (
<div style={{ boxSizing: "border-box", width: "50%" }}>
<JSONSchemaEditor
value={value}
schema={userSchema}
theme={colorMode === "dark" ? "vs-dark" : "vs"}
editorDidMount={(editor) => {
updateState({ editorRef: editor })
}}
height={"70vh"}
key={STRINGIFY_JSON(userSchema)}
/>
</div>
)
}
export default function JSONSchemaDataComponent(): JSX.Element {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
return <JSONSchemaDataInner />
}}
</BrowserOnly>
)
}
@@ -0,0 +1,59 @@
import React from "react"
import BrowserOnly from "@docusaurus/BrowserOnly"
import Schema from "@site/static/aio.json";
import ConfigExample from "@site/static/configExample.json";
import JSONSchemaViewer from "@theme/JSONSchemaViewer"
import JSONSchemaEditor from "@theme/JSONSchemaEditor";
import {useColorMode} from "@docusaurus/theme-common";
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t")
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
function PlaygroundInner(): JSX.Element {
const { colorMode } = useColorMode()
return (
<div
style={{
display: "flex",
flexDirection: "column",
overflowY: "hidden",
overflowX: "hidden",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<div style={{width: '50%'}}>
<JSONSchemaViewer
schema={Schema}
showExamples={true}
/>
</div>
<div style={{ boxSizing: "border-box", width: "50%" }}>
<JSONSchemaEditor
value={STRINGIFY_JSON(ConfigExample)}
theme={colorMode === "dark" ? "vs-dark" : "vs"}
schema={Schema}
height={"70vh"}
/>
</div>
</div>
</div>
)
}
export default function PlaygroundComponent(): JSX.Element {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
return <PlaygroundInner />
}}
</BrowserOnly>
)
}
@@ -0,0 +1,50 @@
import { useContext, createContext } from "react"
// Type I need for useRef
import type { MonacoEditorTypes } from "@theme/MonacoEditor"
export type State = {
// The full schema
// We might need to scope it with a JSON Pointer
fullSchema: unknown
// The current schema displayed (after the json pointer)
userSchema: unknown
// The current json pointer
jsonPointer: string
// Schema editor ref
schemaRef: undefined | MonacoEditorTypes.IStandaloneCodeEditor
// Data editor ref
editorRef: undefined | MonacoEditorTypes.IStandaloneCodeEditor
// Data on the data editor
// When using "Infer schema from data", it would be annoying to lose our written
data: string
// Errors in schema
schemaErrors: MonacoEditorTypes.IMarkerData[]
// Errors in data
dataErrors: MonacoEditorTypes.IMarkerData[]
}
export type Playground = {
// state
state: State
// update function
updateState: (_: Partial<State>) => void
}
export const PlaygroundContext = createContext<Playground>({
state: {
fullSchema: {},
userSchema: {},
jsonPointer: "",
data: "{}",
schemaRef: undefined,
editorRef: undefined,
schemaErrors: [],
dataErrors: [],
},
updateState: () => {},
})
export const usePlaygroundContext = () => useContext(PlaygroundContext)
export const PlaygroundContextProvider = PlaygroundContext.Provider
+30
View File
@@ -0,0 +1,30 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
+103
View File
@@ -0,0 +1,103 @@
---
title: Overview
---
# Overview
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)
<img src={require('/img/icon.png').default} align="right" alt="multi-scrobbler logo" width="180" height="180"/>
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
* Supports scrobbling from many **Sources**
* [Spotify](docs/configuration#spotify)
* [Plex](docs/configuration#plex) or [Tautulli](docs/configuration#tautulli)
* [Subsonic-compatible APIs](docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [Jellyfin](docs/configuration#jellyfin)
* [WebScrobbler](docs/configuration#webscrobbler)
* [Youtube Music](docs/configuration#youtube-music)
* [Last.fm](docs/configuration#lastfm-source)
* [ListenBrainz](docs/configuration#listenbrainz--source-)
* [Deezer](docs/configuration#deezer)
* [MPRIS (Linux Desktop)](docs/configuration#mpris)
* [Mopidy](docs/configuration#mopidy)
* [JRiver](docs/configuration#jriver)
* [Kodi](docs/configuration#kodi)
* [Google Cast (Chromecast)](/docs/configuration#google-cast--chromecast-)
* Supports scrobbling to many **Clients**
* [Maloja](docs/configuration#maloja)
* [Last.fm](docs/configuration#lastfm)
* [ListenBrainz](docs/configuration#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](docs/configuration#webhook-configurations) or [healthcheck endpoint](docs/configuration#health-endpoint)
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
* Install using [Docker images for x86/ARM](docs/installation#docker), [flatpak](docs/installation#flatpak), or [locally with NodeJS](docs/installation#nodejs)
**Why should I use this over a browser extension and/or mobile app scrobbler?**
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
* **Open-source** -- Get peace of mind knowing exactly how your personal data is being handled.
* **Track your activity regardless of where you listen** -- Scrobble from many Sources to one Client with ease and without duplicating tracks.
* **Manage scrobbling for others** -- Scrobble for your friends and family without any setup on their part. Easily silo sources to specific clients to keep plays separate.
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](docs/configuration#lastfm--source-) or [Listenbrainz as a **Source**](docs/configuration#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
<img src={require('/img/status-ui.png').default} width="800"/>
## How Does multi-scrobbler (MS) Work?
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
### Source
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
Source configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Source.
* An optional list of Client names that the Source should scrobble to. If omitted the Source also scrobbles to all configured Clients.
### Client
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Installation
[See the **Installation** documentation](docs/installation)
## Configuration
[See the **Configuration** documentation](docs/configuration)
## Usage
A status page with statistics, recent logs, and some runtime configuration options can be found at
```
http://localhost:9078
```
Output is also provided to stdout/stderr as well as file if specified in configuration.
On first startup you may need to authorize Spotify and/or Last.fm by visiting the callback URL (which can also be accessed from the status page). Visit the status page above to find the applicable link to trigger this.
## Help/FAQ
Having issues with connections or configuration? Check the [FAQ](docs/FAQ) before creating an issue!
## License
MIT
+38
View File
@@ -0,0 +1,38 @@
import React from "react"
import Layout from "@theme/Layout"
import BrowserOnly from "@docusaurus/BrowserOnly"
import ErrorBoundary from "@docusaurus/ErrorBoundary"
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/pages/playground.tsx
function PlaygroundComponent(): JSX.Element {
// No SSR for the live preview
// See https://github.com/facebook/docusaurus/issues/5747
return (
<ErrorBoundary
fallback={({ error, tryAgain }) => (
<div>
<p>Playground component crashed because of error: {error.message}.</p>
<button onClick={tryAgain}>Try Again!</button>
</div>
)}
>
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
const PlaygroundInnerComponent =
require("@site/src/components/PlaygroundInner").default
return <PlaygroundInnerComponent />
}}
</BrowserOnly>
</ErrorBoundary>
)
}
export default function Playground(): JSX.Element {
return (
<Layout
title={`Playground`}
description="Playground of docusaurus-json-schema-plugin"
>
<PlaygroundComponent />
</Layout>
)
}
View File
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
{
"sourceDefaults": {
"maxPollRetries": 0,
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
"clientDefaults": {
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
"baseUrl": "http://localhost",
"sources": [
{
"type": "spotify",
"clients": ["myConfig"],
"name": "mySpotifySource",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
"redirectUri": "http://localhost:9078/callback"
}
}
],
"clients": [
{
"type": "maloja",
"name": "myConfig",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
}
}
],
"webhooks": [
{
"name": "FirstGotifyServer",
"type": "gotify",
"url": "http://localhost:8070",
"token": "MyGotifyToken",
"priorities": {
"info": 5,
"warn": 7,
"error": 10
}
},
{
"type": "ntfy",
"name": "MyNtfyFriendlyNameForLogs",
"url": "http://localhost:9991",
"topic": "MyMultiScrobblerTopic",
"username": "Optional",
"password": "Optional",
"priorities": {
"info": 3,
"warn": 4,
"error": 5
}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<style>
.cls-1 {
fill: #c34a36;
}
</style>
</defs>
<path class="cls-1" d="M76.94,65.49c.87-7.85-5.27-14.51-12.94-14.51-4.56,0-8.57,2.36-10.9,5.91h-4.55c2.36-5.11,7.17-8.87,12.95-9.71V20c0-11.05-8.95-20-20-20H5C2.24,0,0,2.24,0,5V123c0,2.76,2.24,5,5,5H41.5c11.05,0,20-8.95,20-20v-27.17c-5.78-.84-10.59-4.6-12.95-9.72h4.55c2.59,3.96,7.27,6.43,12.46,5.82,6.08-.71,10.71-5.37,11.39-11.45ZM15.3,94.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Zm11.12,14.55c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm37.58-39.53c-2.28,0-4.23-1.41-5.02-3.41h-21.43v28.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v28.39h21.43c.93-2.33,3.42-3.86,6.18-3.29,2.05,.42,3.7,2.08,4.13,4.12,.73,3.5-1.92,6.58-5.29,6.58Z"/>
<path class="cls-1" d="M123,0h-36.5c-11.05,0-20,8.95-20,20v27.18c7.55,1.12,13.5,7.2,14.4,14.82h9.55v-28.28c0-1,.68-1.92,1.66-2.08,1.25-.2,2.34,.76,2.34,1.97v60.67c0,1-.68,1.92-1.66,2.08-1.25,.2-2.34-.76-2.34-1.97v-28.39h-9.55c-.9,7.62-6.84,13.7-14.4,14.82v27.18c0,11.05,8.95,20,20,20h36.5c2.76,0,5-2.24,5-5V5c0-2.76-2.24-5-5-5Zm-17.42,108.94c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm11.12-14.55c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"resolveJsonModule": true,
// Extending "@tsconfig/docusaurus/tsconfig.json".types with "docusaurus-json-schema-plugin"
"types": ["node", "@docusaurus/module-type-aliases", "@docusaurus/theme-classic", "docusaurus-json-schema-plugin"]
}
}
+43
View File
@@ -0,0 +1,43 @@
// @ts-check
import eslint from '@eslint/js';
import tsEslint from 'typescript-eslint';
import arrow from 'eslint-plugin-prefer-arrow-functions';
export default tsEslint.config(
eslint.configs.recommended,
...tsEslint.configs.recommended,
// use to enable typed linting (way more errors) https://typescript-eslint.io/linting/typed-linting
/* ...tsEslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigDirName: import.meta.dirname,
},
},
},*/
{
files: ['src/backend/**/*.ts'],
ignores: ['eslint.config.js'],
plugins: {
"prefer-arrow-functions": arrow
},
rules: {
'no-useless-catch': 'off',
'@typescript-eslint/no-unused-vars': 'off',
"prefer-arrow-functions/prefer-arrow-functions": [
"warn",
{
"allowNamedFunctions": false,
"classPropertiesAllowed": false,
"disallowPrototype": false,
"returnStyle": "unchanged",
"singleReturnOnly": false
}
],
"arrow-body-style": ["warn", "as-needed"],
"@typescript-eslint/no-explicit-any": "warn"
}
}
);
+48
View File
@@ -0,0 +1,48 @@
NOTE: This steps are for building the flatpak entirely locally, from source. If you want to install the application normally then [get it through flathub](/docs/installation.md#flatpak)
The final build repo for the flathub version can be found at [flathub/io.github.foxxmd.multiscrobbler](https://github.com/flathub/io.github.foxxmd.multiscrobbler)
# 1. Install Requirements
## Flatpak and flatpak-builder
Install [Flatpak](https://flatpak.org/setup/)
Install [flatpak-builder](https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak)
## [flatpak-node-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/node)
Requires python 3.7+, [pip](https://pip.pypa.io/en/stable/)/[pipx](https://pypa.github.io/pipx/)
# 2. Update Project source
Set the `branch` `tag` or `commit` to use for MS in the `git` source in [`io.github.foxxmd.multiscrobbler.yml`](/flatpak/io.github.foxxmd.multiscrobbler.yml)
# 3. Use `flatpak-node-generator` to generate sources
First, [make sure `node_modules` is deleted or empty.](https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442)
Then, from MS project root:
```shell
flatpak-node-generator npm package-lock.json
```
Move `generated-sources.json` into [`/flatpak`](/flatpak)
# 4. Build flatpak
From MS project root:
```shell
cd flatpak
flatpak-builder --repo=/home/yourUser/multi-scrobbler-flatpak/repo --state-dir=/home/yourUser/multi-scrobbler-flatpak/state /home/yourUser/multi-scrobbler-flatpak/build io.github.foxxmd.multiscrobbler.yml --force-clean --install --user
```
Add `--install --user` to have the app installed immediately.
# 5. Run (Locally)
If built with `--install --user` you can now run MS using the command
```shell
flatpak run -u io.github.foxxmd.multiscrobbler
```
+6
View File
@@ -0,0 +1,6 @@
{
"logging": {
"file": false
},
"port": 9078
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<style>
.cls-1 {
fill: #c34a36;
}
</style>
</defs>
<path class="cls-1" d="M76.94,65.49c.87-7.85-5.27-14.51-12.94-14.51-4.56,0-8.57,2.36-10.9,5.91h-4.55c2.36-5.11,7.17-8.87,12.95-9.71V20c0-11.05-8.95-20-20-20H5C2.24,0,0,2.24,0,5V123c0,2.76,2.24,5,5,5H41.5c11.05,0,20-8.95,20-20v-27.17c-5.78-.84-10.59-4.6-12.95-9.72h4.55c2.59,3.96,7.27,6.43,12.46,5.82,6.08-.71,10.71-5.37,11.39-11.45ZM15.3,94.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Zm11.12,14.55c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm37.58-39.53c-2.28,0-4.23-1.41-5.02-3.41h-21.43v28.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v28.39h21.43c.93-2.33,3.42-3.86,6.18-3.29,2.05,.42,3.7,2.08,4.13,4.12,.73,3.5-1.92,6.58-5.29,6.58Z"/>
<path class="cls-1" d="M123,0h-36.5c-11.05,0-20,8.95-20,20v27.18c7.55,1.12,13.5,7.2,14.4,14.82h9.55v-28.28c0-1,.68-1.92,1.66-2.08,1.25-.2,2.34,.76,2.34,1.97v60.67c0,1-.68,1.92-1.66,2.08-1.25,.2-2.34-.76-2.34-1.97v-28.39h-9.55c-.9,7.62-6.84,13.7-14.4,14.82v27.18c0,11.05,8.95,20,20,20h36.5c2.76,0,5-2.24,5-5V5c0-2.76-2.24-5-5-5Zm-17.42,108.94c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm11.12-14.55c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=multi-scrobbler
Comment=Scrobbles music from multiple sources to multiple clients
Icon=io.github.foxxmd.multiscrobbler
TryExec=multiscrobbler
Exec=multiscrobbler
Terminal=true
Categories=Utility;Music;ConsoleOnly;
X-Flatpak=io.github.foxxmd.multiscrobbler
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="console-application">
<id>io.github.foxxmd.multiscrobbler</id>
<provides>
<id>io.github.foxxmd.multiscrobbler.desktop</id>
<binary>multiscrobbler</binary>
</provides>
<launchable type="desktop-id">io.github.foxxmd.multiscrobbler.desktop</launchable>
<name>multi-scrobbler</name>
<summary>Scrobbles music from many sources</summary>
<url type="homepage">https://foxxmd.github.io/multi-scrobbler</url>
<url type="help">https://foxxmd.github.io/multi-scrobbler/docs/configuration</url>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<developer id="tld.vendor">
<name>FoxxMD</name>
</developer>
<description>
<p>
Track your music listening history from many different sources:
</p>
<ul>
<li>Spotify</li>
<li>Plex (Tautulli)</li>
<li>Any subsonic-based music player (Airsonic)</li>
<li>Jellyfin</li>
<li>WebScrobbler</li>
<li>Mopidy</li>
<li>Deezer</li>
<li>JRiver</li>
<li>Kodi</li>
<li>Webscrobbler</li>
<li>Youtube Music</li>
<li>MPRIS (linux desktop)</li>
<li>Google Cast (Chromecast)</li>
</ul>
<p>and consolidate your listens (scrobbles) into one or more scrobbling servers/services:</p>
<ul>
<li>Maloja</li>
<li>Last.fm</li>
<li>Listenbrainz</li>
</ul>
<p>multi-scrobbler requires some configuration before use. Please visit the website or help links below to get started.</p>
</description>
<screenshots>
<screenshot type="default">
<caption>Browser-based UI</caption>
<image type="source">https://raw.githubusercontent.com/FoxxMD/multi-scrobbler/f4256d1084f4bdea2945c03e523af6d0ed89b3ae/assets/status-ui.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.7.0" date="2024-03-26"/>
<release version="0.6.5" date="2024-02-05"/>
<release version="0.6.4" date="2024-01-29"/>
<release version="0.6.3" date="2024-01-10"/>
<release version="0.6.2" date="2023-11-29"/>
<release version="0.6.1" date="2023-10-24"/>
<release version="0.6.0" date="2023-10-04"/>
<release version="0.5.2" date="2023-09-21"/>
<release version="0.5.0" date="2023-08-29"/>
<release version="0.4.6" date="2023-06-07"/>
<release version="0.4.4" date="2023-03-20"/>
</releases>
<categories>
<category>Utility</category>
<category>Music</category>
<category>ConsoleOnly</category>
</categories>
<content_rating type="oars-1.0">
<content_attribute id="language-profanity">mild</content_attribute>
</content_rating>
</component>
@@ -0,0 +1,92 @@
app-id: io.github.foxxmd.multiscrobbler
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: multiscrobbler
finish-args:
- --share=network
# used for access to MPRIS in order to scrobble
- --own-name=org.mpris.MediaPlayer2.*
sdk-extensions:
- org.freedesktop.Sdk.Extension.node18
modules:
- name: multiscrobbler
buildsystem: simple
build-options:
append-path: /usr/lib/sdk/node18/bin
env:
XDG_CACHE_HOME: /run/build/multiscrobbler/flatpak-node/cache
npm_config_cache: /run/build/multiscrobbler/flatpak-node/npm-cache
npm_config_nodedir: /usr/lib/sdk/node18
npm_config_offline: 'true'
build-commands:
# install npm dependencies
- npm install --offline
# build app
- NODE_ENV=production npm run build
# makes node and npm available
- mkdir -p /app/bin /app/lib /app/lib/dist /app/lib/src /app/lib/assets
- cp -a /usr/lib/sdk/node18/bin/{node,npm} /app/bin
- cp -a /usr/lib/sdk/node18/lib/* /app/lib
- rm -r /app/lib/node_modules/npm/{docs,man} # remove this when updating to newer runtime-version
# remove dev dependencies
- npm prune --production
# copy node_modules needed to run app
- cp -r node_modules/. /app/lib/node_modules
# even if not using package.json for scripts it must be present
# so that tsx/node runs as esm (needs to see "type": "module" in file)
- cp package.json /app/lib/package.json
# copy app files to runtime dir
- cp -r dist/. /app/lib/dist
- cp -r src/. /app/lib/src
- cp -r config /app/lib/config
- cp -r assets /app/lib/assets
- cp -r public /app/lib/public
- cp vite.config.ts /app/lib/vite.config.ts
- cp tsconfig.json /app/lib/tsconfig.json
- cp tailwind.config.js /app/lib/tailwind.config.js
- cp postcss.config.js /app/lib/postcss.config.js
- cp flatpak/defaultConfig.json /app/lib/config/config.json
- install -Dm644 flatpak/${FLATPAK_ID}.desktop /app/share/applications/${FLATPAK_ID}.desktop
- install -Dm644 flatpak/icon.png /app/share/icons/hicolor/512x512/apps/${FLATPAK_ID}.png
- install -Dm644 assets/icon.svg /app/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
- install -Dm644 flatpak/${FLATPAK_ID}.metainfo.xml /app/share/metainfo/${FLATPAK_ID}.metainfo.xml
- install -Dm755 ../run.sh /app/bin/multiscrobbler
subdir: main
sources:
# use for official releases
# - type: git
# url: https://github.com/FoxxMD/multi-scrobbler
# tag: 0.6.3
# commit: 4bd996eb466137ffc5c3f48305afabde03720525
# dest: main
# use if developing locally
# - type: dir
# path: /home/yourUser/multi-scrobbler
# dest: main
# Wrapper to launch the app
- type: script
dest-filename: run.sh
commands:
# Run config check and copy examples + default config (disable file logging)
- |
echo -e "Checking CONFIG_DIR for existing configs: ${XDG_CONFIG_HOME}"
if [ "$(ls -A ${XDG_CONFIG_HOME})" ]; then
echo "Directory is not empty! Will not add default config/examples"
else
echo "Directory is empty! Adding default config/examples"
cp -r /app/lib/config/. "${XDG_CONFIG_HOME}"/
#cp /app/lib/defaultConfig.json $XDG_CONFIG_HOME/config.json
fi
- cd /app/lib && CONFIG_DIR=$XDG_CONFIG_HOME LOG_DIR=$XDG_CONFIG_HOME IS_LOCAL=true NODE_ENV=production node_modules/.bin/tsx src/backend/index.ts
- generated-sources.json
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="description"
content="Dashboard for Multi Scrobbler"
/>
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="manifest" href="/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multi Scrobbler</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/client/index.tsx"></script>
</body>
</html>
-316
View File
@@ -1,316 +0,0 @@
import fs from "fs";
import {addAsync} from '@awaitjs/express';
import express from 'express';
import winston from 'winston';
import {Writable} from 'stream';
import 'winston-daily-rotate-file';
import {readJson, sleep, writeFile, buildTrackString} from "./utils.js";
import SpotifyWebApi from "spotify-web-api-node";
import MalojaScrobbler from "./clients/MalojaScrobbler.js";
const {format, createLogger, transports} = winston;
const {combine, printf, timestamp} = format;
let output = []
const stream = new Writable()
stream._write = (chunk, encoding, next) => {
output.unshift(chunk.toString());
output = output.slice(0, 51);
next()
}
const streamTransport = new winston.transports.Stream({stream})
const myFormat = printf(({level, message, label = 'App', timestamp}) => {
return `${timestamp} [${label}] ${level}: ${message}`;
});
const logger = createLogger({
level: 'debug',
format: combine(
timestamp(),
myFormat
),
transports: [
new transports.Console(),
streamTransport,
]
});
const scopes = ['user-read-recently-played', 'user-read-currently-playing'];
const state = 'random';
let lastTrackPlayedAt = new Date();
const configDir = process.env.CONFIG_DIR || `${process.cwd()}/config`;
const configLocation = process.env.CONFIG_PATH || `${configDir}/config.json`;
const app = addAsync(express());
try {
(async function () {
let spotifyAsyncFunc = null;
// try to read a configuration file
let config = {};
try {
config = await readJson(configLocation);
} catch (e) {
logger.warn('Could not read config file');
logger.error(e);
}
// setup defaults for other configs and general config
const {
logPath: logPathRaw = process.env.LOG_PATH || true,
interval = 60,
port = process.env.PORT ?? 9078,
spotify: spotifyConfigRaw = process.env.SPOTIFY_CONFIG_PATH || `${configDir}/spotify.json`,
clients = [],
} = config || {};
const localUrl = `http://localhost:${port}`;
// first thing, if user wants to log to file set it up now
if (logPathRaw !== false) {
let logPath = `${process.cwd()}/logs`;
if (typeof logPathRaw === 'string') {
logPath = logPathRaw;
}
logger.add(new winston.transports.DailyRotateFile({
level: 'info', // don't need to add a bunch of noise to files
dirname: logPath,
createSymlink: true,
symlinkName: 'scrobble-current.log',
filename: 'scrobble-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '5m'
}))
}
if (interval < 15) {
console.warn('Interval should be above 30 seconds...😬');
}
let spotifyCreds = {};
try {
spotifyCreds = await readJson('./spotifyCreds.json');
} catch (e) {
logger.warn('Current spotify access token was not parsable or file does not exist (this could be normal)');
}
let spotifyConfig = spotifyConfigRaw;
if (typeof spotifyConfigRaw === 'string') {
try {
spotifyConfig = await readJson(spotifyConfigRaw);
} catch (e) {
logger.warn('Could not read spotify config file');
logger.error(e);
}
}
const {
accessToken = process.env.SPOTIFY_ACCESS_TOKEN,
clientId = process.env.SPOTIFY_CLIENT_ID,
clientSecret = process.env.SPOTIFY_CLIENT_SECRET,
redirectUri = process.env.SPOTIFY_REDIRECT_URI,
refreshToken = process.env.SPOTIFY_REFRESH_TOKEN,
} = spotifyConfig;
const rdUri = redirectUri || `${localUrl}/callback`;
const {token = accessToken, refreshToken: rt = refreshToken} = spotifyCreds;
if (token === undefined) {
if (clientId === undefined) {
throw new Error('ClientId not defined');
}
if (clientSecret === undefined) {
throw new Error('ClientSecret not defined');
}
}
const spotifyApi = new SpotifyWebApi({
clientId,
clientSecret,
accessToken: token,
redirectUri: rdUri,
refreshToken: rt,
});
const scrobbleClients = await createClients(clients, configDir);
if (scrobbleClients.length === 0) {
throw new Error('No scrobble clients were configured');
}
app.getAsync('/authSpotify', async function (req, res) {
logger.info('Redirecting to spotify authorization url');
res.redirect(spotifyApi.createAuthorizeURL(scopes, state));
});
app.postAsync('/pollSpotify', async function (req, res) {
spotifyAsyncFunc = pollSpotify(spotifyApi, interval, scrobbleClients);
res.send('OK');
});
app.getAsync(`/callback`, async function (req, res, next) {
const {error, code} = req.query;
if (error === undefined) {
const tokenResponse = await spotifyApi.authorizationCodeGrant(code);
spotifyApi.setAccessToken(tokenResponse.body['access_token']);
spotifyApi.setRefreshToken(tokenResponse.body['refresh_token']);
await writeFile('spotifyCreds.json', JSON.stringify({
token: tokenResponse.body['access_token'],
refreshToken: tokenResponse.body['refresh_token']
}));
logger.info('Got auth code from callback!');
spotifyAsyncFunc = pollSpotify(spotifyApi, interval, scrobbleClients);
return res.send('OK');
} else {
throw new Error('User denied oauth access');
}
});
if (token === undefined) {
logger.info('No access token found');
logger.info(`Open ${localUrl}/authSpotify to continue`);
} else {
spotifyAsyncFunc = pollSpotify(spotifyApi, interval, scrobbleClients)
logger.info(`Server started at ${localUrl}`);
}
const server = await app.listen(port)
}());
} catch (e) {
logger.error('Exited with uncaught error');
logger.error(e);
}
const pollSpotify = async function (spotifyApi, interval = 60, clients = []) {
logger.info('Starting spotify polling', {label: 'Spotify'});
try {
while (true) {
let data = {};
logger.debug('Refreshing recently played', {label: 'Spotify'})
try {
data = await spotifyApi.getMyRecentlyPlayedTracks({
limit: 20
});
} catch (e) {
if (e.statusCode === 401) {
logger.info('Access token was not valid, attempting to refresh', {label: 'Spotify'});
const tokenResponse = await spotifyApi.refreshAccessToken();
const {body: {
access_token,
// spotify may return a new refresh token
// if it doesn't then continue to use the last refresh token we received
refresh_token = spotifyApi.getRefreshToken(),
} = {}} = tokenResponse;
spotifyApi.setAccessToken(access_token);
await writeFile('spotifyCreds.json', JSON.stringify({
token: access_token,
refreshToken: refresh_token,
}));
data = await spotifyApi.getMyRecentlyPlayedTracks({
limit: 20
});
} else {
throw e;
}
}
let newLastPLayedAt = undefined;
const now = new Date();
for (const playObj of data.body.items) {
const {track: {name: trackName, duration_ms }, played_at} = playObj;
const playDate = new Date(played_at);
// compare play time to most recent track played_at scrobble
if (playDate.getTime() > lastTrackPlayedAt.getTime()) {
logger.info(`New Track => ${buildTrackString(playObj)}`, {label: 'Spotify'});
// so we always get just the most recent played_at
if (newLastPLayedAt === undefined) {
newLastPLayedAt = playDate;
}
const closeToInterval = Math.abs(now.getTime() - playDate.getTime()) / 1000 < 5;
if (closeToInterval) {
// because the interval check was so close to the play date we are going to delay client calls for a few secs
// this way we don't accidentally scrobble ahead of any other clients (we always want to be behind so we can check for dups)
// additionally -- it should be ok to have this in the for loop because played_at will only decrease (be further in the past) so we should only hit this once, hopefully
logger.info('Track is close to polling interval! Delaying scrobble clients refresh by 10 seconds so other clients have time to scrobble first', {label: 'Spotify'});
await sleep(10 * 1000);
}
for (const client of clients) {
if (closeToInterval || client.scrobblesLastCheckedAt().getTime() < now.getTime()) {
await client.refreshScrobbles();
}
if (!client.alreadyScrobbled(trackName, playDate, duration_ms / 1000)) {
await client.scrobble(playObj);
}
}
} else {
break;
}
if (newLastPLayedAt !== undefined) {
lastTrackPlayedAt = newLastPLayedAt;
}
}
// sleep for interval
logger.debug(`Sleeping for interval (${interval}s)`, {label: 'Spotify'});
await sleep(interval * 1000);
}
} catch (e) {
logger.error('Error occurred while in spotify polling loop', {label: 'Spotify'});
logger.error(e, {label: 'Spotify'});
}
};
const createClients = async function (clientConfigs = [], configDir = '.') {
const clients = [];
for (const config of clientConfigs) {
const dataType = typeof config;
if (!['object', 'string'].includes(dataType)) {
throw new Error('All client configs must be objects or strings');
}
const clientType = dataType === 'string' ? config : config.type;
switch (clientType) {
case 'maloja':
let data = `${configDir}/maloja.json`;
if (dataType === 'object') {
const {data: dataProp = process.env.MALOJA_CONFIG_PATH || `${configDir}/maloja.json`} = config;
data = dataProp;
}
let malojaConfig;
if (typeof data === 'string') {
try {
malojaConfig = await readJson(data);
} catch (e) {
logger.warn('Maloja config was not parsable or file does not exist');
}
} else {
malojaConfig = data;
}
const {
url = process.env.MALOJA_URL,
apiKey = process.env.MALOJA_API_KEY
} = malojaConfig;
if (url === undefined && apiKey === undefined) {
// the user probably didn't set anything up for this client at all, don't log
continue;
}
if (url === undefined) {
logger.warn('Maloja url not found in config');
continue;
}
if (apiKey === undefined) {
logger.warn('Maloja api key not found in config');
continue;
}
clients.push(new MalojaScrobbler(logger, malojaConfig));
break;
default:
break;
}
}
return clients;
}
+11558 -600
View File
File diff suppressed because it is too large Load Diff
+155 -15
View File
@@ -1,33 +1,173 @@
{
"name": "maloja-spotify-scrobbler",
"version": "0.1.0",
"description": "",
"name": "multi-scrobbler",
"version": "0.7.0",
"type": "module",
"main": "index.js",
"description": "scrobble plays from multiple sources to multiple clients",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"schema": "npm run -s schema-aio & npm run -s schema-source & npm run -s schema-client & npm run -s schema-aiosource & npm run -s schema-aioclient",
"schema-client": "typescript-json-schema src/backend/tsconfig.json ClientConfig --out src/backend/common/schema/client.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-source": "typescript-json-schema src/backend/tsconfig.json SourceConfig --out src/backend/common/schema/source.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aio": "typescript-json-schema src/backend/tsconfig.json AIOConfig --out src/backend/common/schema/aio.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aiosource": "typescript-json-schema src/backend/tsconfig.json AIOSourceConfig --out src/backend/common/schema/aio-source.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aioclient": "typescript-json-schema src/backend/tsconfig.json AIOClientConfig --out src/backend/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"typedoc": "typedoc",
"circular": "madge --circular --extensions ts src/index.ts",
"test": "npm run -s test:backend",
"test:backend": "mocha --reporter spec --recursive src/backend/tests/**/*.test.ts",
"fileEndings": "jscodeshift --transformFrom js --transformTo none --importTypes relative --extensions=ts --parser tsx --transform codeshift/transform.ts src/backend",
"dev": "APP_VERSION=$npm_package_version nodemon -w src/backend -x tsx src/backend/index.ts",
"start": "APP_VERSION=$npm_package_version NODE_ENV=production tsx src/backend/index.ts",
"build:frontend": "APP_VERSION=$npm_package_version vite build",
"build:backend": "tsc -p src/backend",
"build": "npm run -s build:backend && npm run -s build:frontend",
"postinstall": "patch-package"
},
"exports": {
".": {
"types": "./src/backend/common/infrastructure/typings/lastfm-node-client.d.ts",
"import": "./src/backend/index.js"
}
},
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
"node": ">=18.0.0",
"npm": ">=9.1.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/FoxxMD/maloja-spotify-scrobbler.git"
"url": "git+https://github.com/FoxxMD/multi-scrobbler.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/FoxxMD/maloja-spotify-scrobbler/issues"
"url": "https://github.com/FoxxMD/multi-scrobbler/issues"
},
"homepage": "https://github.com/FoxxMD/maloja-spotify-scrobbler#readme",
"private": true,
"homepage": ".",
"dependencies": {
"@astronautlabs/mdns": "^1.0.7",
"@awaitjs/express": "^0.6.3",
"date-fns": "^2.16.1",
"@curvenote/ansi-to-react": "^7.0.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@foxxmd/chromecast-client": "^1.0.4",
"@foxxmd/logging": "^0.1.13",
"@foxxmd/string-sameness": "^0.4.0",
"@kenyip/backoff-strategies": "^1.0.4",
"@react-nano/use-event-source": "^0.13.0",
"@reduxjs/toolkit": "^1.9.5",
"@supercharge/promise-pool": "^3.0.0",
"address": "^1.2.2",
"ajv": "^8.12.0",
"autoprefixer": "^10.4.17",
"avahi-browse": "^1.1.4",
"better-sse": "^0.8.0",
"body-parser": "^1.19.0",
"castv2": "^0.1.10",
"clsx": "^2.0.0",
"common-tags": "^1.8.2",
"compare-versions": "^4.1.2",
"concat-stream": "^2.0.0",
"dayjs": "^1.10.4",
"dbus-ts": "^0.0.7",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"spotify-web-api-node": "^5.0.0",
"superagent": "^6.1.0",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0"
"express-session": "^1.17.2",
"fixed-size-list": "^0.3.0",
"formidable": "^2.1",
"gotify": "^1.1.0",
"iti": "^0.6.0",
"json5": "^2.2.3",
"kodi-api": "^0.2.1",
"lastfm-node-client": "^2.2.0",
"mopidy": "^1.3.0",
"nanoid": "^3.3.1",
"normalize-url": "^6.1.0",
"ntfy": "1.0.5",
"object-hash": "^3.0.0",
"p-event": "^4.2.0",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"patch-package": "^8.0.0",
"pony-cause": "^1.1.1",
"postcss": "^8.4.33",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"safe-stable-stringify": "^1.1.1",
"sass": "^1.70.0",
"spotify-web-api-node": "^5.0.2",
"superagent": "^8.0.9",
"tailwindcss": "^3.3.3",
"toad-scheduler": "^3.0.0",
"tsx": "^4.7.0",
"vite-express": "^0.13.0",
"xml2js": "0.5.0",
"youtube-music-ts-api": "^1.7.0"
},
"devDependencies": {
"@dbus-types/dbus": "^0.0.4",
"@dbus-types/notifications": "^0.0.5",
"@eslint/js": "^8.56.0",
"@faker-js/faker": "^8.1.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@tsconfig/node18": "^1.0.1",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/clone": "^2.1.2",
"@types/common-tags": "^1.8.1",
"@types/concat-stream": "^2.0.0",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/formidable": "^2.0.5",
"@types/jest": "^27.5.2",
"@types/jscodeshift": "^0.11.6",
"@types/mocha": "^9.1.0",
"@types/node": "^18.0.0",
"@types/object-hash": "^3.0.0",
"@types/passport": "^1.0.12",
"@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7",
"@types/react-window": "^1.8.5",
"@types/spotify-web-api-node": "^5.0.7",
"@types/superagent": "^4.1.16",
"@types/xml2js": "^0.4.11",
"@vitejs/plugin-react": "^4.2.1",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"clone": "^2.1.2",
"eslint": "^8.56.0",
"eslint-plugin-prefer-arrow-functions": "^3.2.4",
"jscodeshift": "^0.15.0",
"mocha": "^10.3.0",
"msw": "^2.1.2",
"nodemon": "^3.0.3",
"ts-essentials": "^9.1.2",
"typedoc": "^0.25",
"typescript": "^5.3.3",
"typescript-eslint": "^7.0.1",
"typescript-json-schema": "~0.55",
"vite": "^5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"
}
}
}

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