Compare commits

...
522 Commits
Author SHA1 Message Date
FoxxMD e80ad81f05 Merge branch 'develop' 2024-08-28 09:35:28 -04:00
FoxxMD a3960421d7 chore: Bump version for release 2024-08-28 09:35:13 -04:00
FoxxMD 2f2eeeb13a docs(youtube): Add FAQ entries for YTM auth and scrobble issues 2024-08-28 09:32:46 -04:00
Matt Foxx eeb026abf0 Merge pull request #163 from FoxxMD/ytMusicDebug
Improve YTM experience
2024-08-28 09:11:38 -04:00
FoxxMD 0fed96f59c Merge branch 'develop' 2024-08-28 09:07:12 -04:00
Matt Foxx 8369939135 Merge pull request #181 from FoxxMD/malojaDuplicateHandling
fix(maloja): Improve handling for Maloja warnings-as-errors
2024-08-27 16:54:11 -04:00
FoxxMD 08056a650f fix: Actually store recent scrobbles
Refresh scrobbles refactor missed actually storing scrobbles on refresh
2024-08-27 09:40:30 -04:00
FoxxMD 5b53b7490c fix(maloja): Improve handling for Maloja warnings-as-errors
* Fixes incorrect handling for duplicate scrobble error from Maloja 3.2.2 #180
* More defensive Maloja scrobble parsing
2024-08-26 11:09:49 -04:00
Matt Foxx ce95c3ae8c Merge pull request #179 from FoxxMD/vlc
Implement VLC as Source
2024-08-26 10:55:01 -04:00
FoxxMD 5e0c4e3e77 docs(vlc): Some fixes for VLC docs
tags <> in code block in details causes parsing error
2024-08-23 09:00:14 -04:00
FoxxMD 10bf48df40 docs(vlc): Add VLC docs 2024-08-22 17:30:37 -04:00
FoxxMD ae935798f3 chore: Update schema 2024-08-22 16:44:15 -04:00
FoxxMD bf71f8cf86 feat(vlc): Use regex from configuration to extract metadata from filenames 2024-08-22 16:44:08 -04:00
FoxxMD 4206696f0d feat(vlc): Better metadata parsing 2024-08-22 15:07:47 -04:00
FoxxMD 2903662f8b feat: Implement POC vlc Source #177 2024-08-22 13:11:12 -04:00
FoxxMD 469847e4b7 feat(youtube): Use just-validated history as recently played on new plays #156
Rather than generating our own just use the known "good" history we just compared against to see if this improves consistency.
2024-08-21 13:14:38 -04:00
FoxxMD d7a86b0401 feat(youtube): Increase level of auth update logging when configured to output changes #158
Increasing to INFO makes the signal better for long-running MS processes instead of requiring logging all of DEBUG
2024-08-21 13:14:38 -04:00
FoxxMD 4fdd346c00 Merge branch 'refs/heads/develop' 2024-08-20 09:18:01 -04:00
FoxxMD 4f26b63ab1 chore: Bump version for release 2024-08-20 09:17:28 -04:00
FoxxMD 0fed91302c chore(ci): Update untagged package schedule to run once a week
Doesn't need to run every day.
2024-08-19 11:55:47 -04:00
FoxxMD 831cefd9a7 docs(deezer): Deprecate due to discontinued API support
Deprecate Source with warning message and update docs. #175
2024-08-19 11:25:11 -04:00
FoxxMD 114a3dda8b feat(ui): Add timestamp to recent pages #167 2024-07-26 10:07:25 -04:00
FoxxMD 55ddd2c5a0 docs: Mention data modification in readme 2024-07-26 09:38:18 -04:00
FoxxMD 1d276186f8 feat: Implement removing field if field is an empty string after transforming 2024-07-26 09:30:58 -04:00
FoxxMD fb3886b981 docs: Modification improvements 2024-07-26 09:19:11 -04:00
FoxxMD 2bed7be654 docs: Fix compare example 2024-07-25 17:47:57 -04:00
FoxxMD 5c460ad747 docs: Add Play Transform docs 2024-07-25 17:28:49 -04:00
FoxxMD 32220d7509 feat: Implement play transforms in source/client logic 2024-07-25 16:16:58 -04:00
FoxxMD b0265001f8 feat: Improve logging for play transforms
* Use a proper child logger and error messages on catch
* Log full play string before => after instead of just parts
2024-07-25 15:12:39 -04:00
FoxxMD 9ede4634cb test: Use faker 9.0RC for more plausible artist/album names in generated data 2024-07-25 15:04:37 -04:00
FoxxMD 0e008ecd5c feat: Add album option to play string building function 2024-07-25 15:03:39 -04:00
FoxxMD 2ae82fdd9e feat: Implement common play transform functionality 2024-07-25 13:43:34 -04:00
Matt Foxx 3b430b95ed Merge pull request #174 from FoxxMD/issue-173-scrobbler-improvement
Enable more agressive scrobble checking behavior
2024-07-25 09:18:07 -04:00
FoxxMD e907892c4a refactor(scrobbler): Change refresh force to user-configurable staleness time
Use refreshStaleAfter to force refresh to user can configure the time.

Allows more nuanced refreshing behavior as well as always refresh (0 seconds)
2024-07-24 12:06:20 -04:00
FoxxMD d9cd3764b9 refactor(scrobbler): Consolidate refresh logic and add more logging
* Move recentScrobbles and generic refresh logic into abstract class
* New abstract function for scrobbler children to implement to get scrobbler-specific play lists on refresh
* Implement shouldRefereshScrobble with logging for better insight into why or why not a scrobbler refreshed upstream scrobbles
2024-07-24 11:20:09 -04:00
FoxxMD 9c2bf05a24 refactor(listenbrainz): Simplify scrobble api calls and logging 2024-07-24 10:31:37 -04:00
FoxxMD 91e52c4c5d feat(musikcube): Provide more error logging and example config
* Update iso-websockets to fix typing exports and get node network error from WS error
* Add url example to config
2024-07-23 12:11:19 -04:00
FoxxMD b41d7f539f feat(scrobble): Enable forcing existing scrobbles refresh on every scrobble #173 2024-07-23 10:48:35 -04:00
FoxxMD 9735b980d7 fix(tests): Do not re-use scrobbler between tests 2024-07-19 14:44:39 -04:00
FoxxMD 706483ed06 feat: On polling error retry check source is initialized and reinitialize if not 2024-07-19 14:36:50 -04:00
FoxxMD 7760776a5f feat(mpd): Use idle event to wake up from polling early 2024-07-19 14:36:27 -04:00
FoxxMD 8b9a7893d3 feat(mpd): Implement MPD Source MVP 2024-07-19 12:10:51 -04:00
FoxxMD 729250a2cc Merge branch 'refs/heads/develop' 2024-07-18 14:18:39 -04:00
FoxxMD f14203f85e docs: More improvements to configuration docs 2024-07-18 11:14:40 -04:00
FoxxMD 60e880f7f2 Merge branch 'refs/heads/develop'
# Conflicts:
#	docsite/docs/configuration/configuration.md
2024-07-17 12:47:23 -04:00
FoxxMD 16cef0ae65 chore: Bump versions for release 2024-07-17 12:46:18 -04:00
FoxxMD 12ff1f421e docs: Use relative paths for volume mounting 2024-07-17 11:46:00 -04:00
FoxxMD 61c33da2b0 docs: Fix quickstart link 2024-07-17 11:39:41 -04:00
FoxxMD 8b0c7205b7 docs: Add quick start guide 2024-07-17 11:33:56 -04:00
FoxxMD b02c96ada8 docs: Parse and render config examples from repository 2024-07-17 09:51:44 -04:00
FoxxMD 52b20e4c8e docs: Update kitchensink code block titles 2024-07-16 15:37:09 -04:00
FoxxMD a94ac8284f docs: refactor installation for docusaurus 2024-07-16 15:28:09 -04:00
FoxxMD ee399136a9 docs: Lean 100% into docusaurus documentation
Since docs are now generated alongside builds and hosted on the app server there is no longer a barrier to using docusaurus docs as the main documentation presentation. Any user looking for docs can use the GH Pages hosted site or a "versioned" docs site hosted on their MS app. Due to this we can fully commit to docusaurus and take advantage of MDX layouts to simplify docs.

* Replace env/file configs headings in configuration with MDX tabs to make reading easier
* Remove superfluous TOCs
* Replace internal links in repo readme with links to docsite
2024-07-16 14:11:14 -04:00
FoxxMD ab54422acf docs: Replace manual notes with admonitions 2024-07-16 11:58:09 -04:00
FoxxMD d56c5720eb docs(flatpak): Fix typo in docs website link 2024-07-16 11:46:13 -04:00
FoxxMD 040de4b000 docs(flatpak): Implement bash setup script and move flatpak instructions to docsite
* setup.sh takes care of building generated sources for project/docsite and building flatpak app
* Modify flatpak manifest to include two generated sources files and build docsite files
* Update flatpak docs with script usage and generated sources changes, move to docusauraus doc site
2024-07-16 11:34:15 -04:00
FoxxMD 815a774e3d docs(fix): Fix broken links/anchors 2024-07-15 14:24:00 -04:00
FoxxMD b2e7f58e14 docs(fix): Add missing docsite build steps to alpine docker variant 2024-07-15 13:50:37 -04:00
FoxxMD 8d124a20ac docs(fix): Update package scripts to use npx to invoke docusarus 2024-07-15 13:44:07 -04:00
FoxxMD d1a4a54d2a docs: Implement self-hosted docs
* Add npm scripts for installing/building docusaurus from main project
* Set base url based on ENV
* Build docsite in docker image
* Add link to doc in dashboard and link to github
* Update nodejs install instructions to include doc build command
* Update GH pages workflow to use correct base url
* Add hint page to main app when docs are not built
2024-07-15 13:26:57 -04:00
FoxxMD 049bb06762 docs: Make docs site more prominent in readme 2024-07-12 16:55:48 -04:00
FoxxMD e5dd473981 Merge branch 'refs/heads/develop' 2024-07-12 14:39:53 -04:00
FoxxMD ca5cc3bb30 docs(feat): Add TOC to dev docs and links in readme 2024-07-12 14:37:52 -04:00
FoxxMD 444d75c803 docs(feat): Add architecture overview, common dev structures, and dev guidance for Sources 2024-07-12 14:31:11 -04:00
FoxxMD ccda3a6098 Merge branch 'refs/heads/develop' 2024-07-09 16:06:05 -04:00
FoxxMD ce1c70a4e1 ci: Ignore flatpak and docsite for image deploy 2024-07-09 16:05:15 -04:00
FoxxMD 5fcfa979c5 docs: Improve TOC for docusaurus and github
* Use lower headers so docusaurs sidebar TOC is more accurate
* Specify header levels for config/FAQ to make sidebar more concise
* Wrap inline TOC in details block -- so docusaurs users don't have to see it twice but its still available to github viewers
2024-07-09 15:56:44 -04:00
FoxxMD ec4d609ba1 docs: Update docusaurs playground schema for MS to latest 2024-07-09 15:55:07 -04:00
FoxxMD b42278a372 docs: Update docusaurus to v3 2024-07-09 15:54:45 -04:00
FoxxMD 1f96035ba9 Merge branch 'refs/heads/develop' 2024-07-09 10:25:29 -04:00
FoxxMD 332edb5dca chore: Bump flatpak release history 2024-07-09 10:25:17 -04:00
FoxxMD c5681d31eb Merge branch 'refs/heads/develop' 2024-07-09 09:53:03 -04:00
FoxxMD 685d70c639 chore: Bump version for release 2024-07-09 09:51:43 -04:00
FoxxMD 848878bc5e docs: Update screenshot 2024-07-09 09:51:33 -04:00
FoxxMD 088d063d38 fix: use correct ordering for version priority 2024-07-05 11:26:23 -04:00
FoxxMD 4e493db987 refactor: Use @foxxmd/get-version to render app version 2024-07-05 10:42:41 -04:00
FoxxMD 6d017d4b27 chore(ci): Update setup-node to v4 to squash node deprecation warnings 2024-07-03 10:11:52 -04:00
FoxxMD a34066d650 fix(docker): Typo in Dockerfile name 2024-07-03 10:09:24 -04:00
FoxxMD 0e34d63c84 feat(docker)!: Use debian image as default
Sporadic DNS issues with alpine have caused a number of issues #160 #126 #134

Use debian as default since it has been proven to be stable. The image size increase is worth squashing these issues.
2024-07-03 10:07:35 -04:00
FoxxMD 9dca8ebfa0 fix(chromecast): Add missing avahi-utils package for debian image 2024-07-03 09:55:58 -04:00
FoxxMD c908aea1ae ci: Better package cleanup and manual triggers 2024-06-26 16:39:42 -04:00
FoxxMD c7a58bbce6 ci: Add package cleanup 2024-06-26 16:26:30 -04:00
FoxxMD b26f303479 ci: remove test workflow 2024-06-26 15:52:52 -04:00
FoxxMD 13d64d2ce2 ci: Implement gated automated PR building
Only checkout/build PR branches if labelled first
2024-06-26 15:44:12 -04:00
FoxxMD ddb7fb3d6e ci: Use checked out repo to get git info rather than GH variables 2024-06-26 14:05:33 -04:00
FoxxMD 457967bfb2 Pass default APP_VERSION env as build arg 2024-06-26 13:23:45 -04:00
FoxxMD 734742ecbe Add APP_VERSION parsing to github actions 2024-06-26 12:37:09 -04:00
FoxxMD 26dab3870a Simplify version info on frontend by using api endpoint 2024-06-26 12:37:08 -04:00
FoxxMD ca9acbb089 Add git versioning check on backend 2024-06-26 12:37:08 -04:00
FoxxMD f78d6fec1d fix(youtube): Catch auth issues that occur during polling #158 2024-06-26 12:37:08 -04:00
FoxxMD bafb852545 feat(youtube): Use full-fat list diffing to watch for new plays #156
* Use superdiff to diff PlayObject lists and detect changes as well as append/prepend scenarios
* Replace YTM recently played logic with list diffing, only accept prepend-validated lists
  * On non-prepend scenarios replace existing recently played and log human readable diff
2024-06-26 12:37:08 -04:00
FoxxMD a755db45b9 fix: Fix ntfy imports 2024-06-26 12:37:08 -04:00
FoxxMD b9715f2159 chore: update dependencies 2024-06-26 12:37:07 -04:00
FoxxMD e90dbaf691 chore: npm audit fixes 2024-06-26 12:37:07 -04:00
FoxxMD 08ac55f718 fix(webscrobbler): Fix formatting of slug url in logs 2024-06-26 12:37:07 -04:00
FoxxMD 5bf831e236 feat(youtube): Sync cookies with those returned from YTM responses #158
* Patch youtube-music-ts-api to use updated cookies from response and provide a callback on update
* Implement currentCreds/build init data and read from MS-updated creds if available
* Write to currentCreds when ytm-ts-api invokes auth update callback and optionally log what parts changed based on config options
2024-06-26 12:37:07 -04:00
FoxxMD 1905804f2a fix(webscrobbler): Add preflight route #157 2024-06-26 12:37:07 -04:00
FoxxMD afc931ac8c docs: Use vars instead of secrets 2024-06-26 12:37:07 -04:00
FoxxMD 9fb032e030 docs: Add scripting on deploy for plausible analytics 2024-06-26 12:37:07 -04:00
FoxxMD 09b0c96263 feat(youtube): Surface library history playlist and raw recently played for debugging #156 2024-06-26 12:37:06 -04:00
FoxxMD 7c66816023 Implement musikcube source 2024-06-26 12:35:13 -04:00
FoxxMD 3e7e5df5de fix: Missed package-lock.json commit 2024-06-05 11:01:12 -04:00
FoxxMD 1c84a74034 fix(vite): Use default base string if no other path detected
* Update Vite dep
* Use inlineViteConfig for vite-express middleware and detect correct base string
* Refactor vite.config.ts to detect correct base string
2024-06-05 10:58:52 -04:00
FoxxMD 09969dd9d7 fix(spotify): Convert URL to string for redirectUri 2024-06-05 10:21:18 -04:00
FoxxMD 63b57e1113 docs: Clarify BASE_URL usage and add more examples #155 2024-06-04 15:51:30 -04:00
FoxxMD 585677e515 fix: Refactor Base URL normalization to cover more cases #155
* Move code into testable function
* Fix port erasure when url uses standard port but it is explicitly specified
* Simplify building default url and fix using undefined mainPort variable
* Fix port erasure when https protocol is explicitly specified
* Refactor localUrl usage as URL instead of string
  * Implement pathname joining on URL to make handling trailing/leading slashes take place in nodejs lib
