Compare commits

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

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

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

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

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

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

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

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

Closes #72
2023-03-20 22:37:54 -04:00
FoxxMD 19e199bd58 Pre-emptive adding flatpak folders to gitignore 2023-03-20 22:31:27 -04:00
FoxxMD 7b0f5801a1 Merge branch 'develop' 2023-03-20 11:06:38 -04:00
FoxxMD 4dbea91a5e docs: some words 2023-03-15 12:28:44 -04:00
FoxxMD 548356aaa4 feat: Implement Kodi as a Source 2023-03-15 12:26:35 -04:00
FoxxMD 8b3f4c636c Merge branch 'develop' 2023-03-15 09:13:20 -04:00
FoxxMD 1b0212f3bb feat: Improve polling retry strategy
* Increase default to 5 for all sources
* Use true exponential backoff for determining delay between polling
2023-03-15 09:06:26 -04:00
FoxxMD 14e9ec0b45 refactor: Remove superfluous 'Client' from scrobbler label
Already know what it is since it is prefixed by [Scrobblers] label
2023-03-15 08:30:18 -04:00
FoxxMD dab113ab76 docs: Missing jriver TOC entry 2023-03-13 14:01:40 -04:00
FoxxMD ad729769ec Implement JRiver Source #26 2023-03-13 13:59:24 -04:00
FoxxMD bb4ae549a3 Remove superfluous spotify label 2023-03-13 10:13:47 -04:00
FoxxMD 0786642a38 Increase default poll retries for Spotify
This seems to be a pain point so increasing ought to help.
2023-03-13 10:09:22 -04:00
FoxxMD 77d5f5884e Use error with cause for spotify api errors 2023-03-13 10:00:23 -04:00
225 changed files with 51018 additions and 4154 deletions
+8
View File
@@ -12,3 +12,11 @@ config/*.json
config/*.bak
/docs
/logs
.flatpak-builder
flatpak/generated-sources.json
flatpak/.flatpak-builder
docsite/build
docsite/node_modules
docsite/.docusaurus
docsite/.cache-loader
build
+2 -2
View File
@@ -7,7 +7,7 @@ assignees: ''
---
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docs/FAQ.md) before submitting a bug report.
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
**Describe the bug**
A clear and concise description of what the bug is.
@@ -23,7 +23,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen.
**Logs**
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docs/FAQ.md#turn-on-debug-logging)
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
```
Copy and paste as much log data as possible related to this issue here.
+42
View File
@@ -0,0 +1,42 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- master
# Review gh actions docs if you want to further define triggers, paths, etc
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
jobs:
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci
working-directory: ./docsite
- name: Build website
run: npm run build
working-directory: ./docsite
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
publish_dir: ./docsite/build
# The following lines assign commit authorship to the official
# GH-Actions bot for deploys to `gh-pages` branch:
# https://github.com/actions/checkout/issues/13#issuecomment-724415212
# The GH actions bot is used by default if you didn't specify the two fields.
# You can swap them out with your own user credentials.
user_name: github-actions[bot]
user_email: 41898282+github-actions[bot]@users.noreply.github.com
@@ -15,22 +15,36 @@ jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
# 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@2541b1294d2704b0964813337f33b291d3f8596b
uses: actions/checkout@v2
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7
uses: docker/login-action@v2
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
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@f2a13332ac1ce8c0a71aeac48a150dbb1838ab67
uses: docker/metadata-action@v3
with:
images: foxxmd/multi-scrobbler
images: |
foxxmd/multi-scrobbler
ghcr.io/foxxmd/multi-scrobbler
# generate Docker tags based on the following events/attributes
tags: |
type=raw,value=latest,enable=${{ endsWith(github.ref, 'master') }}
@@ -40,16 +54,16 @@ jobs:
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25
uses: docker/setup-buildx-action@v2
- name: Build and push Docker image
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
+6 -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,3 +125,8 @@ src/**/**.js
src/**/**.js.map
*.bak
.flatpak-builder
flatpak/generated-sources.json
build
!setupProxy.js
+4
View File
@@ -0,0 +1,4 @@
{
"require": ["./register.js", "source-map-support/register"],
"reporter": "dot"
}
+1 -1
View File
@@ -1 +1 @@
lts/hydrogen
v18.16.0
+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"
}
+34 -6
View File
@@ -9,6 +9,7 @@ RUN \
git \
nodejs \
npm \
#yarn \
openssh && \
echo "**** cleanup ****" && \
rm -rf \
@@ -23,31 +24,58 @@ ENV CONFIG_DIR=$data_dir
COPY docker/root/ /
RUN npm install -g patch-package
WORKDIR /app
FROM base as build
# copy NPM dependencies and install
COPY --chown=abc:abc package*.json ./
COPY --chown=abc:abc tsconfig.json .
# would very much like to use YARN for dependency management but its got show-stopping bad design for prod dependencies
# https://github.com/yarnpkg/yarn/issues/6323 -- always downloads devDependencies -- and im not about to migrate to v2 since alpine doesn't support it yet
# copy dep/TS config and install dev dependencies
#COPY --chown=abc:abc package.json tsconfig.json yarn.lock ./
COPY --chown=abc:abc package*.json tsconfig.json ./
COPY --chown=abc:abc patches ./patches
RUN npm install
#RUN yarn install
COPY --chown=abc:abc . /app
# need to set before build so server/client build is optimized and has constants (if needed)
ENV NODE_ENV=production
RUN npm run build && rm -rf node_modules
#RUN yarn run build && rm -rf node_modules
FROM base as app
COPY --from=build --chown=abc:abc /app /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/build /app/build
COPY --from=base /usr/local/bin /usr/local/bin
COPY --from=base /usr/local/lib /usr/local/lib
ENV NODE_ENV="production"
ENV NODE_ENV=production
ENV IS_DOCKER=true
#
#RUN yarn global add patch-package \
# && yarn install --production=true \
# && yarn global remove patch-package \
# && yarn cache clean --mirror \
# && chown abc:abc node_modules \
# && rm -rf node_modules/ts-node \
# && rm -rf node_modules/typescript
RUN npm install --omit=dev \
&& npm cache clean --force \
&& chown abc:abc node_modules \
&& rm -rf node_modules/ts-node \
&& rm -rf node_modules/typescript
&& rm -rf node_modules/typescript \
&& rm -rf node_modules/@types
ARG webPort=9078
ENV PORT=$webPort
+24 -19
View File
@@ -4,29 +4,34 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/multi-scrobbler)](https://hub.docker.com/r/foxxmd/multi-scrobbler)
<img src="/assets/icon.png" align="right"
alt="multi-scrobbler logo" width="180" height="180">
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
* Supports scrobbling from many **Sources**
* [Spotify](/docs/configuration.md#spotify)
* [Plex](/docs/configuration.md#plex) or [Tautulli](/docs/configuration.md#tautulli)
* [Subsonic-compatible APIs](/docs/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/))
* [Jellyfin](/docs/configuration.md#jellyfin)
* [Youtube Music](/docs/configuration.md#youtube-music)
* [Last.fm](/docs/configuration.md#lastfm-source)
* [ListenBrainz](/docs/configuration.md#listenbrainz--source-)
* [Deezer](/docs/configuration.md#deezer)
* [MPRIS (Linux Desktop)](/docs/configuration.md#mpris)
* [Mopidy](/docs/configuration.md#mopidy)
* [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)
* Supports scrobbling to many **Clients**
* [Maloja](/docs/configuration.md#maloja)
* [Last.fm](/docs/configuration.md#lastfm)
* [ListenBrainz](/docs/configuration.md#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docs/configuration.md#webhook-configurations) or [healthcheck endpoint](/docs/configuration.md#health-endpoint)
* [Maloja](/docsite/docs/configuration/configuration.md#maloja)
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm)
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz)
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
* Web server interface for stats, basic control, and detailed logs
* Smart handling of credentials (persistent, authorization through app)
* Easy configuration through ENVs or JSON
* Docker images for x86/ARM
* Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
**Why should I use this over a browser extension and/or mobile app scrobbler?**
@@ -37,7 +42,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.md#lastfm--source-) or [Listenbrainz as a **Source**](/docs/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.
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">
@@ -66,11 +71,11 @@ Client configurations consist of:
## Installation
[See the **Installation** documentation](/docs/installation.md)
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
## Configuration
[See the **Configuration** documentation](/docs/configuration.md)
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
## Usage
@@ -85,7 +90,7 @@ 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](/docs/FAQ.md) before creating an issue!
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
## License
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+67
View File
@@ -0,0 +1,67 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- *({{commit.scope}})* {{ commit.message | upper_first }}
{%- if commit.breaking %}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{%- endif -%}
{%- endfor -%}
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- *(No Category)* {{ commit.message | upper_first }}
{% if commit.breaking -%}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{% endif -%}
{% endif -%}
{% endfor -%}
{% endfor %}
"""
# remove the leading and trailing whitespaces from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# allow only conventional commits
# https://www.conventionalcommits.org
conventional_commits = true
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
+217
View File
@@ -0,0 +1,217 @@
import {API, Collection, FileInfo, Options} from 'jscodeshift';
import path from 'path';
import fs from 'fs';
export type TransformFrom = 'none' | 'any' | string;
export type TransformTo = 'none' | 'any' | string;
export type ImportTypes = 'any' | 'relative' | 'alias';
const base = process.cwd();
export default function transformer(
file: FileInfo,
{ jscodeshift: j }: API,
options: Options,
) {
const tf = options.transformFrom as string | undefined;
if(tf === undefined) {
throw new Error(`arg '--transformFrom' must be defined.`);
}
const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[];
const tfIsAny = transformFrom.includes('any');
const tt = options.transformTo as string | undefined;
if(tt === undefined) {
throw new Error(`arg '--transformTo' must be defined.`);
}
const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[];
if(transformTo.length > 1 && transformFrom.length !== transformTo.length) {
throw new Error('When more than one Transform To is specified then number of Transform From arguments must match');
}
const itypes = options.importTypes as string | undefined;
if(itypes === undefined) {
throw new Error(`arg '--importTypes' must be defined.`);
}
const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[];
const importsIsAny = importTypes.includes('any');
const fileDir = path.dirname(path.join(base, file.path));
const processingFilePath = file.path;
console.log(`Processing => ${processingFilePath}`);
let source: Collection<any>;
try {
source = j(file.source);
} catch (e) {
console.error(`Failed to parse ${processingFilePath}, will skip`);
console.error(e);
}
const imports = source.find(j.ImportDeclaration)
imports.forEach((x) => {
const importPath = x.value.source.value as string;
const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`;
const pathInfo = path.parse(importPath);
const hasNoExt = pathInfo.ext === '';
let filenameFromDir: string;
if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) {
const pathFull = path.join(fileDir, importPath);
const dir = path.dirname(pathFull);
const normalExt = pathInfo.ext.replace('.', '');
let isRelative: boolean | undefined;
let dirExists: boolean | undefined;
let fileExists: boolean | undefined;
try {
// is dir path real?
fs.realpathSync(dir);
dirExists = true;
} catch (e) {
isRelative = false;
dirExists = false;
}
if(isRelative === undefined) {
// if extension is none we need to check for any file in dir with this name
if(hasNoExt) {
const dirFiles = fs.readdirSync(dir)
filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name);
if(filenameFromDir !== undefined) {
fileExists = false;
isRelative = false;
} else {
fileExists = true;
isRelative = true;
}
} else {
try {
// does a file exist?
fs.realpathSync(pathFull);
isRelative = true;
} catch (e) {
isRelative = false;
fileExists = false;
}
}
}
if(!importsIsAny) {
if(!isRelative && !importTypes.includes('alias')) {
console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`);
return;
} else if(isRelative && !importTypes.includes('relative')) {
console.log(`${pathFull} => Import is relative but import types does not specify relative`);
return;
}
}
// determine transformTo
// if there is only one TO then use it
let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined;
if(derivedTT === undefined) {
// otherwise we find the TO by using the same index as the matching FROM extension type
const tfIndex = transformFrom.findIndex((x) => {
if(x === 'any') {
return true;
}
if(x === 'none' && hasNoExt) {
return true;
}
if(x === normalExt) {
return true;
}
});
if(tfIndex === -1) {
console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`);
return;
}
derivedTT = transformTo[tfIndex];
}
let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`;
let transformedImport: string;
switch(derivedTT) {
case 'none':
if(hasNoExt) {
console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`);
return;
}
transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name);
break;
case 'any':
if(hasNoExt && filenameFromDir) {
transformedImport = filenameFromDir;
} else {
const otherFilename = fs.readdirSync(dir).find(x => {
const pinfo = path.parse(x);
return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext;
});
if(otherFilename === undefined) {
console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
return;
}
transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename);
transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})`
}
break;
default:
transformedImport = combineDirFile(pathInfo.dir, `.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`);
break;
}
console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`);
j(x).replaceWith(
j.importDeclaration(
x.node.specifiers,
j.stringLiteral(transformedImport)
)
);
} else {
console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`);
return;
}
});
/**
* Early exit condition
* -----
* It is often good practice to exit early and return the original source file
* if it does not contain code relevant to the codemod.
* See this page for more information:
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output
*/
// if (/* Some condition here */ true) {
// return file.source;
// }
/**
* Codemod logic goes here 👇
* -----
* This is where the core logic for your codemod will go,
* consider grouping specific actions into 'motions' and running them in sequence
*
* See this page for more information:
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions
*/
//source.findVariableDeclarators('foo').renameTo('bar');
/**
* Return your modified AST here 👇
* -----
* This is where your modified AST will be transformed back into a string
* and written back to the file.
*/
return source.toSource(options.printOptions);
}
const combineDirFile = (dir: string, file: string) => {
if(dir === '') {
return file;
}
return `${dir}${path.sep}${file}`;
}
+2 -2
View File
@@ -8,5 +8,5 @@ These are **NOT** exhaustive examples. You should consult the **configuration**
Documentation at
* [internal docs](/docs/configuration.md)
* External Link: https://github.com/FoxxMD/multi-scrobbler/blob/master/docs/configuration.md
* [internal docs](../docsite/docs/configuration/configuration.md)
* External Link: https://foxxmd.github.io/multi-scrobbler/docs/configuration
+12 -2
View File
@@ -1,8 +1,18 @@
{
"debugMode": false,
"sourceDefaults": {
"maxPollRetries": 0,
"maxPollRetries": 1,
"maxRequestRetries": 1,
"retryMultiplier": 1.5
"retryMultiplier": 1.5,
"scrobbleThresholds": {
"duration": 30,
"percent": 50
},
"options": {
"logPayload": false,
"logFilterFailure": "warn",
"logPlayerState": false
}
},
"clientDefaults": {
"maxRequestRetries": 1,
+5 -4
View File
@@ -4,10 +4,11 @@
"clients": [],
"data": {
"users": ["FoxxMD"],
"servers": ["myServer","anotherServer"],
"options": {
"logPayload": false
}
"servers": ["myServer","anotherServer"]
},
"options": {
"logPayload": false,
"logFilterFailure": "warn"
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "MyJriver",
"data": {
"url": "0.0.0.0",
"username": "auser",
"password": "apassword"
}
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"name": "MyKodi",
"data": {
"url": "http://localhost:8080/jsonrpc",
"username": "myUser",
"password": "myPassword"
}
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
[
{
"name": "brainz",
"configureAs": "client"
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
+4 -1
View File
@@ -5,7 +5,10 @@
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"]
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+4 -1
View File
@@ -5,7 +5,10 @@
"data": {
"user": ["username@gmail.com","anotherUser@gmail.com"],
"libraries": ["music","my podcasts"],
"servers": ["myServer","anotherServer"]
"servers": ["myServer","anotherServer"],
"options": {
"logFilterFailure": "warn"
}
}
}
]
+1 -1
View File
@@ -4,7 +4,7 @@
"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"
}
}
]
+22 -19
View File
@@ -1,19 +1,22 @@
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
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,6 @@
# 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!"
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/build/server.js
-82
View File
@@ -1,82 +0,0 @@
# Installation
## Local
Clone this repository somewhere and then install from the working directory
```bash
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
```
### Local Usage Examples
* The web UI is served on port `9078`. This can be modified using the `PORT` environmental variable.
#### Using [file-based](/docs/configuration.md#file-based-configuration) configuration
```bash
npm start
```
#### Using [env-based](/docs/configuration.md#env-based-configuration) configuration
```bash
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret MALOJA_URL="http://domain.tld" node src/index.js
```
## [Docker](https://hub.docker.com/r/foxxmd/multi-scrobbler)
Cross-platform images are built for x86 (Intel/AMD) and ARM (IE Raspberry Pi)
```
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](/docs/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](/docs/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.
+20
View File
@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+41
View File
@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
+18 -4
View File
@@ -4,12 +4,15 @@
* [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)
# Connection Issues
## Plex/Tautulli/Jellyfin don't connect
These three [sources](/README.md#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 three [sources](/#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
@@ -19,7 +22,6 @@ multi-scrobbler will log information about any server that connects to it for th
2023-02-22T10:55:56-05:00 warn : [Plex Request ] Received valid Plex webhook payload but no Plex sources are configured
```
It also logs if a server tries to connect to a URL that it does not recognize:
```
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
```
@@ -42,7 +44,7 @@ Check the output for any additional information.
#### Check Host name and URL
The URLs examples in the [configuration](/docs/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.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
@@ -69,7 +71,7 @@ Administration -> Dashboard -> Advanced -> Logs
## Jellyfin has warnings about undefined or missing data
Make sure you have
* [Configured the webhook plugin correctly](/docs/configuration.md#jellyfin)
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
@@ -120,3 +122,15 @@ 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
## Last.fm does not scrobble tracks with multiple artists correctly
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
## Jellyfin does not scrobble tracks with multiple artists correctly
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
@@ -0,0 +1,8 @@
{
"label": "Configuration",
"position": 2,
"link": {
"type": "generated-index",
"description": "Configuring Multi-Scrobbler and examples"
}
}
@@ -1,4 +1,11 @@
* [Configuration Overview](#configuration-overview)
---
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)
@@ -15,6 +22,8 @@
* [Youtube Music](#youtube-music)
* [MPRIS (Linux Desktop)](#mpris)
* [Mopidy](#mopidy)
* [JRiver](#jriver)
* [Kodi](#kodi)
* [Client Configurations](#client-configurations)
* [Maloja](#maloja)
* [Last.fm](#lastfm)
@@ -23,13 +32,13 @@
* [Webhooks](#webhook-configurations)
* [Health Endpoint](#health-endpoint)
# Configuration Overview
# Overview
[**Sources** and **Clients**](/README.md#how-does-multi-scrobbler-ms-work) are configured using environmental (ENV) variables and/or json files.
[**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**](/docs/FAQ.md) if you have any issues after configuration!
TIP: Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
## ENV-Based Configuration
@@ -41,18 +50,18 @@ This is done by passing environmental variables and so does not require any file
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](/README.md#client) and one [Source](/README.md#source) IE Plex -> Maloja
* 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](/docs/installation.md#docker) for how to configure this correctly
* 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](/README.md#source)
* 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
@@ -64,14 +73,14 @@ File-based configurations located in the `CONFIG_DIR` directory can be parsed fr
* 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](/config) directory of this project. These can be used as-is by renaming them to `.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**](/docs/kitchensink.md) that provides examples of using all sources/clients in a complex configuration.
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)
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fcommon%2Fschema%2Faio.json)
Example directory structure:
@@ -83,8 +92,7 @@ Example directory structure:
<details>
<summary>Config Example</summary>
```json5
// in config.json
```json5 title="config.json"
{
//...
"sources": [
@@ -117,7 +125,7 @@ Example directory structure:
`config.json` can also be used to set default behavior for all sources/clients using `sourceDefaults` and `clientDefaults` properties.
See [config.json.example](/config/config.json.example) for an annotated example or check out [the kitchen sink example](kitchensink.md).
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
@@ -158,7 +166,7 @@ Example directory structure:
</details>
See the [/config](/config) directory of this project for examples of each type of config file or reference specific files below.
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
@@ -177,7 +185,7 @@ Client ID/Secret. Make sure to also whitelist your redirect URI in the applicati
### File-Based
See [`spotify.json.example`](/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)
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)
@@ -191,7 +199,7 @@ Check the [instructions](plex.md) on how to setup a [webhooks](https://support.p
### File-Based
See [`plex.json.example`](/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)
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)
@@ -205,7 +213,7 @@ Check the [instructions](plex.md) on how to setup a notification agent.
### File-Based
See [`tautulli.json.example`](/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)
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/)
@@ -228,7 +236,7 @@ Can use this source for any application that implements the [Subsonic API](http:
### File-Based
See [`subsonic.json.example`](/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)
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/)
@@ -256,7 +264,7 @@ Must be using Jellyfin 10.7 or greater
### File-Based
See [`jellyfin.json.example`](/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)
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)
@@ -268,9 +276,9 @@ No support for ENV based for Last.fm as a client (only source)
### File-Based
See [`lastfm.json.example`](/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)
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)
## [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/)
@@ -280,7 +288,7 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
### File-Based
See [`listenbrainz.json.example`](/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)
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`**
@@ -312,7 +320,7 @@ After starting multi-scrobbler with credentials in-place open the dashboard (`ht
### File-Based
See [`deezer.json.example`](/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)
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)
@@ -329,7 +337,7 @@ Then...
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](/docs/google-header.jpg)
![Google Headers](google-header.jpg)
NOTES:
@@ -338,7 +346,7 @@ NOTES:
### File-Based
See [`ytmusic.json.example`](/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)
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/)
@@ -346,11 +354,11 @@ MPRIS is a standard interface for communicating with Music Players on **linux op
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](/assets/mpris.jpg)
![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**](/docs/installation.md#local) in order to use MPRIS. This cannot be used from docker.
**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
@@ -362,7 +370,7 @@ multi-scrobbler can listen to this interface and scrobble tracks played by **any
### File-Based
See [`mpris.json.example`](/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)
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/)
@@ -372,7 +380,7 @@ multi-scrobbler can scrobble tracks played from any Mopidy backend source, regar
### File-Based
See [`mopidy.json.example`](/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)
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:
@@ -404,7 +412,8 @@ Part => Default Value
* Port => `6680`
* Path => `/mopidy/ws/`
EX
<details>
<summary>URL Transform Examples</summary>
```json
{
@@ -430,6 +439,8 @@ MS transforms this to: `ws://192.168.0.101:3456/mopidy/ws/`
MS transforms this to: `ws://mopidy.mydomain.com:80/MOPWS`
</details>
#### URI Blacklist/Whitelist
@@ -460,6 +471,142 @@ EX:
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)
@@ -473,7 +620,7 @@ then multi-scrobbler will instead scrobble `Track: My Cool Track, Artist: A Coo
### File-Based
See [`maloja.json.example`](/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)
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)
@@ -496,7 +643,7 @@ or replace `localhost:9078` with your own base URL
### File-Based
See [`lastfm.json.example`](/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)
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)
@@ -515,7 +662,7 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
### File-Based
See [`listenbrainz.json.example`](/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)
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
@@ -532,13 +679,13 @@ Webhooks will **push** a notification to your configured servers on these events
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:
```json
```json5
{
"sources": [
...
//...
],
"clients": [
...
//...
],
"webhooks": [
{
@@ -550,14 +697,14 @@ Webhooks are configured in the main [config.json](#all-in-one-file-configuration
{
"name": "SecondGotifyServer",
"type": "gotify",
...
//...
},
{
"name": "NtfyServerOne",
"type": "ntfy",
...
//...
},
...
//...
]
}
```

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

@@ -1,3 +1,8 @@
---
sidebar_position: 3
title: Kitchen Sink
---
# Example Config using all Possible Features
Scenario:
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

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

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"resolveJsonModule": true,
// Extending "@tsconfig/docusaurus/tsconfig.json".types with "docusaurus-json-schema-plugin"
"types": ["node", "@docusaurus/module-type-aliases", "@docusaurus/theme-classic", "docusaurus-json-schema-plugin"]
}
}
+48
View File
@@ -0,0 +1,48 @@
NOTE: This steps are for building the flatpak entirely locally, from source. If you want to install the application normally then [get it through flathub](/docs/installation.md#flatpak)
The final build repo for the flathub version can be found at [flathub/io.github.foxxmd.multiscrobbler](https://github.com/flathub/io.github.foxxmd.multiscrobbler)
# 1. Install Requirements
## Flatpak and flatpak-builder
Install [Flatpak](https://flatpak.org/setup/)
Install [flatpak-builder](https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak)
## [flatpak-node-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/node)
Requires python 3.7+, [pip](https://pip.pypa.io/en/stable/)/[pipx](https://pypa.github.io/pipx/)
# 2. Update Project source
Set the `branch` `tag` or `commit` to use for MS in the `git` source in [`io.github.foxxmd.multiscrobbler.yml`](/flatpak/io.github.foxxmd.multiscrobbler.yml)
# 3. Use `flatpak-node-generator` to generate sources
First, [make sure `node_modules` is deleted or empty.](https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442)
Then, from MS project root:
```shell
flatpak-node-generator npm package-lock.json
```
Move `generated-sources.json` into [`/flatpak`](/flatpak)
# 4. Build flatpak
From MS project root:
```shell
cd flatpak
flatpak-builder --repo=/home/yourUser/multi-scrobbler-repo /home/yourUser/multi-scrobbler-build io.github.foxxmd.multiscrobbler.yml --force-clean
```
Add `--install --user` to have the app installed immediately.
# 5. Run (Locally)
If built with `--install --user` you can now run MS using the command
```shell
flatpak run -u io.github.foxxmd.multiscrobbler
```
+6
View File
@@ -0,0 +1,6 @@
{
"logging": {
"file": false
},
"port": 9078
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=multi-scrobbler
Comment=Scrobbles music from multiple sources to multiple clients
Icon=io.github.foxxmd.multiscrobbler
TryExec=multiscrobbler
Exec=multiscrobbler
Terminal=true
Categories=Utility;Music;ConsoleOnly;
X-Flatpak=io.github.foxxmd.multiscrobbler
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="console-application">
<id>io.github.foxxmd.multiscrobbler</id>
<provides>
<id>io.github.foxxmd.multiscrobbler.desktop</id>
</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>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<description>
<p>
Track your music listening history from many different sources:
</p>
<ul>
<li>Spotify</li>
<li>Plex (Tautulli)</li>
<li>Any subsonic-based music player (Airsonic)</li>
<li>Jellyfin</li>
<li>Mopidy</li>
<li>Deezer</li>
<li>JRiver</li>
<li>Kodi</li>
<li>MPRIS (linux desktop)</li>
</ul>
<p>and consolidate your listens (scrobbles) into one or more scrobbling servers/services:</p>
<ul>
<li>Maloja</li>
<li>Last.fm</li>
<li>Listenbrainz</li>
</ul>
<p>multi-scrobbler requires some configuration before use. Please visit the website or help links below to get started.</p>
</description>
<screenshots>
<screenshot type="default">
<caption>Browser-based UI</caption>
<image type="source">https://raw.githubusercontent.com/FoxxMD/multi-scrobbler/7b0f5801a1aa728986f440f4c67fe158cecd763d/assets/status-ui.jpg</image>
</screenshot>
</screenshots>
<releases>
<release version="0.5.0" date="2023-08-29"/>
<release version="0.4.6" date="2023-06-07"/>
<release version="0.4.4" date="2023-03-20"/>
</releases>
<categories>
<category>Utility</category>
<category>Music</category>
<category>ConsoleOnly</category>
</categories>
<content_rating type="oars-1.0">
<content_attribute id="language-profanity">mild</content_attribute>
</content_rating>
</component>
@@ -0,0 +1,77 @@
app-id: io.github.foxxmd.multiscrobbler
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: multiscrobbler
finish-args:
- --share=network
# used for access to MPRIS in order to scrobble
- --own-name=org.mpris.MediaPlayer2.*
sdk-extensions:
- org.freedesktop.Sdk.Extension.node18
modules:
- name: multiscrobbler
buildsystem: simple
build-options:
append-path: /usr/lib/sdk/node18/bin
env:
XDG_CACHE_HOME: /run/build/multiscrobbler/flatpak-node/cache
npm_config_cache: /run/build/multiscrobbler/flatpak-node/npm-cache
npm_config_nodedir: /usr/lib/sdk/node18
npm_config_offline: 'true'
build-commands:
# install npm dependencies
- npm install --offline
# build app
- NODE_ENV=production npm run build
# makes node and npm available
- mkdir -p /app/bin /app/lib /app/lib/build
- cp -a /usr/lib/sdk/node18/bin/{node,npm} /app/bin
- cp -a /usr/lib/sdk/node18/lib/* /app/lib
- rm -r /app/lib/node_modules/npm/{docs,man}
# remove dev dependencies
- npm prune --production
# copy node_modules needed to run app
- cp -r node_modules/* /app/lib/node_modules
# copy app files to runtime dir
- cp -r build/* /app/lib/build
- cp -r config /app/lib/config
- cp flatpak/defaultConfig.json /app/lib/config/config.json
- install -Dm644 flatpak/${FLATPAK_ID}.desktop /app/share/applications/${FLATPAK_ID}.desktop
- install -Dm644 flatpak/icon.png /app/share/icons/hicolor/512x512/apps/${FLATPAK_ID}.png
- install -Dm644 assets/icon.svg /app/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
- install -Dm644 flatpak/${FLATPAK_ID}.metainfo.xml /app/share/metainfo/${FLATPAK_ID}.metainfo.xml
- install -Dm755 ../run.sh /app/bin/multiscrobbler
subdir: main
sources:
- type: git
url: https://github.com/FoxxMD/multi-scrobbler
branch: develop
# in official builds use tag/commit instead of branch
# tag: 0.4.6
# commit: ed6bcd60b3448397424ed7c14075479452ff566f
dest: main
# Wrapper to launch the app
- type: script
dest-filename: run.sh
commands:
# Run config check and copy examples + default config (disable file logging)
- |
echo -e "Checking CONFIG_DIR for existing configs: ${XDG_CONFIG_HOME}"
if [ "$(ls -A ${XDG_CONFIG_HOME})" ]; then
echo "Directory is not empty! Will not add default config/examples"
else
echo "Directory is empty! Adding default config/examples"
cp -r /app/lib/config/. "${XDG_CONFIG_HOME}"/
#cp /app/lib/defaultConfig.json $XDG_CONFIG_HOME/config.json
fi
- cd /app/lib && CONFIG_DIR=$XDG_CONFIG_HOME IS_LOCAL=true NODE_ENV=production node build/server.js
- generated-sources.json
+3
View File
@@ -0,0 +1,3 @@
{
"ext": "js"
}
+21474 -537
View File
File diff suppressed because it is too large Load Diff
+87 -21
View File
@@ -1,23 +1,33 @@
{
"name": "multi-scrobbler",
"version": "0.4.0",
"version": "0.5.1",
"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 --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-source": "typescript-json-schema tsconfig.json SourceConfig --out src/common/schema/source.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aio": "typescript-json-schema tsconfig.json AIOConfig --out src/common/schema/aio.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aiosource": "typescript-json-schema tsconfig.json AIOSourceConfig --out src/common/schema/aio-source.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aioclient": "typescript-json-schema tsconfig.json AIOClientConfig --out src/common/schema/aio-client.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage"
"schema-client": "typescript-json-schema src/backend/tsconfig.json ClientConfig --out src/backend/common/schema/client.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-source": "typescript-json-schema src/backend/tsconfig.json SourceConfig --out src/backend/common/schema/source.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aio": "typescript-json-schema src/backend/tsconfig.json AIOConfig --out src/backend/common/schema/aio.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aiosource": "typescript-json-schema src/backend/tsconfig.json AIOSourceConfig --out src/backend/common/schema/aio-source.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"schema-aioclient": "typescript-json-schema src/backend/tsconfig.json AIOClientConfig --out src/backend/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
"typedoc": "typedoc",
"circular": "madge --circular --extensions ts src/index.ts",
"test": "react-scripts test",
"eject": "react-scripts eject",
"dev": "concurrently -p name -c \"yellow,magenta,blue\" -n \"webpack-server,nodemon-server,CRA\" \"npm run dev:server:webpack\" \"npm run dev:server:nodemon\" \"npm run dev:client\"",
"dev:client": "BROWSER=none react-scripts start",
"dev:server": "concurrently -p name -c \"yellow,magenta\" -n \"webpack-server,nodemon-server\" \"npm run dev:server:webpack\" \"npm run dev:server:nodemon\"",
"dev:server:webpack": "webpack --config webpack.config.server.js",
"dev:server:nodemon": "rm -f build/server.js && wait-on -l build/server.js && nodemon build/server.js",
"build": "npm run -s build:client && npm run -s build:server",
"build:client": "react-scripts build",
"build:server": "webpack --config webpack.config.server.js",
"start": "node build/server.js",
"fileEndings": "jscodeshift --transformFrom js --transformTo none --importTypes relative --extensions=ts --parser tsx --transform codeshift/transform.ts src/backend"
},
"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": {
@@ -33,16 +43,25 @@
"bugs": {
"url": "https://github.com/FoxxMD/multi-scrobbler/issues"
},
"homepage": "https://github.com/FoxxMD/multi-scrobbler#readme",
"private": true,
"homepage": ".",
"dependencies": {
"@awaitjs/express": "^0.6.3",
"@foxxmd/string-sameness": "^0.1.3",
"@foxxmd/winston": "3.3.31",
"@foxxmd/youtube-music-ts-api": "1.5.1",
"@kenyip/backoff-strategies": "^1.0.4",
"@react-nano/use-event-source": "^0.13.0",
"@reduxjs/toolkit": "^1.9.5",
"address": "^1.2.2",
"ajv": "^7.2.4",
"better-sse": "^0.8.0",
"body-parser": "^1.19.0",
"common-tags": "^1.8.2",
"compare-versions": "^4.1.2",
"concat-stream": "^2.0.0",
"dayjs": "^1.10.4",
"dbus-next": "^0.10.2",
"ejs": "^3.1.6",
"dbus-next": "0.10.2",
"es6-error": "^4.1.1",
"express": "^4.17.1",
"express-session": "^1.17.2",
@@ -51,42 +70,89 @@
"gotify": "^1.1.0",
"iti": "^0.6.0",
"json5": "^2.2.3",
"kodi-api": "^0.2.1",
"lastfm-node-client": "^2.2.0",
"mopidy": "^1.3.0",
"normalize-url": "^6.1.0",
"ntfy": "^1.0.5",
"ntfy": "1.0.5",
"p-event": "^4.2.0",
"passport": "^0.6.0",
"passport-deezer": "^0.2.0",
"pony-cause": "^1.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
"safe-stable-stringify": "^1.1.1",
"socket.io": "^4.6.1",
"spotify-web-api-node": "^5.0.2",
"superagent": "^8.0.9",
"tailwindcss": "^3.3.3",
"triple-beam": "^1.3.0",
"winston": "github:FoxxMD/winston#fbab8de969ecee578981c77846156c7f43b5f01e",
"wait-on": "^7.0.1",
"web-vitals": "^2.1.4",
"winston-daily-rotate-file": "^4.5.0",
"winston-duplex": "^0.1.1",
"winston-duplex": "0.1.3",
"winston-null": "^2.0.0",
"winston-transport": "^4.4.0",
"youtube-music-ts-api": "^1.4.1"
"xml2js": "^0.4.23"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@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/common-tags": "^1.8.1",
"@types/concat-stream": "^2.0.0",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/formidable": "^2.0.5",
"@types/jest": "^27.5.2",
"@types/jscodeshift": "^0.11.6",
"@types/mocha": "^9.1.0",
"@types/node": "^18.0.0",
"@types/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",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"concurrently": "^8.2.0",
"jscodeshift": "^0.15.0",
"mocha": "^9.2.1",
"nodemon": "^3.0.1",
"nyc": "^15.1.0",
"patch-package": "^8.0.0",
"react-scripts": "5.0.1",
"source-map-support": "^0.5.21",
"ts-essentials": "^9.1.2",
"ts-loader": "^9.4.4",
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.13.0",
"typedoc": "^0.23.28",
"typescript": "^4.9.5",
"typescript-json-schema": "~0.55"
"typescript-json-schema": "~0.55",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
},
"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,12 @@
diff --git a/node_modules/winston-daily-rotate-file/index.js b/node_modules/winston-daily-rotate-file/index.js
index c818660..a8d3040 100644
--- a/node_modules/winston-daily-rotate-file/index.js
+++ b/node_modules/winston-daily-rotate-file/index.js
@@ -1,6 +1,6 @@
'use strict';
-var winston = require('winston');
+var winston = require('@foxxmd/winston');
var DailyRotateFile = require('./daily-rotate-file');
winston.transports.DailyRotateFile = DailyRotateFile;
+10
View File
@@ -0,0 +1,10 @@
diff --git a/node_modules/winston-null/index.js b/node_modules/winston-null/index.js
index 1a8a4ff..5088d6c 100644
--- a/node_modules/winston-null/index.js
+++ b/node_modules/winston-null/index.js
@@ -1,4 +1,4 @@
-const winston = require('winston');
+const winston = require('@foxxmd/winston');
const compat = require('winston-compat');
const semver = require('semver');
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.ico"/>
<meta
name="description"
content="Dashboard for Multi Scrobbler"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Multi Scrobbler</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+25
View File
@@ -0,0 +1,25 @@
{
"short_name": "Multi-Scrobbler",
"name": "Multi-Scrobbler Dashboard",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Overrides the tsconfig used for the app.
* In the test environment we need some tweaks.
*/
const tsNode = require('ts-node');
const tsConfigPaths = require('tsconfig-paths');
const mainTSConfig = require('./src/backend/tsconfig.json');
tsConfigPaths.register({
baseUrl: './src/backend/tests',
paths: {
...mainTSConfig.compilerOptions.paths,
}
});
tsNode.register({
files: true,
transpileOnly: true,
project: './src/backend/tsconfig.json'
});
-260
View File
@@ -1,260 +0,0 @@
import AbstractApiClient from "./AbstractApiClient.js";
import request, {Request} from 'superagent';
import {ListenBrainzClientData} from "../common/infrastructure/config/client/listenbrainz.js";
import {FormatPlayObjectOptions, PlayObject} from "../common/infrastructure/Atomic.js";
import dayjs from "dayjs";
export interface ArtistMBIDMapping {
artist_credit_name: string
artist_mbid: string
join_phrase: string
}
export interface MinimumTrack {
artist_name: string;
track_name: string;
release_name?: string;
}
export interface AdditionalTrackInfo {
artist_mbids?: string[]
release_mbid?: string
release_group_mbid?: string
recording_mbid?: string
submission_client?: string
submission_client_version?: string
spotify_id?: string
media_player?: string
media_player_version?: string
music_service?: string
music_service_name?: string
origin_url?: string
tags?: string[]
duration?: number
duration_ms?: number
track_mbid?: string
work_mbids?: string[]
}
export interface Track {
artist_name: string;
track_name: string;
release_name?: string;
artist_mbids?: string[];
artist_msid?: string;
recording_mbid?: string;
release_mbid?: string;
release_msid?: string;
tags?: string[];
duration?: number
}
export interface AdditionalTrackInfoResponse extends AdditionalTrackInfo {
recording_msid?: string
}
export interface TrackPayload extends MinimumTrack {
additional_info?: AdditionalTrackInfo
}
export interface ListenPayload {
listened_at: Date | number;
recording_msid?: string;
track_metadata: TrackPayload;
}
export interface SubmitPayload {
listen_type: 'single',
payload: [ListenPayload]
}
export interface TrackResponse extends MinimumTrack {
duration: number
additional_info: AdditionalTrackInfoResponse
mbid_mapping: {
artist_mbids?: string[]
artists?: ArtistMBIDMapping[]
caa_id?: number
caa_release_mbid?: string
recording_mbid?: string
release_mbid?: string
}
}
export interface ListensResponse {
count: number;
listens: ListenResponse[];
}
export interface ListenResponse {
inserted_at: number
listened_at: number;
recording_msid?: string;
track_metadata: TrackResponse;
}
export class ListenbrainzApiClient extends AbstractApiClient {
declare config: ListenBrainzClientData;
url: string;
constructor(name: any, config: ListenBrainzClientData, options = {}) {
super('ListenBrainz', name, config, options);
const {
url = 'https://api.listenbrainz.org/'
} = config;
this.url = url;
}
callApi = async <T>(req: Request, retries = 0): Promise<T> => {
const {
maxRequestRetries = 2,
retryMultiplier = 1.5
} = this.config;
try {
req.set('Authorization', `Token ${this.config.token}`);
return await req as T;
} catch (e) {
const {
message,
} = e;
throw e;
}
}
testConnection = async () => {
try {
const resp = await this.callApi(request.get(this.url))
return true;
} catch (e) {
if(e.status === 410) {
return true;
}
return false;
}
}
testAuth = async () => {
try {
const resp = await this.callApi(request.get(`${this.url}1/validate-token`));
return true;
} catch (e) {
return false;
}
}
getUserListens = async (maxTracks: number, user?: string): Promise<ListensResponse> => {
try {
const resp = await this.callApi(request
.get(`${this.url}1/user/${user ?? this.config.username}/listens`)
// this endpoint can take forever, sometimes, and we want to make sure we timeout in a reasonable amount of time for polling sources to continue trying to scrobble
.timeout({
response: 15000, // wait 15 seconds before timeout if server doesn't response at all
deadline: 30000 // wait 30 seconds overall for request to complete
})
.query({
count: maxTracks
}));
const {body: {payload}} = resp as any;
return payload as ListensResponse;
} catch (e) {
throw e;
}
}
getRecentlyPlayed = async (maxTracks: number, user?: string): Promise<PlayObject[]> => {
try {
const resp = await this.getUserListens(maxTracks, user);
return resp.listens.map(x => ListenbrainzApiClient.listenResponseToPlay(x));
} catch (e) {
this.logger.error(`Error encountered while getting User listens | Error => ${e.message}`);
return [];
}
}
submitListen = async (play: PlayObject) => {
try {
const listenPayload: SubmitPayload = {listen_type: 'single', payload: [ListenbrainzApiClient.playToListenPayload(play)]};
await this.callApi(request.post(`${this.url}1/submit-listens`).type('json').send(listenPayload));
return listenPayload;
} catch (e) {
throw e;
}
}
static playToListenPayload = (play: PlayObject): ListenPayload => {
return {
listened_at: (play.data.playDate ?? dayjs()).unix(),
track_metadata: {
artist_name: play.data.artists[0],
track_name: play.data.track,
additional_info: {
duration: play.data.duration !== undefined ? Math.round(play.data.duration) : undefined
}
}
}
}
static listenResponseToPlay = (listen: ListenResponse): PlayObject => {
const {
listened_at,
recording_msid,
track_metadata: {
track_name,
artist_name,
release_name,
duration,
additional_info: {
recording_msid: aRecordingMsid,
recording_mbid: aRecordingMbid,
duration: aDuration,
duration_ms: aDurationMs,
} = {},
mbid_mapping: {
artists: artistMappings = [],
recording_mbid: mRecordingMbid
} = {}
} = {}
} = listen;
const playId = recording_msid ?? aRecordingMsid;
const trackId = aRecordingMbid ?? mRecordingMbid;
let dur = duration ?? aDuration;
if (dur === undefined && aDurationMs !== undefined) {
dur = Math.round(aDurationMs / 1000);
}
let artists: string[] = [artist_name];
if (artistMappings.length > 0) {
const secondaryArtists = artistMappings.filter(x => x.artist_credit_name !== artist_name);
artists = artists.concat(secondaryArtists.map(x => x.artist_credit_name));
}
return {
data: {
playDate: dayjs.unix(listened_at),
track: track_name,
artists: artists,
album: release_name,
duration: dur
},
meta: {
source: 'listenbrainz',
trackId,
playId
}
}
}
static formatPlayObj = (obj: any, options: FormatPlayObjectOptions): PlayObject => {
return ListenbrainzApiClient.listenResponseToPlay(obj);
}
}
+43
View File
@@ -0,0 +1,43 @@
/**
* Shamelessly stolen from https://stackoverflow.com/a/43593634/1469797
* */
class TupleMap<X,Y,Z> {
private map = new Map<string, Z>();
set(key: [X, Y], value: Z): this {
this.map.set(JSON.stringify(key), value);
return this;
}
get(key: [X, Y]): Z | undefined {
return this.map.get(JSON.stringify(key));
}
clear() {
this.map.clear();
}
delete(key: [X, Y]): boolean {
return this.map.delete(JSON.stringify(key));
}
has(key: [X, Y]): boolean {
return this.map.has(JSON.stringify(key));
}
get size() {
return this.map.size;
}
values() {
return this.map.values();
}
forEach(callbackfn: (value: Z, key: [X, Y], map: Map<string, Z>) => void, thisArg?: any): void {
this.map.forEach((value, key) => {
callbackfn.call(thisArg, value, JSON.parse(key), this.map);
});
}
}
export default TupleMap
+9
View File
@@ -0,0 +1,9 @@
import EventEmitter from "events";
export class WildcardEmitter extends EventEmitter {
emit(type: string, ...args: any) {
const argsWithName = args.length === 0 ? [type] : [...args, type];
super.emit('*', ...argsWithName);
return super.emit(type, ...argsWithName) || super.emit('', ...argsWithName);
}
}
@@ -4,5 +4,5 @@ import * as path from 'path';
//const __filename = fileURLToPath(import.meta.url);
//const __dirname = path.dirname(__filename);
export const projectDir = path.resolve(__dirname, '../../');
export const projectDir = process.cwd(); //path.resolve(__dirname, '../../../');
export const configDir: string = path.resolve(projectDir, './config');
+194
View File
@@ -0,0 +1,194 @@
import {Dayjs} from "dayjs";
import {FixedSizeList} from 'fixed-size-list';
import {Logger} from '@foxxmd/winston';
import TupleMap from "../TupleMap";
import {Request, Response} from "express";
import {NextFunction, ParamsDictionary, Query} from "express-serve-static-core";
import { LogLevel, logLevels, PlayMeta, PlayObject } from "../../../core/Atomic";
export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic' | 'mpris' | 'mopidy' | 'listenbrainz' | 'jriver' | 'kodi';
export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris', 'mopidy', 'listenbrainz', 'jriver', 'kodi'];
export const lowGranularitySources: SourceType[] = ['subsonic','ytmusic'];
export type ClientType = 'maloja' | 'lastfm' | 'listenbrainz';
export const clientTypes: ClientType[] = ['maloja', 'lastfm', 'listenbrainz'];
export type InitState = 0 | 1 | 2;
export const NOT_INITIALIZED: InitState = 0;
export const INITIALIZING: InitState = 1;
export const INITIALIZED: InitState = 2;
export const initStates: InitState[] = [NOT_INITIALIZED, INITIALIZING, INITIALIZED];
export type ReadyState = 0 | 1 | 2;
export const NOT_READY: ReadyState = 0;
export const GETTING_READY: ReadyState = 1;
export const READY: ReadyState = 2;
export const readyStates: ReadyState[] = [NOT_READY, GETTING_READY, READY];
export interface InternalConfig {
localUrl: string
configDir: string
logger: Logger
}
export type ReportedPlayerStatus = 'playing' | 'stopped' | 'paused' | 'unknown';
export const REPORTED_PLAYER_STATUSES = {
playing: 'playing' as ReportedPlayerStatus,
stopped: 'stopped' as ReportedPlayerStatus,
paused: 'paused' as ReportedPlayerStatus,
unknown: 'unknown' as ReportedPlayerStatus
}
export type CalculatedPlayerStatus = ReportedPlayerStatus | 'stale' | 'orphaned';
export const CALCULATED_PLAYER_STATUSES = {
...REPORTED_PLAYER_STATUSES,
stale: 'stale' as CalculatedPlayerStatus,
orphaned: 'orphaned' as CalculatedPlayerStatus,
}
export interface ConfigMeta {
source: string
mode?: string
configureAs: string
}
export type SourceData = (PlayObject | PlayerStateData);
export interface PlayerStateData {
platformId: PlayPlatformId
play: PlayObject
status?: ReportedPlayerStatus
position?: number
timestamp?: Dayjs
}
export const asPlayerStateData = (obj: object): obj is PlayerStateData => {
return 'platformId' in obj && 'play' in obj;
}
export interface FormatPlayObjectOptions {
newFromSource?: boolean
parsedFrom?: string
[key: string]: any
}
export interface ProgressAwarePlayObject extends PlayObject {
meta: PlayMeta & {
initialTrackProgressPosition?: number
}
}
export type DeviceId = string;
export type PlayUserId = string;
export type PlayPlatformId = [DeviceId, PlayUserId];
export type GroupedPlays = TupleMap<DeviceId,PlayUserId,ProgressAwarePlayObject[]>;
export type GroupedFixedPlays = TupleMap<DeviceId,PlayUserId,FixedSizeList<ProgressAwarePlayObject>>;
export const NO_DEVICE = 'NoDevice';
export const NO_USER = 'SingleUser';
export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];
export interface ScrobbledPlayObject {
play: PlayObject
scrobble: PlayObject
}
export interface RemoteIdentityParts {
host: string,
proxy: string | undefined,
agent: string | undefined
}
export interface LogConfig {
level?: string
file?: string | false
stream?: string
console?: string
}
export interface LogOptions {
/**
* Specify the minimum log level for all log outputs without their own level specified.
*
* Defaults to env `LOG_LEVEL` or `info` if not specified.
*
* @default 'info'
* */
level?: LogLevel
/**
* Specify the minimum log level to output to rotating files. If `false` no log files will be created.
* */
file?: LogLevel | false
/**
* Specify the minimum log level streamed to the UI
* */
stream?: LogLevel
/**
* Specify the minimum log level streamed to the console (or docker container)
* */
console?: LogLevel
}
export const asLogOptions = (obj: LogConfig = {}): obj is LogOptions => {
return Object.entries(obj).every(([key, val]) => {
if(key !== 'file') {
return val === undefined || logLevels.includes(val.toLocaleLowerCase());
}
return val === undefined || val === false || logLevels.includes(val.toLocaleLowerCase());
});
}
// https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist#comment116238286_49725198
export type RequireAtLeastOne<T, R extends keyof T = keyof T> = Omit<T, R> & { [ P in R ] : Required<Pick<T, P>> & Partial<Omit<T, P>> }[R];
export const DEFAULT_SCROBBLE_DURATION_THRESHOLD: number = 30;
export interface ScrobbleThresholdResult {
passes: boolean
duration: {
passes?: boolean
value?: number
threshold?: number
}
percent: {
passes?: boolean
value?: number
threshold?: number
}
}
export interface RegExResult {
match: string,
groups: string[],
index: number
named: NamedGroup
}
export interface NamedGroup {
[name: string]: any
}
export type ExpressRequest = Request<ParamsDictionary, any, any, Query, Record<string, any>>;
export type ExpressResponse = Response<any, Record<string, any>>;
export type ExpressHandler = (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>
export interface numberFormatOptions {
toFixed: number,
defaultVal?: any,
prefix?: string,
suffix?: string,
round?: {
type?: string,
enable: boolean,
indicate?: boolean,
}
}
export const DELIMITERS = [',','&','\/','\\'];
@@ -0,0 +1,54 @@
import { CommonSourceOptions, ScrobbleThresholds, SourceRetryOptions } from "./source/index";
import { RequestRetryOptions } from "./common";
import { SourceAIOConfig } from "./source/sources";
import { ClientAIOConfig } from "./client/clients";
import { WebhookConfig } from "./health/webhooks";
import { LogOptions } from "../Atomic";
export interface SourceDefaults extends SourceRetryOptions {
/**
* Set thresholds for when multi-scrobbler should consider a tracked play to be "scrobbable". If both duration and percent are defined then if either condition is met the track is scrobbled.
* */
scrobbleThresholds?: ScrobbleThresholds
options?: CommonSourceOptions
}
export interface AIOConfig {
sourceDefaults?: SourceDefaults
clientDefaults?: RequestRetryOptions
sources?: SourceAIOConfig[]
clients?: ClientAIOConfig[]
webhooks?: WebhookConfig[]
/**
* Set the port the multi-scrobbler UI will be served from
*
* @default 9078
* @examples [9078]
* */
port?: number
logging?: LogOptions
/**
* Enables ALL relevant logging and debug options for all sources/clients, when none are defined.
*
* This is a convenience shortcut for enabling all output needed to troubleshoot an issue and does not need to be on for normal operation.
*
* It can also be enabled with the environmental variable DEBUG_MODE=true
*
* @default false
* @examples [false]
* */
debugMode?: boolean
}
export interface AIOClientConfig {
clientDefaults?: RequestRetryOptions
clients?: ClientAIOConfig[]
}
export interface AIOSourceConfig {
sourceDefaults?: SourceRetryOptions
sources?: SourceAIOConfig[]
}
@@ -0,0 +1,7 @@
import { MalojaClientAIOConfig, MalojaClientConfig } from "./maloja";
import { LastfmClientAIOConfig, LastfmClientConfig } from "./lastfm";
import { ListenBrainzClientAIOConfig, ListenBrainzClientConfig } from "./listenbrainz";
export type ClientConfig = MalojaClientConfig | LastfmClientConfig | ListenBrainzClientConfig;
export type ClientAIOConfig = MalojaClientAIOConfig | LastfmClientAIOConfig | ListenBrainzClientAIOConfig;
@@ -1,4 +1,4 @@
import {CommonConfig, CommonData} from "../common.js";
import { CommonConfig, CommonData } from "../common";
/**
* Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging.
@@ -1,5 +1,5 @@
import {CommonClientConfig, CommonClientData} from "./index.js";
import {RequestRetryOptions} from "../common.js";
import { CommonClientConfig, CommonClientData } from "./index";
import { RequestRetryOptions } from "../common";
export interface LastfmData extends RequestRetryOptions {
/**
@@ -1,5 +1,5 @@
import {CommonClientConfig, CommonClientData} from "./index.js";
import {RequestRetryOptions} from "../common.js";
import { CommonClientConfig, CommonClientData } from "./index";
import { RequestRetryOptions } from "../common";
export interface ListenBrainzData extends RequestRetryOptions{
/**
@@ -1,5 +1,5 @@
import {CommonClientConfig, CommonClientData} from "./index.js";
import {RequestRetryOptions} from "../common.js";
import { CommonClientConfig, CommonClientData } from "./index";
import { RequestRetryOptions } from "../common";
export interface MalojaClientData extends RequestRetryOptions, CommonClientData {
/**
@@ -1,4 +1,4 @@
import {CommonSourceConfig, CommonSourceData} from "./index.js";
import { CommonSourceConfig, CommonSourceData } from "./index";
export interface DeezerData extends CommonSourceData {
/**
@@ -0,0 +1,83 @@
import { CommonConfig, CommonData, RequestRetryOptions } from "../common";
export interface SourceRetryOptions extends RequestRetryOptions {
/**
* default # of automatic polling restarts on error
*
* @default 5
* @examples [5]
* */
maxPollRetries?: number
}
export interface ScrobbleThresholds {
/**
* The number of seconds before a track should be considered scrobbled.
*
* @default 30
* @examples [30]
* */
duration?: number
/**
* The percentage of a track that should have been seen played before it should be scrobbled. Only used if the Source provides information about how long the track is.
*
* NOTE: This should be used with care when the Source is a "polling" type (has an 'interval' property). If the track is short and the interval is too high MS may ignore the track if percentage is high because it had not "seen" the track for long enough from first discovery, even if you have been playing the track for longer.
* */
percent?: number
}
export interface CommonSourceOptions {
/**
* If this source has INGRESS to MS (sends a payload, rather than MS GETTING requesting a payload)
* then setting this option to true will make MS log the payload JSON to DEBUG output
*
* @default false
* @examples [false]
* */
logPayload?: boolean
/**
* If this source has INGRESS to MS and has filters this determines how MS logs when a payload (event) fails a defined filter (IE users/servers/library filters)
*
* * `false` => do not log
* * `debug` => log to DEBUG level
* * `warn` => log to WARN level (default)
*
* Hint: This is useful if you are sure this source is setup correctly and you have multiple other sources. Set to `debug` or `false` to reduce log noise.
*
* @default warn
* @examples ["warn"]
* */
logFilterFailure?: false | 'debug' | 'warn'
/**
* For Sources that track Player State (currently playing) this logs a simple player state/summary to DEBUG output
*
* @default false
* @examples [false]
* */
logPlayerState?: boolean
}
export interface CommonSourceData extends CommonData, SourceRetryOptions {
/**
* Set thresholds for when multi-scrobbler should consider a tracked play to be "scrobbable". If both duration and percent are defined then if either condition is met the track is scrobbled.
* */
scrobbleThresholds?: ScrobbleThresholds
}
export interface CommonSourceConfig extends CommonConfig {
/**
* Unique identifier for this source.
* */
name?: string
/**
* Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.
*
* @examples [["MyMalojaConfigName","MyLastFMConfigName"]]
* */
clients?: string[]
data?: CommonSourceData
options?: CommonSourceOptions
}
@@ -1,4 +1,4 @@
import {CommonSourceConfig, CommonSourceData} from "./index.js";
import { CommonSourceConfig, CommonSourceData } from "./index";
export interface JellyData extends CommonSourceData {
/**
@@ -29,6 +29,20 @@ export interface JellyData extends CommonSourceData {
* @examples [false]
* */
logPayload?: boolean
/**
* How MS should log when a Jellyfin event fails a defined filter (users/servers)
*
* * `false` => do not log
* * `debug` => log to DEBUG level
* * `warn` => log to WARN level (default)
*
* Hint: This is useful if you are sure this source is setup correctly and you have multiple other Jellyfin sources. Set to `debug` or `false` to reduce log noise.
*
* @default warn
* @examples ["warn"]
* */
logFilterFailure?: false | 'debug' | 'warn'
}
}
@@ -0,0 +1,41 @@
import { CommonSourceConfig, CommonSourceData } from "./index";
import { PollingOptions } from "../common";
export interface JRiverData extends CommonSourceData, PollingOptions {
/**
* URL of the JRiver HTTP server to connect to
*
* multi-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:52199/MCWS/v1/`
*
* The URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.
*
* Parts => [default value]
*
* * Protocol => `http://`
* * Hostname => `localhost`
* * Port => `52199`
* * Path => `/MCWS/v1/`
*
*
* @examples ["http://localhost:52199/MCWS/v1/"]
* @default "http://localhost:52199/MCWS/v1/"
* */
url: string
/**
* If you have enabled authentication, the username you set
* */
username?: string
/**
* If you have enabled authentication, the password you set
* */
password?: string
}
export interface JRiverSourceConfig extends CommonSourceConfig {
data: JRiverData
}
export interface JRiverSourceAIOConfig extends JRiverSourceConfig {
type: 'jriver'
}
@@ -0,0 +1,42 @@
import { CommonSourceConfig, CommonSourceData } from "./index";
import { PollingOptions } from "../common";
export interface KodiData extends CommonSourceData, PollingOptions {
/**
* URL of the Kodi HTTP server to connect to
*
* multi-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`
*
* The URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.
*
* Parts => [default value]
*
* * Protocol => `http://`
* * Hostname => `localhost`
* * Port => `8080`
* * Path => `/jsonrpc`
*
*
* @examples ["http://localhost:8080/jsonrpc"]
* @default "http://localhost:8080/jsonrpc"
* */
url: string
/**
* The username set for Remote Control via Web Sever
* */
username: string
/**
* The password set for Remote Control via Web Sever
* */
password: string
}
export interface KodiSourceConfig extends CommonSourceConfig {
data: KodiData
}
export interface KodiSourceAIOConfig extends KodiSourceConfig {
type: 'kodi'
}

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