Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1acab0fed | ||
|
|
d39a7099c7 | ||
|
|
7b2c4f99f3 | ||
|
|
b941ff08dc | ||
|
|
210a74c75f | ||
|
|
eafce71cd0 | ||
|
|
d5777b501d | ||
|
|
dd60e5eb21 | ||
|
|
c57dbf23e1 | ||
|
|
cb52adb4de | ||
|
|
a75b0fc549 | ||
|
|
1a546b72d2 | ||
|
|
fd13d268d0 | ||
|
|
5837fc11a5 | ||
|
|
efdbbe40e8 | ||
|
|
5103cf1c12 | ||
|
|
e855a3de7d | ||
|
|
b90e38d595 | ||
|
|
1864f39be0 | ||
|
|
89e80b6f8b | ||
|
|
564295ee64 | ||
|
|
61c7f26007 | ||
|
|
54b9ece5b6 | ||
|
|
335136561a | ||
|
|
56f1cacd07 | ||
|
|
558e358d0b | ||
|
|
290e6eecc4 | ||
|
|
f512f55782 | ||
|
|
ad6eba58d2 | ||
|
|
b675aee8ff | ||
|
|
c29ea9c89c | ||
|
|
50016a31a8 | ||
|
|
1cecffc30f | ||
|
|
833273c19d | ||
|
|
35603d0caf | ||
|
|
de1377afdd | ||
|
|
a22ddd10cc | ||
|
|
280add7107 | ||
|
|
b34abcab56 | ||
|
|
788b26949e | ||
|
|
c11da3df3d | ||
|
|
0d68fea0eb | ||
|
|
c98672bcef | ||
|
|
bb0c079757 | ||
|
|
891144546e | ||
|
|
86611d764d | ||
|
|
e604268004 | ||
|
|
396c3da277 | ||
|
|
0591b90089 | ||
|
|
6763be53ac | ||
|
|
aee23f7550 | ||
|
|
57b5528d43 | ||
|
|
bd8cf18a1a | ||
|
|
84acdd901a | ||
|
|
e213a95996 | ||
|
|
78cd2eecf9 | ||
|
|
ebfeea1087 | ||
|
|
ddca623792 | ||
|
|
f0da35d115 | ||
|
|
10eb355f92 | ||
|
|
85702c534a | ||
|
|
e2dfe317a9 | ||
|
|
7008ae8672 | ||
|
|
10d9fa3581 | ||
|
|
2b85927739 | ||
|
|
8c402238ed | ||
|
|
758770d278 | ||
|
|
ee7ebb159b | ||
|
|
f47a66060f | ||
|
|
8929a630ed | ||
|
|
077f5d7547 | ||
|
|
369ad42946 | ||
|
|
4bd996eb46 | ||
|
|
a6ddc2b9f5 | ||
|
|
afb2e76b2d | ||
|
|
47812546e3 | ||
|
|
5ab6b85b30 | ||
|
|
9709d6348a | ||
|
|
10fea9b23d | ||
|
|
051dd5d128 | ||
|
|
369e18b1c2 | ||
|
|
fd49d17de4 | ||
|
|
e0f034fa99 | ||
|
|
80bc629ac2 | ||
|
|
1a90c23e76 | ||
|
|
48ddd4272f | ||
|
|
ea408ac7a4 | ||
|
|
79c48af3e9 | ||
|
|
12c2c0a132 | ||
|
|
78abfc2168 | ||
|
|
0f721b94c2 | ||
|
|
f126bf0ec0 | ||
|
|
064a435c79 | ||
|
|
c0e5bb872a | ||
|
|
e9561b5783 | ||
|
|
77c3a2f0c7 | ||
|
|
db4e64dc2a | ||
|
|
fcc05a8b19 | ||
|
|
c37d94e71b | ||
|
|
aa59bc4607 | ||
|
|
a993df4f0b | ||
|
|
7e83e0f085 | ||
|
|
5b2449adb0 | ||
|
|
5811912745 | ||
|
|
b7d6e9e497 | ||
|
|
5fd0cc3fe4 | ||
|
|
6e2e6f4492 | ||
|
|
ed6f7cfe5f | ||
|
|
6b17babd7a | ||
|
|
c77e4355fc | ||
|
|
25de65f41f | ||
|
|
af037ee215 | ||
|
|
39c4764b40 | ||
|
|
ec28d2aeb5 | ||
|
|
baf8635587 | ||
|
|
a3839a28a2 | ||
|
|
4247f545c5 | ||
|
|
c54c3ff5de | ||
|
|
1d84f07a40 | ||
|
|
b47c80dc11 | ||
|
|
75e2b2b337 | ||
|
|
08ca7fff35 | ||
|
|
3140df28b7 | ||
|
|
14546b8d74 | ||
|
|
341e896eca | ||
|
|
b3b4636e7f | ||
|
|
43f5aeb5dc | ||
|
|
d38aabf7b8 | ||
|
|
c64b8d16e6 | ||
|
|
1938fc3a27 | ||
|
|
36181396c7 | ||
|
|
bb76a3b214 | ||
|
|
339bb7fcd2 | ||
|
|
3a25057d05 | ||
|
|
14ab6032f5 | ||
|
|
3e1f785437 | ||
|
|
91bb9d6089 | ||
|
|
9cf373e9f4 | ||
|
|
9ec22e9000 | ||
|
|
9616860e23 | ||
|
|
0f6638dfdf | ||
|
|
fe9f49779c | ||
|
|
3aab6a207b | ||
|
|
49e9fd4b3b | ||
|
|
0143ae62aa | ||
|
|
2882f65b69 | ||
|
|
62e9f7f620 | ||
|
|
57f2ebf2b5 | ||
|
|
884134cab9 | ||
|
|
0c90de78ab | ||
|
|
1c5530ee2a | ||
|
|
fbb67a8fe6 | ||
|
|
c8c5f5dcb7 | ||
|
|
eb449eeca6 | ||
|
|
7cfdb10813 | ||
|
|
be599b5562 | ||
|
|
07d97a4937 | ||
|
|
7037d6f833 | ||
|
|
2deca322e7 | ||
|
|
ab84cb396d | ||
|
|
bb63c61ca8 | ||
|
|
f4256d1084 | ||
|
|
fc87b86f52 | ||
|
|
5dff617ed6 | ||
|
|
f2a171c596 | ||
|
|
336970c1f7 | ||
|
|
d04bc038d4 | ||
|
|
1ab12355f3 | ||
|
|
fb5d9fb7b0 | ||
|
|
9aae57f966 | ||
|
|
fefc82c521 | ||
|
|
43ebdcb10c | ||
|
|
adfb166320 | ||
|
|
1c54ae6465 | ||
|
|
9309f7d8cf | ||
|
|
8717fad08a | ||
|
|
983b4c7417 | ||
|
|
a663e9af14 | ||
|
|
249a606bf5 | ||
|
|
0ec8d8814d | ||
|
|
f0cee72f43 | ||
|
|
a82aafa6b8 | ||
|
|
3b624205ad | ||
|
|
c6d38e40d0 | ||
|
|
1ca7834feb | ||
|
|
77c9977d27 | ||
|
|
521130562d | ||
|
|
9512ca7e84 | ||
|
|
159901f79e | ||
|
|
9758568ded | ||
|
|
a953f83d07 | ||
|
|
2bbe2e6ba0 | ||
|
|
ec4ab8c078 | ||
|
|
7885c4ca9c | ||
|
|
8371ec3a01 | ||
|
|
43cbadcd2d | ||
|
|
6e1e2c3534 | ||
|
|
a46389e745 | ||
|
|
ce5f8d5fca | ||
|
|
bdbbce4cb0 | ||
|
|
034cb321ef | ||
|
|
ac2d453aa2 | ||
|
|
860d2d953f | ||
|
|
a486102747 | ||
|
|
735264cb2b | ||
|
|
7dece447df | ||
|
|
3fb5d6aae9 | ||
|
|
d5dfc33fce | ||
|
|
6dc40e50ad | ||
|
|
b8ebd8ca7e | ||
|
|
e9eb7d357f | ||
|
|
3f2cb8ac0c | ||
|
|
8e36c27ec9 | ||
|
|
fa90a3b821 | ||
|
|
2d2319fa27 | ||
|
|
ba161b7b92 | ||
|
|
d438c8b256 | ||
|
|
bbb4555c0e | ||
|
|
6e8ea96f32 | ||
|
|
b06de77f42 | ||
|
|
3aa8279914 | ||
|
|
a64724e114 | ||
|
|
052234a3d9 | ||
|
|
aa2c565afd | ||
|
|
d0fd225d99 | ||
|
|
c40acdd7c7 | ||
|
|
590610022d | ||
|
|
f7dae32f50 | ||
|
|
3cbbc0837f | ||
|
|
0197e70a1a | ||
|
|
40a7d9e398 | ||
|
|
c7b2dcbbbe | ||
|
|
a524e18820 | ||
|
|
81894d691b | ||
|
|
0fb192dd41 | ||
|
|
3f608c6c8e | ||
|
|
dcff9ee73d | ||
|
|
f1cd66d3e8 | ||
|
|
d95fef09a3 | ||
|
|
d1060e724e | ||
|
|
a7faaaac86 | ||
|
|
3762f95bae | ||
|
|
b9f04deb28 | ||
|
|
9cc820c76e | ||
|
|
f7195fb40c | ||
|
|
9c0f029992 | ||
|
|
f370b8ad1e | ||
|
|
7c962ec43a | ||
|
|
2013a6692e | ||
|
|
4a5eb91f16 | ||
|
|
a606801bb0 | ||
|
|
db395e40e8 | ||
|
|
1c498a2c71 | ||
|
|
e4a647d425 | ||
|
|
c1a91ab610 | ||
|
|
093007a72b | ||
|
|
cb5cdb09f7 | ||
|
|
8f2e1ad7e7 | ||
|
|
ed6bcd60b3 | ||
|
|
edbe131f1b | ||
|
|
881e537a89 | ||
|
|
abb286f4a8 | ||
|
|
04ffc0a589 | ||
|
|
4cccdeeff0 | ||
|
|
2295cf4756 | ||
|
|
dcd63ff093 | ||
|
|
c4bf076d0a | ||
|
|
5b32602aca | ||
|
|
82167ab5ec | ||
|
|
aac963a21c | ||
|
|
fc84d846b2 | ||
|
|
7674593bfe | ||
|
|
500dc809bf | ||
|
|
b4e9c52301 | ||
|
|
6efac246ff | ||
|
|
bec841e833 | ||
|
|
25ebfbcccf | ||
|
|
74c2bed572 | ||
|
|
a1672dfa45 | ||
|
|
abe494e2b0 | ||
|
|
fabcfc1377 | ||
|
|
9f89507ce4 | ||
|
|
f277557e83 | ||
|
|
59dfdf1400 | ||
|
|
322b25db00 | ||
|
|
a2b424de93 | ||
|
|
09e6e5e534 | ||
|
|
5450c8ddf1 | ||
|
|
99e772e9f6 | ||
|
|
69fb3f1dbd | ||
|
|
e9c4f9f9bb | ||
|
|
fa6e46f453 | ||
|
|
2d6bafecaf | ||
|
|
7dffc3def7 | ||
|
|
922f4be14f | ||
|
|
e7bab87e6e | ||
|
|
dd67501337 | ||
|
|
4f60f2fa9a | ||
|
|
7cb215be52 | ||
|
|
e2b472cf44 | ||
|
|
64c46ac87f | ||
|
|
334b782ca3 | ||
|
|
9e214243c9 | ||
|
|
77bf84fef4 | ||
|
|
cde6046ffb | ||
|
|
af4ac98357 | ||
|
|
6f66813db0 | ||
|
|
f42fef85b7 | ||
|
|
400f448108 | ||
|
|
4659735c12 | ||
|
|
c08fe0e54b | ||
|
|
37d4a1c81a | ||
|
|
857971517f | ||
|
|
51d16c9026 | ||
|
|
6b4fdf53eb | ||
|
|
09dd5b7c5f | ||
|
|
c182767e5e | ||
|
|
efa93a1529 | ||
|
|
6efd1e54a8 | ||
|
|
231bd03d9a | ||
|
|
38c096b4d6 | ||
|
|
68ca275673 | ||
|
|
de05612873 | ||
|
|
bcb0c7471b | ||
|
|
f856f79e51 | ||
|
|
fbea08dded | ||
|
|
dc52376395 | ||
|
|
1e2bc0ca85 | ||
|
|
7849d6098a | ||
|
|
0b101b758e | ||
|
|
756ff0e0c1 | ||
|
|
2ddeca7d29 | ||
|
|
1de934e30a | ||
|
|
0409c526ee | ||
|
|
a7c340188f | ||
|
|
5dc54849cc | ||
|
|
058fbee0c8 | ||
|
|
daea76eef3 | ||
|
|
b81b1d94a3 | ||
|
|
226b7c741c | ||
|
|
e452bd761c | ||
|
|
19c3d97a46 | ||
|
|
64164b28de | ||
|
|
b03cbe7a0a | ||
|
|
91bd702deb | ||
|
|
ede22e10e9 | ||
|
|
c648ad4ae3 | ||
|
|
a0fbd1129a | ||
|
|
b90ea03924 | ||
|
|
88efe2ad87 | ||
|
|
39f933418e | ||
|
|
cc8c7d84e2 | ||
|
|
6d9783c5f5 | ||
|
|
c10d46b774 | ||
|
|
a5417277a8 | ||
|
|
5a1c931513 | ||
|
|
09f4503593 | ||
|
|
045cae66f5 | ||
|
|
e67ef5fb0f | ||
|
|
1677c0021f | ||
|
|
3400e747a8 | ||
|
|
c08ff4843c | ||
|
|
5e905eae17 | ||
|
|
7ebe73ccb3 | ||
|
|
574911cfa0 | ||
|
|
91a84dc4f4 | ||
|
|
9a578e54a8 | ||
|
|
1a8d5eded3 | ||
|
|
94765057db | ||
|
|
86c639ad2c | ||
|
|
f3045debb3 | ||
|
|
ffc826f04d | ||
|
|
e70a4bbf1c | ||
|
|
a9eca51bfa | ||
|
|
de4a0aecd5 | ||
|
|
f097fac8ea | ||
|
|
a20b35f7cd | ||
|
|
2d51a0e159 | ||
|
|
e524077242 | ||
|
|
a5b5fe8d2a | ||
|
|
ddb3e0e02d | ||
|
|
ec97ec428a | ||
|
|
49b834af5d | ||
|
|
6329aaf6a9 | ||
|
|
fd6e5ab084 | ||
|
|
1dbb1b9ba3 | ||
|
|
fc65a63d43 | ||
|
|
4d1ddde57b | ||
|
|
7d14fb5341 | ||
|
|
e310b3aec8 | ||
|
|
056457d028 | ||
|
|
73183e760e | ||
|
|
3fff729d32 | ||
|
|
19e199bd58 | ||
|
|
7b0f5801a1 | ||
|
|
4dbea91a5e | ||
|
|
548356aaa4 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
## Checklist before requesting a review
|
||||
|
||||
- [ ] **I am opening this PR for the [`develop` branch](https://github.com/FoxxMD/multi-scrobbler/tree/develop) and NOT `master`.**
|
||||
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## Describe your changes
|
||||
|
||||
|
||||
|
||||
## Issue number and link, if applicable
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"reporter": "dot"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Creating a Pull Request
|
||||
|
||||
Please follow these guidelines when contributing code to this repository:
|
||||
|
||||
* The PR **must be for the [`develop` branch.](https://github.com/FoxxMD/multi-scrobbler/tree/develop)** The `master` branch is for releases only.
|
||||
* Use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format when creating commits.
|
||||
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `develop`.
|
||||
* Ensure that if your code is covered by [an existing test](./src/backend/tests) that you have updated the test accordingly
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM lsiobase/alpine:3.17 as base
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
|
||||
@@ -6,9 +6,12 @@ RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
alpine-base \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
#yarn \
|
||||
openssh && \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
@@ -23,31 +26,59 @@ ENV CONFIG_DIR=$data_dir
|
||||
|
||||
COPY docker/root/ /
|
||||
|
||||
RUN npm install -g patch-package \
|
||||
&& chown -R root:root /usr/local/lib/node_modules/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
|
||||
|
||||
RUN npm install
|
||||
# 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 \
|
||||
&& chown -R root:root node_modules
|
||||
#RUN yarn install
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
|
||||
# need to set before build so server/client build is optimized and has constants (if needed)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run build && rm -rf node_modules
|
||||
#RUN yarn run build && rm -rf node_modules
|
||||
|
||||
FROM base as app
|
||||
|
||||
COPY --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/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
ENV 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
|
||||
&& chown -R abc:abc node_modules \
|
||||
&& rm -rf node_modules/@types
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
|
||||
@@ -4,30 +4,37 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](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)
|
||||
* [JRiver](/docs/configuration.md#jriver)
|
||||
* [Spotify](/docsite/docs/configuration/configuration.md#spotify)
|
||||
* [Plex](/docsite/docs/configuration/configuration.md#plex) or [Tautulli](/docsite/docs/configuration/configuration.md#tautulli)
|
||||
* [Subsonic-compatible APIs](/docsite/docs/configuration/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
|
||||
* [WebScrobbler](/docsite/docs/configuration/configuration.md#webscrobbler)
|
||||
* [Youtube Music](/docsite/docs/configuration/configuration.md#youtube-music)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm-source)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz--source-)
|
||||
* [Deezer](/docsite/docs/configuration/configuration.md#deezer)
|
||||
* [MPRIS (Linux Desktop)](/docsite/docs/configuration/configuration.md#mpris)
|
||||
* [Mopidy](/docsite/docs/configuration/configuration.md#mopidy)
|
||||
* [JRiver](/docsite/docs/configuration/configuration.md#jriver)
|
||||
* [Kodi](/docsite/docs/configuration/configuration.md#kodi)
|
||||
* [Google Cast (Chromecast)](/docsite/docs/configuration/configuration.md#google-cast--chromecast-)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](/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
|
||||
* Graceful network and client failure handling (queued scrobbles that auto-retry)
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through ENVs or JSON
|
||||
* 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?**
|
||||
|
||||
@@ -38,9 +45,9 @@ 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">
|
||||
<img src="/assets/status-ui.png" width="800">
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
@@ -67,11 +74,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
|
||||
|
||||
@@ -86,7 +93,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
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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 |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 527 KiB |
@@ -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"
|
||||
@@ -0,0 +1,217 @@
|
||||
import {API, Collection, FileInfo, Options} from 'jscodeshift';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
export type TransformFrom = 'none' | 'any' | string;
|
||||
export type TransformTo = 'none' | 'any' | string;
|
||||
export type ImportTypes = 'any' | 'relative' | 'alias';
|
||||
|
||||
const base = process.cwd();
|
||||
export default function transformer(
|
||||
file: FileInfo,
|
||||
{ jscodeshift: j }: API,
|
||||
options: Options,
|
||||
) {
|
||||
|
||||
const tf = options.transformFrom as string | undefined;
|
||||
if(tf === undefined) {
|
||||
throw new Error(`arg '--transformFrom' must be defined.`);
|
||||
}
|
||||
const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[];
|
||||
const tfIsAny = transformFrom.includes('any');
|
||||
|
||||
const tt = options.transformTo as string | undefined;
|
||||
if(tt === undefined) {
|
||||
throw new Error(`arg '--transformTo' must be defined.`);
|
||||
}
|
||||
const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[];
|
||||
|
||||
if(transformTo.length > 1 && transformFrom.length !== transformTo.length) {
|
||||
throw new Error('When more than one Transform To is specified then number of Transform From arguments must match');
|
||||
}
|
||||
|
||||
const itypes = options.importTypes as string | undefined;
|
||||
if(itypes === undefined) {
|
||||
throw new Error(`arg '--importTypes' must be defined.`);
|
||||
}
|
||||
const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[];
|
||||
const importsIsAny = importTypes.includes('any');
|
||||
|
||||
const fileDir = path.dirname(path.join(base, file.path));
|
||||
const processingFilePath = file.path;
|
||||
console.log(`Processing => ${processingFilePath}`);
|
||||
let source: Collection<any>;
|
||||
try {
|
||||
source = j(file.source);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse ${processingFilePath}, will skip`);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const imports = source.find(j.ImportDeclaration)
|
||||
imports.forEach((x) => {
|
||||
const importPath = x.value.source.value as string;
|
||||
const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`;
|
||||
|
||||
const pathInfo = path.parse(importPath);
|
||||
const hasNoExt = pathInfo.ext === '';
|
||||
let filenameFromDir: string;
|
||||
if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) {
|
||||
const pathFull = path.join(fileDir, importPath);
|
||||
const dir = path.dirname(pathFull);
|
||||
const normalExt = pathInfo.ext.replace('.', '');
|
||||
let isRelative: boolean | undefined;
|
||||
let dirExists: boolean | undefined;
|
||||
let fileExists: boolean | undefined;
|
||||
try {
|
||||
// is dir path real?
|
||||
fs.realpathSync(dir);
|
||||
dirExists = true;
|
||||
} catch (e) {
|
||||
isRelative = false;
|
||||
dirExists = false;
|
||||
}
|
||||
|
||||
if(isRelative === undefined) {
|
||||
// if extension is none we need to check for any file in dir with this name
|
||||
if(hasNoExt) {
|
||||
const dirFiles = fs.readdirSync(dir)
|
||||
filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name);
|
||||
if(filenameFromDir === undefined) {
|
||||
fileExists = false;
|
||||
isRelative = false;
|
||||
} else {
|
||||
fileExists = true;
|
||||
isRelative = true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// does a file exist?
|
||||
fs.realpathSync(pathFull);
|
||||
isRelative = true;
|
||||
} catch (e) {
|
||||
isRelative = false;
|
||||
fileExists = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!importsIsAny) {
|
||||
if(!isRelative && !importTypes.includes('alias')) {
|
||||
console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`);
|
||||
return;
|
||||
} else if(isRelative && !importTypes.includes('relative')) {
|
||||
console.log(`${pathFull} => Import is relative but import types does not specify relative`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// determine transformTo
|
||||
|
||||
// if there is only one TO then use it
|
||||
let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined;
|
||||
if(derivedTT === undefined) {
|
||||
// otherwise we find the TO by using the same index as the matching FROM extension type
|
||||
const tfIndex = transformFrom.findIndex((x) => {
|
||||
if(x === 'any') {
|
||||
return true;
|
||||
}
|
||||
if(x === 'none' && hasNoExt) {
|
||||
return true;
|
||||
}
|
||||
if(x === normalExt) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(tfIndex === -1) {
|
||||
console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`);
|
||||
return;
|
||||
}
|
||||
derivedTT = transformTo[tfIndex];
|
||||
}
|
||||
|
||||
let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`;
|
||||
|
||||
let transformedImport: string;
|
||||
|
||||
switch(derivedTT) {
|
||||
case 'none':
|
||||
if(hasNoExt) {
|
||||
console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`);
|
||||
return;
|
||||
}
|
||||
transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name);
|
||||
break;
|
||||
case 'any':
|
||||
if(hasNoExt && filenameFromDir) {
|
||||
transformedImport = filenameFromDir;
|
||||
} else {
|
||||
const otherFilename = fs.readdirSync(dir).find(x => {
|
||||
const pinfo = path.parse(x);
|
||||
return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext;
|
||||
});
|
||||
if(otherFilename === undefined) {
|
||||
console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
|
||||
return;
|
||||
}
|
||||
transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename);
|
||||
transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})`
|
||||
}
|
||||
break;
|
||||
default:
|
||||
transformedImport = combineDirFile(pathInfo.dir, `${path.parse(filenameFromDir).name}.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`);
|
||||
j(x).replaceWith(
|
||||
j.importDeclaration(
|
||||
x.node.specifiers,
|
||||
j.stringLiteral(transformedImport)
|
||||
)
|
||||
);
|
||||
|
||||
} else {
|
||||
console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Early exit condition
|
||||
* -----
|
||||
* It is often good practice to exit early and return the original source file
|
||||
* if it does not contain code relevant to the codemod.
|
||||
* See this page for more information:
|
||||
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output
|
||||
*/
|
||||
// if (/* Some condition here */ true) {
|
||||
// return file.source;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Codemod logic goes here 👇
|
||||
* -----
|
||||
* This is where the core logic for your codemod will go,
|
||||
* consider grouping specific actions into 'motions' and running them in sequence
|
||||
*
|
||||
* See this page for more information:
|
||||
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions
|
||||
*/
|
||||
//source.findVariableDeclarators('foo').renameTo('bar');
|
||||
|
||||
/**
|
||||
* Return your modified AST here 👇
|
||||
* -----
|
||||
* This is where your modified AST will be transformed back into a string
|
||||
* and written back to the file.
|
||||
*/
|
||||
return source.toSource(options.printOptions);
|
||||
}
|
||||
|
||||
const combineDirFile = (dir: string, file: string) => {
|
||||
if(dir === '') {
|
||||
return file;
|
||||
}
|
||||
return `${dir}${path.sep}${file}`;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"data": {
|
||||
"blacklistedDevices": [],
|
||||
"whitelistedDevices": [],
|
||||
"blacklistedApps": [],
|
||||
"whitelistedApps": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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,
|
||||
@@ -11,6 +21,7 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "spotify",
|
||||
"enable": true,
|
||||
"clients": ["myConfig"],
|
||||
"name": "mySpotifySource",
|
||||
"data": {
|
||||
@@ -23,6 +34,7 @@
|
||||
"clients": [
|
||||
{
|
||||
"type": "maloja",
|
||||
"enable": true,
|
||||
"name": "myConfig",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "FoxxMDeezer",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
[
|
||||
{
|
||||
"name": "MyJellyfin",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"users": ["FoxxMD"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logPayload": false
|
||||
}
|
||||
"servers": ["myServer","anotherServer"]
|
||||
},
|
||||
"options": {
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyJriver",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "0.0.0.0",
|
||||
"username": "auser",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MyKodi",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://localhost:8080/jsonrpc",
|
||||
"username": "myUser",
|
||||
"password": "myPassword"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myLastFm",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "brainz",
|
||||
"configureAs": "client"
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "FoxxMD"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myMaloja",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyMopidy",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "localhost",
|
||||
"uriBlacklist": [],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "ubuntu",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"whitelist": ["vlc", "mpd"],
|
||||
"blacklist": ["spotify"]
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
[
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"]
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MySpotify",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MySubsonic",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://localhost:4040/airsonic",
|
||||
"user": "yourUser",
|
||||
"password": "yourPassword",
|
||||
"password": "yourPassword"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
[
|
||||
{
|
||||
"name": "MyTautuilli",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"]
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "MyWebScrobbler",
|
||||
"data": {
|
||||
"slug": null,
|
||||
"whitelist": [],
|
||||
"blacklist": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,10 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MyYTMusic",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"cookie": "VISITOR_INFO1_LIVE=jMDXz2_L8rY; __Secure-3PAPISID=3AxsXpSXGqOInSDn1jEKn; DEVICE_INFO=ChxOekU0TmTBpjek5EWZ0G; YSC=7gZdl3Zdl3; SID=TwhNsaZRXYTAtXxzGyu6rZdpg2HvGROeW8J4Ym_FhkhoZMUYEQ.; __Secure-1PSID=TwhNOsaZRXYTyRBe4rxAtXRIKsIEtk_Qot2VLBNfHQrQ.; __Secure-3PSID=ZRXYTAtXRIKsIEtk_Qot2yRBerZdpg2HvvZRXYTAtXRIKsIEtk_Qot2yRBerkuZICFQ.; HSID=A1UMmELW79; SSID=AKhomOs; APISID=IlHHmuzkPdQzZZDhHn3; SAPISID=3AxsXpy0u75Qb/n1jEKn; __Secure-1PAPISID=3AxsXpQb/AkSDn1jEKn; LOGIN_INFO=AFmP6vFpyVCZZAIgDwbkhWMBBhluaIWAPP:QUQ314UW5NWMjNmd2ZUJnYnJsakdIMjZoaE5zVVMjNmd2ZZUiHRlb3ZlV3ZIcUVyRVIMjNmdjNmd2ZZUivYlNqX2ZNZUiHdUNFNFdaYmJIW1NkJRX3hqdlU2YnFESkFuSS1uTldnZVRmLXNjWFc5OUJuR3dTd3JsZGZYa2EtZFQ2a0k2Ry1KQQ==; PREF=volume=26; SIDCC=AFvI_94PxXwls-ndqpGfPgFX3FWj80y_94PxXwls-ndqfSh15sP; __Secure-1PSIDCC=AFvIBnUbRr96I96UCIp2U4T8HRVk2B0HfKzhzxwsiP; __Secure-3PSIDCC=AFvIB3bINuUN0ETDR9gO91wpwWIVmpGki3BxT3bINuUN0ETDR9gO91wCH",
|
||||
"authUser": "0",
|
||||
"authUser": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- SPOTIFY_CLIENT_ID=
|
||||
#- SPOTIFY_CLIENT_SECRET=
|
||||
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
|
||||
#- MALOJA_URL=http://maloja:42010
|
||||
#- MALOJA_API_KEY=
|
||||
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
volumes:
|
||||
- /path_on_host/multi-scrobbler-config:/config
|
||||
#networks:
|
||||
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
|
||||
ports:
|
||||
- 9078:9078 # first port is the HOST port multi-scrobbler will serve UI on
|
||||
restart: unless-stopped
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- SPOTIFY_CLIENT_ID=
|
||||
#- SPOTIFY_CLIENT_SECRET=
|
||||
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
|
||||
#- MALOJA_URL=http://maloja:42010
|
||||
#- MALOJA_API_KEY=
|
||||
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
|
||||
# set if using a source/client with redirect URI that you have not explicitly set and MS is NOT running on the same machine that you will view the dashboard from
|
||||
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
|
||||
#- BASE_URL=http://MyHostIP:9078
|
||||
volumes:
|
||||
- /path_on_host/multi-scrobbler-config:/config
|
||||
#networks:
|
||||
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
|
||||
ports:
|
||||
- 9078:9078 # first port is the HOST port multi-scrobbler will serve UI and API on
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
|
||||
# NODE_ARGS can be passed by ENV in docker command like "docker run foxxmd/multi-scrobbler -e NODE_ARGS=--optimize_for_size"
|
||||
echo -e "\nmulti-scrobbler is starting!"
|
||||
export NODE_ENV=production
|
||||
cd /app || exit
|
||||
exec \
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/src/index.js run
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/node_modules/.bin/tsx /app/src/backend/index.ts
|
||||
|
||||
@@ -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.
|
||||
@@ -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*
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,15 +1,23 @@
|
||||
<!-- TOC -->
|
||||
* [Connection Issues](#connection-issues)
|
||||
* [Plex/Tautulli/Jellyfin don't connect](#plextautullijellyfin-dont-connect)
|
||||
* [Jellyfin has warnings about undefined or missing data](#jellyfin-has-warnings-about-undefined-or-missing-data)
|
||||
* [Jellyfin has warnings about missing headers](#jellyfin-has-warnings-about-missing-headers)
|
||||
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
|
||||
* [Configuration Issues](#configuration-issues)
|
||||
* [Config could not be parsed](#config-could-not-be-parsed)
|
||||
* [Scrobbling Issues](#scrobbling-issues)
|
||||
* [Last.fm does not scrobble tracks with multiple artists correctly](#lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
* [Jellyfin does not scrobble tracks with multiple artists correctly](#jellyfin-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
* [Google Cast track information is missing/incorrect or MS player has weird times](#google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times)
|
||||
* [Google Cast device does not track media](#google-cast-device-does-not-track-media)
|
||||
<!-- TOC -->
|
||||
|
||||
# Connection Issues
|
||||
|
||||
## Plex/Tautulli/Jellyfin don't connect
|
||||
|
||||
These three [sources](/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 +27,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 +49,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 +76,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`.
|
||||
@@ -93,6 +100,22 @@ You can verify the payload sent from the webhook by modifying your jellyfin conf
|
||||
|
||||
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
|
||||
|
||||
## Jellyfin has warnings about missing headers
|
||||
|
||||
If you experience issues trying to scrobble with Jellyfin and find this in your MS logs
|
||||
|
||||
```
|
||||
[API] Jellyfin is not sending a request with valid headers...
|
||||
```
|
||||
|
||||
A workaround that may fix this:
|
||||
|
||||
* In Webhook settings:
|
||||
* [In the webhook you have already configured...](configuration/configuration.md#jellyfin)
|
||||
* Add Request Header...
|
||||
* **Key:** `Content-Type`
|
||||
* **Value:** `application/json`
|
||||
* Then Save
|
||||
|
||||
## Spotify/Deezer/LastFM won't authenticate
|
||||
|
||||
@@ -120,3 +143,39 @@ 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)
|
||||
|
||||
## Google Cast track information is missing/incorrect or MS player has weird times
|
||||
|
||||
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
|
||||
|
||||
To diagnose these issues you [**must enable payload logging**](configuration/configuration.md#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
|
||||
|
||||
## Google Cast device does not track media
|
||||
|
||||
It is likely the app playing on the cast device is incorrectly reporting the media type as **not music**.
|
||||
|
||||
MS logs will tell you what type the media is reported as with lines like:
|
||||
|
||||
```
|
||||
My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track
|
||||
```
|
||||
|
||||
Refer to [Allow Unknown Media Type](configuration/configuration.md#allow-unknown-media-type) section to fix this
|
||||
|
||||
```
|
||||
My Artist - Example Track has 'movie' media type so will not track
|
||||
```
|
||||
|
||||
Refer to [Force Media Tracking](configuration/configuration.md#forcing-media-tracking) section to fix this
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Configuring Multi-Scrobbler and examples"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
* [Configuration Overview](#configuration-overview)
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
<!-- TOC -->
|
||||
* [Overview](#overview)
|
||||
* [ENV-Based Configuration](#env-based-configuration)
|
||||
* [File-Based Configuration](#file-based-configuration)
|
||||
* [All-in-One File Configuration](#all-in-one-file-configuration)
|
||||
* [Specific File Configuration](#specific-file-configuration)
|
||||
* [Application Options](#application-options)
|
||||
* [Base URL](#base-url)
|
||||
* [Source Configurations](#source-configurations)
|
||||
* [Spotify](#spotify)
|
||||
* [Plex](#plex)
|
||||
@@ -13,24 +23,34 @@
|
||||
* [Listenbrainz (Source)](#listenbrainz--source-)
|
||||
* [Deezer](#deezer)
|
||||
* [Youtube Music](#youtube-music)
|
||||
* [MPRIS (Linux Desktop)](#mpris)
|
||||
* [MPRIS](#mpris)
|
||||
* [Mopidy](#mopidy)
|
||||
* [JRiver](#jriver)
|
||||
* [Kodi](#kodi)
|
||||
* [WebScrobbler](#webscrobbler)
|
||||
* [Multiple Users](#multiple-users)
|
||||
* [Google Cast (Chromecast)](#google-cast--chromecast-)
|
||||
* [Connecting Devices](#connecting-devices)
|
||||
* [What Media Does MS Scrobble?](#what-media-does-ms-scrobble)
|
||||
* [Cast Troubleshooting](#cast-troubleshooting)
|
||||
* [Client Configurations](#client-configurations)
|
||||
* [Maloja](#maloja)
|
||||
* [Last.fm](#lastfm)
|
||||
* [Listenbrainz](#listenbrainz)
|
||||
* [Monitoring](#monitoring)
|
||||
* [Webhooks](#webhook-configurations)
|
||||
* [Webhook Configurations](#webhook-configurations)
|
||||
* [Gotify](#gotify)
|
||||
* [Ntfy](#ntfy)
|
||||
* [Health Endpoint](#health-endpoint)
|
||||
<!-- TOC -->
|
||||
|
||||
# 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
|
||||
|
||||
@@ -42,18 +62,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
|
||||
@@ -65,14 +85,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%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
Example directory structure:
|
||||
|
||||
@@ -84,8 +104,7 @@ Example directory structure:
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
```json5
|
||||
// in config.json
|
||||
```json5 title="config.json"
|
||||
{
|
||||
//...
|
||||
"sources": [
|
||||
@@ -118,7 +137,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
|
||||
|
||||
@@ -159,7 +178,22 @@ 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.
|
||||
|
||||
# Application Options
|
||||
|
||||
These options affect multi-scrobbler's behavior and are not specific to any source/client.
|
||||
|
||||
#### Base URL
|
||||
|
||||
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](#spotify), [lastfm](#lastfm), and [deezer](#deezer) -- as well as some logging hints.
|
||||
|
||||
* Default => `http://localhost`
|
||||
* Set with [ENV](#env-based-configuration) `BASE_URL` or `baseUrl` [all-in-one configuration](#all-in-one-file-configuration)
|
||||
|
||||
EX: Lastfm Redirect Url is `BASE_URL:PORT/lastfm/callback` -- Set `BASE_URL=http://192.168.0.101` => Redirect URL is `http://192.168.0.101:9078/lastfm/callback` (when no other redirectUri is specified for [lastfm configuration](#lastfm))
|
||||
|
||||
Useful when running with [docker](../installation/installation.md#docker) so that you do not need to specify redirect URLs for each configuration.
|
||||
|
||||
# Source Configurations
|
||||
|
||||
@@ -168,6 +202,8 @@ See the [/config](/config) directory of this project for examples of each type o
|
||||
To access your Spotify history you must [register an application](https://developer.spotify.com/dashboard) to get a
|
||||
Client ID/Secret. Make sure to also whitelist your redirect URI in the application settings.
|
||||
|
||||
**NOTE:** If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-is-Automix/ta-p/5257278) enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is "mixed" in your queue with a shorter play time than its actual length and [Spotify does not report this modified play time in its API.](https://community.spotify.com/t5/Spotify-for-Developers/Wrong-duration-ms-of-track-with-Automix/m-p/5429147) This **does not affect MS's ability to scrobble** from Spotify but it will affect the accuracy of the duration MS reports was played.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
@@ -178,7 +214,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Plex](https://plex.tv)
|
||||
|
||||
@@ -192,7 +228,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Tautulli](https://tautulli.com)
|
||||
|
||||
@@ -206,11 +242,11 @@ 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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Subsonic](http://www.subsonic.org/)
|
||||
|
||||
Can use this source for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) (such as [Airsonic](https://airsonic.github.io/))
|
||||
Can use this source for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) and supports the [`getNowPlaying`](http://www.subsonic.org/pages/api.jsp#getNowPlaying) endpoint (such as [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
|
||||
**Known Issues:**
|
||||
* "Time played at" is somewhat inaccurate since the api only reports "played X minutes ago" so...
|
||||
@@ -229,7 +265,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Jellyfin](https://jellyfin.org/)
|
||||
|
||||
@@ -248,6 +284,8 @@ Must be using Jellyfin 10.7 or greater
|
||||
* Check `Send All Properties`
|
||||
* Save
|
||||
|
||||
If you see errors in the MS logs regarding `missing headers` when using Jellyfin [see this workaround.](../FAQ.md#jellyfin-has-warnings-about-missing-headers)
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
@@ -257,7 +295,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Last.fm (Source)](https://www.last.fm)
|
||||
|
||||
@@ -269,7 +307,7 @@ 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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Listenbrainz (Source)](https://listenbrainz.org)
|
||||
|
||||
@@ -281,7 +319,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
**Change `configureAs` to `source`**
|
||||
|
||||
@@ -313,7 +351,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Youtube Music](https://music.youtube.com)
|
||||
|
||||
@@ -330,7 +368,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
|
||||
|
||||

|
||||

|
||||
|
||||
NOTES:
|
||||
|
||||
@@ -339,7 +377,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/)
|
||||
|
||||
@@ -347,11 +385,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.
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -363,7 +401,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Mopidy](https://mopidy.com/)
|
||||
|
||||
@@ -373,7 +411,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
Configuration Options:
|
||||
|
||||
@@ -530,7 +568,263 @@ MS transforms this to: `http://mydomain.com:80/jriverReverse/MCWS/v1/`
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`jriver.json.example`](/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)
|
||||
See [`jriver.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/jriver.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FJRiverSourceConfig/%23%2Fdefinitions%2FJRiverData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Kodi](https://kodi.tv/)
|
||||
|
||||
In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can can be in the Kodi GUI:
|
||||
|
||||
* Settings -> Services -> Control
|
||||
* Check `Allow remote control via HTTP`
|
||||
* Ensure you have a **Username** and **Password** set, you will need to provide them in the ENV/File configuration below.
|
||||
|
||||
#### URL
|
||||
|
||||
If you do not provide a URL then a default is used which assumes Kodi is installed on the same server as multi-scrobbler: `http://localhost:8080/jsonrpc`
|
||||
|
||||
* Make sure the port number matches what is found in **Port** in the [Control](#kodi) section mentioned above.
|
||||
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
|
||||
|
||||
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
|
||||
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
|
||||
|
||||
Part => Default Value
|
||||
|
||||
* Protocol => `http://`
|
||||
* Hostname => `localhost`
|
||||
* Port => `8080`
|
||||
* Path => `/jsonrpc`
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "kodi.mydomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://kodi.mydomain.com:8080/jsonrpc`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "192.168.0.101:3456"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://192.168.0.101:3456/jsonprc`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "mydomain.com:80/kodiReverse/jsonrpc"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://mydomain.com:80/kodiReverse/jsonrpc`
|
||||
|
||||
</details>
|
||||
|
||||
### ENV-Based
|
||||
|
||||
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|-------------------------------|----------------------------|
|
||||
| KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
|
||||
| KODI_USERNAME | No | | The username set |
|
||||
| KODI_PASSWORD | No | | The password set |
|
||||
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`kodi.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/kodi.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FKodiSourceConfig/%23%2Fdefinitions%2FKodiData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [WebScrobbler](https://web-scrobbler.com/)
|
||||
|
||||
After installing the extension open the preferences/settings for it:
|
||||
|
||||
* Under **Accounts**
|
||||
* **Add Webhook**
|
||||
* API URL: `http://localhost:9078/api/webscrobbler`
|
||||
* Application name: `(whatever you want)`
|
||||
|
||||
Reload the extension after adding the webhook.
|
||||
|
||||
* **On Firefox** - Only FQNs (domain.tld), `localhost`, and `127.0.0.1` are supported for API URL due to [firefox requiring https](https://github.com/web-scrobbler/web-scrobbler/issues/4183#issuecomment-1749222006)
|
||||
* **On Chromium-based Browsers** - Any domain will work for API URL
|
||||
* All Other browsers are untested
|
||||
|
||||
#### Multiple Users
|
||||
|
||||
If you would like use multiple WebScrobbler sources they can be matched using a **slug** at the end of the **API URL.** This requires using [a file-based config.](#file-based-configuration)
|
||||
|
||||
Example:
|
||||
|
||||
In `webscrobbler.json`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "aUserWS",
|
||||
"clients": [
|
||||
"client1Maloja"
|
||||
],
|
||||
"data": {
|
||||
"slug": "usera"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bUserWS",
|
||||
"clients": [
|
||||
"client2Maloja"
|
||||
],
|
||||
"data": {
|
||||
"slug": "userb"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
* To use `aUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/usera`
|
||||
* To use `bUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/userb`
|
||||
|
||||
Note: `http://localhost:9078/api/webscrobbler` is matched with the first source that _that does not have a slug defined._
|
||||
|
||||
##### Connectors Black/Whitelist
|
||||
|
||||
MS can be configured to only scrobble, or NOT scrobble, from some WS connectors. Use the name of the website from the [supported websites](https://web-scrobbler.com/) or from the **Connectors** tab in the extension. Note that this **only** affects MS's behavior and does not affect the general connector behavior you have configured within the WebScrobbler extension.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------------------------------------------|
|
||||
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
|
||||
| WS_WHITELIST | No | | Only scrobble from these WebScrobbler Connectors. Comma-delimited list |
|
||||
| WS_BLACKLIST | No | | Do not scrobble from these WebScrobbler Connectors. Comma-delimited list |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`webscrobbler.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/webscrobbler.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FWebScrobblerSourceConfig/%23%2Fdefinitions%2FWebScrobblerData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Google Cast (Chromecast)](https://www.google.com/chromecast/built-in/)
|
||||
|
||||
**NOTE:** Google Cast support is **experimental**. You may experience crashes and errors while using this Source. Please open an issue if you experience problems and include all information detailed in the issue template to help debug your issue.
|
||||
|
||||
If your media device can be **Cast** to using this button  on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.
|
||||
|
||||
**Note:** This source relies on common, **basic** music data provided by the cast device which will always be less exhaustive than data parsed from full source integrations. If there is an existing [Source](#source-configurations) it is recommended to configure for it and blacklist the app on Google Cast, rather than relying solely on Google Cast for scrobbling.
|
||||
|
||||
### Connecting Devices
|
||||
|
||||
Cast devices can be manually configured using [File-based configuration](#file-based-14) OR automatically discovered using **mDNS.**
|
||||
|
||||
##### mDNS Discovery
|
||||
|
||||
The host machine running multi-scrobbler must be configured to allow [mDNS traffic on port 5353/UDP](https://book.hacktricks.xyz/network-services-pentesting/5353-udp-multicast-dns-mdns).
|
||||
|
||||
##### Linux
|
||||
|
||||
**Docker**
|
||||
|
||||
The host machine must have [avahi-daemon](https://avahi.org/) running to circumvent limitations with DNS resolution due to musl in Alpine. Most major linux distributions package avahi and many have it built-in. Once avahi is running you must pass D-Bus and the avahi daemon socket to your container like so:
|
||||
|
||||
```
|
||||
docker run ... -v /var/run/dbus:/var/run/dbus -v /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket ... foxxmd/multi-scrobbler
|
||||
```
|
||||
|
||||
**Flatpak/Nodejs**
|
||||
|
||||
No additional steps are required.
|
||||
|
||||
##### Windows
|
||||
|
||||
**Docker**
|
||||
|
||||
Unsupported at this time.
|
||||
|
||||
**Nodejs**
|
||||
|
||||
No additional steps are required.
|
||||
|
||||
### What Media Does MS Scrobble?
|
||||
|
||||
Cast devices report what type of media the current activity is [(see `metadata` property here)](https://developers.google.com/cast/docs/media/messages#MediaInformation). The reported type is dependent on the application playing the media to correctly report it, the cast device does not magically know what the media is. If an application does not report a type it is always classified as `unknown`.
|
||||
|
||||
**By default, MS will only track media that is reported as `MusicTrack`.**
|
||||
|
||||
#### Allow Unknown Media Type
|
||||
|
||||
Media with an Unknown (`Generic`) media type can be explicitly allowed by setting `"allowUnknownMedia": true` in the [file-based configuration.](#file-based-14) This can also be configured to only allow unknown media types for specific applications by using a list of application names like:
|
||||
|
||||
```json5
|
||||
// in chromecast.json or config.json sources
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"type": "chromecast",
|
||||
"data": {
|
||||
// only allow unknown if app name contains any of these phrases
|
||||
"allowUnknownMedia": ["smarttube", "default media receiver"]
|
||||
},
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Forcing Media Tracking
|
||||
|
||||
MS can be forced to track media from an application regardless of media type. This is useful if an application incorrectly reports a media type you are sure should be music. Set `"forceMediaRecognitionOn"` in the [file-based configuration.](#file-based-14) to a list of application names that should always be tracked like:
|
||||
|
||||
```json5
|
||||
// in chromecast.json or config.json sources
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"type": "chromecast",
|
||||
"data": {
|
||||
// media from applications that contains these phrases will always be tracked, regardless of media type reported
|
||||
"forceMediaRecognitionOn": ["smarttube", "default media receiver"]
|
||||
},
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
### Cast Troubleshooting
|
||||
|
||||
Please include any/all logs with raw output if there are any errors encountered as this is critical to diagnosing issues.
|
||||
|
||||
To diagnose bad/incomplete track information or strange MS player behavior please turn on **payload logging** and include log output of the source running to help diagnose this issue:
|
||||
|
||||
```json5
|
||||
// in chromecast.json or config.json sources
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"type": "chromecast",
|
||||
"data": {
|
||||
//...
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### ENV-Based
|
||||
|
||||
Note: [Manually configuring cast device connections](#connecting-devices) is only available through [File-based config.](#file-based-14)
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------------------------------------------------------|
|
||||
| CC_ENABLE | No | | Set to 'true' to enable Cast monitoring without needing to define other ENVs |
|
||||
| CC_WHITELIST_DEVICES | No | | Only scrobble from these Cast devices. Comma-delimited list. EX mini-home, family-tv |
|
||||
| CC_BLACKLIST_DEVICES | No | | Do not scrobble from these Cast devices. Comma-delimited list |
|
||||
| CC_WHITELIST_APPS | No | | Only scrobble from these casted Apps. Comma-delimited list. EX spotify, pandora |
|
||||
| CC_BLACKLIST_APPS | No | | Do not scrobble from these casted Apps. Comma-delimited list |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`chromecast.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/chromecast.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FChromecastSourceConfig/%23%2Fdefinitions%2FChromecastData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
# Client Configurations
|
||||
|
||||
@@ -545,7 +839,7 @@ See [`jriver.json.example`](/config/jriver.json.example) or [explore the schema
|
||||
|
||||
### 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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
## [Last.fm](https://www.last.fm)
|
||||
|
||||
@@ -568,7 +862,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
## [Listenbrainz](https://listenbrainz.org)
|
||||
|
||||
@@ -587,7 +881,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%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
# Monitoring
|
||||
|
||||
@@ -604,13 +898,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": [
|
||||
{
|
||||
@@ -622,21 +916,21 @@ Webhooks are configured in the main [config.json](#all-in-one-file-configuration
|
||||
{
|
||||
"name": "SecondGotifyServer",
|
||||
"type": "gotify",
|
||||
...
|
||||
//...
|
||||
},
|
||||
{
|
||||
"name": "NtfyServerOne",
|
||||
"type": "ntfy",
|
||||
...
|
||||
//...
|
||||
},
|
||||
...
|
||||
//...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [Gotify](https://gotify.net/)
|
||||
|
||||
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Faio.json)
|
||||
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
@@ -658,7 +952,7 @@ EX
|
||||
|
||||
### [Ntfy](https://ntfy.sh/)
|
||||
|
||||
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Faio.json)
|
||||
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
|
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:
|
||||
|
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
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"`
|
||||
|
||||
##### Base URL
|
||||
|
||||
Optionally, when
|
||||
|
||||
* using a [Source or Client](../configuration/configuration.md) that has a "Redirect URI" that you have not explicitly defined
|
||||
* and
|
||||
* using a bridge network or
|
||||
* installing MS on a different machine than the one used to view the dashboard
|
||||
|
||||
set the [Base URL](../configuration/configuration.md#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
|
||||
|
||||
* With docker: `-e BASE_URL="http://hostMachineIP"` (first port is the port on the host to use)
|
||||
* With docker-compose: [see comments in docker-compose.yml](../../../docker-compose.yml)
|
||||
|
||||
#### Other
|
||||
|
||||
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
|
||||
|
||||
### Linux Host
|
||||
|
||||
If you are
|
||||
|
||||
* using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
|
||||
* running docker on MacOS or Windows
|
||||
|
||||
this **DOES NOT** apply to you.
|
||||
|
||||
If you are running Docker on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||
|
||||
To get the UID and GID for the current user run these commands from a terminal:
|
||||
|
||||
* `id -u` -- prints UID
|
||||
* `id -g` -- prints GID
|
||||
|
||||
## Docker Usage Examples
|
||||
|
||||
If installing on a different machine make sure all redirect URIs are defined or that you have set a [Base URL](#base-url).
|
||||
|
||||
### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
|
||||
|
||||
```bash
|
||||
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
|
||||
```
|
||||
|
||||
### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
|
||||
|
||||
```bash
|
||||
docker run -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
|
||||
```
|
||||
|
||||
See the [docker-compose.yml](../../../docker-compose.yml) file for how to use with docker-compose.
|
||||
@@ -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!
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
<img src={require('/img/icon.png').default} align="right" alt="multi-scrobbler logo" width="180" height="180"/>
|
||||
|
||||
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
|
||||
|
||||
* Supports scrobbling from many **Sources**
|
||||
* [Spotify](docs/configuration#spotify)
|
||||
* [Plex](docs/configuration#plex) or [Tautulli](docs/configuration#tautulli)
|
||||
* [Subsonic-compatible APIs](docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [Jellyfin](docs/configuration#jellyfin)
|
||||
* [WebScrobbler](docs/configuration#webscrobbler)
|
||||
* [Youtube Music](docs/configuration#youtube-music)
|
||||
* [Last.fm](docs/configuration#lastfm-source)
|
||||
* [ListenBrainz](docs/configuration#listenbrainz--source-)
|
||||
* [Deezer](docs/configuration#deezer)
|
||||
* [MPRIS (Linux Desktop)](docs/configuration#mpris)
|
||||
* [Mopidy](docs/configuration#mopidy)
|
||||
* [JRiver](docs/configuration#jriver)
|
||||
* [Kodi](docs/configuration#kodi)
|
||||
* [Google Cast (Chromecast)](/docs/configuration#google-cast--chromecast-)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](docs/configuration#maloja)
|
||||
* [Last.fm](docs/configuration#lastfm)
|
||||
* [ListenBrainz](docs/configuration#listenbrainz)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](docs/configuration#webhook-configurations) or [healthcheck endpoint](docs/configuration#health-endpoint)
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Graceful network and client failure handling (queued scrobbles that auto-retry)
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through ENVs or JSON
|
||||
* Install using [Docker images for x86/ARM](docs/installation#docker), [flatpak](docs/installation#flatpak), or [locally with NodeJS](docs/installation#nodejs)
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
|
||||
* **Open-source** -- Get peace of mind knowing exactly how your personal data is being handled.
|
||||
* **Track your activity regardless of where you listen** -- Scrobble from many Sources to one Client with ease and without duplicating tracks.
|
||||
* **Manage scrobbling for others** -- Scrobble for your friends and family without any setup on their part. Easily silo sources to specific clients to keep plays separate.
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](docs/configuration#lastfm--source-) or [Listenbrainz as a **Source**](docs/configuration#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
<img src={require('/img/status-ui.png').default} width="800"/>
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
|
||||
|
||||
### Source
|
||||
|
||||
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
* A friendly name.
|
||||
* Any data needed to communicate or authenticate with the Source.
|
||||
* An optional list of Client names that the Source should scrobble to. If omitted the Source also scrobbles to all configured Clients.
|
||||
|
||||
### Client
|
||||
|
||||
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
* A friendly name.
|
||||
* Any data needed to communicate or authenticate with the Client.
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](docs/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](docs/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
A status page with statistics, recent logs, and some runtime configuration options can be found at
|
||||
|
||||
```
|
||||
http://localhost:9078
|
||||
```
|
||||
Output is also provided to stdout/stderr as well as file if specified in configuration.
|
||||
|
||||
On first startup you may need to authorize Spotify and/or Last.fm by visiting the callback URL (which can also be accessed from the status page). Visit the status page above to find the applicable link to trigger this.
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](docs/FAQ) before creating an issue!
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"sourceDefaults": {
|
||||
"maxPollRetries": 0,
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5
|
||||
},
|
||||
"clientDefaults": {
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5
|
||||
},
|
||||
"baseUrl": "http://localhost",
|
||||
"sources": [
|
||||
{
|
||||
"type": "spotify",
|
||||
"clients": ["myConfig"],
|
||||
"name": "mySpotifySource",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/callback"
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"type": "maloja",
|
||||
"name": "myConfig",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
}
|
||||
}
|
||||
],
|
||||
"webhooks": [
|
||||
{
|
||||
"name": "FirstGotifyServer",
|
||||
"type": "gotify",
|
||||
"url": "http://localhost:8070",
|
||||
"token": "MyGotifyToken",
|
||||
"priorities": {
|
||||
"info": 5,
|
||||
"warn": 7,
|
||||
"error": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ntfy",
|
||||
"name": "MyNtfyFriendlyNameForLogs",
|
||||
"url": "http://localhost:9991",
|
||||
"topic": "MyMultiScrobblerTopic",
|
||||
"username": "Optional",
|
||||
"password": "Optional",
|
||||
"priorities": {
|
||||
"info": 3,
|
||||
"warn": 4,
|
||||
"error": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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 |
|
After Width: | Height: | Size: 527 KiB |
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
NOTE: This steps are for building the flatpak entirely locally, from source. If you want to install the application normally then [get it through flathub](/docs/installation.md#flatpak)
|
||||
|
||||
The final build repo for the flathub version can be found at [flathub/io.github.foxxmd.multiscrobbler](https://github.com/flathub/io.github.foxxmd.multiscrobbler)
|
||||
|
||||
# 1. Install Requirements
|
||||
|
||||
## Flatpak and flatpak-builder
|
||||
|
||||
Install [Flatpak](https://flatpak.org/setup/)
|
||||
|
||||
Install [flatpak-builder](https://docs.flatpak.org/en/latest/first-build.html#building-your-first-flatpak)
|
||||
|
||||
## [flatpak-node-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/node)
|
||||
|
||||
Requires python 3.7+, [pip](https://pip.pypa.io/en/stable/)/[pipx](https://pypa.github.io/pipx/)
|
||||
|
||||
# 2. Update Project source
|
||||
|
||||
Set the `branch` `tag` or `commit` to use for MS in the `git` source in [`io.github.foxxmd.multiscrobbler.yml`](/flatpak/io.github.foxxmd.multiscrobbler.yml)
|
||||
|
||||
# 3. Use `flatpak-node-generator` to generate sources
|
||||
|
||||
First, [make sure `node_modules` is deleted or empty.](https://github.com/flatpak/flatpak-builder-tools/issues/354#issuecomment-1478518442)
|
||||
|
||||
Then, from MS project root:
|
||||
|
||||
```shell
|
||||
flatpak-node-generator npm package-lock.json
|
||||
```
|
||||
|
||||
Move `generated-sources.json` into [`/flatpak`](/flatpak)
|
||||
|
||||
# 4. Build flatpak
|
||||
|
||||
From MS project root:
|
||||
|
||||
```shell
|
||||
cd flatpak
|
||||
flatpak-builder --repo=/home/yourUser/multi-scrobbler-flatpak/repo --state-dir=/home/yourUser/multi-scrobbler-flatpak/state /home/yourUser/multi-scrobbler-flatpak/build io.github.foxxmd.multiscrobbler.yml --force-clean --install --user
|
||||
```
|
||||
Add `--install --user` to have the app installed immediately.
|
||||
# 5. Run (Locally)
|
||||
|
||||
If built with `--install --user` you can now run MS using the command
|
||||
|
||||
```shell
|
||||
flatpak run -u io.github.foxxmd.multiscrobbler
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"logging": {
|
||||
"file": false
|
||||
},
|
||||
"port": 9078
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -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,66 @@
|
||||
<?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://foxxmd.github.io/multi-scrobbler</url>
|
||||
<url type="help">https://foxxmd.github.io/multi-scrobbler/docs/configuration</url>
|
||||
<metadata_license>MIT</metadata_license>
|
||||
<project_license>MIT</project_license>
|
||||
<description>
|
||||
<p>
|
||||
Track your music listening history from many different sources:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Spotify</li>
|
||||
<li>Plex (Tautulli)</li>
|
||||
<li>Any subsonic-based music player (Airsonic)</li>
|
||||
<li>Jellyfin</li>
|
||||
<li>WebScrobbler</li>
|
||||
<li>Mopidy</li>
|
||||
<li>Deezer</li>
|
||||
<li>JRiver</li>
|
||||
<li>Kodi</li>
|
||||
<li>Webscrobbler</li>
|
||||
<li>Youtube Music</li>
|
||||
<li>MPRIS (linux desktop)</li>
|
||||
<li>Google Cast (Chromecast)</li>
|
||||
</ul>
|
||||
<p>and consolidate your listens (scrobbles) into one or more scrobbling servers/services:</p>
|
||||
<ul>
|
||||
<li>Maloja</li>
|
||||
<li>Last.fm</li>
|
||||
<li>Listenbrainz</li>
|
||||
</ul>
|
||||
<p>multi-scrobbler requires some configuration before use. Please visit the website or help links below to get started.</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Browser-based UI</caption>
|
||||
<image type="source">https://raw.githubusercontent.com/FoxxMD/multi-scrobbler/f4256d1084f4bdea2945c03e523af6d0ed89b3ae/assets/status-ui.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="0.6.4" date="2024-01-29"/>
|
||||
<release version="0.6.3" date="2024-01-10"/>
|
||||
<release version="0.6.2" date="2023-11-29"/>
|
||||
<release version="0.6.1" date="2023-10-24"/>
|
||||
<release version="0.6.0" date="2023-10-04"/>
|
||||
<release version="0.5.2" date="2023-09-21"/>
|
||||
<release version="0.5.0" date="2023-08-29"/>
|
||||
<release version="0.4.6" date="2023-06-07"/>
|
||||
<release version="0.4.4" date="2023-03-20"/>
|
||||
</releases>
|
||||
<categories>
|
||||
<category>Utility</category>
|
||||
<category>Music</category>
|
||||
<category>ConsoleOnly</category>
|
||||
</categories>
|
||||
<content_rating type="oars-1.0">
|
||||
<content_attribute id="language-profanity">mild</content_attribute>
|
||||
</content_rating>
|
||||
</component>
|
||||
@@ -0,0 +1,87 @@
|
||||
app-id: io.github.foxxmd.multiscrobbler
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: '22.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: multiscrobbler
|
||||
finish-args:
|
||||
- --share=network
|
||||
# used for access to MPRIS in order to scrobble
|
||||
- --own-name=org.mpris.MediaPlayer2.*
|
||||
sdk-extensions:
|
||||
- org.freedesktop.Sdk.Extension.node18
|
||||
modules:
|
||||
- name: multiscrobbler
|
||||
buildsystem: simple
|
||||
build-options:
|
||||
append-path: /usr/lib/sdk/node18/bin
|
||||
env:
|
||||
XDG_CACHE_HOME: /run/build/multiscrobbler/flatpak-node/cache
|
||||
npm_config_cache: /run/build/multiscrobbler/flatpak-node/npm-cache
|
||||
npm_config_nodedir: /usr/lib/sdk/node18
|
||||
npm_config_offline: 'true'
|
||||
build-commands:
|
||||
# install npm dependencies
|
||||
- npm install --offline
|
||||
|
||||
# build app
|
||||
- NODE_ENV=production npm run build
|
||||
|
||||
# makes node and npm available
|
||||
- mkdir -p /app/bin /app/lib /app/lib/dist /app/lib/src /app/lib/assets
|
||||
- cp -a /usr/lib/sdk/node18/bin/{node,npm} /app/bin
|
||||
- cp -a /usr/lib/sdk/node18/lib/* /app/lib
|
||||
- rm -r /app/lib/node_modules/npm/{docs,man}
|
||||
|
||||
# remove 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 dist/. /app/lib/dist
|
||||
- cp -r src/. /app/lib/src
|
||||
- cp -r config /app/lib/config
|
||||
- cp -r assets /app/lib/assets
|
||||
- cp -r public /app/lib/public
|
||||
- cp vite.config.ts /app/lib/vite.config.ts
|
||||
- cp tsconfig.json /app/lib/tsconfig.json
|
||||
- cp tailwind.config.js /app/lib/tailwind.config.js
|
||||
- cp postcss.config.js /app/lib/postcss.config.js
|
||||
- cp flatpak/defaultConfig.json /app/lib/config/config.json
|
||||
|
||||
- install -Dm644 flatpak/${FLATPAK_ID}.desktop /app/share/applications/${FLATPAK_ID}.desktop
|
||||
- install -Dm644 flatpak/icon.png /app/share/icons/hicolor/512x512/apps/${FLATPAK_ID}.png
|
||||
- install -Dm644 assets/icon.svg /app/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
|
||||
- install -Dm644 flatpak/${FLATPAK_ID}.metainfo.xml /app/share/metainfo/${FLATPAK_ID}.metainfo.xml
|
||||
|
||||
- install -Dm755 ../run.sh /app/bin/multiscrobbler
|
||||
subdir: main
|
||||
sources:
|
||||
# use for official releases
|
||||
- type: git
|
||||
url: https://github.com/FoxxMD/multi-scrobbler
|
||||
tag: 0.6.3
|
||||
commit: 4bd996eb466137ffc5c3f48305afabde03720525
|
||||
dest: main
|
||||
# use if developing locally
|
||||
# - type: dir
|
||||
# path: /home/yourUser/multi-scrobbler
|
||||
# dest: main
|
||||
# Wrapper to launch the app
|
||||
- type: script
|
||||
dest-filename: run.sh
|
||||
commands:
|
||||
# Run config check and copy examples + default config (disable file logging)
|
||||
- |
|
||||
echo -e "Checking CONFIG_DIR for existing configs: ${XDG_CONFIG_HOME}"
|
||||
if [ "$(ls -A ${XDG_CONFIG_HOME})" ]; then
|
||||
echo "Directory is not empty! Will not add default config/examples"
|
||||
else
|
||||
echo "Directory is empty! Adding default config/examples"
|
||||
cp -r /app/lib/config/. "${XDG_CONFIG_HOME}"/
|
||||
#cp /app/lib/defaultConfig.json $XDG_CONFIG_HOME/config.json
|
||||
fi
|
||||
- cd /app/lib && CONFIG_DIR=$XDG_CONFIG_HOME LOG_DIR=$XDG_CONFIG_HOME IS_LOCAL=true NODE_ENV=production node_modules/.bin/tsx src/backend/index.ts
|
||||
|
||||
- generated-sources.json
|
||||
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Dashboard for Multi Scrobbler"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Multi Scrobbler</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/client/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +1,28 @@
|
||||
{
|
||||
"name": "multi-scrobbler",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.4",
|
||||
"type": "module",
|
||||
"description": "scrobble plays from multiple sources to multiple clients",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc",
|
||||
"start": "node src/index.js",
|
||||
"schema": "npm run -s schema-aio & npm run -s schema-source & npm run -s schema-client & npm run -s schema-aiosource & npm run -s schema-aioclient",
|
||||
"schema-client": "typescript-json-schema tsconfig.json ClientConfig --out src/common/schema/client.json --required --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": "npm run -s test:backend",
|
||||
"test:backend": "NODE_OPTIONS='--loader tsx' mocha --extension ts --reporter spec --recursive src/backend/tests/**/*.test.ts",
|
||||
"fileEndings": "jscodeshift --transformFrom js --transformTo none --importTypes relative --extensions=ts --parser tsx --transform codeshift/transform.ts src/backend",
|
||||
"dev": "APP_VERSION=$npm_package_version nodemon -w src/backend -x tsx src/backend/index.ts",
|
||||
"start": "APP_VERSION=$npm_package_version NODE_ENV=production tsx src/backend/index.ts",
|
||||
"build": "APP_VERSION=$npm_package_version vite build"
|
||||
},
|
||||
"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,17 +38,35 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/FoxxMD/multi-scrobbler/issues"
|
||||
},
|
||||
"homepage": "https://github.com/FoxxMD/multi-scrobbler#readme",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"dependencies": {
|
||||
"@astronautlabs/mdns": "^1.0.7",
|
||||
"@awaitjs/express": "^0.6.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@foxxmd/chromecast-client": "^1.0.1",
|
||||
"@foxxmd/string-sameness": "^0.2.0",
|
||||
"@foxxmd/winston": "3.3.31",
|
||||
"@kenyip/backoff-strategies": "^1.0.4",
|
||||
"ajv": "^7.2.4",
|
||||
"@react-nano/use-event-source": "^0.13.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@supercharge/promise-pool": "^3.0.0",
|
||||
"address": "^1.2.2",
|
||||
"ajv": "^8.12.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"avahi-browse": "^1.1.4",
|
||||
"better-sse": "^0.8.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"castv2": "^0.1.10",
|
||||
"clsx": "^2.0.0",
|
||||
"common-tags": "^1.8.2",
|
||||
"compare-versions": "^4.1.2",
|
||||
"concat-stream": "^2.0.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"dbus-next": "^0.10.2",
|
||||
"ejs": "^3.1.6",
|
||||
"dbus-next": "0.10.2",
|
||||
"dotenv": "^10.0.0",
|
||||
"es6-error": "^4.1.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
@@ -52,44 +75,98 @@
|
||||
"gotify": "^1.1.0",
|
||||
"iti": "^0.6.0",
|
||||
"json5": "^2.2.3",
|
||||
"kodi-api": "^0.2.1",
|
||||
"lastfm-node-client": "^2.2.0",
|
||||
"mopidy": "^1.3.0",
|
||||
"nanoid": "^3.3.1",
|
||||
"normalize-url": "^6.1.0",
|
||||
"ntfy": "^1.0.5",
|
||||
"ntfy": "1.0.5",
|
||||
"object-hash": "^3.0.0",
|
||||
"p-event": "^4.2.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-deezer": "^0.2.0",
|
||||
"pony-cause": "^1.1.1",
|
||||
"postcss": "^8.4.33",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"safe-stable-stringify": "^1.1.1",
|
||||
"socket.io": "^4.6.1",
|
||||
"sass": "^1.70.0",
|
||||
"spotify-web-api-node": "^5.0.2",
|
||||
"superagent": "^8.0.9",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"toad-scheduler": "^3.0.0",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston": "github:FoxxMD/winston#fbab8de969ecee578981c77846156c7f43b5f01e",
|
||||
"tsx": "^4.7.0",
|
||||
"vite-express": "^0.13.0",
|
||||
"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",
|
||||
"xml2js": "^0.4.23",
|
||||
"youtube-music-ts-api": "^1.4.1"
|
||||
"youtube-music-ts-api": "^1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.1.0",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@tsconfig/node18": "^1.0.1",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/clone": "^2.1.2",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/concat-stream": "^2.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jscodeshift": "^0.11.6",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/object-hash": "^3.0.0",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/react": "^18.2.18",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/spotify-web-api-node": "^5.0.7",
|
||||
"@types/superagent": "^4.1.16",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"clone": "^2.1.2",
|
||||
"concurrently": "^8.2.0",
|
||||
"jscodeshift": "^0.15.0",
|
||||
"mocha": "10.1.0",
|
||||
"msw": "^2.1.2",
|
||||
"nodemon": "^3.0.3",
|
||||
"nyc": "^15.1.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"ts-essentials": "^9.1.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"tsconfig-paths": "^3.13.0",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-json-schema": "~0.55"
|
||||
"typedoc": "^0.25",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-json-schema": "~0.55",
|
||||
"vite": "^5.0.11",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"wtfnode": "^0.9.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"spotify-web-api-node": {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
diff --git a/node_modules/chromecast-client/dist/cjs/src/apps/application.js b/node_modules/chromecast-client/dist/cjs/src/apps/application.js
|
||||
index d91a6ff..dfc417e 100644
|
||||
--- a/node_modules/chromecast-client/dist/cjs/src/apps/application.js
|
||||
+++ b/node_modules/chromecast-client/dist/cjs/src/apps/application.js
|
||||
@@ -4,7 +4,9 @@ exports.join = exports.launchAndJoin = void 0;
|
||||
const receiver_1 = require("../controllers/receiver");
|
||||
const utils_1 = require("../utils");
|
||||
const _getJoinableTransportId = (status) => {
|
||||
- const app = status.applications.find(a => a.namespaces.map(e => e.name).includes('urn:x-cast:com.google.cast.media'));
|
||||
+ const app = status.applications === undefined
|
||||
+ ? undefined
|
||||
+ : status.applications.find(a => a.namespaces.map(e => e.name).includes('urn:x-cast:com.google.cast.media'));
|
||||
return app === undefined ? utils_1.Result.Err(new Error('failed to find joinable application')) : utils_1.Result.Ok(app.transportId);
|
||||
};
|
||||
const _join = async (status, factory) => {
|
||||
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts
|
||||
index cbc2af8..e0f60dc 100644
|
||||
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts
|
||||
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.d.ts
|
||||
@@ -63,7 +63,7 @@ export declare const Application$: z.ZodObject<{
|
||||
}>;
|
||||
export type Application = z.infer<typeof Application$>;
|
||||
export declare const ReceiverStatus$: z.ZodObject<{
|
||||
- applications: z.ZodArray<z.ZodObject<{
|
||||
+ applications: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
appId: z.ZodString;
|
||||
displayName: z.ZodString;
|
||||
iconUrl: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
@@ -103,7 +103,7 @@ export declare const ReceiverStatus$: z.ZodObject<{
|
||||
}[];
|
||||
sessionId: string;
|
||||
transportId: string;
|
||||
- }>, "many">;
|
||||
+ }>, "many">>;
|
||||
volume: z.ZodObject<{
|
||||
controlType: z.ZodEffects<z.ZodEnum<["ATTENUATION", "FIXED", "MASTER"]>, "ATTENUATION" | "FIXED" | "MASTER", unknown>;
|
||||
level: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
|
||||
@@ -121,13 +121,7 @@ export declare const ReceiverStatus$: z.ZodObject<{
|
||||
stepInterval: number;
|
||||
}>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
- volume: {
|
||||
- level?: number | null | undefined;
|
||||
- muted?: boolean | null | undefined;
|
||||
- controlType: "ATTENUATION" | "FIXED" | "MASTER";
|
||||
- stepInterval: number;
|
||||
- };
|
||||
- applications: {
|
||||
+ applications?: {
|
||||
iconUrl?: string | null | undefined;
|
||||
isIdleScreen?: boolean | null | undefined;
|
||||
launchedFromCloud?: boolean | null | undefined;
|
||||
@@ -139,15 +133,15 @@ export declare const ReceiverStatus$: z.ZodObject<{
|
||||
}[];
|
||||
sessionId: string;
|
||||
transportId: string;
|
||||
- }[];
|
||||
-}, {
|
||||
+ }[] | undefined;
|
||||
volume: {
|
||||
level?: number | null | undefined;
|
||||
muted?: boolean | null | undefined;
|
||||
- controlType?: unknown;
|
||||
+ controlType: "ATTENUATION" | "FIXED" | "MASTER";
|
||||
stepInterval: number;
|
||||
};
|
||||
- applications: {
|
||||
+}, {
|
||||
+ applications?: {
|
||||
iconUrl?: string | null | undefined;
|
||||
isIdleScreen?: boolean | null | undefined;
|
||||
launchedFromCloud?: boolean | null | undefined;
|
||||
@@ -159,6 +153,12 @@ export declare const ReceiverStatus$: z.ZodObject<{
|
||||
}[];
|
||||
sessionId: string;
|
||||
transportId: string;
|
||||
- }[];
|
||||
+ }[] | undefined;
|
||||
+ volume: {
|
||||
+ level?: number | null | undefined;
|
||||
+ muted?: boolean | null | undefined;
|
||||
+ controlType?: unknown;
|
||||
+ stepInterval: number;
|
||||
+ };
|
||||
}>;
|
||||
export type ReceiverStatus = z.infer<typeof ReceiverStatus$>;
|
||||
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js
|
||||
index 33bfb4b..29e79fe 100644
|
||||
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js
|
||||
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/index.js
|
||||
@@ -51,7 +51,7 @@ exports.Application$ = zod_1.z.object({
|
||||
transportId: zod_1.z.string(),
|
||||
});
|
||||
exports.ReceiverStatus$ = zod_1.z.object({
|
||||
- applications: zod_1.z.array(exports.Application$),
|
||||
+ applications: zod_1.z.array(exports.Application$).optional(),
|
||||
volume: exports.Volume$,
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts
|
||||
index 53e42b2..0d77bb2 100644
|
||||
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts
|
||||
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.d.ts
|
||||
@@ -80,7 +80,7 @@ export declare const MediaStatus$: z.ZodObject<{
|
||||
playbackRate: z.ZodNumber;
|
||||
playerState: z.ZodEffects<z.ZodEnum<["IDLE", "PLAYING", "BUFFERING", "PAUSED"]>, "IDLE" | "PLAYING" | "BUFFERING" | "PAUSED", unknown>;
|
||||
supportedMediaCommands: z.ZodNumber;
|
||||
- volume: z.ZodObject<{
|
||||
+ volume: z.ZodNullable<z.ZodOptional<z.ZodObject<{
|
||||
level: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
|
||||
muted: z.ZodNullable<z.ZodOptional<z.ZodBoolean>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
@@ -89,7 +89,7 @@ export declare const MediaStatus$: z.ZodObject<{
|
||||
}, {
|
||||
level?: number | null | undefined;
|
||||
muted?: boolean | null | undefined;
|
||||
- }>;
|
||||
+ }>>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
media?: {
|
||||
metadata?: Record<string, unknown> | null | undefined;
|
||||
@@ -102,15 +102,15 @@ export declare const MediaStatus$: z.ZodObject<{
|
||||
} | null | undefined;
|
||||
customData?: Record<string, unknown> | null | undefined;
|
||||
idleReason?: "CANCELLED" | "INTERRUPTED" | "FINISHED" | "ERROR" | null | undefined;
|
||||
+ volume?: {
|
||||
+ level?: number | null | undefined;
|
||||
+ muted?: boolean | null | undefined;
|
||||
+ } | null | undefined;
|
||||
currentTime: number;
|
||||
mediaSessionId: number;
|
||||
playbackRate: number;
|
||||
playerState: "IDLE" | "PLAYING" | "BUFFERING" | "PAUSED";
|
||||
supportedMediaCommands: number;
|
||||
- volume: {
|
||||
- level?: number | null | undefined;
|
||||
- muted?: boolean | null | undefined;
|
||||
- };
|
||||
}, {
|
||||
media?: {
|
||||
metadata?: Record<string, unknown> | null | undefined;
|
||||
@@ -124,13 +124,13 @@ export declare const MediaStatus$: z.ZodObject<{
|
||||
customData?: Record<string, unknown> | null | undefined;
|
||||
idleReason?: unknown;
|
||||
playerState?: unknown;
|
||||
+ volume?: {
|
||||
+ level?: number | null | undefined;
|
||||
+ muted?: boolean | null | undefined;
|
||||
+ } | null | undefined;
|
||||
currentTime: number;
|
||||
mediaSessionId: number;
|
||||
playbackRate: number;
|
||||
supportedMediaCommands: number;
|
||||
- volume: {
|
||||
- level?: number | null | undefined;
|
||||
- muted?: boolean | null | undefined;
|
||||
- };
|
||||
}>;
|
||||
export type MediaStatus = z.infer<typeof MediaStatus$>;
|
||||
diff --git a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js
|
||||
index 2af6558..138f550 100644
|
||||
--- a/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js
|
||||
+++ b/node_modules/chromecast-client/dist/cjs/src/cast-types/media.types.js
|
||||
@@ -28,6 +28,6 @@ exports.MediaStatus$ = zod_1.z.object({
|
||||
playbackRate: zod_1.z.number(),
|
||||
playerState: exports.PlayerState$,
|
||||
supportedMediaCommands: zod_1.z.number(),
|
||||
- volume: exports.Volume$,
|
||||
+ volume: exports.Volume$.nullish(),
|
||||
});
|
||||
//# sourceMappingURL=media.types.js.map
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/chromecast-client/dist/cjs/src/platform.d.ts b/node_modules/chromecast-client/dist/cjs/src/platform.d.ts
|
||||
index 2f9770b..1247d8a 100644
|
||||
--- a/node_modules/chromecast-client/dist/cjs/src/platform.d.ts
|
||||
+++ b/node_modules/chromecast-client/dist/cjs/src/platform.d.ts
|
||||
@@ -65,7 +65,7 @@ export declare const createPlatform: (client: PersistentClient) => {
|
||||
controlType: "ATTENUATION" | "FIXED" | "MASTER";
|
||||
stepInterval: number;
|
||||
};
|
||||
- applications: {
|
||||
+ applications?: {
|
||||
iconUrl?: string | null | undefined;
|
||||
isIdleScreen?: boolean | null | undefined;
|
||||
launchedFromCloud?: boolean | null | undefined;
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -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 |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {ResponseError} from "superagent";
|
||||
|
||||
export const isSuperAgentResponseError = (e: any): e is ResponseError => {
|
||||
return typeof e === 'object'
|
||||
&& (
|
||||
('timeout' in e && typeof e.timeout === 'boolean')
|
||||
||
|
||||
('status' in e
|
||||
&& (typeof e.status === 'number' || e.status === undefined)
|
||||
&& 'response' in e
|
||||
&& (typeof e.response === 'object' || e.response === undefined))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { isArbitraryObject } from "../infrastructure/Atomic.js";
|
||||
import ErrnoException = NodeJS.ErrnoException;
|
||||
import { findCauseByFunc } from "../../utils.js";
|
||||
|
||||
export type NodeNetworkErrorCode = 'ENOTFOUND' | 'ETIMEDOUT' | 'EAI_AGAIN' | 'ECONNRESET' | 'ECONNREFUSED' | 'ERRADDRINUSE' | 'EADDRNOTAVAIL' | 'ECONNABORTED' | 'EHOSTUNREACH';
|
||||
export const NETWORK_ERROR_CODES = ['ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN', 'ECONNRESET', 'ECONNREFUSED', 'ERRADDRINUSE', 'EADDRNOTAVAIL', 'ECONNABORTED', 'EHOSTUNREACH'];
|
||||
|
||||
export const isErrnoException = (error: unknown): error is ErrnoException => {
|
||||
return isArbitraryObject(error) &&
|
||||
error instanceof Error &&
|
||||
(typeof error.errno === "number" || typeof error.errno === "undefined") &&
|
||||
(typeof error.code === "string" || typeof error.code === "undefined") &&
|
||||
(typeof error.path === "string" || typeof error.path === "undefined") &&
|
||||
(typeof error.syscall === "string" || typeof error.syscall === "undefined");
|
||||
}
|
||||
|
||||
export interface NodeNetworkException extends ErrnoException {
|
||||
code: NodeNetworkErrorCode
|
||||
}
|
||||
|
||||
export const isNodeNetworkException = (error: unknown): error is NodeNetworkException => {
|
||||
return isErrnoException(error) && NETWORK_ERROR_CODES.includes(error.code);
|
||||
}
|
||||
|
||||
export const hasNodeNetworkException = (error: unknown): boolean => {
|
||||
return getNodeNetworkException(error) !== undefined;
|
||||
}
|
||||
|
||||
export const getNodeNetworkException = (error: unknown): Error | undefined => {
|
||||
return findCauseByFunc(error, isNodeNetworkException);
|
||||
}
|
||||