* Implements tests for URL building/manipulation
2024-06-04 15:44:28 -04:00
FoxxMD 57f2b5f7e5 feat: Update normalize-url to prevent port erasure #155 2024-06-04 15:39:47 -04:00
FoxxMD 3597735556 feat(client): Implement initial refresh count and config control #153
* Refresh recent scrobbles with a higher limit after initialization (maximum supported by api call)
* Implement controlling count in client config
2024-05-16 15:22:24 -04:00
FoxxMD 2442cf2286 refactor: Move all init logic into common abstract base class 2024-05-16 15:01:07 -04:00
FoxxMD 97438e13d2 refactor(client): Use same init logic as source 2024-05-16 14:50:26 -04:00
FoxxMD 57dff9d122 feat!(source): Increase backlog limit defaults and add config control #153
* Increase default backlog limits to maximum pagination supported by each source
* Add scrobbleBacklogCount config property so user can explicitly set # of tracks to backlog
2024-05-16 13:43:44 -04:00
FoxxMD 963fc792a0 fix: Fix clientDefaults interface 2024-05-10 12:53:04 -04:00
FoxxMD 35a6418f38 docs!: Update schema 2024-05-10 12:48:08 -04:00
FoxxMD 3c0b951132 refactor(config)!: Move common options out of client config and into new option property 2024-05-10 12:44:16 -04:00
FoxxMD 3dc9e7d8b4 refactor(config)!: Move remaining common options out of source data and into options
* Move logPayload and logFilterFailure to options
* Disallow use of "options" property in common data
2024-05-10 12:38:09 -04:00
FoxxMD ab372b8357 refactor(config)!: Move source retry config to options 2024-05-10 12:07:39 -04:00
FoxxMD c79f04581f refactor(config)!: Move scrobble thresholds to options 2024-05-10 12:01:45 -04:00
FoxxMD 0dcd049453 Merge branch 'refs/heads/develop' 2024-04-11 09:39:08 -04:00
FoxxMD 7170b09586 chore: Bump version for release 2024-04-11 09:38:55 -04:00
FoxxMD 3d893b126f refactor(logging): Balance debug/verbose level logging pt.1
* Move some debug => verbose for non-repeating logs that are relevant to state, notifications, and scrobbling
* Move some verbose => debug that were repeating and making verbose level noisy
2024-04-10 11:25:27 -04:00
FoxxMD fb73b8b106 feat(mpris): Less noisy error handling
* Convert DBus non-standard thrown error data to regular Errors
* Only warn about position errors if error message is unexpected
2024-04-10 10:57:02 -04:00
FoxxMD cede77e3fa refactor: Remove pony-cause dependency
Use built-in error cause and adapt helper functions from pony-cause so we can remove a dependency
2024-04-10 10:56:04 -04:00
FoxxMD 0572d6a308 refactor(logging): Reduce noise for polling interval logging 2024-04-10 10:19:58 -04:00
FoxxMD 5967e3df46 feat: Implement notifications via Apprise 2024-04-05 12:07:41 -04:00
FoxxMD 6b1ee83654 docs: Add config section and config example for disabling web server #150 2024-04-04 09:35:27 -04:00
FoxxMD 0af1496a0f chore: Update proxies 2024-04-04 09:30:42 -04:00
FoxxMD 52b08efd88 feat: Implement option for disabling web server #150
If a user does not need the dashboard or has security concerns about ingress and cannot control their hosting environment this enables reducing attack surface.

Closes #150
2024-04-04 09:30:33 -04:00
FoxxMD 7a3e4e56e2 chore: Remove some unused packages 2024-04-02 10:15:41 -04:00
FoxxMD abe6638f66 refactor: Use updated logger to reduce log file path logic complexity 2024-03-28 12:28:56 -04:00
FoxxMD eb62c2cea9 refactor: optimize imports
* Remove unused imports
* Normalize spaces in curly braces
* Sort by module (npm packages, then relative imports)
2024-03-28 09:20:46 -04:00
FoxxMD 7e6eddce3e refactor: Reduce usage of ErrorWithCause
Error Cause (https://github.com/tc39/proposal-error-cause) was finalized and introduced in ES2022 so we don't need to ponyfill this anymore. Still using the helpful helper functions thought.
2024-03-28 09:20:45 -04:00
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
332 changed files with 48316 additions and 17462 deletions
+9 -1
View File
@@ -2,6 +2,7 @@
node_modules
npm-debug.log
Dockerfile
*.Dockerfile
.dockerignore
.gitignore
.github
@@ -10,8 +11,15 @@ Dockerfile
.idea
config/*.json
config/*.bak
config/*.p8
/docs
/logs
.flatpak-builder
flatpak/generated-sources.json
**/generated-sources.*
flatpak/.flatpak-builder
docsite/build
docsite/node_modules
docsite/.docusaurus
docsite/.cache-loader
build
*.secrets
+14
View File
@@ -0,0 +1,14 @@
Testing GH Actions with [ACT]
### Test Branch Push
```shell
act -W '.github/act/actTest.yml' -e '.github/act/actBranchEvent.json'
```
### Test Tag Push
```shell
act -W '.github/act/actTest.yml' -e '.github/act/actTagEvent.json'
```
+4
View File
@@ -0,0 +1,4 @@
{
"ref": "refs/heads/master",
"sha": "0a1a94d9ad4efa373f8d34aace5e7f0a3fff42ad"
}
+5
View File
@@ -0,0 +1,5 @@
{
"ref": "refs/tags/0.8.0",
"ref_name": "0.8.0",
"sha": "0a1a94d9ad4efa373f8d34aace5e7f0a3fff42ad"
}
+104
View File
@@ -0,0 +1,104 @@
name: Publish Docker image to Dockerhub
on:
push:
branches:
- 'master'
- 'develop'
tags:
- '*.*.*'
# don't trigger if just updating docs
paths-ignore:
- '**.md'
# use release instead of tags once version is correctly parsed
# https://github.com/docker/metadata-action/issues/422
# https://github.com/docker/metadata-action/issues/240
# release:
# types: [ published ]
jobs:
test:
name: Build and push container images
runs-on: ubuntu-latest
# strategy:
# fail-fast: false
# matrix:
# include:
# - dockerfile: ./Dockerfile
# suffix: ''
# platforms: 'linux/amd64'
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Debug
run: echo $JSON
env:
JSON: ${{ toJSON(github) }}
- name: Set git state to ENV
id: vars
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
# short sha available under env.COMMIT_SHORT_SHA
run: |
calculatedSha=$(git rev-parse --short HEAD)
branchName=$(git rev-parse --abbrev-ref HEAD)
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
# - name: Set push event short ref
# run: |
# shortEventRef=${github.event.push.ref:10}
# echo "SHORT_REF=$shortEventRef" >> $GITHUB_ENV
- name: Check App Version
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
run: |
echo $APP_VERSION
# - name: Extract metadata (tags, labels) for Docker
# id: meta
# uses: docker/metadata-action@v5
# with:
# # 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={{endsWith(github.ref, 'master')}},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
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
uses: docker/build-push-action@v5
with:
context: .
# https://github.com/docker/build-push-action/issues/1026#issue-2041857786
build-args: |
APP_BUILD_VERSION=${{env.APP_VERSION}}
file: ${{ matrix.dockerfile }}
push: false
tags: mstest:latest
#tags: ${{ steps.meta.outputs.tags }}
#labels: ${{ steps.meta.outputs.labels }}
#platforms: ${{ matrix.platforms }}
+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
+9 -3
View File
@@ -1,9 +1,11 @@
name: Deploy to GitHub Pages
on:
workflow_dispatch:
push:
branches:
- master
- developDocs
# 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
@@ -12,16 +14,20 @@ jobs:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
working-directory: ./docsite
- name: Build website
env:
ANALYTICS: ${{ vars.ANALYTICS }}
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
DOCS_BASE: '/multi-scrobbler'
run: npm run build
working-directory: ./docsite
@@ -0,0 +1,21 @@
name: Delete Untagged Packages
on:
schedule:
- cron: '30 1 * * 0'
workflow_run:
workflows: ["Publish Docker image to Dockerhub"]
types:
- completed
workflow_dispatch:
jobs:
delete-untagged:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: dataaxiom/ghcr-cleanup-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
# maybe set up for PR close later
# https://github.com/dataaxiom/ghcr-cleanup-action?tab=readme-ov-file#delete-image-when-pull-request-is-closed
+122
View File
@@ -0,0 +1,122 @@
name: PR Workflow
on:
pull_request_target:
types:
- labeled
- synchronize
- reopened
- opened
branches:
- 'develop'
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
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
release-snapshot:
name: Release snapshot
runs-on: ubuntu-latest
needs: test
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
permissions:
packages: write
contents: read
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./Dockerfile
suffix: ''
platforms: 'linux/amd64,linux/arm64'
- dockerfile: ./alpine.Dockerfile
suffix: '-alpine'
platforms: 'linux/amd64,linux/arm64'
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
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
tags: |
type=ref,event=pr,suffix=${{ matrix.suffix }}
flavor: |
latest=false
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
env:
APP_VERSION: ${{ format('pr{0}-{1}', github.event.number, github.event.pull_request.head.sha ) }}
with:
context: .
build-args: |
APP_BUILD_VERSION=${{env.APP_VERSION}}
file: ${{ matrix.dockerfile }}
push: ${{ !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platforms }}
combine-and-comment:
name: Leave comment
runs-on: ubuntu-latest
needs: release-snapshot
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
steps:
- name: Create comment
uses: marocchino/sticky-pull-request-comment@v2
with:
recreate: true
header: "pr-release"
message: |
#### :package: A new release has been made for this pull request.
To play around with this PR, pull an image:
* `foxxmd/multi-scrobbler:pr-${{ github.event.number }}`
* `foxxmd/multi-scrobbler:pr-${{ github.event.number }}-alpine`.
Images are available for x86_64 and ARM64.
> Latest commit: ${{ github.event.pull_request.head.sha }}
+76 -17
View File
@@ -1,6 +1,7 @@
name: Publish Docker image to Dockerhub
on:
workflow_dispatch:
push:
branches:
- 'master'
@@ -9,30 +10,76 @@ on:
- '*.*.*'
# don't trigger if just updating docs
paths-ignore:
- '**.md'
- 'README.md'
- '.github/**'
- 'flatpak/**'
# use release instead of tags once version is correctly parsed
# https://github.com/docker/metadata-action/issues/422
# https://github.com/docker/metadata-action/issues/240
# release:
# types: [ published ]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
test:
name: Run Tests
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
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
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix:
include:
- dockerfile: ./Dockerfile
suffix: ''
platforms: 'linux/amd64,linux/arm64'
- dockerfile: ./alpine.Dockerfile
suffix: '-alpine'
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@v2
uses: actions/checkout@v4
- name: Set short git commit SHA
id: vars
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
# short sha available under env.COMMIT_SHORT_SHA
run: |
calculatedSha=$(git rev-parse --short HEAD)
branchName=$(git rev-parse --abbrev-ref HEAD)
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
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@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -40,30 +87,42 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
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=${{ endsWith(github.ref, 'master') }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
type=semver,pattern={{version}}
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@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v4
env:
# use release instead of tags once version is correctly parsed
#APP_VERSION: ${{ github.event.release.tag_name }}
# https://github.com/actions/runner/issues/409#issuecomment-752775072
# https://stackoverflow.com/a/69919067/1469797
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
# https://github.com/docker/build-push-action/issues/1026#issue-2041857786
build-args: |
APP_BUILD_VERSION=${{env.APP_VERSION}}
file: ${{ matrix.dockerfile }}
push: ${{ !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
platforms: ${{ matrix.platforms }}
+7 -1
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*
@@ -125,5 +125,11 @@ src/**/**.js
src/**/**.js.map
*.bak
*.p8
.flatpak-builder
flatpak/generated-sources.json
*.secrets
build
!setupProxy.js
**/generated-sources.*
+5
View File
@@ -0,0 +1,5 @@
{
"reporter": "dot",
"extension": "ts",
"import": "tsx/esm"
}
+1 -1
View File
@@ -1 +1 @@
lts/hydrogen
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
+82 -22
View File
@@ -1,19 +1,51 @@
FROM lsiobase/alpine:3.17 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 ****" && \
apk add --no-cache \
alpine-base \
git \
nodejs \
npm \
openssh && \
apt-get update && \
apt-get install --no-install-recommends -y \
#ca-certificates \
xz-utils \
avahi-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 && \
npm update -g npm && \
echo "**** cleanup ****" && \
rm -rf \
/root/.cache \
/tmp/*
# 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
@@ -21,33 +53,61 @@ ARG data_dir=/config
VOLUME $data_dir
ENV CONFIG_DIR=$data_dir
COPY docker/root/ /
COPY docker/root /
WORKDIR /app
FROM base as build
# copy NPM dependencies and install
COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc tsconfig.json .
COPY --chown=abc:abc package*.json tsconfig.json ./
COPY --chown=abc:abc patches ./patches
RUN npm install
# 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 FAILED for node < 20 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
COPY --chown=abc:abc . /app
RUN npm run build && rm -rf node_modules
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run docs:install && npm run build && rm -rf node_modules && rm -rf docsite/node_modules
FROM base as app
COPY --from=build --chown=abc:abc /app /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=build --chown=abc:abc /app/docsite /app/docsite
COPY --from=base /usr/bin /usr/bin
COPY --from=base /usr/lib /usr/lib
ENV NODE_ENV="production"
ENV NODE_ENV=production
ENV IS_DOCKER=true
RUN npm install --omit=dev \
# https://stackoverflow.com/a/63640896/1469797
ARG APP_BUILD_VERSION
ENV APP_VERSION=$APP_BUILD_VERSION
RUN npm ci --omit=dev \
&& npm cache clean --force \
&& chown abc:abc node_modules \
&& rm -rf node_modules/ts-node \
&& rm -rf node_modules/typescript
&& chown -R abc:abc node_modules \
&& rm -rf node_modules/@types \
/root/.cache
ARG webPort=9078
ENV PORT=$webPort
+42 -23
View File
@@ -3,6 +3,8 @@
[![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)
[![Docs](https://img.shields.io/badge/Read%20The%20Docs-1082c2)](https://foxxmd.github.io/multi-scrobbler/)
<img src="/assets/icon.png" align="right"
alt="multi-scrobbler logo" width="180" height="180">
@@ -10,28 +12,39 @@ 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/))
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
* [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)
* [Spotify](https://foxxmd.github.io/multi-scrobbler/docs/configuration#spotify)
* [Plex](https://foxxmd.github.io/multi-scrobbler/docs/configuration#plex) or [Tautulli](https://foxxmd.github.io/multi-scrobbler/docs/configuration#tautulli)
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jellyfin)
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webscrobbler)
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/docs/configuration#youtube-music)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-source)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-source)
* [~~Deezer~~](https://foxxmd.github.io/multi-scrobbler/docs/configuration#deezer)
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris)
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mopidy)
* [JRiver](https://foxxmd.github.io/multi-scrobbler/docs/configuration#jriver)
* [Kodi](https://foxxmd.github.io/multi-scrobbler/docs/configuration#kodi)
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#google-cast-chromecast)
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/docs/configuration#muikcube)
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpd-music-player-daemon)
* [VLC](https://foxxmd.github.io/multi-scrobbler/docs/configuration#vlc)
* 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)
* [Maloja](https://foxxmd.github.io/multi-scrobbler/docs/configuration#maloja)
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm)
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/docs/configuration#webhook-configurations) or [healthcheck endpoint](https://foxxmd.github.io/multi-scrobbler/docs/configuration#health-endpoint)
* Supports 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)
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/docs/transforms)
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/docs/installation#docker), [flatpak](https://foxxmd.github.io/multi-scrobbler/docs/installationr#flatpak), or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/docs/installation#nodejs)
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
<img src="/assets/status-ui.png" width="800">
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -42,9 +55,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
<img src="/assets/status-ui.jpg" width="800">
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/docs/configuration#lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/docs/configuration#listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
@@ -69,13 +80,17 @@ Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Quick Start
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/docs/quickstart)
## Installation
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/docs/installation)
## Configuration
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration)
## Usage
@@ -90,7 +105,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
## Help/FAQ
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/docs/development/dev-common)
## License
+71
View File
@@ -0,0 +1,71 @@
FROM lsiobase/alpine:3.18 as base
ENV TZ=Etc/GMT
RUN \
echo "**** install build packages ****" && \
apk add --no-cache \
avahi \
avahi-tools \
nodejs \
npm \
&& \
echo "**** cleanup ****" && \
rm -rf \
/root/.cache \
/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 dep/TS config and install dev dependencies
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 docs:install && npm run build && rm -rf node_modules && rm -rf docsite/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=build --chown=abc:abc /app/docsite /app/docsite
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
# https://stackoverflow.com/a/63640896/1469797
ARG APP_BUILD_VERSION
ENV APP_VERSION=$APP_BUILD_VERSION
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
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.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+2 -2
View File
@@ -3,10 +3,10 @@
<defs>
<style>
.cls-1 {
fill: #231f20;
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>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

+2 -2
View File
@@ -8,5 +8,5 @@ These are **NOT** exhaustive examples. You should consult the **configuration**
Documentation at
* [internal docs](../docsite/docs/configuration/configuration.md)
* External Link: https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/configuration/configuration.md
* [internal docs](../docsite/docs/configuration/configuration.mdx)
* External Link: https://foxxmd.github.io/multi-scrobbler/docs/configuration
+11
View File
@@ -0,0 +1,11 @@
[
{
"name": "MyCast",
"data": {
"blacklistedDevices": [],
"whitelistedDevices": [],
"blacklistedApps": [],
"whitelistedApps": []
}
}
]
+12 -1
View File
@@ -1,6 +1,15 @@
{
"debugMode": false,
"disableWeb": false,
"sourceDefaults": {
"maxPollRetries": 0,
"logPayload": false,
"logFilterFailure": "warn",
"logPlayerState": false,
"scrobbleThresholds": {
"duration": 30,
"percent": 50
},
"maxPollRetries": 1,
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
@@ -11,6 +20,7 @@
"sources": [
{
"type": "spotify",
"enable": true,
"clients": ["myConfig"],
"name": "mySpotifySource",
"data": {
@@ -23,6 +33,7 @@
"clients": [
{
"type": "maloja",
"enable": true,
"name": "myConfig",
"data": {
"url": "http://localhost:42010",
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "FoxxMDeezer",
"enable": true,
"clients": [],
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
+6 -5
View File
@@ -1,14 +1,15 @@
[
{
"name": "MyJellyfin",
"enable": true,
"clients": [],
"data": {
"users": ["FoxxMD"],
"servers": ["myServer","anotherServer"],
"options": {
"logPayload": false,
"logFilterFailure": "warn"
}
"servers": ["myServer","anotherServer"]
},
"options": {
"logPayload": false,
"logFilterFailure": "warn"
}
}
]
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyJriver",
"enable": true,
"data": {
"url": "0.0.0.0",
"username": "auser",
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyKodi",
"enable": true,
"data": {
"url": "http://localhost:8080/jsonrpc",
"username": "myUser",
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "client",
"data": {
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "brainz",
"enable": true,
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "myMaloja",
"enable": true,
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyMopidy",
"enable": true,
"data": {
"url": "localhost",
"uriBlacklist": [],
+13
View File
@@ -0,0 +1,13 @@
[
{
"enable": true,
"name": "MyMPD",
"data": {
"url": "192.168.0.100:6600",
"password": "MY_PASSWORD"
},
"options": {
"disableDiscovery": false
}
}
]
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "ubuntu",
"enable": true,
"data": {
"whitelist": ["vlc", "mpd"],
"blacklist": ["spotify"]
+11
View File
@@ -0,0 +1,11 @@
[
{
"type": "musikcube",
"enable": true,
"name": "musikcube",
"data": {
"url": "ws://localhost:7905",
"password": "MY_PASSWORD"
}
}
]
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyPlex",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MySpotify",
"enable": true,
"clients": [],
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
+2 -1
View File
@@ -1,10 +1,11 @@
[
{
"name": "MySubsonic",
"enable": true,
"data": {
"url": "http://localhost:4040/airsonic",
"user": "yourUser",
"password": "yourPassword",
"password": "yourPassword"
}
}
]
+1
View File
@@ -1,6 +1,7 @@
[
{
"name": "MyTautuilli",
"enable": true,
"clients": [],
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
+10
View File
@@ -0,0 +1,10 @@
[
{
"enable": true,
"name": "MyVlc",
"data": {
"url": "localhost:8080",
"password": "changeme"
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "MyWebScrobbler",
"data": {
"slug": null,
"whitelist": [],
"blacklist": []
}
}
]
+2 -1
View File
@@ -1,10 +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",
"authUser": "0"
}
}
]
+26 -19
View File
@@ -1,19 +1,26 @@
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
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 on
restart: unless-stopped
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
@@ -4,5 +4,7 @@
# 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/src/index.js run
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/node_modules/.bin/tsx /app/src/backend/index.ts
+102 -35
View File
@@ -1,20 +1,15 @@
* [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)
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
* [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)
---
toc_min_heading_level: 2
toc_max_heading_level: 5
---
# Connection Issues
## Connection Issues
## Plex/Tautulli/Jellyfin don't connect
### Plex/Tautulli/Jellyfin/Webscrobbler 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.
These sources are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server (or Webscrobbler extension) to contact multi-scrobbler, as opposed to multi-scrobbler contacting them.
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:
```
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] Received request from a new remote address: ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59)
@@ -25,15 +20,15 @@ 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.**
**So, if you do not see either of these in your logs then Plex/Tautulli/Jellyfin/Webscrobbler is not able to connect to your multi-scrobbler instance at all.**
This is not something multi-scrobbler can fix and means you have an issue in your network.
### Troubleshooting
#### Troubleshooting
Check or try all these steps before submitting an issue:
#### Turn on Debug Logging
##### Turn on Debug Logging
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `LOG_LEVEL=debug`:
@@ -42,36 +37,40 @@ First, turn on **debug** logging for multi-scrobbler by setting the environmenta
Check the output for any additional information.
#### Check Host name and URL
##### 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.**
The URLs examples in the [configuration](configuration/configuration.mdx) documentation assume you are running Plex/Tautulli/Jellyfin/Webscrobbler on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
#### Check Firewall and Port Forwarding
##### Check Firewall and Port Forwarding
If the machine multi-scrobbler is running on has a firewall ensure that port **9078** is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine.
#### Check Source Service Logs
##### Check Source Service Logs
Plex/Tautulli/Jellyfin all have logs that will log if they cannot connect to multi-scrobbler. Check these for further information.
##### Plex
###### Plex
Settings -> Manage -> Console
##### Tautulli
###### Tautulli
Check the command-line output of the application or docker logs.
##### Jellyfin
###### Jellyfin
Administration -> Dashboard -> Advanced -> Logs
## Jellyfin has warnings about undefined or missing data
###### Webscrobbler
See [Debugging the extension](https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension) to get logs which should have information about failed requests.
### Jellyfin has warnings about undefined or missing data
Make sure you have
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
* [Configured the webhook plugin correctly](configuration/configuration.mdx#jellyfin)
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
@@ -84,10 +83,10 @@ You can verify the payload sent from the webhook by modifying your jellyfin conf
"name": "MyJellyfin",
"clients": [],
"data": {
"users": ["FoxxMD"],
"options": {
"logPayload": true
}
"users": ["FoxxMD"]
},
"options": {
"logPayload": true
}
}
]
@@ -95,8 +94,24 @@ You can verify the payload sent from the webhook by modifying your jellyfin conf
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
## Spotify/Deezer/LastFM won't authenticate
If you experience issues trying to scrobble with Jellyfin and find this in your MS logs
```
[API] Jellyfin is not sending a request with valid headers...
```
A workaround that may fix this:
* In Webhook settings:
* [In the webhook you have already configured...](configuration/configuration.mdx#jellyfin)
* Add Request Header...
* **Key:** `Content-Type`
* **Value:** `application/json`
* Then Save
### Spotify/Deezer/LastFM won't authenticate
Ensure any **client id** or **secrets** are correct in your configuration.
@@ -109,9 +124,25 @@ If multi-scrobbler is not running on the same machine your browser is on then th
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
# Configuration Issues
### Deezer is not working
## Config could not be parsed
Deezer has discontinued support for their API and the Deezer Source is now [**deprecated.**](configuration/configuration.mdx#deezer) See [this issue for more discussion.](https://github.com/FoxxMD/multi-scrobbler/issues/175#issuecomment-2296776625)
### Youtube Music fails after some time
The Youtube Music library relies on scraping the YTM site (pretending to be a browser) by using cookies/auth from your actual browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control.
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
```
Error: Could not send the specified request to browse. Status code: 401
```
then YTM has invalidated your access. [Follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler]((configuration/configuration.mdx#youtube-music)) and then restart MS to potentially resolve the problem. See [this issue](https://github.com/FoxxMD/multi-scrobbler/issues/158) for further discussion of the problem.
## Configuration Issues
### Config could not be parsed
If you see something like this in your logs:
@@ -123,14 +154,50 @@ If you see something like this in your logs:
It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like [JSONLint](https://jsonlint.com/) to find out where errors you have and fix them.
# Scrobbling Issues
## Scrobbling Issues
## Last.fm does not scrobble tracks with multiple artists correctly
### 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
### 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.mdx#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
### 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.mdx#allow-unknown-media-type) section to fix this
```
My Artist - Example Track has 'movie' media type so will not track
```
Refer to [Force Media Tracking](configuration/configuration.mdx#forcing-media-tracking) section to fix this
### VLC is not scrobbling fields correctly
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](configuration/configuration.mdx#vlc-information-reporting)
### Youtube Music misses scrobbles
In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs **a source of truth.** For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).
In order to compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.
See [this issue](https://github.com/FoxxMD/multi-scrobbler/issues/156#issuecomment-2312533486) for further discussion and a more detailed explanation of why this is happening and how multi-scrobbler compensates for it.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Configuration",
"position": 2,
"position": 3,
"link": {
"type": "generated-index",
"description": "Configuring Multi-Scrobbler and examples"
-767
View File
@@ -1,767 +0,0 @@
---
sidebar_position: 2
title: Overview
---
# Configuration
* [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)
* [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 (Linux Desktop)](#mpris)
* [Mopidy](#mopidy)
* [JRiver](#jriver)
* [Kodi](#kodi)
* [Client Configurations](#client-configurations)
* [Maloja](#maloja)
* [Last.fm](#lastfm)
* [Listenbrainz](#listenbrainz)
* [Monitoring](#monitoring)
* [Webhooks](#webhook-configurations)
* [Health Endpoint](#health-endpoint)
# 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%2Fdevelop%2Fsrc%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.
# 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.
### 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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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) (such as [Airsonic](https://airsonic.github.io/))
**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%2Fdevelop%2Fsrc%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
### 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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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%2Fdevelop%2Fsrc%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**
File diff suppressed because it is too large Load Diff
+9 -21
View File
@@ -21,9 +21,7 @@ Scenario:
### All-in-one Config
Using just one config file located at `CONFIG_DIR/config.json`:
```json5
```json5 title="CONFIG_DIR/config.json"
{
"sourceDefaults": {
"maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config
@@ -42,6 +40,8 @@ Using just one config file located at `CONFIG_DIR/config.json`:
"data": {
"clientId": "foxxSpotifyAppId",
"clientSecret": "foxxSpotifyAppSecret",
},
"options": {
"maxRequestRetries": 2, // override default max retries because spotify can...spotty
}
},
@@ -158,9 +158,7 @@ Using just one config file located at `CONFIG_DIR/config.json`:
### Separate JSON files
In `CONFIG_DIR/spotify.json`:
```json5
```json5 title="CONFIG_DIR/spotify.json"
[
{
// may omit 'type' property since app knows this is file is for spotify configs
@@ -191,9 +189,7 @@ In `CONFIG_DIR/spotify.json`:
]
```
In `CONFIG_DIR/plex.json`
```json5
```json5 title="CONFIG_DIR/plex.json"
[
{
"name": "fredPlex",
@@ -219,9 +215,7 @@ In `CONFIG_DIR/plex.json`
]
```
In `CONFIG_DIR/jellyfin.json`
```json5
```json5 title="CONFIG_DIR/jellyfin.json"
[
{
"name": "FredJelly",
@@ -232,9 +226,7 @@ In `CONFIG_DIR/jellyfin.json`
]
```
In `CONFIG_DIR/ytmusic.json`
```json5
```json5 title="CONFIG_DIR/ytmusic.json"
[
{
"type": "ytmusic",
@@ -248,9 +240,7 @@ In `CONFIG_DIR/ytmusic.json`
]
```
In `CONFIG_DIR/maloja.json`:
```json5
```json5 title="CONFIG_DIR/maloja.json"
[
{
"name": "foxxMaloja",
@@ -276,9 +266,7 @@ In `CONFIG_DIR/maloja.json`:
]
```
In `CONFIG_DIR/lastfm.json`:
```json5
```json5 title="CONFIG_DIR/lastfm.json"
[
{
"name": "maryLFM",
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+320
View File
@@ -0,0 +1,320 @@
---
sidebar_position: 4
title: Scrobble Modification
toc_max_heading_level: 4
---
Multi-scrobbler configs support the ability to modify scrobble data in an automated fashion by matching and replacing strings in **title, artists, and album** at many different times in multi-scrobbler's lifecycle.
### Why?
You may need to "clean up" data from a Source or before sending to a scrobble Client due to any number of reasons:
* ID3 tags in your music collection are dirty or have repeating garbage IE `[YourMusicSource.com] My Artist - My Title`
* A Source's service often incorrectly adds data to some field IE `My Artist - My Title (Album Version)` when the title should just be `My Title`
* An Artist you listen to often is spelled different between a Source and a Client which causes duplicate scrobbles
In any scenario where a repeating pattern can be found in the data it would be nice to be able to fix it before the data gets downstream or to help prevent duplicate scrobbling. Multi-scrobbler can help you do this.
## Overview
### Journey of a Scrobble
First, let's recap the lifecycle of a scrobble in multi-scrobbler:
**Sources** are the beginning of the journey for a **Play** (song you've listened to long enough to be scrobblable)
* A Source finds a new valid **Play**
* The Source **compares** this new Play to all the other Plays it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Source **discovers** the Play, adds it to Plays it has seen already, and broadcasts the Play should be scrobbled to all Clients
Scrobble **Clients** listen for discovered Plays from Sources, then...
* A Client receives a **Play** from a Source
* The Client **compares** this Play to all the other scrobbles it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
* The Client **scrobbles** the Play downstream to the scrobble service and adds it as a Scrobble it has seen already
### Lifecyle Hooks
You'll notice there is a pattern above that looks like this:
* **Before** data is compared
* Data is **compared**
* **After** data is compared
These points, during both Source and Client processes, are when you can hook into the scrobble lifecycle and modify it.
#### TLDR
In more concrete terms this is the structure of hooks within a configuration (can be used in any **Source** or **Client**):
```json5 title="lastfm.json" {10-14}
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": {/* ... */},
"compare": {/* ... */},
"postCompare": {/* ... */}
}
}
}
]
```
##### Hook
For **Sources**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if Play was already discovered
* `postCompare` - modify Play data before sending to scrobble **Clients**
For **Clients**:
* `preCompare` - modify Play data immediately when received
* `compare` - temporarily modify Play data when it is being compared to see if it was already scrobbled
* `postCompare` - modify Play data before scrobbling it to downstream service and adding to already seen scrobbles
:::tip
Keep in mind that modifying Scrobble/Play data earlier in the lifecycle will affect that data at all times later in the lifecycle.
For example, to modify the track so it's the same anywhere it is processed in multi-scrobbler you only need to modify it in the **Source's** `preCompare` hook because all later processes will receive the data with the modified track.
:::
### Modification Parts
Each [**hook**](#hook) (`preCompare` etc...) is an object that specifies what part of the **Play** to modify:
```json5
{
"title": [/* ... */],
"artists": [/* ... */],
"album": [/* ... */]
}
```
##### Expression
and then a **list** what pattern/replacements (expressions) to use for the modification by using either simple strings or `search-replace` objects:
```json5
[
"badTerm", // remove all instances of 'badTerm'
{
"search": "anotherBadTerm", // and also match all instances of 'anotherBadTerm'
"replace": "goodTerm" // replace with the string 'goodTerm'
}
]
```
Putting it all together:
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
"enable": true,
"configureAs": "source",
"data": {
// ...
},
"options": {
"playTransform": {
"preCompare": {
"title": [
[
"badTerm",
{
"search": "badTerm",
"replace": "goodTerm"
}
]
]
},
}
}
}
]
```
:::note
If the value of the field (title, an artist, album) is an empty string after transforming then the field is **removed.**
:::
:::tip
Modifications can also be applied to **all Sources** or **all Clients** when using the [AIO Config](./configuration.mdx?configType=aio#configuration-types) `config.json` by setting `playTransform` in `sourceDefaults` or `clientDefaults`:
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": { // will apply playTransform to all sources
"playTransform": {
"preCompare": {
"title": [
"(Album Version)"
]
}
}
},
"sources": [/* ... */],
"clients": [/* ... */]
}
```
</details>
:::
#### Compare Hook
The `compare` [hook](#hook) is slightly different than `preCompare` and `postCompare`. It consists of an object where you define which side(s) of the comparison should be modified. It also **does not modify downstream data!** Instead, the modifications are made only for use in the comparison.
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": {
"candidate": {/* ... */}, // modify the "new" Play being compared
"existing": {/* ... */}, // modify all "existing" Play/Scrobbles the new Play is being compared against
},
}
}
}
]
```
#### Regular Expressions
In addition to plain strings [expressions](#expression) that are matched and removed you can also use Regular Expressions. Write your regex like you normally would, but as a string, and it'll automatically be parsed:
```json5
[
"/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)' and removes it
{
"search": "/^\(\w+.com)/i", // matches any string that starts with '(YourMusic.com)'
"replace": "[MySite.com]" // replace with the string '[MySite.com]'
}
]
```
The `replace` property uses javascript's [`replace()` function and so can use any special string characters.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement)
## Examples
### Remove phrase from Title in all new Plays
Removes the phrase `(Album Version)` from the Title of a Play
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"title": [
"(Album Version)"
]
}
}
}
}
```
</details>
### Remove all parenthesized content from the end of a title
<details>
<summary>Example</summary>
```json5 title="lastfm.json"
[
{
"name": "myLastFm",
// ...
"options": {
"playTransform": {
"compare": {
"candidate": {
"title": [
"/(\(.+\))\s*$/"
]
},
"existing": {
"title": [
"/(\(.+\))\s*$/"
]
},
},
}
}
}
]
```
</details>
### Rename misspelled artist in all new Plays
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"artists": [
{
"search": "Boz Skaggs",
"replace": "Boz Scaggs"
}
]
}
}
}
}
```
</details>
### Remove "Various Artists" albums in all new Plays
<details>
<summary>Example</summary>
```json5 title="config.json"
{
"sourceDefaults": {
"playTransform": {
"preCompare": {
"album": [
{
"search": "Various Artists",
"replace": ""
}
]
}
}
}
}
```
</details>
+8
View File
@@ -0,0 +1,8 @@
{
"label": "Development",
"position": 4,
"link": {
"type": "generated-index",
"description": "Developing for Multi-Scrobbler and tutorials"
}
}
+8
View File
@@ -0,0 +1,8 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 3
title: Client Development/Tutorial
---
To do...
+168
View File
@@ -0,0 +1,168 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 1
title: Common Development
description: Start here for MS development
---
# Development
## Architecture
Multi-scrobbler is written entirely in [Typescript](https://www.typescriptlang.org/). It consists of a backend and frontend. The backend handles all Source/Client logic, mounts web server endpoints that listen for Auth callbacks and Source ingress using [expressjs](https://expressjs.com/), and serves the frontend. The frontend is a standalone [Vitejs](https://vitejs.dev/) app that communicates via API to the backend in order to render the dashboard.
## Project Setup
Development requires [Node v18.19.1](https://nodejs.org/en) or higher is installed on your system. It is recommended to use [nvm](https://github.com/nvm-sh/nvm) to manage the installed node version.
Clone this repository somewhere and then install from the working directory
```shell
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
git checkout --track origin/develop
nvm use # optional, to set correct Node version
npm install
npm run start
```
Use the [`develop`](https://github.com/FoxxMD/multi-scrobbler/tree/develop) branch as the target for any Pull Requests. The `master` branch is reserved for releases and minor documentation updates only.
## Common Development
:::info
In this document, when referring to aspects of Sources and Clients that are shared between both, the Source/Client will be referred to as a **Component.**
:::
A Component is composed of two parts:
* Typescript interfaces describing structure of configuration for that Component
* A concrete class inheriting from a common "startup" abstract class that enforces how the Component is built and operates
In both parts Source/Clients share some common properties/behavior before diverging in how they operate.
### Config
The configuration for a Component should always have this minimum shape, enforced respectively by the interfaces [CommonSourceConfig](https://github.com/FoxxMD/multi-scrobbler/blob/develop/src/backend/common/infrastructure/config/source/index.ts#L105) and [CommonClientConfig](https://github.com/FoxxMD/multi-scrobbler/blob/ce1c70a4e1e87fb5bea7cca960eaafbd15881a1f/src/backend/common/infrastructure/config/client/index.ts#L68):
```ts
interface MyConfig {
name: string
data?: object
options?: object
}
```
* `data` contains data that is required for a Component to operate such as credentials, callback urls, api keys, endpoints, etc...
* `options` are **optional** settings that can be used to fine-tune the usage of the Component but are not required or do not majorly affect behavior. EX additional logging toggles
### Concrete Class
Components inherit from an abstract base class, [`AbstractComponent`](https://github.com/FoxxMD/multi-scrobbler/blob/develop/src/backend/common/AbstractComponent.ts), that defines different "stages" of how a Component is built and initialized when MS first starts as well as when restarting the Component in the event it stops due to an error/network failure/etc...
#### Stages
Stages below are invoked in the order listed. All stages are asynchronous to allow fetching network requests or reading files.
The stage function (described in each stage below) should return a value or throw:
* return `null` if the stage is not required
* return `true` if the stage succeeded
* return a `string` if the stage succeeded and you wish to append a result to the log output for this stage
* throw an `Exception` if the stage failed for any reason and the Component should not continue to run/start up
##### Stage: Build Data
This stage should be used to validate user configuration, parse any additional data from async sources (file, network), and finalize the shape of any configuration/data needed for the Component to operate.
:::info
Implement [`doBuildInitData`](https://github.com/FoxxMD/multi-scrobbler/blob/develop/src/backend/common/AbstractComponent.ts#L71) in your child class to invoke this stage.
::::
<details>
<summary>Examples</summary>
* Parse a full URL like `http://SOME_IP:7000/subfolder/api` from user config containing a base url like `data.baseUrl: 'SOME_IP'` and then store this in the class config
* Validate that config `data` contains required properties `user` `password` `salt`
* Read stored credentials from `${this.configDir}/currentCreds-MySource-${name}.json`;
</details>
##### Stage: Check Connection
This stage is used to validate that MS can communicate with the service the Component is interacting with. This stage is invoked on MS startup as well as any time the Component tries to restart after a failure.
If the Component depends on **ingress** (like Jellyfin/Plex webhook) this stage is not necessary.
:::info
Implement [`doCheckConnection`](https://github.com/FoxxMD/multi-scrobbler/blob/develop/src/backend/common/AbstractComponent.ts#L103) in your child class to invoke this stage.
::::
<details>
<summary>Examples</summary>
* Make a [`request`](https://nodejs.org/docs/latest-v18.x/api/http.html#httprequesturl-options-callback) to the service's server to ensure it is accessible
* Open a websocket connection and check for a ping-pong
</details>
##### Stage: Test Auth
MS determines if Auth is required for a Component based on two class properties. You should set these properties during `constructor` initialization for your Component class:
* `requiresAuth` - (default `false`) Set to `true` if MS should check/test Auth for this Component
* `requiresAuthInteraction` - (default `false`) Set to `true` if user interaction is required to complete auth IE user needs to visit a callback URL
If the Component requires authentication in order to communicate with a service then any required data should be built in this stage and a request made to the service to ensure the authentication data is valid.
This stage should return:
* `true` if auth succeeded
* `false` if auth failed without unexpected errors
* IE the authentication data is not valid and requires user interaction to resolve the failure
* throw an exception if network failure or unexpected error occurred
You _should_ attempt to re-authenticate, if possible. Only throw an exception or return `false` if there is no way to recover from an authentication failure.
:::info
Implement [`doAuthentication`](https://github.com/FoxxMD/multi-scrobbler/blob/develop/src/backend/common/AbstractComponent.ts#L111) in your child class to invoke this stage.
::::
<details>
<summary>Examples</summary>
* Generate a Bearer Token for Basic Auth from user/password given in config and store in class properties
* Make a request to a known endpoint with Authorization token from read credentials file to see if succeeds or returns 403
* Catch a 403 and attempt to reauthenticate at an auth endpoint with user/password given in config
</details>
### Play Object
The **PlayObject** is the standard data structure MS uses to store listen (track) information and data required for scrobbling. It consists of:
* Track Data -- a standard format for storing track, artists, album, track duration, the date the track was played at, etc...
* Listen Metadata -- Optional but useful data related to the specific play or specifics about the Source/Client context for this play such as
* Platform specific ID, web URL to track, device/user ID that played this track, etc...
Both Sources and Clients use the **PlayObject** interface. When a Component receives track info from its corresponding service it must transform this data into a PlayObject before it can be interacted with.
For more refer to the TS documentation for `PlayObject` or [`AmbPlayObject`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/core/Atomic.ts#L141) in your project
## Creating Clients and Sources
* [Source Development and Tutorial](dev-source.md)
* [Client Development and Tutorial](dev-client.md)
+552
View File
@@ -0,0 +1,552 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 2
title: Source Development/Tutorial
---
This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review [Common Development](dev-common.md#common-development).
## Scenario
You are the developer of a fancy, new self-hosted web-based media player called **Cool Player.** Cool Player has a slick interface and many bells and whistles, but most importantly it has an API. The API:
* Has an unauthenticated health endpoint at `/api/health` that returns `200` if the service is running properly
* Has authenticated endpoints that require a user-generated token in the header `Authorization MY_TOKEN`
* Has a `/api/recent` endpoint that lists recently played tracks with a timestamp
* Has a `/api/now-playing` endpoint that returns information about the state of the player like current track, player position in the track, etc...
* Cool Player is by default accessed on port `6969`
* Your personal instance of Cool Player is hosted at `http://192.168.0.100:6969` and the api is accessed at `http://192.168.0.100:6969/api`
Because there is an API that MS can actively read this will be a **polling** Source where MS sends requests to Cool Player to get scrobble information -- as opposed to an **ingress** Source like Jellyfin/Plex that uses webhooks from the service to send data to MS.
## Minimal Implementation
### Define and Implement Config
We will create a new config interface for Cool Player using the [Common Config](dev-common.md#config) and tell MS it is a valid config that can be used.
Create a new file for your config:
```ts title="/src/backend/common/infrastructure/config/source/coolplayer.ts"
import { PollingOptions } from "../common.js";
import { CommonSourceConfig, CommonSourceData } from "./index.js";
// all of the required data for the Build Data and Test Auth stages (from Common Development docs)
// should go here
export interface CoolPlayerSourceData extends CommonSourceData, PollingOptions {
// remember to annotation your properties!
/**
* The user-generated token for Cool Player auth created in Cool Player -> Settings -> User -> Tokens
*
* @example f243331e-cf5b-49d7-846b-0845bdc965b4
* */
token: string
/**
* The host and port where Cool Player is hosted
*
* @example http://192.168.0.100:6969
* */
baseUrl: string
}
export interface CoolPlayerSourceConfig extends CommonSourceConfig {
data: CoolPlayerSourceData
}
export interface CoolPlayerSourceAIOConfig extends CoolPlayerSourceConfig {
// when using the all-in-one 'config.json' this is how users will identify this source
type: 'coolplayer'
}
```
Add the new interface to the list of valid interfaces for sources:
```ts title="src/backend/common/infrastructure/config/source/sources.ts"
import { ChromecastSourceAIOConfig, ChromecastSourceConfig } from "./chromecast.js";
// ...
// highlight-next-line
import { CoolPlayerSourceAIOConfig, CoolPlayerSourceConfig } from "./coolplayer.js";
export type SourceConfig =
SpotifySourceConfig
// ...
// highlight-next-line
| CoolPlayerSourceConfig;
export type SourceAIOConfig =
SpotifySourceAIOConfig
// ...
// highlight-next-line
| CoolPlayerSourceAIOConfig;
```
Finally, add the source type identifier to the list of valid identifiers
```ts title="src/backend/common/infrastructure/Atomic.ts"
export type SourceType =
'spotify'
// ...
// highlight-next-line
| 'coolplayer';
export const sourceTypes: SourceType[] = [
'spotify',
// ...
// highlight-next-line
'coolplayer'
];
```
Now we will create a new Source inheriting from [`AbstractComponent`](dev-common.md#concrete-class) that:
* accepts our config interface
* implements a function to transform CoolPlayer's track data into a [**PlayObject**](dev-common.md#play-object)
* implements required [stages](dev-common.md#stages)
* implements required methods to current player state and/or now playing track
### Create CoolPlayer Source
First we create a new Source called `CoolPlayerSource` and setup our constructor to accept the config and [specify Auth behavior.](dev-common.md#stage-test-auth)
```ts title="src/backend/sources/SpotifySource.ts"
import { CoolPlayerSourceConfig } from "../common/infrastructure/config/source/coolplayer.js";
import MemorySource from "./MemorySource.js";
import {
InternalConfig,
} from "../common/infrastructure/Atomic.js";
// MemorySource is the base class used for polling-type Sources
export default class CoolPlayerSource extends MemorySource {
// type hints for TS to know what the base class config looks like
declare config: CoolPlayerSourceConfig;
constructor(name: any, config: CoolPlayerSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
super('coolplayer', name, config, internal, emitter);
// Cool Player required authentication
this.requiresAuth = true;
// but does not require user interaction for auth to work
this.requiresAuthInteraction = false;
// tells MS this is a Source that can be activity monitored through API
this.canPoll = true;
}
}
```
### Initialize Source from Config
When MS starts it reads all configs and determines which Source to build based on the configs found. We need to tell it to build a `CoolPlayerSource` when a `coolplayer` config type is found.
We modify `ScrobbleSources.ts` to add `CoolPlayerSource` as an option:
```ts title="src/backend/sources/ScrobbleSources.ts"
// ...
import { CoolPlayerSource, CoolPlayerData } from "./CoolPlayerSource.js";
export default class ScrobbleSources {
// ...
buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = []) => {
// ...
// if CoolPlayerSource should be able to be built from ENVs only
// then add it as a case statement here
for (const sourceType of sourceTypes) {
switch (sourceType) {
// ...
case 'musikcube':
// ...
break;
// highlight-start
case 'coolplayer':
const cp = {
baseUrl: process.env.COOL_URL,
token: process.env.COOL_TOKEN
}
if (!Object.values(cp).every(x => x === undefined)) {
configs.push({
type: 'coolplayer',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
data: cp as CoolPlayerData
});
}
break;
// highlight-end
default:
break;
}
}
}
// ...
// (required) create new CoolPlayerSource if source config type is 'coolplayer'
addSource = async (clientConfig: ParsedConfig, defaults: SourceDefaults = {}) => {
// ...
let newSource: AbstractSource;
switch (type) {
// ...
case 'musikcube':
// ...
break;
// highlight-start
case 'coolplayer':
newSource = await new CoolPlayerSource(name, compositeConfig as CoolPlayerSourceConfig, internal, this.emitter);
break;
// highlight-end
default:
break;
}
}
}
```
### Implement Play Object Transform
Now we will create a static function that is used to take the track data returned from Cool Player's API and return a standard [`PlayObject`.](dev-common.md#play-object)
```ts title="src/backend/sources/CoolPlayerSource.ts"
import dayjs from "dayjs";
import {
FormatPlayObjectOptions,
} from "../common/infrastructure/Atomic.js";
import { PlayObject } from "../../core/Atomic.js";
export default class CoolPlayerSource extends MemorySource {
// ...
// 'obj' should ideally be a real TS interface
// if CoolPlayer has a ts/js client we would use that otherwise
// we should build our own interfaces to represent track data from Cool Player's API
static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {
const {
trackName,
artistName,
albumName,
duration,
playedAt,
} = obj;
return {
data: {
artists: [artistName],
album: albumName,
track: trackName,
// assuming seconds
duration,
// assuming playedAt is an ISO8601 timestamp
playDate: dayjs(playedAt)
},
meta: {
source: 'CoolPlayer'
}
}
}
}
```
### Implement Stages
Next we will implement the [Stages](dev-common.md#stages) required to get CoolPlayerSource running.
#### Build Data
First we implement the [Build Data Stage](dev-common.md#stage-build-data). We will check that the `token` and `baseUrl` properties have been provided by the user. Additionally, we will parse the baseUrl and add default ports/prefix.
```ts title="src/backend/sources/CoolPlayerSource.ts"
import { URL } from "url";
// ...
export default class CoolPlayerSource extends MemorySource {
baseUrl!: URL;
// ...
static parseConnectionUrl(val: string) {
const normal = normalizeUrl(val, {removeTrailingSlash: false, normalizeProtocol: true})
const url = new URL(normal);
if (url.port === null || url.port === '') {
url.port = '6969';
}
if (url.pathname === '/') {
url.pathname = '/api/';
}
return url;
}
protected async doBuildInitData(): Promise<true | string | undefined> {
const {
token,
baseUrl
} = this.config;
if (token === null || token === undefined || (typeof token === 'string' && token.trim() === '')) {
throw new Error(`'token' must be defined`);
}
if (baseUrl === null || baseUrl === undefined || (typeof baseUrl === 'string' && baseUrl.trim() === '')) {
throw new Error(`'baseUrl' must be defined`);
}
try {
this.baseUrl = CoolPlayerSource.parseConnectionUrl(baseUrl);
} catch (e) {
throw new Error(`Could not parse baseUrl: ${baseUrl}`, {cause: e});
}
this.logger.verbose(`Config URL: ${baseUrl} => Normalized: '${this.url.toString()}'`);
return true;
}
}
```
#### Check Connection
Second we will implement the [Check Connection Stage](dev-common.md#stage-check-connection):
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import { UpstreamError } from "../common/errors/UpstreamError.js";
// ...
export default class CoolPlayerSource extends MemorySource {
// ...
protected async doCheckConnection(): Promise<true | string | undefined> {
try {
const resp = await request.get(`${this.baseUrl}/health`);
return true;
// if /health returned version info we could instead read response and return a string with version info for visibility to the user
// return `Cool Player Version: ${resp.body.version}`;
} catch (e) {
throw e;
}
}
}
```
#### Test Auth
Finally, we will implement [Auth Test Stage](dev-common.md#stage-test-auth):
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import { UpstreamError } from "../common/errors/UpstreamError.js";
// ...
export default class CoolPlayerSource extends MemorySource {
// ...
doAuthentication = async () => {
try {
const resp = await request
.get(`${this.baseUrl}/recent`)
.set('Authorization', `Token ${this.config.token}`);
return true;
} catch (e) {
// if Cool Player returned an error as json we could parse it from error body and throw new Error with the message
throw e;
}
}
}
```
### Implement Polling
The majority of Sources MS monitors primarily operate as a source of truth for a **music player** rather than a **played music history.** Only Listenbrainz and Last.fm operate as a source of music history.
To this end, MS implements a [state machine](https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/) that emulates the behavior of a music player in order to keep track of when a song you are listening to should be scrobbled. It does this by monitoring the "currently playing" track reported by a Source's service, with varying degrees of accuracy depending on what information is returned from the service. The state machine is implemented in `MemorySource` which our `CoolPlayerSource` inherits from.
For a polling Source to work properly we need to implement a function, [`getRecentlyPlayed`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L92), that returns PlayObjects that are "newly" played. These are then checked against previously "discovered" plays and their timestamp to determine if they should be surfaced to Clients to scrobble.
To take advantage of the `MemorySource` state machine we will additionally use [`processRecentPlays`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/MemorySource.ts#L113) from `MemorySource` inside `getRecentlyPlayed`. We pass track and/or player state returned from the Source service to `processRecentPlayers`. It then takes care of deriving Source player state based on how this data changes over time. The advantage to using `processRecentPlays` is that our Source service does not necessarily need to pass any player information -- as long as the track info has a **duration** we can more-or-less determine if it has been played long enough to scrobble.
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import {
SourceData,
PlayerStateData,
SINGLE_USER_PLATFORM_ID
} from "../common/infrastructure/Atomic.js";
// ...
export default class CoolPlayerSource extends MemorySource {
// ...
protected async getRecentlyPlayed(options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> {
const plays: SourceData[] = [];
try {
// currently playing tracks/player state data
const resp = await request
.get(`${this.baseUrl}/now-playing`)
.set('Authorization', `Token ${this.config.token}`);
const {
body: {
playerState, // 'playing' or 'stopped' or 'paused'...
position, // number of seconds into the track IE at position 48 -> ( 0:48/3:56 in player UI )
play: { /* track data */}
}
} = resp;
// transform into standard player state data
const playerData: PlayerStateData = {
platformId: SINGLE_USER_PLATFORM_ID,
play: CoolPlayerSource.formatPlayObj(play),
position
};
// if Cool Player does return player state we can also push a regular PlayObject to this list
plays.push(playerData);
} catch (e) {
throw e;
}
// process player state through state machine
// if the track changes or player state changes
// and currently played track has been listened to long enough to be scrobbled it will return in newPlays
const newPlays = this.processRecentPlays(plays);
// finally, we return new plays and MS checks to see if they have been previously seen
// before signalling to Clients that they can be scrobbled
return newPlays;
}
}
```
Congratulations! Your `CoolPlayerSource` has been minimally implemented and can now be used in multi-scrobbler.
## Further Implementation
### Backlog
To have your Source try to scrobble "missed" tracks when MS starts up the Source's service must be able to provide:
* track information
* timestamp of when the track was played
In your Source implement [`getBackloggedPlays`](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/AbstractSource.ts#L235) and set setting in constructor indicating it has backlogging capabilities:
```ts title="src/backend/sources/CoolPlayerSource.ts"
import request from 'superagent';
import {
PlayObject,
} from "../common/infrastructure/Atomic.js";
// ...
export default class CoolPlayerSource extends MemorySource {
constructor(/* ... */) {
super(/* ... */);
// ...
// tell MS it should try to get backlogged tracks on startup
this.canBacklog = true;
}
// ...
protected getBackloggedPlays = async (options: RecentlyPlayedOptions): Promise<PlayObject[]> => {
try {
const resp = await request
.get(`${this.baseUrl}/recent`)
.set('Authorization', `Token ${this.config.token}`);
// assuming list from body looks like track info returned in
// "Implement Play Object Transform" section
const {
body = []
} = resp;
return body.map(x => CoolPlayerSource.formatPlayObj(x));
} catch (e) {
throw new Error('Error occurred while getting recently played', {cause: e});
}
}
}
```
### Other Source Types
There are some scenarios where polling and/or state machine is not the right tool to handle determining if incoming data should be scrobbled:
* The Source service handles scrobble threshold internally, the data being received should always be scrobbled (WebScrobbler, Plex, Tautulli, Listenbrainz, Last.fm)
* You prefer to handle the scrobble determination yourself
#### Music History Source
If the Source is still polling but the track returned should always be scrobbled if not already seen IE the Source service is a **music history source** (Listenbrainz, Last.fm), rather than a music player, then simply indicate to MS the source of truth type by setting it in the constructor. The state machine will always return a track if it is new and not seen, regardless of how recently it was seen:
```ts title="src/backend/sources/CoolPlayerSource.ts"
import { SOURCE_SOT } from "../../core/Atomic.js";
// ...
export default class CoolPlayerSource extends MemorySource {
constructor(/* ... */) {
super(/* ... */);
// ...
// tell MS it should immediately scrobble any new, unseen tracks from the upstream service
this.playerSourceOfTruth = SOURCE_SOT.HISTORY;
}
}
```
#### Non-Polling Source
**Ingress** Sources (like Plex, Tautulli, Webscrobbler, Jellyfin) do not having a polling mechanism because the upstream service contacts MS when there is an event, rather than MS contacting the upstream service.
For these Sources you will need to implement endpoints in `src/service/api.ts` and corresponding files. See the existing Sources in the project as references for how to do this.
You may still wish to use the state machine `MemorySource` (like Jellyfin) if the events received are not "scrobble" events but instead of implementing `getRecentlyPlayed` you will implement your own function in your Source class, like `handle()`, that receives data and then uses `processRecentPlays`.
After new plays have been determined see the next section for how to scrobble...
#### Basic Source
At the core of a Source that implements `AbstractSource`'s functionality is the ability to **Discover** and **Scrobble** plays.
These functions are not seen in the MVP `CoolPlayerSource` because they are automatically done by the polling functionality after being returned from `getRecentlyPlayed`.
##### Discovery
A Source keeps track of all the "plays" that are determined to be valid for scrobbling. When a play is valid it is checked to see if it has already been "discovered" by comparing the track info and timestamp of the play against already discovered plays. This prevents duplicate scrobbling by using the Source's own data and simplifies scrobbling for Sources by allowing your implementation to "always" ingest track data without having to worry about whether its new or not -- `AbstractSource` and `discover()` will take care of that for you.
```ts title="src/backend/sources/MyBasicSource.ts"
export default class MyBasicSource extends AbstractSource {
handle(somePlay: PlayObject) {
// if the track is "new" and not seen before it is returned in the discovered list
// we then know it is OK to be sent to Clients for scrobbling
const discovered: PlayObject[] = this.discover([somePlay]);
}
}
```
This additionally will be surfaced to the user in the Dashboard in the "Tracks Discovered" page.
##### Scrobbling
After a play is verified to be discovered we can then scrobble it. This will emit the plays to the ScrobbleClients service which then disseminates the play to all Clients that were configured to listen in the Source's config.
```ts title="src/backend/sources/MyBasicSource.ts"
export default class MyBasicSourceSource extends AbstractSource {
handle(somePlay: PlayObject) {
const discovered: PlayObject[] = this.discover([somePlay]);
// emit plays that can be scrobbled by clients
this.scrobble(discovered);
}
}
```
If your service only emits an event when a play is scrobbled you can _technically_ skip using `discover()` but it is good practice to use it unless you have a very good reason not to.
:::note
Using `scrobble()` does not guarantee a track is actually scrobbled! The Scrobble Clients also check the play against their own "recently scrobbled" list to prevent duplicates.
:::
+104
View File
@@ -0,0 +1,104 @@
---
toc_min_heading_level: 2
toc_max_heading_level: 5
sidebar_position: 4
title: Flatpak
description: Building Flatpak App locally
---
:::note
These steps are for building the flatpak from source. If you want to install the application normally then [get it through flathub](../installation/installation.mdx#flatpak)
:::
The final build repo for the flathub version can be found at [flathub/io.github.foxxmd.multiscrobbler](https://github.com/flathub/io.github.foxxmd.multiscrobbler)
## Install Requirements
### Flatpak and flatpak-builder
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/)
## Update Project source
Set the `branch` `tag` or `commit` to use for MS in the `source` section of [`io.github.foxxmd.multiscrobbler.yml`](https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/io.github.foxxmd.multiscrobbler.yml)
## Generate Sources and Build
### Use Setup Script
A convenience bash script is provided that automates generating offline sources and building the flatpak app for you. This is the recommend method to use.
Located in the project at [`flatpak/setup.sh`](https://github.com/FoxxMD/multi-scrobbler/blob/master/flatpak/setup.sh), run it from the `flatpak` directory with this syntax:
```shell
./setup.sh -o -b /path/to/flatpak/build/dir
```
```
Args:
-o => Delete and overwrite any existing generated sources
-b => The absolute path to the directory that should be used for flatpak build/artifacts. If not defined will use `CWD/build`
-y => Proceed without confirming settings
```
### Manual Setup
If you cannot use `setup.sh` follow the below to manually generate sources and build the flatpak app:
<details>
<summary>Instructions</summary>
#### 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) for both the project and `docsite` directories.
Then, from MS project root:
```shell title="PROJECT_DIR"
flatpak-node-generator npm package-lock.json
```
Move `generated-sources.json` into `PROJECT_DIR/flatpak` and rename `generated-sources.0.json`
Then, generate `docsite` sources:
```shell title="PROJECT_DIR"
flatpak-node-generator npm docsite/package-lock.json
```
Move `generated-sources.json` into `PROJECT_DIR/flatpak` and rename `generated-sources.1.json`
#### Build flatpak
In the below command replace `/home/yourUser/multi-scrobbler-flatpak` with a directory to use for storing flatpak build/artifacts.
```shell title="PROJECT_DIR/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
```
:::info
Append `--install --user` to the above command to have the app installed immediately.
::::
</details>
# Run App
If built with `--install --user` (default when using `setup.sh`) you can now run MS using the command
```shell
flatpak run -u io.github.foxxmd.multiscrobbler
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Installation",
"position": 1,
"position": 2,
"link": {
"type": "generated-index",
"description": "Way to install Multi-Scrobbler"
-123
View File
@@ -1,123 +0,0 @@
---
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 build
npm start
```
### Usage Examples
* The web UI 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 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
```
### 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.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:
* If you must **bind a host directory into the container for storing configurations and credentials:**
* [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`
* (Optionally) map the web UI port in the container **9078** to the host
* 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"`
* (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
## Docker Usage Examples
### 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.
+344
View File
@@ -0,0 +1,344 @@
---
sidebar_position: 1
title: 'Overview'
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::tip
For the difference between **ENV** and **File** examples in this document see [Configuration Types](../configuration/configuration.mdx#configuration-types).
:::
## Local Installation
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
### Nodejs
Clone this repository somewhere and then install from the working directory
```shell
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
nvm use # optional, to set correct Node version
npm install
npm run docs:install && npm run build
npm run start
```
#### Rollup build error
During building if you encounter an error like: `Your current platform "XXX" and architecture "XXX" combination is not yet supported by the native Rollup build.`
Modify `overrides` in `package.json` to use `@rollup/wasm-node` as a drop-in replacement for rollup:
```json
"overrides": {
"spotify-web-api-node": {
"superagent": "$superagent"
}
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.9.6"
}
}
```
See [this issue](https://github.com/FoxxMD/multi-scrobbler/issues/135#issuecomment-1927080260) for more detail.
#### Usage Examples
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
```shell
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret MALOJA_URL="http://domain.tld" node src/index.js
```
</TabItem>
<TabItem value="file" label="File">
<details>
<summary>`./config/config.json`</summary>
```json title="./config/config.json"
{
"sources": [
{
"type": "spotify",
"clients": ["myConfig"],
"name": "mySpotifySource",
"data": {
"clientId": "a89cba1569901a0671d5a9875fed4be1",
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
}
}
],
"clients": [
{
"type": "maloja",
"name": "myConfig",
"data": {
"url": "http://localhost:42010",
"apiKey": "myMalojaKey"
}
}
],
}
```
</details>
```shell
npm run start
```
</TabItem>
</Tabs>
:::tip
The web UI and API is served on port `9078`. This can be modified using the `PORT` environmental variable.
:::
### Flatpak
You must have [Flatpak](https://flatpak.org/) installed on your system.
```shell
flatpak install flathub io.github.foxxmd.multiscrobbler
```
:::warning
Flatpak users have experienced issues when using multi-scrobbler as a long-running process. Due to the relative difficulty in debugging issues with flatpak installations it is recommended:
* to use a [Docker](#docker) installation if possible or
* only if you need access to host-level resources like dbus for [MPRIS](https://foxxmd.github.io/multi-scrobbler/docs/configuration#mpris) and cannot run a [nodejs](#nodejs) installation
::::
#### Usage Examples
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
There are a few [options for running flatpak applications with temporary or permanent environmental variables.](https://ardasevinc.dev/launch-flatpak-apps-with-custom-args-and-environment-variables)
```shell
flatpak run --env=SPOTIFY_CLIENT_ID=yourId --envSPOTIFY_CLIENT_SECRET=yourSecret --env=MALOJA_URL="http://domain.tld" io.github.foxxmd.multiscrobbler
```
</TabItem>
<TabItem value="file" label="File">
The config directory for multi-scrobbler as a flatpak can be found under `/home/YourUser/.var/app/io.github.foxxmd.multiscrobbler/config`
```shell
flatpak run io.github.foxxmd.multiscrobbler
```
</TabItem>
</Tabs>
## Docker
Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
:::info[Available Images]
<Tabs>
<TabItem value="docker" label="Dockerhub">
[Repository Page](https://hub.docker.com/r/foxxmd/multi-scrobbler)
```
docker.io/foxxmd/multi-scrobbler:latest
```
</TabItem>
<TabItem value="ghcr" label="Github Packages">
[Repository Page](https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler)
```
ghcr.io/foxxmd/multi-scrobbler:latest
```
</TabItem>
</Tabs>
:::
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 **should** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
[Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount):
```shell
docker run ... -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
[Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
volumes:
- "./config:/config"
```
</TabItem>
</Tabs>
</details>
#### 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`.
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
[Docker `run` publish options](https://docs.docker.com/engine/reference/commandline/run/#publish):
```shell
docker run ... -p 9078:9078 foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
ports:
- "9078:9078"
```
</TabItem>
</Tabs>
</details>
##### Base URL
Optionally, when
* using a [Source or Client](../configuration/configuration.mdx) 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.mdx#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
<details>
<summary>Example</summary>
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
```shell
docker run ... -e BASE_URL="http://hostMachineIP" foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
```yaml title="docker-compose.yml"
services:
multi-scrobbler:
# ...
environment:
- BASE_URL="http://hostMachineIP"
```
</TabItem>
</Tabs>
</details>
#### 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
::::info
:::note
If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) or are running docker on MacOS/Windows this **DOES NOT** apply to you.
:::
If you are running Docker on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
To get the UID and GID for the current user run these commands from a terminal:
* `id -u` -- prints UID
* `id -g` -- prints GID
::::
### Docker Usage Example
:::tip
See the [**Quick Start Guide**](../quickstart.mdx) for another guided docker-compose example
:::
The example scenario:
* [Spotify **Source**](../configuration/configuration.mdx#spotify)
* [Maloja **Client**](../configuration/configuration.mdx#maloja)
* Serving app on port `9078`
* Docker container located on a different IP (`192.168.0.100`) so use [Base URL](../configuration/configuration.mdx#base-url)
* Config/data directory on host machine in a directory next to `docker-compose.yml`
* Linux uid/gid is `1000:1000`
<Tabs groupId="runType" queryString>
<TabItem value="docker" label="Docker">
<Tabs groupId="configType" queryString>
<TabItem value="env" label="ENV">
```bash
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -e "BASE_URL=192.168.0.100" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /home/myUser/ms:/config foxxmd/multi-scrobbler
```
</TabItem>
<TabItem value="file" label="File">
```bash
docker run -e "PUID=1000" -e "PGID=1000" -e "BASE_URL=192.168.0.100" -p 9078:9078 -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
```
</TabItem>
</Tabs>
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
See [`docker-compose.yml`](../../../docker-compose.yml) file for more options and annotations.
```yaml title="docker-compose.yml"
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=yourId
- SPOTIFY_CLIENT_SECRET=yourSecret
- BASE_URL="http://192.168.0.100:9078"
- MALOJA_URL=http://domain.tld:42010
- MALOJA_API_KEY=1234
- PUID=1000
- PGID=1000
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
</TabItem>
</Tabs>
+5 -5
View File
@@ -3,7 +3,7 @@ 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.
If you have multi-scrobbler installed [locally](installation.mdx#nodejs) 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!
@@ -17,7 +17,7 @@ This setup will create a [user service](https://wiki.archlinux.org/title/systemd
Create a new service file for multi-scrobbler under your HOME config:
```console
```bash
mkdir -p ~/.config/systemd/user
touch ~/.config/systemd/user/multi-scrobbler.service
```
@@ -38,11 +38,11 @@ Restart=no
WantedBy=default.target
```
The above assumes you [installed multi-scrobbler using flatpak](installation.md#flatpak)
The above assumes you [installed multi-scrobbler using flatpak](installation.mdx#flatpak)
### Node.js Installs
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](installation.md#nodejs) you should modify the `[Service]`:
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](installation.mdx#nodejs) you should modify the `[Service]`:
```ini
[Service]
@@ -56,7 +56,7 @@ Restart=no
Save the file then run:
```console
```bash
systemctl daemon-reload
systemctl --user enable multi-scrobbler.service
systemctl --user start multi-scrobbler.service
+165
View File
@@ -0,0 +1,165 @@
---
title: 'Quickstart'
sidebar_position: 1
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
This guide will get you up and running with multi-scrobbler using [Docker](https://www.docker.com/) and [`docker compose`](https://docs.docker.com/compose/). At the end of the guide you will have:
* the dashboard served on port `9078` of a host machine which has an IP of `192.168.0.100`
* data saved to the same directory as the `docker-compose.yml` file
* multi-scrobbler monitoring [Spotify](./configuration/configuration.mdx#spotify) and/or [Jellyfin](./configuration/configuration.mdx#jellyfin) for listening activity
* multi-scrobbler scrobbling to [Lastfm](./configuration/configuration.mdx#lastfm) and/or [Maloja](./configuration/configuration.mdx#maloja)
:::note
If the multi-scrobbler container is on the same machine you will be viewing the dashboard from (IE `localhost`) you can remove and ignore `BASE_URL` usage below. Additionally, replace usage of `192.168.0.100` with `localhost`.
:::
## Create Docker Compose File
Create a new folder for multi-scrobbler related data and then create `docker-compose.yml` with this content:
```yaml title="~/msData/docker-compose.yml"
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
- BASE_URL="http://192.168.0.100:9078"
# all Environmental Variables in below examples go here!
volumes:
- "./config:/config"
ports:
- "9078:9078"
restart: unless-stopped
```
## Setup Sources
**Sources** are the services multi-scrobbler monitors to look for listening activity.
<Tabs>
<TabItem value="jellyfin" label="Jellyfin">
Follow the Jellyfin configuration [instructions for setting up a **webhook**.](./configuration/configuration.mdx#jellyfin)
After webhook is setup add **at least one** of these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- JELLYFIN_USER=myUserName # comma-separated list of users to monitor
- JELLYFIN_SERVER=myServerName # comma-separated list of servers that should be monitored
```
</TabItem>
<TabItem value="spotify" label="Spotify">
To access your Spotify activity you must [register a Spotify application](https://developer.spotify.com/dashboard) to get a
**Client ID/Secret**.
When creating the application add this to **Redirect URIs**
```
http://192.168.0.100:9078/callback
```
After the application is created add these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- SPOTIFY_CLIENT_ID=yourClientId
- SPOTIFY_CLIENT_SECRET=yourClientSecret
```
Later, after [starting multi-scrobbler](#start-multi-scrobbler), visit the dashboard at `http://192.168.0.100:9078` and click **(Re)authenticate** on the Spotify card to authorize multi-scrobbler to use your account. Monitoring will begin automatically after authorization is complete.
</TabItem>
</Tabs>
## Setup Clients
**Clients** are services that store scrobbles. Multi-scrobbler will scrobble all listening activity from the **Sources** you configured to all **Clients** you configure here.
<Tabs>
<TabItem value="maloja" label="Maloja">
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
<details>
<summary>Maloja Setup Intructions</summary>
Using Maloja's example `docker-compose.yml`:
```yaml reference title="~/malojaData/docker-compose.yml"
https://github.com/krateng/maloja/blob/master/example-compose.yml
```
Uncomment `environment` and add `MALOJA_FORCE_PASSWORD=CHANGE_ME` to set an admin password
Start the container:
```shell title="~/malojaData"
docker compose up -d
```
</details>
* Navigate to the Admin Panel (Cog in upper-right corner) -> API Keys (or at http://192.168.0.100:42010/admin_apikeys)
* Create a **New Key** and then copy the generated key value
Finally, add these values to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- MALOJA_URL="http://192.168.0.100:42010"
- MALOJA_API_KEY=myApiKey
```
</TabItem>
<TabItem value="lastfm" label="Last.fm">
[Register for an API account at Last.fm.](https://www.last.fm/api/account/create)
Use the following for **Callback URL**:
```
http://192.168.0.100:9078/lastfm/callback
```
After account creation use the displayed information and add these values to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
```yaml title="~/msData/docker-compose.yml"
- LASTFM_API_KEY=myApiKey
- LASTFM_SECRET=myApiSecret
```
</TabItem>
</Tabs>
## Start Multi-Scrobbler
:::tip
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](./installation/installation.mdx#linux-host)
:::
From the same directory as the [`docker-compose.yml` you created earlier](#create-docker-compose-file) start the container:
```shell title="~/msData"
docker compose up -d
```
You're done! Multi-scrobbler is now running. It will monitor the sources you configured and scrobble to clients you set up.
Visit `http://192.168.0.100:9078` to see the dashboard where
* configured Sources/Clients
* show current status and authentication options
* display statistics about discovered/scrobbled tracks and Now Playing status
* a real-time log shows multi-scrobbler's activity
## Next Steps
* See more advanced docker options as well as other install methods in the [**Installation**](./installation/installation.mdx#docker) docs
* Review the [**Configuration**](./configuration/configuration.mdx) docs
* Learn about how to configure multi-scrobbler using files for more complicated Source/Client scenarios
* See all available Sources/Clients alongside configuration examples
* Learn how to set up [notification webhooks](./configuration/configuration.mdx#webhook-configurations)
* Check out the [kitchensink example](./configuration/kitchensink.md)
* Consult the [**FAQ**](./FAQ.md) for solutions to common problems
@@ -1,20 +1,18 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
import type * as Preset from '@docusaurus/preset-classic';
import type { Config } from '@docusaurus/types';
import * as themes from 'prism-react-renderer';
//import sidebars from './sidebars';
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'My Site',
tagline: 'Dinosaurs are cool',
const config: 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',
baseUrl: process.env.DOCS_BASE !== undefined && process.env.DOCS_BASE !== '' ? process.env.DOCS_BASE : '/docs',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
@@ -23,7 +21,7 @@ const config = {
trailingSlash: false,
onBrokenLinks: 'throw',
onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internalization, you can use this field to set useful
@@ -33,14 +31,14 @@ const config = {
defaultLocale: 'en',
locales: ['en'],
},
scripts: [
],
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
{
docs: {
sidebarPath: require.resolve('./sidebars.js'),
sidebarPath: './sidebars.ts',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
@@ -56,23 +54,33 @@ const config = {
blog: false,
theme: {
customCss: require.resolve('./src/css/custom.css'),
customCss: './src/css/custom.css',
},
}),
} satisfies Preset.Options,
],
],
themes: ["docusaurus-json-schema-plugin"],
plugins: [
themes: [
"docusaurus-json-schema-plugin",
[
require.resolve("@cmfcmf/docusaurus-search-local"),
"@easyops-cn/docusaurus-search-local",
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
{
// ... Your options.
// `hashed` is recommended as long-term-cache of index file is possible.
hashed: true,
indexBlog: false,
// For Docs using Chinese, The `language` is recommended to set to:
// ```
// language: ["en", "zh"],
// ```
},
],
'docusaurus-theme-github-codeblock'
],
plugins: [
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
{
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
navbar: {
@@ -98,6 +106,11 @@ const config = {
label: 'GitHub',
position: 'right',
},
{
href: 'https://foxxmd.github.io/multi-scrobbler/',
label: 'Website',
position: 'right',
},
],
},
footer: {
@@ -106,33 +119,24 @@ const config = {
{
title: 'Docs',
items: [
{
label: 'Overview',
to: '/',
},
{
label: 'Installation',
to: '/docs/installation',
to: 'docs/installation',
},
{
label: 'Configuration',
to: '/docs/configuration',
to: 'docs/configuration',
},
{
label: 'Development',
to: 'docs/development/dev-common',
},
],
},
/* {
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: [
@@ -140,16 +144,42 @@ const config = {
label: 'GitHub',
href: 'https://github.com/foxxmd/multi-scrobbler',
},
{
label: 'Website',
href: 'https://foxxmd.github.io/multi-scrobbler/',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Multi-Scrobbler. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
theme: themes.themes.github,
darkTheme: themes.themes.dracula,
additionalLanguages: ['json','json5','typescript', 'docker', 'bash', 'ini']
},
}),
colorMode: {
defaultMode: 'dark',
disableSwitch: false,
respectPrefersColorScheme: false,
},
codeblock: {
showGithubLink: true,
githubLinkLabel: 'View on GitHub',
showRunmeLink: false,
runmeLinkLabel: 'Checkout via Runme'
}
} satisfies Preset.ThemeConfig,
};
module.exports = config;
if (process.env.ANALYTICS !== undefined && process.env.ANALYTICS !== '') {
const script = {
src: process.env.ANALYTICS,
}
if (process.env.ANALYTICS_DOMAIN !== undefined && process.env.ANALYTICS_DOMAIN !== '') {
script['data-domain'] = process.env.ANALYTICS_DOMAIN;
}
config.scripts.push(script)
}
export default config;
+6283 -4056
View File
File diff suppressed because it is too large Load Diff
+29 -25
View File
@@ -3,32 +3,36 @@
"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",
"start": "npx docusaurus start",
"build": "npx docusaurus build",
"swizzle": "npx docusaurus swizzle",
"deploy": "npx docusaurus deploy",
"clear": "npx docusaurus clear",
"serve": "npx docusaurus serve",
"write-translations": "npx docusaurus write-translations",
"write-heading-ids": "npx 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"
"@docusaurus/core": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
"@easyops-cn/docusaurus-search-local": "^0.44.3",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-json-schema-plugin": "^1.12.1",
"docusaurus-theme-github-codeblock": "^2.0.2",
"micromark-extension-directive": "^3.0.1",
"prism-react-renderer": "^2.3.0",
"raw-loader": "^4.0.2",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
"@docusaurus/module-type-aliases": "^3.4.0",
"@docusaurus/tsconfig": "^3.4.0",
"@docusaurus/types": "^3.4.0",
"@types/react": "^18.2.29",
"typescript": "~5.2.2"
},
"browserslist": {
"production": [
@@ -37,12 +41,12 @@
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=16.14"
"node": ">=18.0"
}
}
+4 -5
View File
@@ -1,3 +1,5 @@
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
@@ -9,10 +11,7 @@
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
const sidebars: SidebarsConfig = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [
{type: 'autogenerated', dirName: '.'},
@@ -37,4 +36,4 @@ const sidebars = {
*/
};
module.exports = sidebars;
export default sidebars;
+50
View File
@@ -0,0 +1,50 @@
import React, { Fragment } from "react"
import CodeBlock from '@theme/CodeBlock';
import Admonition from '@theme/Admonition';
import ErrorBoundary from "@docusaurus/ErrorBoundary"
import Error from "@theme/Error"
import { Simulate } from "react-dom/test-utils";
import error = Simulate.error;
export interface AIOProps {
data: string
client?: boolean
name: string
}
const AIOExample = (props: AIOProps) => {
const {
data,
name,
client = false
} = props;
let configObj;
// eslint-disable-next-line prefer-const
try {
configObj = JSON.parse(data);
} catch (e) {
console.error(e);
return <Admonition type="danger" title="Unexpected Error">
<p>Example component crashed because of error!</p>
<CodeBlock>{e.message}</CodeBlock>
</Admonition>
}
configObj[0].type = name;
const configType = client ? 'clients' : 'sources';
const aio = {[configType]: configObj};
return <CodeBlock title="CONFIG_DIR/config.json" language="json5">{JSON.stringify(aio, null, 2)}</CodeBlock>
}
const WrappedAIOExample = (props: AIOProps) => {
return <ErrorBoundary
fallback={({error}) => (
<div>
<p>Example component crashed because of error: {error.message}.</p>
</div>
)}
><AIOExample {...props} /></ErrorBoundary>
}
export default WrappedAIOExample;
+27
View File
@@ -0,0 +1,27 @@
import React, {PropsWithChildren, Fragment} from "react"
export interface SchemaLinkProps {
objectName: string
lower?: boolean
client?: boolean
}
const sourceURL = 'https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json';
const clientURL = 'https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json';
const SchemaLink = (props: PropsWithChildren<SchemaLinkProps>) => {
const {
children,
lower,
client = false
} = props;
let content = children;
if(content === undefined) {
content = <Fragment>{lower ? 'explore' : 'Explore'} the schema with an example and live editor/validator</Fragment>
}
const definition = `https://json-schema.app/view/%23/%23%2Fdefinitions%2F${props.objectName}`;
const url = client ? clientURL : sourceURL;
return <a target="_blank" href={`${definition}?url=${url}`}>{content}</a>
}
export default SchemaLink;
+26 -9
View File
@@ -1,8 +1,8 @@
---
title: Multi-Scrobbler
title: Overview
---
# multi-scrobbler
# Overview
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
@@ -15,27 +15,38 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
* 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/))
* [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)
* [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)
* [Musikcube](docs/configuration#musikcube)
* [MPD (Music Player Daemon)](docs/configuration#mpd-music-player-daemon)
* [VLC](docs/configuration#vlc)
* 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)
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](docs/configuration#webhook-configurations) or [healthcheck endpoint](docs/configuration#health-endpoint)
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Graceful network and client failure handling (queued scrobbles that auto-retry)
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
* Modify data before scrobbling with [regular expression or search patterns](docs/transforms)
* Install using [Docker images for x86/ARM](docs/installation#docker), [flatpak](docs/installation#flatpak), or [locally with NodeJS](docs/installation#nodejs)
[**Quick Start Guide**](docs/quickstart)
<img src={require('/img/status-ui.png').default} width="800"/>
**Why should I use this over a browser extension and/or mobile app scrobbler?**
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
@@ -45,9 +56,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
Yes! You can use [Last.fm as a **Source**](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.jpg').default} width="800"/>
Yes! You can use [Last.fm as a **Source**](docs/configuration#lastfm-source) or [Listenbrainz as a **Source**](docs/configuration#listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
## How Does multi-scrobbler (MS) Work?
@@ -72,6 +81,10 @@ Client configurations consist of:
* A friendly name.
* Any data needed to communicate or authenticate with the Client.
## Quick Start
[See the **Quick Start Guide**](docs/quickstart)
## Installation
[See the **Installation** documentation](docs/installation)
@@ -95,6 +108,10 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
Having issues with connections or configuration? Check the [FAQ](docs/FAQ) before creating an issue!
## Development
[Detailed architecture and development guides for Sources/Clients](docs/development/dev-common)
## License
MIT
+2
View File
@@ -17,7 +17,9 @@ function PlaygroundComponent(): JSX.Element {
>
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
// required to keep SSR from trying to render monaco/etc...
const PlaygroundInnerComponent =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("@site/src/components/PlaygroundInner").default
return <PlaygroundInnerComponent />
}}
+1208 -675
View File
File diff suppressed because it is too large Load Diff
@@ -13,62 +13,74 @@
],
"definitions": {
"CommonClientData": {
"title": "CommonClientData",
"type": "object"
},
"CommonClientOptions": {
"properties": {
"checkExistingScrobbles": {
"default": true,
"description": "Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.",
"examples": [
true
],
"title": "checkExistingScrobbles",
"type": "boolean"
},
"deadLetterRetries": {
"default": 1,
"description": "Number of times MS should automatically retry scrobbles in dead letter queue",
"examples": [
1
],
"title": "deadLetterRetries",
"type": "number"
},
"maxRequestRetries": {
"default": 1,
"description": "default # of http request retries a client can make before error is thrown.",
"description": "default # of http request retries a source/client can make before error is thrown",
"examples": [
1
],
"title": "maxRequestRetries",
"type": "number"
},
"options": {
"properties": {
"checkExistingScrobbles": {
"default": true,
"description": "Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.",
"examples": [
true
],
"title": "checkExistingScrobbles",
"type": "boolean"
},
"refreshEnabled": {
"default": true,
"description": "Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history",
"examples": [
true
],
"title": "refreshEnabled",
"type": "boolean"
},
"verbose": {
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
"properties": {
"match": {
"$ref": "#/definitions/MatchLoggingOptions",
"title": "match"
}
},
"title": "verbose",
"type": "object"
}
},
"title": "options",
"type": "object"
"refreshEnabled": {
"default": true,
"description": "Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history",
"examples": [
true
],
"title": "refreshEnabled",
"type": "boolean"
},
"refreshInitialCount": {
"description": "The number of tracks to retrieve on initial refresh (related to scrobbleBacklogCount). If not specified this is the maximum supported for the client.",
"title": "refreshInitialCount",
"type": "number"
},
"retryMultiplier": {
"default": 1.5,
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying).",
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)",
"examples": [
1.5
],
"title": "retryMultiplier",
"type": "number"
},
"verbose": {
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
"properties": {
"match": {
"$ref": "#/definitions/MatchLoggingOptions",
"title": "match"
}
},
"title": "verbose",
"type": "object"
}
},
"title": "CommonClientData",
"title": "CommonClientOptions",
"type": "object"
},
"LastfmClientConfig": {
@@ -98,6 +110,15 @@
"description": "Specific data required to configure this client",
"title": "data"
},
"enable": {
"default": true,
"description": "Should MS use this client/source? Defaults to true",
"examples": [
true
],
"title": "enable",
"type": "boolean"
},
"name": {
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
"examples": [
@@ -105,6 +126,10 @@
],
"title": "name",
"type": "string"
},
"options": {
"$ref": "#/definitions/CommonClientOptions",
"title": "options"
}
},
"required": [
@@ -126,7 +151,7 @@
},
"maxRequestRetries": {
"default": 1,
"description": "default # of http request retries a source can make before error is thrown",
"description": "default # of http request retries a source/client can make before error is thrown",
"examples": [
1
],
@@ -192,6 +217,15 @@
"description": "Specific data required to configure this client",
"title": "data"
},
"enable": {
"default": true,
"description": "Should MS use this client/source? Defaults to true",
"examples": [
true
],
"title": "enable",
"type": "boolean"
},
"name": {
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
"examples": [
@@ -199,6 +233,10 @@
],
"title": "name",
"type": "string"
},
"options": {
"$ref": "#/definitions/CommonClientOptions",
"title": "options"
}
},
"required": [
@@ -212,48 +250,13 @@
"properties": {
"maxRequestRetries": {
"default": 1,
"description": "default # of http request retries a source can make before error is thrown",
"description": "default # of http request retries a source/client can make before error is thrown",
"examples": [
1
],
"title": "maxRequestRetries",
"type": "number"
},
"options": {
"properties": {
"checkExistingScrobbles": {
"default": true,
"description": "Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.",
"examples": [
true
],
"title": "checkExistingScrobbles",
"type": "boolean"
},
"refreshEnabled": {
"default": true,
"description": "Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history",
"examples": [
true
],
"title": "refreshEnabled",
"type": "boolean"
},
"verbose": {
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
"properties": {
"match": {
"$ref": "#/definitions/MatchLoggingOptions",
"title": "match"
}
},
"title": "verbose",
"type": "object"
}
},
"title": "options",
"type": "object"
},
"retryMultiplier": {
"default": 1.5,
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)",
@@ -300,6 +303,15 @@
"description": "Specific data required to configure this client",
"title": "data"
},
"enable": {
"default": true,
"description": "Should MS use this client/source? Defaults to true",
"examples": [
true
],
"title": "enable",
"type": "boolean"
},
"name": {
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
"examples": [
@@ -307,6 +319,10 @@
],
"title": "name",
"type": "string"
},
"options": {
"$ref": "#/definitions/CommonClientOptions",
"title": "options"
}
},
"required": [
@@ -328,48 +344,13 @@
},
"maxRequestRetries": {
"default": 1,
"description": "default # of http request retries a source can make before error is thrown",
"description": "default # of http request retries a source/client can make before error is thrown",
"examples": [
1
],
"title": "maxRequestRetries",
"type": "number"
},
"options": {
"properties": {
"checkExistingScrobbles": {
"default": true,
"description": "Check client for an existing scrobble at the same recorded time as the \"new\" track to be scrobbled. If an existing scrobble is found this track is not track scrobbled.",
"examples": [
true
],
"title": "checkExistingScrobbles",
"type": "boolean"
},
"refreshEnabled": {
"default": true,
"description": "Try to get fresh scrobble history from client when tracks to be scrobbled are newer than the last scrobble found in client history",
"examples": [
true
],
"title": "refreshEnabled",
"type": "boolean"
},
"verbose": {
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
"properties": {
"match": {
"$ref": "#/definitions/MatchLoggingOptions",
"title": "match"
}
},
"title": "verbose",
"type": "object"
}
},
"title": "options",
"type": "object"
},
"retryMultiplier": {
"default": 1.5,
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)",
+1
View File
@@ -8,6 +8,7 @@
"maxRequestRetries": 1,
"retryMultiplier": 1.5
},
"baseUrl": "http://localhost",
"sources": [
{
"type": "spotify",
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+2 -2
View File
@@ -3,10 +3,10 @@
<defs>
<style>
.cls-1 {
fill: #231f20;
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>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

+739 -481
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"resolveJsonModule": true,
+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"
}
}
);
+1 -41
View File
@@ -1,41 +1 @@
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:
```
flatpak-node-generator npm package-lock.json
```
Move `generated-sources.json` into [`/flatpak`](/flatpak)
# 4. Build flatpak
From MS project root:
```console
cd flatpak
flatpak-builder --repo=/home/yourUser/multi-scrobbler-repo /home/yourUser/multi-scrobbler-build io.github.foxxmd.multiscrobbler.yml --force-clean
```
Add `--install --user` to have the app installed immediately.
See flatpak docs on the [docs website](https://foxxmd.github.io/multi-scrobbler/docs/development/flatpak) or at [`/docsite/docs/development/flatpak.md`](../docsite/docs/development/flatpak.md)
+2 -1
View File
@@ -1,5 +1,6 @@
{
"logging": {
"file": false
}
},
"port": 9078
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+1 -1
View File
@@ -3,7 +3,7 @@
<defs>
<style>
.cls-1 {
fill: #231f20;
fill: #c34a36;
}
</style>
</defs>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -3,14 +3,18 @@
<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 to many clients</summary>
<url type="homepage">https://github.com/FoxxMD/multi-scrobbler</url>
<url type="help">https://github.com/FoxxMD/multi-scrobbler/blob/flatpak-clean/docs/configuration.md</url>
<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:
@@ -18,13 +22,19 @@
<ul>
<li>Spotify</li>
<li>Plex (Tautulli)</li>
<li>Any subsonic-based music player (Airsonic)</li>
<li>Any subsonic-based music player (Airsonic or Navidrome)</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>
<li>Musikcube</li>
<li>MPDF (Music Player Daemon)</li>
<li>VLC</li>
</ul>
<p>and consolidate your listens (scrobbles) into one or more scrobbling servers/services:</p>
<ul>
@@ -37,10 +47,24 @@
<screenshots>
<screenshot type="default">
<caption>Browser-based UI</caption>
<image type="source">https://raw.githubusercontent.com/FoxxMD/multi-scrobbler/7b0f5801a1aa728986f440f4c67fe158cecd763d/assets/status-ui.jpg</image>
<image type="source">https://raw.githubusercontent.com/FoxxMD/multi-scrobbler/f4256d1084f4bdea2945c03e523af6d0ed89b3ae/assets/status-ui.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.8.3" date="2024-08-28"/>
<release version="0.8.2" date="2024-08-20"/>
<release version="0.8.1" date="2024-07-17"/>
<release version="0.8.0" date="2024-07-08"/>
<release version="0.7.1" date="2024-04-11"/>
<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>
+34 -15
View File
@@ -21,29 +21,41 @@ modules:
npm_config_offline: 'true'
build-commands:
# install npm dependencies
- npm install --offline
- npm ci --offline
- cd docsite && npm ci --offline && cd ../
# build app
- npm run build
- NODE_ENV=production npm run build
# makes node and npm available
- mkdir -p /app/bin /app/lib /app/lib/src
- 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}
- rm -r /app/lib/node_modules/npm/{docs,man} # remove this when updating to newer runtime-version
# remove dev dependencies
- npm prune --production
# remove docsite dependencies since we've built static site
- rm -r docsite/node_modules
# copy node_modules needed to run app
- cp -r node_modules/* /app/lib/node_modules
- cp -r node_modules/. /app/lib/node_modules
# delete typescript files (not needed for running app)
- find src/ -name "*.ts" -type f -delete
# 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 src/* /app/lib/src
- cp -r dist/. /app/lib/dist
- cp -r src/. /app/lib/src
- cp -r docsite/. /app/lib/docsite
- 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
@@ -54,11 +66,17 @@ modules:
- install -Dm755 ../run.sh /app/bin/multiscrobbler
subdir: main
sources:
- type: git
url: https://github.com/FoxxMD/multi-scrobbler
tag: 0.4.6
commit: b90ea039245491767baeefc53966b0350016aa4d
dest: main
# 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
@@ -73,6 +91,7 @@ modules:
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 IS_LOCAL=true node src/index.js
- 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
- generated-sources.0.json
- generated-sources.1.json
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
OVERWRITE="0"
CONFIRM="1"
# https://stackoverflow.com/a/14203146
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-b|--buildpath)
BUILDPATH="$2"
shift # past argument
shift # past value
;;
-o|--overwrite)
OVERWRITE="1"
shift # past argument
;;
-y|--yes)
CONFIRM="0"
shift # past argument
;;
-*|--*)
echo "Unknown option $1"
exit 1
;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}"
if [ ! -f "./io.github.foxxmd.multiscrobbler.yml" ]; then
echo "Run this script inside the 'flatpak' directory!"
exit 1
fi
if [ -z "${BUILDPATH}" ]; then
printf "\nNo build path set, using ./build\n"
BUILDPATH="${PWD##*/}/build"
fi
printf '\nBuild Path: %s' "${BUILDPATH}"
if [ "$OVERWRITE" = "1" ]; then echo 'Overwrite Sources: True'; else printf 'Overwrite Sources: False\n'; fi
if [ "$CONFIRM" = "1" ]; then
read -p "Continue? (Y/N): " confirm && [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]] || exit 1
fi
printf '\n'
cd ../
if [ -d ./node_modules ]; then
echo 'Project node_modules exists, deleting...'
rm -r node_modules
fi
if [ -d ./docsite/node_modules ]; then
echo 'Docsite node_modules exists, deleting...'
rm -r docsite/node_modules
fi
GENERATE_SOURCES="1"
if [ -f "flatpak/generated-sources.0.json" ] || [ -f "flatpak/generated-sources.1.json" ]; then
if [ "$OVERWRITE" = "0" ]; then
echo 'Generated sources exist, will not overwrite.';
GENERATE_SOURCES=0
else
echo 'Deleting existing sources...';
rm -f flatpak/generated-sources.0.json
rm -f flatpak/generated-sources.1.json
fi
fi
if [ "$GENERATE_SOURCES" = "1" ]; then
printf '\nGenerating project sources...\n'
rm -f generated-sources.json
flatpak-node-generator npm package-lock.json
mv generated-sources.json flatpak/generated-sources.0.json
printf '\nGenerating docsite sources...\n'
flatpak-node-generator npm docsite/package-lock.json
mv generated-sources.json flatpak/generated-sources.1.json
fi
cd flatpak || exit
mkdir -p "$BUILDPATH"
printf '\nBuilding flatpak app...\n'
set -x
flatpak-builder --repo="$BUILDPATH"/repo --state-dir="$BUILDPATH"/state "$BUILDPATH"/build io.github.foxxmd.multiscrobbler.yml --force-clean --install --user
# https://stackoverflow.com/questions/2853803/how-to-echo-shell-commands-as-they-are-executed#comment135696350_13718771
{ set +x; } &> /dev/null
echo 'Done!'
+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>
+8510 -1834
View File
File diff suppressed because it is too large Load Diff
+113 -38
View File
@@ -1,28 +1,36 @@
{
"name": "multi-scrobbler",
"version": "0.4.8",
"version": "0.8.3",
"type": "module",
"description": "scrobble plays from multiple sources to multiple clients",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "node src/index.js",
"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 tsconfig.json ClientConfig --out src/common/schema/client.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-source": "typescript-json-schema tsconfig.json SourceConfig --out src/common/schema/source.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aio": "typescript-json-schema tsconfig.json AIOConfig --out src/common/schema/aio.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aiosource": "typescript-json-schema tsconfig.json AIOSourceConfig --out src/common/schema/aio-source.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aioclient": "typescript-json-schema tsconfig.json AIOClientConfig --out src/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"typedoc": "typedoc"
"schema-client": "typescript-json-schema src/backend/tsconfig.json ClientConfig --out src/backend/common/schema/client.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage --constAsEnum",
"schema-source": "typescript-json-schema src/backend/tsconfig.json SourceConfig --out src/backend/common/schema/source.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage --constAsEnum",
"schema-aio": "typescript-json-schema src/backend/tsconfig.json AIOConfig --out src/backend/common/schema/aio.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage --constAsEnum",
"schema-aiosource": "typescript-json-schema src/backend/tsconfig.json AIOSourceConfig --out src/backend/common/schema/aio-source.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage --constAsEnum",
"schema-aioclient": "typescript-json-schema src/backend/tsconfig.json AIOClientConfig --out src/backend/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage --constAsEnum",
"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",
"dev": "nodemon -w src/backend -x tsx src/backend/index.ts",
"start": "NODE_ENV=production tsx src/backend/index.ts",
"build:frontend": "vite build",
"build:backend": "tsc -p src/backend",
"build": "npm run -s build:backend && npm run -s build:frontend && npm run -s docs:build",
"docs:install": "cd docsite && npm ci",
"docs:start": "cd docsite && npm start",
"docs:build": "cd docsite && npm run build",
"postinstall": "patch-package"
},
"exports": {
".": {
"types": "./src/common/infrastructure/typings/lastfm-node-client.d.ts",
"import": "./src/index.js"
"types": "./src/backend/common/infrastructure/typings/lastfm-node-client.d.ts",
"import": "./src/backend/index.js"
}
},
"engines": {
"node": ">=18.0.0",
"node": ">=18.19.1",
"npm": ">=9.1.0"
},
"repository": {
@@ -34,65 +42,132 @@
"bugs": {
"url": "https://github.com/FoxxMD/multi-scrobbler/issues"
},
"homepage": "https://github.com/FoxxMD/multi-scrobbler#readme",
"private": true,
"homepage": ".",
"dependencies": {
"@astronautlabs/mdns": "^1.0.7",
"@awaitjs/express": "^0.6.3",
"@foxxmd/winston": "3.3.31",
"@curvenote/ansi-to-react": "^7.0.0",
"@donedeal0/superdiff": "^1.1.1",
"@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/get-version": "^0.0.3",
"@foxxmd/logging": "^0.1.14",
"@foxxmd/regex-buddy-core": "^0.1.0",
"@foxxmd/string-sameness": "^0.4.0",
"@kenyip/backoff-strategies": "^1.0.4",
"ajv": "^7.2.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",
"cors": "^2.8.5",
"dayjs": "^1.10.4",
"dbus-next": "^0.10.2",
"ejs": "^3.1.6",
"es6-error": "^4.1.1",
"dbus-ts": "^0.0.7",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"fixed-size-list": "^0.3.0",
"formidable": "^2.1",
"gotify": "^1.1.0",
"iso-websocket": "^0.3.0",
"iti": "^0.6.0",
"json5": "^2.2.3",
"kodi-api": "^0.2.1",
"lastfm-node-client": "^2.2.0",
"mopidy": "^1.3.0",
"normalize-url": "^6.1.0",
"ntfy": "^1.0.5",
"mpd-api": "^1.1.2",
"nanoid": "^3.3.1",
"normalize-url": "^8.0.1",
"ntfy": "^1.5.4",
"object-hash": "^3.0.0",
"p-event": "^4.2.0",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"pony-cause": "^1.1.1",
"safe-stable-stringify": "^1.1.1",
"socket.io": "^4.6.1",
"patch-package": "^8.0.0",
"postcss": "^8.4.33",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"sass": "^1.70.0",
"spotify-web-api-node": "^5.0.2",
"superagent": "^8.0.9",
"triple-beam": "^1.3.0",
"winston-daily-rotate-file": "^4.5.0",
"winston-duplex": "0.1.3",
"winston-null": "^2.0.0",
"winston-transport": "^4.4.0",
"xml2js": "^0.4.23",
"youtube-music-ts-api": "^1.4.1"
"tailwindcss": "^3.4.4",
"toad-scheduler": "^3.0.0",
"tsx": "^4.7.0",
"vite-express": "^0.16.0",
"vlc-client": "^1.1.1",
"xml2js": "0.5.0",
"youtube-music-ts-api": "^1.7.0"
},
"devDependencies": {
"@dbus-types/notifications": "^0.0.5",
"@eslint/js": "^8.56.0",
"@faker-js/faker": "^9.0.0-rc.1",
"@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/cors": "^2.8.17",
"@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/triple-beam": "^1.3.2",
"@types/xml2js": "^0.4.11",
"@vitejs/plugin-react": "^4.2.1",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"clone": "^2.1.2",
"eslint": "^8.56.0",
"eslint-plugin-prefer-arrow-functions": "^3.2.4",
"mocha": "^10.3.0",
"msw": "^2.1.2",
"nodemon": "^3.0.3",
"ts-essentials": "^9.1.2",
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.13.0",
"typescript": "^4.9.5",
"typescript-json-schema": "~0.55",
"typedoc": "^0.23.28"
"typescript": "^5.3.3",
"typescript-eslint": "^7.0.1",
"typescript-json-schema": "^0.61.0",
"vite": "^5.2.12"
},
"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": {
@@ -0,0 +1,186 @@
diff --git a/node_modules/chromecast-client/dist/cjs/src/apps/application.js b/node_modules/chromecast-client/dist/cjs/src/apps/application.js
index d91a6ff..dfc417e 100644
--- a/node_modules/chromecast-client/dist/cjs/src/apps/application.js
+++ b/node_modules/chromecast-client/dist/cjs/src/apps/application.js
@@ -4,7 +4,9 @@ exports.join = exports.launchAndJoin = void 0;
const receiver_1 = require("../controllers/receiver");
const utils_1 = require("../utils");
const _getJoinableTransportId = (status) => {
- const app = status.applications.find(a => a.namespaces.map(e => e.name).includes('urn:x-cast:com.google.cast.media'));
+ const app = status.applications === undefined
+ ? undefined
+ : status.applications.find(a => a.namespaces.map(e => e.name).includes('urn:x-cast:com.google.cast.media'));
return app === undefined ? utils_1.Result.Err(new Error('failed to find joinable application')) : utils_1.Result.Ok(app.transportId);
};
const _join = async (status, factory) => {
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts
index cbc2af8..e0f60dc 100644
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts
@@ -63,7 +63,7 @@ export declare const Application$: z.ZodObject<{
}>;
export type Application = z.infer<typeof Application$>;
export declare const ReceiverStatus$: z.ZodObject<{
- applications: z.ZodArray<z.ZodObject<{
+ applications: z.ZodOptional<z.ZodArray<z.ZodObject<{
appId: z.ZodString;
displayName: z.ZodString;
iconUrl: z.ZodNullable<z.ZodOptional<z.ZodString>>;
@@ -103,7 +103,7 @@ export declare const ReceiverStatus$: z.ZodObject<{
}[];
sessionId: string;
transportId: string;
- }>, "many">;
+ }>, "many">>;
volume: z.ZodObject<{
controlType: z.ZodEffects<z.ZodEnum<["ATTENUATION", "FIXED", "MASTER"]>, "ATTENUATION" | "FIXED" | "MASTER", unknown>;
level: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
@@ -121,13 +121,7 @@ export declare const ReceiverStatus$: z.ZodObject<{
stepInterval: number;
}>;
}, "strip", z.ZodTypeAny, {
- volume: {
- level?: number | null | undefined;
- muted?: boolean | null | undefined;
- controlType: "ATTENUATION" | "FIXED" | "MASTER";
- stepInterval: number;
- };
- applications: {
+ applications?: {
iconUrl?: string | null | undefined;
isIdleScreen?: boolean | null | undefined;
launchedFromCloud?: boolean | null | undefined;
@@ -139,15 +133,15 @@ export declare const ReceiverStatus$: z.ZodObject<{
}[];
sessionId: string;
transportId: string;
- }[];
-}, {
+ }[] | undefined;
volume: {
level?: number | null | undefined;
muted?: boolean | null | undefined;
- controlType?: unknown;
+ controlType: "ATTENUATION" | "FIXED" | "MASTER";
stepInterval: number;
};
- applications: {
+}, {
+ applications?: {
iconUrl?: string | null | undefined;
isIdleScreen?: boolean | null | undefined;
launchedFromCloud?: boolean | null | undefined;
@@ -159,6 +153,12 @@ export declare const ReceiverStatus$: z.ZodObject<{
}[];
sessionId: string;
transportId: string;
- }[];
+ }[] | undefined;
+ volume: {
+ level?: number | null | undefined;
+ muted?: boolean | null | undefined;
+ controlType?: unknown;
+ stepInterval: number;
+ };
}>;
export type ReceiverStatus = z.infer<typeof ReceiverStatus$>;
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js
index 33bfb4b..29e79fe 100644
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js
@@ -51,7 +51,7 @@ exports.Application$ = zod_1.z.object({
transportId: zod_1.z.string(),
});
exports.ReceiverStatus$ = zod_1.z.object({
- applications: zod_1.z.array(exports.Application$),
+ applications: zod_1.z.array(exports.Application$).optional(),
volume: exports.Volume$,
});
//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts
index 53e42b2..0d77bb2 100644
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts
@@ -80,7 +80,7 @@ export declare const MediaStatus$: z.ZodObject<{
playbackRate: z.ZodNumber;
playerState: z.ZodEffects<z.ZodEnum<["IDLE", "PLAYING", "BUFFERING", "PAUSED"]>, "IDLE" | "PLAYING" | "BUFFERING" | "PAUSED", unknown>;
supportedMediaCommands: z.ZodNumber;
- volume: z.ZodObject<{
+ volume: z.ZodNullable<z.ZodOptional<z.ZodObject<{
level: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
muted: z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
}, "strip", z.ZodTypeAny, {
@@ -89,7 +89,7 @@ export declare const MediaStatus$: z.ZodObject<{
}, {
level?: number | null | undefined;
muted?: boolean | null | undefined;
- }>;
+ }>>>;
}, "strip", z.ZodTypeAny, {
media?: {
metadata?: Record<string, unknown> | null | undefined;
@@ -102,15 +102,15 @@ export declare const MediaStatus$: z.ZodObject<{
} | null | undefined;
customData?: Record<string, unknown> | null | undefined;
idleReason?: "CANCELLED" | "INTERRUPTED" | "FINISHED" | "ERROR" | null | undefined;
+ volume?: {
+ level?: number | null | undefined;
+ muted?: boolean | null | undefined;
+ } | null | undefined;
currentTime: number;
mediaSessionId: number;
playbackRate: number;
playerState: "IDLE" | "PLAYING" | "BUFFERING" | "PAUSED";
supportedMediaCommands: number;
- volume: {
- level?: number | null | undefined;
- muted?: boolean | null | undefined;
- };
}, {
media?: {
metadata?: Record<string, unknown> | null | undefined;
@@ -124,13 +124,13 @@ export declare const MediaStatus$: z.ZodObject<{
customData?: Record<string, unknown> | null | undefined;
idleReason?: unknown;
playerState?: unknown;
+ volume?: {
+ level?: number | null | undefined;
+ muted?: boolean | null | undefined;
+ } | null | undefined;
currentTime: number;
mediaSessionId: number;
playbackRate: number;
supportedMediaCommands: number;
- volume: {
- level?: number | null | undefined;
- muted?: boolean | null | undefined;
- };
}>;
export type MediaStatus = z.infer<typeof MediaStatus$>;
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js
index 2af6558..138f550 100644
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js
@@ -28,6 +28,6 @@ exports.MediaStatus$ = zod_1.z.object({
playbackRate: zod_1.z.number(),
playerState: exports.PlayerState$,
supportedMediaCommands: zod_1.z.number(),
- volume: exports.Volume$,
+ volume: exports.Volume$.nullish(),
});
//# sourceMappingURL=media.types.js.map
\ No newline at end of file
diff --git a/node_modules/chromecast-client/dist/cjs/src/platform.d.ts b/node_modules/chromecast-client/dist/cjs/src/platform.d.ts
index 2f9770b..1247d8a 100644
--- a/node_modules/chromecast-client/dist/cjs/src/platform.d.ts
+++ b/node_modules/chromecast-client/dist/cjs/src/platform.d.ts
@@ -65,7 +65,7 @@ export declare const createPlatform: (client: PersistentClient) => {
controlType: "ATTENUATION" | "FIXED" | "MASTER";
stepInterval: number;
};
- applications: {
+ applications?: {
iconUrl?: string | null | undefined;
isIdleScreen?: boolean | null | undefined;
launchedFromCloud?: boolean | null | undefined;
+38
View File
@@ -0,0 +1,38 @@
diff --git a/node_modules/lastfm-node-client/lib/ApiRequest.js b/node_modules/lastfm-node-client/lib/ApiRequest.js
index ecd39d0..c8e1cc5 100644
--- a/node_modules/lastfm-node-client/lib/ApiRequest.js
+++ b/node_modules/lastfm-node-client/lib/ApiRequest.js
@@ -91,7 +91,7 @@ class ApiRequest {
const paramsStr = querystring.stringify(paramsObj);
const options = {
hostname: "ws.audioscrobbler.com",
- path: "/2.0"
+ path: "/2.0",
};
if (method === "POST") {
@@ -113,7 +113,11 @@ class ApiRequest {
httpResponse.on("data", chunk => data += chunk);
httpResponse.on("end", () => resolve(data));
httpResponse.on("error", err => reject(err));
- });
+ })
+ // stop waiting for request if it takes longer than 3 seconds
+ .setTimeout(3000, () => {
+ httpRequest.destroy(new Error('ETIMEDOUT - socket hang up'));
+ });
httpRequest.on("error", err => reject(err));
diff --git a/node_modules/lastfm-node-client/lib/LastFm.js b/node_modules/lastfm-node-client/lib/LastFm.js
index 2393a4d..75bdf37 100644
--- a/node_modules/lastfm-node-client/lib/LastFm.js
+++ b/node_modules/lastfm-node-client/lib/LastFm.js
@@ -1045,7 +1045,6 @@ class LastFm {
* @param {callback} [callback]
* @returns {(Promise|LastFm)}
*/
-
trackUpdateNowPlaying(params, callback) {
const apiRequest = new ApiRequest()
.set(params)
@@ -0,0 +1,13 @@
diff --git a/node_modules/typescript-json-schema/dist/typescript-json-schema.js b/node_modules/typescript-json-schema/dist/typescript-json-schema.js
index 5b5ecec..1e48e77 100644
--- a/node_modules/typescript-json-schema/dist/typescript-json-schema.js
+++ b/node_modules/typescript-json-schema/dist/typescript-json-schema.js
@@ -55,7 +55,7 @@ var crypto_1 = require("crypto");
var ts = require("typescript");
var path_equal_1 = require("path-equal");
var vm = require("vm");
-var REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g;
+var REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"(, \{ assert: \{ "resolution-mode": "(import|require)" \} \})?\)|".*?")\.| /g;// /(\bimport\(".*?"\)|".*?")\.| /g;
var REGEX_TSCONFIG_NAME = /^.*\.json$/;
var REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g;
var REGEX_GROUP_JSDOC = /^[.]?([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g;
File diff suppressed because one or more lines are too long

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