mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4580f1df | ||
|
|
38d7a9cfce | ||
|
|
83a425a3d4 | ||
|
|
7ae727a8fe | ||
|
|
fe4b947d26 | ||
|
|
c5cdc465f3 | ||
|
|
c86819b0b3 | ||
|
|
5228d4894c | ||
|
|
e6709c442f | ||
|
|
0a594d5f83 | ||
|
|
49be95a4d1 | ||
|
|
fbf3af8474 | ||
|
|
146916bc6d | ||
|
|
a410721acf | ||
|
|
af5aec2ed3 | ||
|
|
51b7b3fd40 | ||
|
|
c22867a9b1 | ||
|
|
f93c1fa4db | ||
|
|
1f199e28ef | ||
|
|
811615dd06 | ||
|
|
fc6ec840da | ||
|
|
eb2159de27 | ||
|
|
d0a490b200 | ||
|
|
53870ac1f2 | ||
|
|
d2f8717555 | ||
|
|
756d9b84e1 | ||
|
|
a1ad250dcc | ||
|
|
f36fd16517 | ||
|
|
28ade6ac28 | ||
|
|
fa1051b29a | ||
|
|
3cdbfc337e | ||
|
|
80474a96fe | ||
|
|
957db35c30 | ||
|
|
90f0bdb5ef | ||
|
|
640d944b4c | ||
|
|
c7fce06480 | ||
|
|
8cffd0d068 | ||
|
|
b8a9623def | ||
|
|
d922f37359 | ||
|
|
dcc53d1656 | ||
|
|
0cb20b83b4 | ||
|
|
e814cf58fd | ||
|
|
f211a4a4c4 | ||
|
|
4d7162a1a4 | ||
|
|
a5926d71c1 | ||
|
|
d95f323dc0 | ||
|
|
1b53d22e00 | ||
|
|
75c0230dae | ||
|
|
630edd7b3f | ||
|
|
092cf907a2 | ||
|
|
562b388e3e | ||
|
|
f788894494 | ||
|
|
d0379e1bd6 | ||
|
|
5eb967f61c | ||
|
|
0bf9d5e786 | ||
|
|
102f9795f8 | ||
|
|
7a7d7bb975 | ||
|
|
d772c8c8cd | ||
|
|
48b6b555c1 | ||
|
|
8be56457e2 | ||
|
|
d9a5aa1ce0 | ||
|
|
f6799955a0 | ||
|
|
077c3d33b5 | ||
|
|
524afb60a9 | ||
|
|
12ba99437c | ||
|
|
8882e7ab87 | ||
|
|
8de382f581 | ||
|
|
691db9215c | ||
|
|
22c0916544 | ||
|
|
cbe996db95 | ||
|
|
8fd870b9ef | ||
|
|
272c3cc8be | ||
|
|
54da8b0add | ||
|
|
cfb4131d5d | ||
|
|
b8494986d5 | ||
|
|
f9f923041f | ||
|
|
5774be1d22 | ||
|
|
4bc460f598 | ||
|
|
0f6aa56fe5 | ||
|
|
439f0e05aa | ||
|
|
952ddbdcc3 | ||
|
|
765b13f910 | ||
|
|
47caf8de8c | ||
|
|
3c3faf22fc | ||
|
|
f94538ea1f | ||
|
|
e9b2990e15 | ||
|
|
3ef48547a7 | ||
|
|
047de49ee6 | ||
|
|
2a52d1e050 | ||
|
|
8f049a089b | ||
|
|
adf6b9ed58 | ||
|
|
59d1050b2b | ||
|
|
561c62b426 | ||
|
|
f38f10d2a0 | ||
|
|
7aa5595cc9 | ||
|
|
46389fd81d | ||
|
|
c0384317b1 | ||
|
|
6e66cb06a9 | ||
|
|
cb1ff7d030 | ||
|
|
3b5468499a | ||
|
|
c7361a8976 | ||
|
|
60a6ddbc41 | ||
|
|
92e027bfc4 | ||
|
|
b388ca6106 | ||
|
|
84b43d7c39 | ||
|
|
99ebb53fc6 | ||
|
|
cda33ba04f | ||
|
|
dda73cd96a | ||
|
|
5771fbf496 | ||
|
|
e9d1c81380 | ||
|
|
d59cb2668d | ||
|
|
3c51149d9a | ||
|
|
40234e64b3 | ||
|
|
2df8bfe6aa | ||
|
|
fec39b2e2e | ||
|
|
83f501df0d | ||
|
|
c40add7d70 | ||
|
|
569c5a8690 | ||
|
|
11f7299c0e | ||
|
|
8a6ae698a1 | ||
|
|
d6ed03028d | ||
|
|
2818d1d275 | ||
|
|
8127e4b6ce | ||
|
|
8e829a53b5 | ||
|
|
1562f8bffc | ||
|
|
e04514f6a1 | ||
|
|
00e9755fb1 | ||
|
|
5ba4729f83 | ||
|
|
e5df4f2752 | ||
|
|
d11db4cb8d | ||
|
|
14f5f60c11 | ||
|
|
4d7eadbd38 | ||
|
|
747b62289a | ||
|
|
1eeb5e6b08 | ||
|
|
ac01348266 | ||
|
|
3028f83d47 | ||
|
|
a02d43acea | ||
|
|
c7679a0a62 | ||
|
|
d11ac3d6ed | ||
|
|
8a09c91713 | ||
|
|
81d24f1753 | ||
|
|
5be12aab15 | ||
|
|
47c600ed8d | ||
|
|
5f84fefd5d | ||
|
|
a143aa4193 | ||
|
|
23a3dfa5d1 | ||
|
|
3005d36db0 | ||
|
|
ada9d70be9 | ||
|
|
9d839db2e1 | ||
|
|
ca20b23fbc | ||
|
|
577cff3abf | ||
|
|
c4786bdfe2 | ||
|
|
df0da7054c | ||
|
|
272a336a22 | ||
|
|
ec4a51074e | ||
|
|
5a4bcb2949 | ||
|
|
77387c292b | ||
|
|
4caf1c8cdb | ||
|
|
981805aa96 | ||
|
|
60e3242c44 | ||
|
|
e7e1417b0b | ||
|
|
bf765be174 | ||
|
|
e27c301932 | ||
|
|
fc4606585d | ||
|
|
fd80101a7a | ||
|
|
2cbc5542d1 | ||
|
|
6f15f15722 | ||
|
|
ff1d9248c6 | ||
|
|
218ece8f5c | ||
|
|
3adac7c6e0 | ||
|
|
1c78da7985 | ||
|
|
457279465c | ||
|
|
d551c8563f | ||
|
|
75bd4a04d6 | ||
|
|
339232c368 | ||
|
|
8741ff25ea | ||
|
|
8a952cb576 | ||
|
|
eeb55aa02e | ||
|
|
6a81fc1be2 | ||
|
|
a47e2a6242 | ||
|
|
dadaa6df35 | ||
|
|
fb19191209 | ||
|
|
2f53a60da1 | ||
|
|
ab2f397fab | ||
|
|
db2dd8fb8b | ||
|
|
135439f509 | ||
|
|
d743b849fe | ||
|
|
9885768cad | ||
|
|
c6e6bbb87b | ||
|
|
bbc2351ebc | ||
|
|
b547db9045 | ||
|
|
28c2e9873a | ||
|
|
6bcd3f9516 | ||
|
|
11b2632d02 | ||
|
|
fb6dbe307d | ||
|
|
2474b792c3 | ||
|
|
3007c949a0 | ||
|
|
63533534df | ||
|
|
5c03062976 | ||
|
|
dbe41fa5ab | ||
|
|
5454f817e5 | ||
|
|
8d8f8260b5 | ||
|
|
62bb00571f | ||
|
|
c10537d658 | ||
|
|
e9ccc319e9 | ||
|
|
54803c6ef9 | ||
|
|
a3d37a864a | ||
|
|
394570e5a3 | ||
|
|
2bbfdf424f | ||
|
|
9785ad6ba7 | ||
|
|
fe7d77c03d | ||
|
|
0e7aa92f5b | ||
|
|
2ff80283b9 | ||
|
|
4f34b1a055 | ||
|
|
4effdd6df5 | ||
|
|
af3ce9dea7 | ||
|
|
f0c5cb524f | ||
|
|
46883384b6 | ||
|
|
7bc5ab3d0f | ||
|
|
fd15d15124 | ||
|
|
14da0795f1 | ||
|
|
3faf0ee7f9 | ||
|
|
78ddb15dcf | ||
|
|
760373c227 | ||
|
|
6bb7b17e97 | ||
|
|
231156d452 | ||
|
|
d85628b4c9 | ||
|
|
db0e74aec3 | ||
|
|
a918a96d34 | ||
|
|
bb51915085 | ||
|
|
7c177dfe03 | ||
|
|
cf5e7ca1fe | ||
|
|
fd43ff86c8 | ||
|
|
194a0e78ff | ||
|
|
04027e87e7 | ||
|
|
a998060bca | ||
|
|
5b16e8857e | ||
|
|
c53ce7c517 | ||
|
|
7897dc9503 | ||
|
|
160ddac3e4 | ||
|
|
59b4f27294 | ||
|
|
21f42bf45b | ||
|
|
3e8f170f9a | ||
|
|
15fbed09a5 | ||
|
|
52b301e729 | ||
|
|
0bef197521 | ||
|
|
f16750609a | ||
|
|
c39ef29597 | ||
|
|
72639616b7 | ||
|
|
effe870104 | ||
|
|
ee753389f7 | ||
|
|
5f4a5cb815 | ||
|
|
0607aedf04 | ||
|
|
1f165096b3 | ||
|
|
fb6f6a18c8 | ||
|
|
03f8b20704 | ||
|
|
c49cdba718 | ||
|
|
de67879af5 | ||
|
|
9d0cf8f845 | ||
|
|
8d9fd11c79 | ||
|
|
745343c589 | ||
|
|
e5935c38da | ||
|
|
cac13fa3da | ||
|
|
ca4803713e | ||
|
|
e53dd55ad8 | ||
|
|
1f2772353a | ||
|
|
78b48107a0 | ||
|
|
bd00055ae5 | ||
|
|
b174b23b3b | ||
|
|
9e6319ee73 | ||
|
|
6b5b3990cd | ||
|
|
f2465f0f88 | ||
|
|
c68ec8dbf6 | ||
|
|
6e1cdba58c | ||
|
|
df4bb27d3a | ||
|
|
de7707b45a | ||
|
|
c884b4776c | ||
|
|
0893b62f3b | ||
|
|
7d071c763d | ||
|
|
664395a0ee | ||
|
|
007d553d59 | ||
|
|
7c8c185c4c | ||
|
|
972eccd0bf | ||
|
|
034cfe7a8e | ||
|
|
7a8cf7c004 | ||
|
|
30cdc9f553 | ||
|
|
3f19abb899 | ||
|
|
a601757759 | ||
|
|
fe03bcd651 | ||
|
|
1f319cb9f6 | ||
|
|
a0ac0f8e46 | ||
|
|
06945fda52 | ||
|
|
306c4c858e | ||
|
|
81885632a0 | ||
|
|
23467481c1 | ||
|
|
e716de2160 | ||
|
|
dda06a0873 | ||
|
|
eca64d68e7 | ||
|
|
1065643b23 | ||
|
|
d92153f85f | ||
|
|
22d76c459a | ||
|
|
ec87ab96d6 | ||
|
|
f62fe270fb | ||
|
|
677474beed | ||
|
|
cb9dd0fbc5 | ||
|
|
0685d4b06d | ||
|
|
82c0a4faec | ||
|
|
ccbd5e304a | ||
|
|
4f81a192fa | ||
|
|
22cf95e58c | ||
|
|
358355c612 | ||
|
|
c5f9600047 | ||
|
|
703731e654 | ||
|
|
0dfa4c7aad | ||
|
|
68e473ec02 | ||
|
|
a8d20734aa | ||
|
|
ebdb76cf6c | ||
|
|
e62e1ac2dd | ||
|
|
e68b46f647 | ||
|
|
7605cd3f1d | ||
|
|
2735c283ce | ||
|
|
0daaec81ca | ||
|
|
2085df4406 | ||
|
|
9b8664d3a3 | ||
|
|
e027a33e8d | ||
|
|
f04a19d34d | ||
|
|
4b27671a91 | ||
|
|
5f4943e025 | ||
|
|
66cdb94d57 | ||
|
|
5ebb0e6a9c | ||
|
|
dfb7f43c00 | ||
|
|
ed85d35117 | ||
|
|
7d69e77ac6 | ||
|
|
03973d215b | ||
|
|
1b182d89cd | ||
|
|
3d5c2c7450 | ||
|
|
d6264dd994 | ||
|
|
4c18123207 | ||
|
|
71e7f41ea5 | ||
|
|
60cf5d1019 | ||
|
|
3977057dfa | ||
|
|
943bdc3e88 | ||
|
|
44710048f4 | ||
|
|
9f9f5655b1 | ||
|
|
e621811364 | ||
|
|
685bc846e8 | ||
|
|
3a9d58fae9 | ||
|
|
33639c6e4c | ||
|
|
7c235e3179 | ||
|
|
559d749207 | ||
|
|
53feae0f8c | ||
|
|
0941ccc0ac | ||
|
|
82ecd96ba4 | ||
|
|
e118dc5db1 | ||
|
|
1a23491ac2 | ||
|
|
d916f167a5 | ||
|
|
0102249549 | ||
|
|
2ca96dab42 | ||
|
|
345e60b7a3 | ||
|
|
b9fb6c9d35 | ||
|
|
5d0ccd8867 | ||
|
|
b36f484f3b | ||
|
|
d2f7f986c7 | ||
|
|
9137d03b9f | ||
|
|
f6c0d13a8b | ||
|
|
c472b4e33a | ||
|
|
f6c3fc6b50 | ||
|
|
8ba146495e | ||
|
|
68c46fefb2 | ||
|
|
eddda41559 | ||
|
|
5945fdc2ef | ||
|
|
d110dcbdcf | ||
|
|
ebc0aa8a4e | ||
|
|
d4113b464a | ||
|
|
753dfe1fb3 | ||
|
|
013cccaac5 | ||
|
|
504f8ffbeb | ||
|
|
81d1eb2553 | ||
|
|
c9327b0e92 | ||
|
|
36aa236d82 | ||
|
|
3c37aa4449 | ||
|
|
f1bf3cbe64 | ||
|
|
17a9356040 | ||
|
|
97c799bf2c | ||
|
|
99c02daec7 | ||
|
|
4cb792641d | ||
|
|
fbc2950854 | ||
|
|
08e0bdb437 | ||
|
|
73dcab4de1 | ||
|
|
00594a14ed | ||
|
|
52d5bab0f5 | ||
|
|
a41f4c56f7 | ||
|
|
8ea83962d6 | ||
|
|
9d97cc6e4c | ||
|
|
023579de9a | ||
|
|
214d99f1c3 | ||
|
|
d6fc6b9b10 | ||
|
|
cbdab93f92 | ||
|
|
abf37f0687 | ||
|
|
e993a43399 | ||
|
|
bc79969fd0 | ||
|
|
801f9ed403 | ||
|
|
3d9f6f8f58 | ||
|
|
c2676fb620 | ||
|
|
cdb4ab23e9 | ||
|
|
b3217f2d9c | ||
|
|
7b61385ad5 | ||
|
|
b5c86e74ad | ||
|
|
a3031a7eb9 | ||
|
|
7c49a0535f | ||
|
|
797e5b0794 | ||
|
|
bc6cd78c86 | ||
|
|
98a5b40be5 | ||
|
|
87c174fe0f | ||
|
|
0cbb45e4ad | ||
|
|
b11226af94 | ||
|
|
27dc5b5e19 | ||
|
|
d892b83110 | ||
|
|
c28984524a | ||
|
|
5d4fb63baa | ||
|
|
ca2574d1e5 | ||
|
|
758038e278 | ||
|
|
cb803b1a24 | ||
|
|
c985af5722 | ||
|
|
1ede54acbb | ||
|
|
a73ca6a95d | ||
|
|
25f4095276 | ||
|
|
d242c133dd | ||
|
|
da3e8044b7 | ||
|
|
b6d5939fbf | ||
|
|
d23ef88647 | ||
|
|
f374f130de | ||
|
|
b0c2c8611b | ||
|
|
6f6f56ce64 | ||
|
|
4b28ec4b3a | ||
|
|
d189084c15 | ||
|
|
975d9540bc | ||
|
|
212bfba950 | ||
|
|
619e32cd83 | ||
|
|
ce9ff53ae1 | ||
|
|
d13fb33884 | ||
|
|
253e91250f | ||
|
|
b23668372b | ||
|
|
5b847c136e | ||
|
|
0d304a793d | ||
|
|
906e1a1a19 | ||
|
|
87f46f1e02 | ||
|
|
56b4be1fe1 | ||
|
|
a08dcf05f1 | ||
|
|
460882f6c1 | ||
|
|
71fc94ba63 | ||
|
|
b587d834bc | ||
|
|
9b63e022ed | ||
|
|
324cbd76ee | ||
|
|
ceab13f672 | ||
|
|
10e17ed239 | ||
|
|
4f84479513 | ||
|
|
216ad4ed32 | ||
|
|
85e4555322 | ||
|
|
a90e443a88 | ||
|
|
a9c54b5f91 | ||
|
|
6d21435aab | ||
|
|
9c3bb11a66 | ||
|
|
d983080584 | ||
|
|
852b96009c | ||
|
|
c82617d857 | ||
|
|
6f19256187 | ||
|
|
479ede334b | ||
|
|
e1349255de | ||
|
|
4a600dc01d | ||
|
|
315256ce96 | ||
|
|
bbead20723 | ||
|
|
bb77c1c0bb | ||
|
|
2a2fb9ad55 | ||
|
|
ea5b66cdae | ||
|
|
1a978c7ee3 | ||
|
|
cfd458f463 | ||
|
|
61b8277822 | ||
|
|
53c8036e34 | ||
|
|
f5c8ce5109 | ||
|
|
17b11f19a3 | ||
|
|
2993279d78 | ||
|
|
b95b7e3781 | ||
|
|
c57200132e | ||
|
|
f6d9921581 | ||
|
|
3aada673d5 | ||
|
|
1456070044 | ||
|
|
883797a51d | ||
|
|
1b93512b0a | ||
|
|
7c99262a79 | ||
|
|
d10ecb88bb | ||
|
|
bb0f8e1a9a | ||
|
|
8be1ace9a1 | ||
|
|
80d6879d2a | ||
|
|
0b8dfaa5b5 | ||
|
|
3e25be1e9d | ||
|
|
4e550d14af | ||
|
|
eab33559ca | ||
|
|
83076a0770 | ||
|
|
db65b3140a | ||
|
|
70b703df8b | ||
|
|
00070a1bd2 | ||
|
|
c7bf419ac4 | ||
|
|
850f9f081a | ||
|
|
1110b6b047 | ||
|
|
3c99adc01c | ||
|
|
55cec9a16f | ||
|
|
c36c7c6440 | ||
|
|
7a4e0ea213 | ||
|
|
5a7e1c0ae5 | ||
|
|
9c3b079620 | ||
|
|
c75fff6833 | ||
|
|
6adf2851ba | ||
|
|
c6e1b9c629 | ||
|
|
09d7bad4a6 | ||
|
|
4e0bc1552a | ||
|
|
205d626cd6 | ||
|
|
dee2e5ecf8 | ||
|
|
995f857b08 | ||
|
|
073af2d3b4 | ||
|
|
4b0eaff99c | ||
|
|
2aac56c97a | ||
|
|
67f6880d32 | ||
|
|
1f2816e6fb | ||
|
|
50565fe312 | ||
|
|
6332da52e6 | ||
|
|
fa06d65046 | ||
|
|
1ce27a2543 | ||
|
|
406a4b3448 | ||
|
|
c0259ac409 | ||
|
|
d474ae24d3 | ||
|
|
62b06c0f56 | ||
|
|
af6851562c | ||
|
|
cb62a6a659 | ||
|
|
7411f7324a | ||
|
|
b3467e97b7 | ||
|
|
5b83e774e6 | ||
|
|
9cc2ca1010 | ||
|
|
77aaefa3c3 | ||
|
|
7a09950312 | ||
|
|
2244eb4bf0 | ||
|
|
5f9907adf7 | ||
|
|
64eeae78a8 | ||
|
|
a053104b13 | ||
|
|
d2ea1adcb0 | ||
|
|
20d86905c0 | ||
|
|
5fba6436ab | ||
|
|
dea6d68228 | ||
|
|
2ac9547f6a | ||
|
|
49529af813 | ||
|
|
54eabf49f0 | ||
|
|
16ad50ca4d | ||
|
|
49d09d0420 | ||
|
|
50e613748f | ||
|
|
69d76d2d44 | ||
|
|
65d9026f01 | ||
|
|
5b6ad26b2e | ||
|
|
59a132e536 | ||
|
|
9ec9de519e | ||
|
|
54c631eb40 | ||
|
|
ef96241743 | ||
|
|
0b51c3a5fa | ||
|
|
9ccdd53bd8 | ||
|
|
8e6730a5ce | ||
|
|
a2f4c3088a | ||
|
|
ecb65a29c1 | ||
|
|
b4d8b9ad37 | ||
|
|
d3cde0e373 | ||
|
|
28667067ca | ||
|
|
c469ef02dd | ||
|
|
f48990e31f | ||
|
|
2a0733968e | ||
|
|
cc7c90c012 | ||
|
|
02a54ce8bc | ||
|
|
25256321b2 | ||
|
|
370e3e90b0 | ||
|
|
c0c449ecc2 | ||
|
|
4f9cfcf46d | ||
|
|
b05ff9814e | ||
|
|
fa34e83474 | ||
|
|
1c73dd5efa | ||
|
|
8f66914dfb | ||
|
|
424a764a9c | ||
|
|
53ef315f50 | ||
|
|
9e3c15c9e8 | ||
|
|
240fb2ca7d | ||
|
|
4657df2900 | ||
|
|
a3aa0b9988 | ||
|
|
c33ed61220 | ||
|
|
d7cf3f82ee | ||
|
|
1ec492725c | ||
|
|
03d9dc36ec | ||
|
|
f0ffa1ad8a | ||
|
|
528d269dde | ||
|
|
5a642c4c06 | ||
|
|
867f382cc0 | ||
|
|
203411a875 | ||
|
|
9833c6c7bb | ||
|
|
4af0200161 | ||
|
|
d27ec5f031 | ||
|
|
9482b66846 | ||
|
|
dcf35a6930 | ||
|
|
e526fe79eb | ||
|
|
ad61d29f08 | ||
|
|
2485267666 | ||
|
|
46f4fbe585 | ||
|
|
a7a99eec05 | ||
|
|
bad960d786 | ||
|
|
a3110dbddc | ||
|
|
01c95d79c1 | ||
|
|
2b7cbc41f3 | ||
|
|
8bf1bffd70 | ||
|
|
df163a26de | ||
|
|
a98f8448d6 | ||
|
|
541892a59e | ||
|
|
8dcb6528ac | ||
|
|
210f9a75ed | ||
|
|
50e3a3bbfe | ||
|
|
7dd238a48a | ||
|
|
8a157947a6 | ||
|
|
5e309250f5 | ||
|
|
0875aefacf | ||
|
|
083dcc494e |
@@ -1,7 +1,7 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||
app:
|
||||
multi-scrobbler:
|
||||
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:24-bookworm
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"dockerComposeFile": [
|
||||
"compose.yml"
|
||||
// uncomment to add kasm/discord service
|
||||
//"../deploy/docker-compose.discord.yml"
|
||||
],
|
||||
|
||||
"service": "app",
|
||||
"service": "multi-scrobbler",
|
||||
|
||||
"workspaceFolder": "/workspaces",
|
||||
|
||||
@@ -18,7 +20,7 @@
|
||||
"forwardPorts": [9078,3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
"postCreateCommand": ".devcontainer/postCreate.sh",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
@@ -28,7 +30,9 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"TakumiI.markdowntable"
|
||||
"TakumiI.markdowntable",
|
||||
"YoavBls.pretty-ts-errors",
|
||||
"bowlerr.sqlite-intelliview-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo 'Adding node_modules to path'
|
||||
echo 'export PATH=/workspaces/node_modules/.bin:/workspaces/docsite/node_modules/.bin:$PATH' >> /home/node/.bashrc
|
||||
|
||||
echo 'Updating npm...'
|
||||
npm install -g npm@11.12.1
|
||||
|
||||
echo 'Installing concurrently...'
|
||||
npm install -g concurrently
|
||||
|
||||
echo 'If you have freshly cloned this project or node_modules folder does not exist then you should run "npm run install:parallel" now'
|
||||
+5
-1
@@ -13,6 +13,7 @@ config/*.json
|
||||
config/*.bak
|
||||
config/*.p8
|
||||
/docs
|
||||
dist
|
||||
/logs
|
||||
.flatpak-builder
|
||||
**/generated-sources.*
|
||||
@@ -27,4 +28,7 @@ build
|
||||
.vscode
|
||||
**/act/*
|
||||
tmp-*
|
||||
.env
|
||||
.env
|
||||
*.env
|
||||
.storybook
|
||||
.stories
|
||||
@@ -7,7 +7,9 @@ body:
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- label: I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
|
||||
- label: I have [read the Help docs](https://foxxmd.github.io/multi-scrobbler/help/)
|
||||
required: true
|
||||
- label: I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
|
||||
required: true
|
||||
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
|
||||
required: true
|
||||
@@ -23,6 +25,16 @@ body:
|
||||
placeholder: The app should have scrobbled from Spotify but instead it crashed with an error about undefined properties, etc...
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: What configuration types are you using for multi-scrobbler? Check all that apply.
|
||||
options:
|
||||
- label: "[Environmental Variables (ENV)](https://foxxmd.github.io/multi-scrobbler/configuration/?configType=env#configuration-types)"
|
||||
required: false
|
||||
- label: "[Individual](https://foxxmd.github.io/multi-scrobbler/configuration/?configType=file#configuration-types) Config Files"
|
||||
required: false
|
||||
- label: "[All-In-One](https://foxxmd.github.io/multi-scrobbler/configuration/?configType=aio#configuration-types) Config File"
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
@@ -52,8 +64,8 @@ body:
|
||||
description: |
|
||||
Please copy and paste any/all relevant from the DEBUG level log output.
|
||||
Even if your issue does not seem to be shown in the logs you should still add as much detail as possible, a _lack_ of issues in logs may be a good hint during debugging.
|
||||
Your logs will be automatically formatted into code, do not need use backticks.
|
||||
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/docs/configuration#debug-mode).
|
||||
Your logs will be automatically formatted into code. Do not use fenced code blocks (backticks).
|
||||
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/configuration#debug-mode).
|
||||
render: shell
|
||||
placeholder: |
|
||||
[2025-02-14 12:47:57.170 -0500] VERBOSE: [Init] Config Dir ENV: undefined -> Resolved: CWD/config
|
||||
@@ -61,6 +73,22 @@ body:
|
||||
[2025-02-14 12:47:57.258 -0500] INFO : [Init] Version: master
|
||||
[2025-02-14 12:47:57.259 -0500] INFO : [Init] Generating schema definitions...
|
||||
[2025-02-14 12:47:59.148 -0500] DEBUG : [App] [Scrobblers] [Maloja - default] Found 100 recent scrobbles
|
||||
- type: textarea
|
||||
id: playDebug
|
||||
attributes:
|
||||
label: Play Debug Data
|
||||
description: |
|
||||
If relevent, please copy and paste all [**Debug Data**](https://foxxmd.github.io/multi-scrobbler/help#copy-play-debug-data) related to the Plays you are having issues with.
|
||||
Your data will be automatically formatted into code. Do not use fenced code blocks (backticks) in the text area below.
|
||||
render: shell
|
||||
placeholder: |
|
||||
{
|
||||
"input": {
|
||||
"addedAt": 1690250024,
|
||||
"art": "/library/metadata/186983/art/1698213665",
|
||||
"duration": 243983,
|
||||
"grandparentArt": "/library/metadata/186983/art/1698213665",
|
||||
...
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
|
||||
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
|
||||
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
|
||||
required: true
|
||||
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
|
||||
required: true
|
||||
|
||||
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
|
||||
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/configuration/)
|
||||
required: true
|
||||
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
|
||||
required: true
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
groups:
|
||||
sec-updates:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
directories:
|
||||
- "/"
|
||||
- "/docsite"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
# Look for a `Dockerfile` in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
groups:
|
||||
sec-updates:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
version-updates:
|
||||
applies-to: "version-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,6 +1,6 @@
|
||||
## Checklist before requesting a review
|
||||
|
||||
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
|
||||
- [ ] I have read the [contributing guidelines.](../blob/master/CONTRIBUTING.md)
|
||||
|
||||
## Type of change
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Install app dev dependencies
|
||||
@@ -28,22 +28,24 @@ jobs:
|
||||
- name: Install docusaurus dependencies
|
||||
run: npm ci
|
||||
working-directory: ./docsite
|
||||
|
||||
- name: Build website
|
||||
env:
|
||||
ANALYTICS: ${{ vars.ANALYTICS }}
|
||||
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
|
||||
RY_ANALYTICS: ${{ vars.RY_ANALYTICS }}
|
||||
RY_ANALYTICS_SITEID: ${{ vars.RY_ANALYTICS_SITEID }}
|
||||
RY_ANALYTICS_REPLAY: ${{ vars.RY_ANALYTICS_REPLAY }}
|
||||
GSITEVERIFICATION: ${{ vars.GSITEVERIFICATION }}
|
||||
DOCS_BASE: '/multi-scrobbler'
|
||||
run: npm run build
|
||||
working-directory: ./docsite
|
||||
|
||||
- name: Replace canonical
|
||||
working-directory: ./docsite
|
||||
run: node --experimental-strip-types canonical-replace.ts ./build
|
||||
|
||||
# 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@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #4.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build output to publish to the `gh-pages` branch:
|
||||
@@ -55,3 +57,24 @@ jobs:
|
||||
# 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
|
||||
|
||||
- name: Trigger dependent build
|
||||
if: ${{ vars.DOCS_REPO != ''}}
|
||||
run: |
|
||||
# Trigger builds in dependent repositories
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ vars.DOCS_REPO }}/dispatches" \
|
||||
-d "{
|
||||
\"event_type\": \"release\",
|
||||
\"client_payload\": {
|
||||
\"commit\": \"${{ github.sha }}\"
|
||||
}
|
||||
}"
|
||||
- name: Trigger CF Pages build
|
||||
if: ${{ vars.CF_PAGES == 'true' }}
|
||||
run: |
|
||||
# trigger CF pages hook deployment
|
||||
curl -d "" \
|
||||
"${{ secrets.CF_PAGES_HOOK }}"
|
||||
@@ -51,7 +51,9 @@ jobs:
|
||||
strings=("${VARS_DOCKERHUB_SLUG}" "${VARS_GHCR_SLUG}")
|
||||
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
|
||||
joined_string=$(IFS=, ; echo "${strings[*]}")
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "BUILD_DATE=$date" >> $GITHUB_ENV
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
@@ -104,6 +106,7 @@ jobs:
|
||||
context: ${{github.event.pull_request.head.repo.clone_url}}#${{github.event.pull_request.head.sha}}
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
@@ -117,7 +120,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #7.0.0
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -136,7 +139,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -162,7 +165,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #6.0.0
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
@@ -220,7 +223,7 @@ jobs:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #2.9.4
|
||||
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff #3.0.3
|
||||
with:
|
||||
recreate: true
|
||||
header: "pr-release"
|
||||
|
||||
@@ -66,13 +66,15 @@ jobs:
|
||||
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
|
||||
joined_string=$(IFS=, ; echo "${strings[*]}")
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "BUILD_DATE=$date" >> $GITHUB_ENV
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -126,6 +128,7 @@ jobs:
|
||||
with:
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
@@ -140,7 +143,7 @@ jobs:
|
||||
env:
|
||||
VARS_DIGEST: ${{ steps.build.outputs.digest }}
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #7.0.0
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -159,7 +162,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -185,7 +188,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #6.0.0
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
node-version:
|
||||
description: "Node version"
|
||||
required: false
|
||||
default: '20.x'
|
||||
default: '24.x'
|
||||
type: string
|
||||
ref:
|
||||
description: "Ref to checkout"
|
||||
@@ -18,13 +18,18 @@ jobs:
|
||||
name: Tests / Build / Sanity Run
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
- name: Check out the repo (gha)
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out the repo (act)
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
+19
@@ -73,6 +73,7 @@ web_modules/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
*.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
@@ -127,6 +128,14 @@ config/*.cache
|
||||
|
||||
src/**/**.js
|
||||
src/**/**.js.map
|
||||
src/backend/common/schema/*.json
|
||||
!src/backend/common/schema/aio-client.json
|
||||
!src/backend/common/schema/aio-source.json
|
||||
!src/backend/common/schema/aio.json
|
||||
!src/backend/common/schema/client.json
|
||||
!src/backend/common/schema/source.json
|
||||
docsite/static/schemas/*.json
|
||||
|
||||
|
||||
*.bak
|
||||
*.p8
|
||||
@@ -139,3 +148,13 @@ build
|
||||
**/generated-sources.*
|
||||
.github/act/out
|
||||
tmp-*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
*.db
|
||||
*.db.*
|
||||
*.db-*lib
|
||||
*.db
|
||||
*.db.*
|
||||
*.db-*lib
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"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,45 @@
|
||||
import { defineMain } from "@storybook/react-vite/node";
|
||||
import { mergeConfig } from "vite";
|
||||
|
||||
|
||||
export default defineMain({
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
"@chromatic-com/storybook",
|
||||
// "@storybook/addon-vitest",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-docs",
|
||||
// "@storybook/addon-onboarding"
|
||||
],
|
||||
features: {
|
||||
// https://github.com/storybookjs/storybook/discussions/33279
|
||||
sidebarOnboardingChecklist: false,
|
||||
},
|
||||
"framework": "@storybook/react-vite",
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
optimizeDeps: {
|
||||
include: ["@storybook/addon-docs"],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "fix-mdx-react-shim",
|
||||
enforce: "pre",
|
||||
resolveId(source: any) {
|
||||
if (
|
||||
source.startsWith("file://") &&
|
||||
source.includes("mdx-react-shim.js")
|
||||
) {
|
||||
// Convert file:///... path to normal filesystem path for Vite
|
||||
return new URL(source).pathname;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { addons } from 'storybook/manager-api';
|
||||
import { themes } from 'storybook/theming';
|
||||
|
||||
addons.setConfig({
|
||||
theme: themes.dark,
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import addonDocs from "@storybook/addon-docs";
|
||||
import addonA11y from "@storybook/addon-a11y";
|
||||
import { definePreview } from "@storybook/react-vite";
|
||||
|
||||
import { themes } from 'storybook/theming';
|
||||
|
||||
export default definePreview({
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo'
|
||||
},
|
||||
docs: {
|
||||
theme: themes.dark,
|
||||
},
|
||||
},
|
||||
|
||||
addons: [addonA11y(), addonDocs()]
|
||||
});
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"sanjulaganepola.github-local-actions",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
}
|
||||
Vendored
+13
@@ -1,6 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Canonical Replace",
|
||||
"program": "${workspaceFolder}/docsite/canonical-replace.ts",
|
||||
"runtimeArgs": ["--experimental-strip-types"],
|
||||
"args": [
|
||||
"${workspaceFolder}/docsite/build"
|
||||
],
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"name": "dev",
|
||||
"type": "node",
|
||||
|
||||
Vendored
+2
-1
@@ -4,7 +4,8 @@
|
||||
"mochaExplorer.exit": true,
|
||||
"search.exclude": {
|
||||
"**/.docusaurus": true,
|
||||
"**/build": true
|
||||
"**/build": true,
|
||||
"**/schema": true
|
||||
},
|
||||
"tailwindCSS.files.exclude": [
|
||||
"docsite/**"
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
coc@foxxmd.dev.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+27
-3
@@ -1,7 +1,7 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
ENV NODE_VERSION=20.18.1
|
||||
ENV NODE_VERSION=24.14.0
|
||||
|
||||
# borrowing openssl header removal trick from offical docker-node
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
|
||||
@@ -49,6 +49,8 @@ RUN \
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN echo "Node: $(node -v)\nNPM: $(npm -v)"
|
||||
|
||||
RUN npm install -g concurrently
|
||||
|
||||
ARG data_dir=/config
|
||||
@@ -63,7 +65,8 @@ FROM base AS build
|
||||
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --chown=abc:abc docsite/package*.json tsconfig.json ./docsite/
|
||||
COPY --chown=abc:abc docsite/patches ./docsite/patches
|
||||
COPY --chown=abc:abc docsite/package*.json docsite/tsconfig* ./docsite/
|
||||
|
||||
# for debugging, so the build fails faster when timing out (arm64)
|
||||
#RUN npm config set fetch-retries 1 && \
|
||||
@@ -92,24 +95,45 @@ FROM base AS app
|
||||
|
||||
COPY --chown=abc:abc *.json *.js *.ts index.html ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
# frontend build from vite/esbuild
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
# backend
|
||||
COPY --from=build --chown=abc:abc /app/src/backend /app/src/backend
|
||||
COPY --from=build --chown=abc:abc /app/src/core /app/src/core
|
||||
# docusaurus docs
|
||||
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
|
||||
COPY --from=base /usr/bin /usr/bin
|
||||
COPY --from=base /usr/lib /usr/lib
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV IS_DOCKER=true
|
||||
ENV COLORED_STD=true
|
||||
|
||||
# https://stackoverflow.com/a/63640896/1469797
|
||||
ARG APP_BUILD_VERSION
|
||||
ENV APP_VERSION=$APP_BUILD_VERSION
|
||||
|
||||
RUN npm ci --omit=dev --no-audit \
|
||||
# we used compiled schema json for production so this isn't needed
|
||||
&& npm uninstall ts-json-schema-generator \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules \
|
||||
&& npx @usex/prune-mod -w \
|
||||
&& rm -rf /root/.cache
|
||||
|
||||
ARG BUILD_DATE=0
|
||||
|
||||
LABEL org.opencontainers.image.version="$APP_BUILD_VERSION" \
|
||||
org.opencontainers.image.source="https://github.com/FoxxMD/multi-scrobbler" \
|
||||
org.opencontainers.image.documentation="https://docs.multi-scrobbler.app" \
|
||||
org.opencontainers.image.description="Scrobble from multiple sources to multiple clients" \
|
||||
org.opencontainers.image.title="Multi-Scrobbler" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.authors="FoxxMD" \
|
||||
org.opencontainers.image.created="$BUILD_DATE" \
|
||||
org.opencontainers.image.url="https://docs.multi-scrobbler.app" \
|
||||
maintainer="FoxxMD" \
|
||||
build_version="$APP_BUILD_VERSION"
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
|
||||
@@ -3,61 +3,66 @@
|
||||
[](https://github.com/FoxxMD/multi-scrobbler/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://hub.docker.com/r/foxxmd/multi-scrobbler)
|
||||
[](https://foxxmd.github.io/multi-scrobbler/)
|
||||
[](https://docs.multi-scrobbler.app/)
|
||||
|
||||
|
||||
<img src="/assets/icon.png" align="right"
|
||||
alt="multi-scrobbler logo" width="180" height="180">
|
||||
|
||||
A javascript app to scrobble music you listen to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), [ListenBrainz](https://listenbrainz.org), and other services.
|
||||
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
|
||||
|
||||
* Supports scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
|
||||
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/azuracast)
|
||||
* [Deezer](https://foxxmd.github.io/multi-scrobbler/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/google-cast)
|
||||
* [Icecast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/icecast)
|
||||
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jellyfin)
|
||||
* [JRiver](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jriver)
|
||||
* [Kodi](https://foxxmd.github.io/multi-scrobbler/configuration/sources/kodi)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/sources/koito-source)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-endpoint)
|
||||
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-endpoint)
|
||||
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/sources/maloja)
|
||||
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mopidy)
|
||||
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpris)
|
||||
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/configuration/sources/musikcube)
|
||||
* [Plex](https://foxxmd.github.io/multi-scrobbler/configuration/sources/plex)
|
||||
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/sources/rocksky-source)
|
||||
* [Spotify](https://foxxmd.github.io/multi-scrobbler/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/configuration/sources/webscrobbler)
|
||||
* [VLC](https://foxxmd.github.io/multi-scrobbler/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/yamaha-musiccast)
|
||||
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/clients/koito)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/lastfm)
|
||||
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/clients/listenbrainz)
|
||||
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/clients/maloja)
|
||||
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/clients/rocksky)
|
||||
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/tealfm)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/configuration#webhook-configurations) or [healthcheck endpoint](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint)
|
||||
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/configuration/clients#now-playing) for scrobble Clients
|
||||
* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
|
||||
* [Azuracast](https://docs.multi-scrobbler.app/configuration/sources/azuracast)
|
||||
* [Deezer](https://docs.multi-scrobbler.app/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://docs.multi-scrobbler.app/configuration/sources/google-cast)
|
||||
* [Icecast](https://docs.multi-scrobbler.app/configuration/sources/icecast)
|
||||
* [Jellyfin](https://docs.multi-scrobbler.app/configuration/sources/jellyfin)
|
||||
* [JRiver](https://docs.multi-scrobbler.app/configuration/sources/jriver)
|
||||
* [Kodi](https://docs.multi-scrobbler.app/configuration/sources/kodi)
|
||||
* [Koito](https://docs.multi-scrobbler.app/configuration/sources/koito-source)
|
||||
* [Last.fm](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/lastfm-endpoint)
|
||||
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/sources/librefm-source)
|
||||
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint)
|
||||
* [Maloja](https://docs.multi-scrobbler.app/configuration/sources/maloja)
|
||||
* [Mopidy](https://docs.multi-scrobbler.app/configuration/sources/mopidy)
|
||||
* [MPD (Music Player Daemon)](https://docs.multi-scrobbler.app/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](https://docs.multi-scrobbler.app/configuration/sources/mpris)
|
||||
* [Musikcube](https://docs.multi-scrobbler.app/configuration/sources/musikcube)
|
||||
* [Music Assistant](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint#music-assistant)
|
||||
* [Plex](https://docs.multi-scrobbler.app/configuration/sources/plex)
|
||||
* [Rocksky](https://docs.multi-scrobbler.app/configuration/sources/rocksky-source)
|
||||
* [Sonos](https://docs.multi-scrobbler.app/configuration/sources/sonos)
|
||||
* [Spotify](https://docs.multi-scrobbler.app/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](https://docs.multi-scrobbler.app/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](https://docs.multi-scrobbler.app/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](https://docs.multi-scrobbler.app/configuration/sources/webscrobbler)
|
||||
* [VLC](https://docs.multi-scrobbler.app/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](https://docs.multi-scrobbler.app/configuration/sources/yamaha-musiccast)
|
||||
* [Youtube Music](https://docs.multi-scrobbler.app/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](https://docs.multi-scrobbler.app/configuration/clients)
|
||||
* [Discord](https://docs.multi-scrobbler.app/configuration/clients/discord) (Now Playing)
|
||||
* [Koito](https://docs.multi-scrobbler.app/configuration/clients/koito)
|
||||
* [Last.fm](https://docs.multi-scrobbler.app/configuration/clients/lastfm)
|
||||
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/clients/librefm)
|
||||
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/clients/listenbrainz)
|
||||
* [Maloja](https://docs.multi-scrobbler.app/configuration/clients/maloja)
|
||||
* [Rocksky](https://docs.multi-scrobbler.app/configuration/clients/rocksky)
|
||||
* [teal.fm](https://docs.multi-scrobbler.app/configuration/clients/tealfm)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://docs.multi-scrobbler.app/configuration#webhook-configurations), [healthcheck endpoints](https://docs.multi-scrobbler.app/configuration#health-endpoint), or [prometheus metrics](https://docs.multi-scrobbler.app/configuration/#prometheus).
|
||||
* Supports [Now Playing](https://docs.multi-scrobbler.app/configuration/clients#now-playing) for scrobble Clients
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Graceful network and client failure handling (queued scrobbles that auto-retry)
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://docs.multi-scrobbler.app/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://docs.multi-scrobbler.app/installation#docker) or [locally with NodeJS](https://docs.multi-scrobbler.app/installation#nodejs)
|
||||
|
||||
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
|
||||
[**Quick Start Guide**](https://docs.multi-scrobbler.app//quickstart)
|
||||
|
||||
<img src="/assets/status-ui.png" width="800">
|
||||
<img src="/docsite/static/img/status-ui.png" width="800">
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
@@ -68,15 +73,15 @@ A javascript app to scrobble music you listen to, to [Maloja](https://github.com
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
Yes! You can use [Last.fm as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
You set up [configurations](https://docs.multi-scrobbler.app/configuration) for one or more [**Sources**](https://docs.multi-scrobbler.app/configuration/sources) and one or more [**Clients**](https://docs.multi-scrobbler.app/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
|
||||
### Source
|
||||
|
||||
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, like a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
A [**Source**](https://docs.multi-scrobbler.app/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
@@ -86,7 +91,7 @@ Source configurations consist of:
|
||||
|
||||
### Client
|
||||
|
||||
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
A [**Client**](https://docs.multi-scrobbler.app/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
@@ -95,15 +100,15 @@ Client configurations consist of:
|
||||
|
||||
## Quick Start
|
||||
|
||||
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
|
||||
[See the **Quick Start Guide**](https://docs.multi-scrobbler.app/quickstart)
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
|
||||
[See the **Installation** documentation](https://docs.multi-scrobbler.app/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
|
||||
[See the **Configuration** documentation](https://docs.multi-scrobbler.app/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -118,11 +123,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
|
||||
Having issues with connections or configuration? Check the [FAQ](https://docs.multi-scrobbler.app/FAQ) before creating an issue!
|
||||
|
||||
## Development
|
||||
|
||||
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
|
||||
[Detailed architecture and development guides for Sources/Clients](https://docs.multi-scrobbler.app/development/dev-common)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
|
||||
RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
nodejs \
|
||||
npm \
|
||||
&& \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/tmp/*
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ARG data_dir=/config
|
||||
VOLUME $data_dir
|
||||
ENV CONFIG_DIR=$data_dir
|
||||
|
||||
COPY docker/root/ /
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as build
|
||||
|
||||
# copy dep/TS config and install dev dependencies
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
|
||||
RUN npm ci \
|
||||
&& chown -R root:root node_modules
|
||||
#RUN yarn install
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
|
||||
# need to set before build so server/client build is optimized and has constants (if needed)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run docs:install && npm run build && rm -rf node_modules && rm -rf docsite/node_modules
|
||||
|
||||
FROM base as app
|
||||
|
||||
#COPY --chown=abc:abc package.json yarn.lock ./
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV IS_DOCKER=true
|
||||
ENV COLORED_STD=true
|
||||
|
||||
# https://stackoverflow.com/a/63640896/1469797
|
||||
ARG APP_BUILD_VERSION
|
||||
ENV APP_VERSION=$APP_BUILD_VERSION
|
||||
|
||||
RUN npm ci --omit=dev \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules \
|
||||
&& rm -rf node_modules/@types
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
EXPOSE $PORT
|
||||
@@ -0,0 +1 @@
|
||||
* Attribution for [`default-artwork.png`](/assets/default-artwork.png): https://www.flaticon.com/free-icon/music_15795471
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 699 KiB |
+42
-29
@@ -1,18 +1,20 @@
|
||||
# configuration file for git-cliff (0.1.0)
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
# A Tera template to be rendered as the changelog's header.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
header = """
|
||||
# Changelog
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
# A Tera template to be rendered for each release in the changelog.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
## Unreleased
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
@@ -24,6 +26,7 @@ body = """
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
@@ -33,35 +36,45 @@ body = """
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespaces from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
# A Tera template to be rendered as the changelog's footer.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
# allow only conventional commits
|
||||
# https://www.conventionalcommits.org
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# regex for parsing and grouping commits
|
||||
# Exclude commits that do not match the conventional commits specification.
|
||||
filter_unconventional = true
|
||||
# An array of regex based parsers for extracting data from the commit message.
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
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"},
|
||||
{ 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 = "^ci", group = "🧑🍳 Continuous Integration" },
|
||||
{ message = "^build", group = "🐋 Build" },
|
||||
{ message = "^chore\\(release\\)", skip = true },
|
||||
{ message = "^chore", group = "⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "🛡️ Security" },
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
# Exclude commits that are not matched by any commit parser.
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order = false
|
||||
# Order of commits in each group/release within the changelog.
|
||||
# Allowed values: newest, oldest
|
||||
sort_commits = "oldest"
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MS",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"applicationId": "8190211179716453570",
|
||||
"ipcLocations": ["192.168.0.105:6655"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MS",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
|
||||
"applicationId": "8190211179716453570"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"librariesAllow": ["GoodMusic"],
|
||||
"librariesBlock": ["BadMusic"],
|
||||
"additionalAllowedLibraryTypes": ["musicvideos"],
|
||||
"allowUnknown": false,
|
||||
"allowMediaTypes": ["audio"],
|
||||
"frontendUrlOverride": "https://myJellyfinExternal.tld"
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "myLibreFmClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "anything",
|
||||
"secret": "anything",
|
||||
"redirectUri": "http://localhost:9078/librefm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myLibreFmSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"apiKey": "anything",
|
||||
"secret": "anything",
|
||||
"redirectUri": "http://localhost:9078/librefm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -16,7 +16,7 @@
|
||||
"options": {
|
||||
"logPayload": true,
|
||||
"logFilterFailure": "debug",
|
||||
"ignoreInvalidCert": "false"
|
||||
"ignoreInvalidCert": false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[
|
||||
// DEPRECATED, use API Source instead
|
||||
// rename files to plex.json to use
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MySonos",
|
||||
"data": {
|
||||
"host": "192.168.0.150",
|
||||
"devicesAllow": ["move","beam"],
|
||||
"devicesBlock": ["roam"],
|
||||
"groupsAllow": ["family"],
|
||||
"groupsBlock": ["bedroom"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,15 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "MyTautuilli",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -12,7 +12,11 @@
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3"
|
||||
"appPassword": "twog-phu7-4dhe-y4j3",
|
||||
|
||||
// everything below is optional
|
||||
"serviceAllow": ["plyr"],
|
||||
"serviceDeny": ["last.fm"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "Yandex Music",
|
||||
"type": "ymbridge",
|
||||
"data": {
|
||||
"url": "http://192.168.1.161:9980",
|
||||
"apiKey": "change-me",
|
||||
"interval": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
multi-scrobbler:
|
||||
volumes:
|
||||
- kasm_run:/run
|
||||
kasmcord:
|
||||
container_name: kasmcord
|
||||
image: kasmweb/discord:1.14.0
|
||||
ports:
|
||||
# VNC port to use in web browser
|
||||
- 6901:6901
|
||||
shm_size: 512m
|
||||
environment:
|
||||
# set a password for VNC access
|
||||
VNC_PW: ${KASM_PW:-hunter2}
|
||||
XDG_RUNTIME_DIR: /run/user/1000
|
||||
volumes:
|
||||
- kasm_run:/run/user/1000
|
||||
- kasm_config:/home/kasm-user/.config
|
||||
user: "0"
|
||||
entrypoint: sh -c "chmod 700 /run/user/1000 && chown -R kasm-user:kasm-user /run/user/1000 && chown -R kasm-user:kasm-user /home/kasm-user/.config && su kasm-user -c '/dockerstartup/kasm_default_profile.sh /dockerstartup/vnc_startup.sh /dockerstartup/kasm_startup.sh'"
|
||||
|
||||
volumes:
|
||||
kasm_run:
|
||||
name: kasm_run
|
||||
kasm_config:
|
||||
name: kasm_config
|
||||
@@ -4,6 +4,8 @@ services:
|
||||
multi-scrobbler:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
BUILD_DATE: "${BUILD_DATE}"
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
|
||||
@@ -18,3 +18,4 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lib
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
var args = process.argv.slice(2);
|
||||
|
||||
const linkRegex = new RegExp(/<link data-rh="true" rel="\w+"\shref="(https:\/\/foxxmd\.github\.io\/multi-scrobbler)/g,);
|
||||
const replacement = process.env.CANONICAL_HREF ?? 'https://docs.multi-scrobbler.app';
|
||||
|
||||
/**
|
||||
* Replace canonical and alternative <link> nodes with the "real" domain of the site
|
||||
* so that SEO (google) chooses the correct domain when showing search results
|
||||
*
|
||||
*/
|
||||
|
||||
(async function () {
|
||||
|
||||
if(replacement === undefined || replacement === '') {
|
||||
console.warn('No replacement value found in process.env.CANONICAL_HREF');
|
||||
return;
|
||||
}
|
||||
|
||||
const buildDir = path.resolve(args[0]);
|
||||
console.log(`Reading dir recursively ${buildDir}`);
|
||||
const files = (await fs.readdir(buildDir, { recursive: true })).filter(x => x.includes('.html'));
|
||||
console.log(`Found ${files.length} files with .html extensions`);
|
||||
let modifications = 0;
|
||||
|
||||
await Promise.all(files.map(async (x) => {
|
||||
const filePath = path.resolve(path.join(buildDir, x));
|
||||
//console.log(`Replacing at ${filePath}`);
|
||||
await fs.writeFile(filePath, (await fs.readFile(filePath)).toString().replace(linkRegex, (match, capture, offset, string, groups) => {
|
||||
// this may not be fully accurate since we're mutating concurrently/async
|
||||
// but its a good enough signal for logging to tell if replacements happened
|
||||
modifications++;
|
||||
return match.replace(capture, replacement);
|
||||
}));
|
||||
}));
|
||||
|
||||
console.log(`Done with ${modifications} replacements`);
|
||||
|
||||
}());
|
||||
+24
-2
@@ -1,6 +1,7 @@
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
title: FAQ
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
@@ -60,6 +61,21 @@ If multi-scrobbler is not running on the same machine your browser is on then th
|
||||
|
||||
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
|
||||
|
||||
### I get errors with `ECONNRESET` `ETIMEDOUT` `ENOTFOUND` `EAI_AGAIN`
|
||||
|
||||
These are errors indicating a network issue is occuring between you and the Source/Client.
|
||||
|
||||
* `ECONNRESET` means the upstream service unexpectedly closed the connection
|
||||
* `ETIMEDOUT` means the upstream service did not respond to a request MS
|
||||
* `ENOTFOUND` means that MS could not find a DNS record for the domain of the upstream service
|
||||
* `EAI_AGAIN` means that a DNS server did not respond to MS's request to lookup the record for a domain
|
||||
|
||||
`ECONNRESET` and timeout issues can occur when you have a poor connection or if the upstream service is overloaded or is generally not behaving well. This is a common occurrence for Listenbrainz/Musicbrainz related requests.
|
||||
|
||||
Please check the [Upstream Services Status Page](https://status.multi-scrobbler.app) to independently verify if the service is misbehaving. If the service is up and has good uptime then the issue may be in your network.
|
||||
|
||||
`ENOTFOUND` and `EAI_AGAIN` and generally caused by DNS caching (stale records) or misbehaving DNS servers. Though this can happen at the upstream service's end, it is *usually* an issue in your network. If you run a Pihole, AdGaurd, or other DNS solution try flushing the cache for the mishaving domain and check that your DNS application is working correctly.
|
||||
|
||||
### Youtube Music fails after some time
|
||||
|
||||
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
|
||||
@@ -70,7 +86,7 @@ Error: Could not send the specified request to browse. Status code: 401
|
||||
|
||||
then YTM has invalidated your authentication.
|
||||
|
||||
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#authentication) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
|
||||
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
|
||||
|
||||
#### When using OAuth Client Authentication
|
||||
|
||||
@@ -80,7 +96,7 @@ Refresh your authentication by using the **(Re)authenticate** link from MS's web
|
||||
|
||||
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
|
||||
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth) and then restart MS to potentially resolve the problem.
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
@@ -98,6 +114,12 @@ It means the JSON in your configuration file is not valid. Copy and paste your c
|
||||
|
||||
## Scrobbling Issues
|
||||
|
||||
### How do I prevent duplicate scrobbles?
|
||||
|
||||
Multi-scrobbler has a [robust duplicate scrobble detection system](/configuration/duplicates#how-duplicates-are-detected) that should handle this for you in the majority of usecases. If you have a [simple setup without any circular dependencies](/configuration/duplicates#valid-configs) then you are already protected against duplicates.
|
||||
|
||||
If your scenario is more complex or you have a service(s) acting as **both** a **Client** and **Source** you should refer to the [**Duplicate Scrobble Guidance**](/configuration/duplicates#valid-configs) page to determine if you can avoid a configuration that may result in duplicate scrobbles.
|
||||
|
||||
### Last.fm does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Scrobble Clients
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
@@ -12,7 +13,9 @@ A **Client** is an application that stores the historical information about what
|
||||
|
||||
| Name | Now Playing |
|
||||
| :-------------------------------------------------- | :---------- |
|
||||
| [Discord](/configuration/clients/discord) | ✅ |
|
||||
| [Last.fm](/configuration/clients/lastfm) | ✅ |
|
||||
| [Libre.fm](/configuration/clients/librefm) | ✅ |
|
||||
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
|
||||
| [Koito](/configuration/clients/koito) | ❌ |
|
||||
| [Maloja](/configuration/clients/maloja) | ❌ |
|
||||
@@ -21,6 +24,20 @@ A **Client** is an application that stores the historical information about what
|
||||
|
||||
## Features
|
||||
|
||||
### Duplicate Detection
|
||||
|
||||
Before a Play is scrobbled to a client MS checks existing scrobbles from the Client's API to see if the Play has already been scrobbled.
|
||||
|
||||
For each Play, MS fetches (cached) scrobbles from the Client in a time range inclusive of the Play's listening timestamp and then scores existing scrobbles against the Play based on:
|
||||
|
||||
* Similarity of Title, Artists, and Album
|
||||
* Temporal closeness of the Play's timestamp to the existing scrobble's timestamp
|
||||
* Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)
|
||||
|
||||
Detailed scoring breakdowns against each existing scrobble are logged at the `TRACE` logging level or in the [debug data](/help#copy-play-debug-data) for a scrobble.
|
||||
|
||||
See the [**Duplicate Scrobble Guidance**](/configuration/duplicates#how-duplicates-are-detected) page to learn how duplicates are detected and how you can ensure your configuration avoids possible duplicates.
|
||||
|
||||
### Dead Scrobbles
|
||||
|
||||
If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Discord",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "configuration/clients/discord/discord"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
---
|
||||
title: Discord (App)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import TcpWarning from "@site/src/components/snippets/_discord-port-warn.mdx";
|
||||
import JsonConfig from '!!raw-loader!@site/../config/discord-local.json.example';
|
||||
|
||||
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
This integration uses an **existing Discord application** to set your Rich Presence.
|
||||
|
||||
:::tip
|
||||
|
||||
Check the [main Discord docs page](/configuration/clients/discord) for a comparison between **App** and **Headless**.
|
||||
|
||||
:::
|
||||
|
||||
## Required Setup
|
||||
|
||||
### Discord Application
|
||||
|
||||
You must create a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) in order to use this integration. Creating an App is free and simple.
|
||||
|
||||
Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
|
||||
* Click on "New Application" and give it a name
|
||||
* Copy the **Application Id** (Client Id) shown after creation
|
||||
* Add this Id to your Multi-Scrobbler config
|
||||
* Env Config => `DISCORD_APPLICATION_ID=12345678`
|
||||
* File Config => `"applicationId": "12345678"`
|
||||
|
||||
### Discord Connection
|
||||
|
||||
This integration uses an existing, running Discord application to communicate with Discord and set Rich Presence. Multi-scrobbler must be able to communicate with the Discord application, either by being on the same machine or over a network.
|
||||
|
||||
<Tabs groupId="discordType" queryString>
|
||||
|
||||
<TabItem value="container" label="Containerized Discord">
|
||||
|
||||
[Kasm Workspaces](https://kasm.com) provides a self-hostable [Discord container image](https://hub.docker.com/r/kasmweb/discord). This image provides a desktop-like experience, through browser-based VNC, to use the official Discord application.
|
||||
|
||||
<img src={require('/img/discord_kasm.png').default} height="400"/>
|
||||
|
||||
This containerized approach can be used instead of configuring MS to use a [Native Discord](./?discordType=native#discord-connection) application on a specific machine.
|
||||
|
||||
**Advantages**
|
||||
|
||||
* Setup is much simpler
|
||||
* Discord runs in the same stack as multi-scrobbler
|
||||
* **Configuration is copy-paste.** Skip all of the [Discord Connection](#discord-connection) instructions.
|
||||
* Get a [Headless](/configuration/clients/discord/discord-headless)-like experience without needing to break Discord TOS
|
||||
|
||||
**Disadvantages**
|
||||
|
||||
* Image only available for x86 arch
|
||||
* Image and container are large (1GB+)
|
||||
* Discord does not notify *other* clients of notifications in a channel if *any* client is viewing that channel.
|
||||
* To avoid this, make sure your container discord instance is not viewing any server/channel/dm.
|
||||
* Sometimes, Discord does not deliver "notifications you missed" to mobile because this depends on *all* clients being offline.
|
||||
|
||||
<h2>Setup</h2>
|
||||
|
||||
Setup is basically the same as using the instructions for [Discord on Linux](./?osType=linux&discordType=native#find-ipc-connection) with [MS on Same Host (Docker)](./?osType=linux&discordType=native#pass-ipc-to-multi-scrobbler).
|
||||
|
||||
<h3>1. Add Discord Service to Multi-Scrobbler Docker Compose Stack</h3>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Detailed Explanation</summary>
|
||||
|
||||
* Add the `kasmweb/discord` service to your compose stack
|
||||
* Mount the directory containing Discord's unix socket to a named volume
|
||||
* Configure MS to use Discord's unix socket
|
||||
* Mount the unix socket volume into MS's container
|
||||
* Add ENV `DISCORD_IPC_LOCATIONS` to point to the mounted volume
|
||||
|
||||
</details>
|
||||
|
||||
Use this example `docker-compose.yaml` to modify your existing multi-scrobbler `docker-compose.yaml`:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler:latest
|
||||
environment:
|
||||
# ...
|
||||
- DISCORD_APPLICATION_ID=12345678
|
||||
# add below line
|
||||
// highlight-start
|
||||
- DISCORD_IPC_LOCATIONS=/run/discord-ipc-0
|
||||
// highlight-end
|
||||
volumes:
|
||||
# ...
|
||||
# add below line
|
||||
// highlight-start
|
||||
- kasm_run:/run
|
||||
// highlight-end
|
||||
# ...
|
||||
|
||||
# .. add discord service below
|
||||
// highlight-start
|
||||
kasmcord:
|
||||
container_name: kasmcord
|
||||
image: kasmweb/discord:1.14.0
|
||||
ports:
|
||||
# VNC port to use in web browser
|
||||
- 6901:6901
|
||||
shm_size: 512m
|
||||
environment:
|
||||
# set a password for VNC access
|
||||
VNC_PW: hunter2
|
||||
XDG_RUNTIME_DIR: /run/user/1000
|
||||
volumes:
|
||||
- kasm_run:/run/user/1000
|
||||
- kasm_config:/home/kasm-user/.config
|
||||
user: "0"
|
||||
entrypoint: sh -c "chmod 700 /run/user/1000 && chown -R kasm-user:kasm-user /run/user/1000 && chown -R kasm-user:kasm-user /home/kasm-user/.config && su kasm-user -c '/dockerstartup/kasm_default_profile.sh /dockerstartup/vnc_startup.sh /dockerstartup/kasm_startup.sh'"
|
||||
|
||||
volumes:
|
||||
kasm_run:
|
||||
name: kasm_run
|
||||
kasm_config:
|
||||
name: kasm_config
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Then re-deploy your stack.
|
||||
|
||||
<h3>2. Login to Discord</h3>
|
||||
|
||||
Navigate in your browser to the IP/host Multi-Scrobbler/Discord are running on. Use the `port` set in the `kasmcord` service:
|
||||
|
||||
```
|
||||
# basic auth login
|
||||
# user: kasm_user
|
||||
# pass is what was set in env VNC_PW
|
||||
https://192.168.MY.HOST:6901
|
||||
```
|
||||
|
||||
Login to Discord normally. After login is complete MS should connect automatically when a Now Playing event happens. If it does not or reports authentication failure then restart MS (but not Discord).
|
||||
|
||||
If MS works then you are done and can move on to [Optional Setup](#optional-setup).
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="native" label="Native Discord">
|
||||
|
||||
Use this method if you want to connect MS to Discord running on a physical desktop.
|
||||
|
||||
<DetailsAdmo type="tip" summary="Local MS Installation Requires No Config">
|
||||
|
||||
If MS was [installed locally](/installation#local-installation) (not Docker) on the same machine as the Discord application then MS should [automatically detect the correct method](#find-ipc-connection) and [use it](#pass-ipc-to-multi-scrobbler) **without** any configuration from you. This will work for **any OS**.
|
||||
|
||||
If MS cannot determine this automatically, follow the steps for your OS below.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
Follow the sections below to configure MS for your specific environment.
|
||||
|
||||
#### Find IPC Connection
|
||||
|
||||
Discord uses OS-specific methods to communicate with other applications on the same host, called [IPC](https://en.wikipedia.org/wiki/Inter-process_communication). You may need to determine some details about the IPC method in order for MS to communicate with it correctly.
|
||||
|
||||
|
||||
<Tabs groupId="osType" queryString>
|
||||
|
||||
<TabItem value="linux" label="Discord on Linux">
|
||||
|
||||
On Linux, Discord uses a [Unix Socket](https://en.wikipedia.org/wiki/Unix_domain_socket) for IPC.
|
||||
|
||||
With Discord running, run this command:
|
||||
|
||||
```
|
||||
$ sudo netstat -apeen | grep discord
|
||||
|
||||
unix 2 [ ACC ] STREAM LISTENING 11026149 1641787/Discord /home/foxx/.var/app/com.discordapp.Discord/cache/scoped_dirnnxNIn/SingletonSocket
|
||||
unix 2 [ ACC ] STREAM LISTENING 11036791 1642030/exe /run/user/1000/discord-ipc-0
|
||||
```
|
||||
|
||||
We see that the path we need is:
|
||||
|
||||
```
|
||||
/run/user/1000/discord-ipc-0
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="macos" label="Discord on MacOS">
|
||||
|
||||
On MacOS, Discord uses a [Unix Socket](https://en.wikipedia.org/wiki/Unix_domain_socket) for IPC.
|
||||
|
||||
With Discord running, run this command:
|
||||
|
||||
```
|
||||
$ sudo netstat -apeen | grep discord
|
||||
|
||||
unix 2 [ ACC ] STREAM LISTENING 11026149 1641787/Discord /home/foxx/.var/app/com.discordapp.Discord/cache/scoped_dirnnxNIn/SingletonSocket
|
||||
unix 2 [ ACC ] STREAM LISTENING 11036791 1642030/exe /run/user/1000/discord-ipc-0
|
||||
```
|
||||
|
||||
We see that the path we need is:
|
||||
|
||||
```
|
||||
/run/user/1000/discord-ipc-0
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="windows" label="Discord on Windows">
|
||||
|
||||
Windows uses [**Named Pipes**](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) for IPC. These aren't easily iteratable without external software, like [Pipetap](https://github.com/sensepost/pipetap).
|
||||
|
||||
The syntax for a named pipe is like this:
|
||||
|
||||
```
|
||||
\\.\pipe\app_named_pipe_foo
|
||||
```
|
||||
|
||||
and Discord's names follow this pattern:
|
||||
|
||||
```
|
||||
\\.\pipe\discord-ipc-0
|
||||
```
|
||||
|
||||
Where `0` increments for each different instance of Discord (official, canary, nightly, etc...).
|
||||
|
||||
The default Discord pipe name is: `discord-ipc-0`
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
#### Pass IPC to Multi-Scrobbler
|
||||
|
||||
After [finding your IPC connection path](#find-ipc-connection) you will need to make it accessible to Multi-Scrobbler.
|
||||
|
||||
<Tabs groupId="osType" queryString>
|
||||
|
||||
<TabItem value="linux" label="Discord on Linux">
|
||||
|
||||
<Tabs groupId="msConnection">
|
||||
|
||||
<TabItem value="remote" label="MS on Different Host">
|
||||
|
||||
<TcpWarning/>
|
||||
|
||||
Use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's [unix socket](#find-ipc-connection) to a listening TCP port on the same host. Likely, you will want to setup a service to start this command on login.
|
||||
|
||||
```shell
|
||||
$ socat -v TCP-LISTEN:6655,reuseaddr,fork UNIX-CONNECT:/run/user/1000/discord-ipc-0
|
||||
```
|
||||
|
||||
In your MS docker compose file add the `host:port` of the host running Discord/`socat` to `DISCORD_IPC_LOCATIONS` `environment:`
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
environment:
|
||||
# ...
|
||||
DISCORD_IPC_LOCATIONS=192.168.DISCORD_HOST.IP:6655
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="local-docker" label="MS on Same Host (Docker)">
|
||||
|
||||
In your MS docker compose file, pass the path of Discord's [unix socket](#find-ipc-connection) as a volume and add this path to `DISCORD_IPC_LOCATIONS` in `environment:`
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
environment:
|
||||
# ...
|
||||
DISCORD_IPC_LOCATIONS=/run/discord-ipc-0
|
||||
volumes:
|
||||
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="local" label="MS on Same Host (Non-Docker)">
|
||||
|
||||
No configuration is required. If MS does not automatically work for you then try the **MS on Same Host (Docker)** method using the `DISCORD_IPC_LOCATIONS` environment variable.
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="macos" label="Discord on MacOS">
|
||||
|
||||
<Tabs groupId="msConnection">
|
||||
|
||||
<TabItem value="remote" label="MS on Different Host">
|
||||
|
||||
<TcpWarning/>
|
||||
|
||||
Use one of the methods below to setup `socat` on your macOS machine.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>`socat` installed natively</summary>
|
||||
|
||||
Install [`socat`](https://formulae.brew.sh/formula/socat) using homebrew to use it as a native command on your host.
|
||||
|
||||
Then, use [`socat`](https://linux.die.net/man/1/socat) to bidirectionally relay communication from Discord's [unix socket](#find-ipc-connection) to a listening TCP port on the same host. Likely, you will want to setup a service to start this command on login.
|
||||
|
||||
```shell
|
||||
$ socat -v TCP-LISTEN:6655,reuseaddr,fork UNIX-CONNECT:/run/user/1000/discord-ipc-0
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>`socat` in Docker</summary>
|
||||
|
||||
Use the [`socat` container](https://hub.docker.com/r/alpine/socat) in combination with the Discord [unix socket](#find-ipc-connection) found earlier:
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
services:
|
||||
socat:
|
||||
image: alpine/socat
|
||||
ports:
|
||||
- "6655:6655"
|
||||
volumes:
|
||||
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
|
||||
command:
|
||||
- '-v'
|
||||
- 'TCP-LISTEN:6655,reuseaddr,fork'
|
||||
- 'UNIX-CONNECT:/run/discord-ipc-0'
|
||||
```
|
||||
</details>
|
||||
|
||||
In your MS docker compose file add the `host:port` of the host running Discord/`socat` to `DISCORD_IPC_LOCATIONS` `environment:`
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
environment:
|
||||
# ...
|
||||
DISCORD_IPC_LOCATIONS=192.168.DISCORD_HOST.IP:6655
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="local-docker" label="MS on Same Host (Docker)">
|
||||
|
||||
In your MS docker compose file, pass the path of Discord's [unix socket](#find-ipc-connection) as a volume and add this path to `DISCORD_IPC_LOCATIONS` in `environment:`
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
environment:
|
||||
# ...
|
||||
DISCORD_IPC_LOCATIONS=/discord-ipc-0
|
||||
volumes:
|
||||
- /run/user/1000/discord-ipc-0:/run/discord-ipc-0
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="local" label="MS on Same Host (Non-Docker)">
|
||||
|
||||
No configuration is required. If MS does not automatically work for you then try the **MS on Same Host (Docker)** method using the `DISCORD_IPC_LOCATIONS` environment variable.
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="windows" label="Discord on Windows">
|
||||
|
||||
<Tabs groupId="msConnection">
|
||||
|
||||
<TabItem value="remote" label="MS in Docker">
|
||||
|
||||
:::note
|
||||
|
||||
While it is possible to use [named pipes directly in Docker](https://forums.docker.com/t/volume-mount-path-error/143626/3) this has not been tested and [may only work for windows containers](https://github.com/docker/for-win/issues/6813#issuecomment-632859857). If you are adventurous, try this out and if you can get it working please [report your findings in an issue.](https://github.com/FoxxMD/multi-scrobbler/issues)
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
This method is **experimental** and may be buggy and prone to crashes. If you are a Windows or Rust developer please consider helping improve the [pipe proxy implementation](https://github.com/FoxxMD/named-pipe-to-tcp-proxy).
|
||||
|
||||
:::
|
||||
|
||||
<TcpWarning/>
|
||||
|
||||
Use [**pipeProxy**](https://github.com/FoxxMD/named-pipe-to-tcp-proxy) to connect to the Discord named pipe and expose it on a TCP port.
|
||||
|
||||
Download the latest [`pipeProxy.exe`](https://github.com/FoxxMD/named-pipe-to-tcp-proxy/releases) from the releases page.
|
||||
|
||||
The host/ip should be the IP of the machine running discord.
|
||||
|
||||
Run pipeProxy from the windows command line:
|
||||
|
||||
```
|
||||
pipeProxy.exe --pipe "\\.\pipe\discord-ipc-0" --tcp "192.168.0.100:6655"
|
||||
```
|
||||
|
||||
In your MS docker compose file add the `host:port` of the host running `pipeProxy` to `DISCORD_IPC_LOCATIONS` `environment:`
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
environment:
|
||||
# ...
|
||||
DISCORD_IPC_LOCATIONS=192.168.0.100:6655
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="local" label="MS on Same Host (Non-Docker)">
|
||||
|
||||
No configuration is required. If MS does not automatically work for you then try the **MS in Docker** method using the `DISCORD_IPC_LOCATIONS` environment variable.
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Optional Setup
|
||||
|
||||
:::note
|
||||
|
||||
Due to the limitations of the Discord local API the other listening activity detection and online status customizations found in [Headless](/configuration/clients/discord/discord-headless#optional-setup) are not available for the Local integration.
|
||||
|
||||
:::
|
||||
|
||||
### Artwork
|
||||
|
||||
Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
|
||||
|
||||
The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
|
||||
|
||||
#### Artwork URL Types
|
||||
|
||||
<Tabs groupId="artworkUrl" queryString>
|
||||
<TabItem label="External URLs" value="external">
|
||||
MS can use external URLs for artwork.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Why Should I Care?</summary>
|
||||
|
||||
There are some scenarios where you may not want your artwork URLs to be visible, for example:
|
||||
|
||||
* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
|
||||
* A Source may be internet-facing but you do not want to expose this information to discord/other users
|
||||
* A Source may be internet-facing but requires authentication to view the image
|
||||
</details>
|
||||
|
||||
MS will use your artwork URLs only if:
|
||||
|
||||
* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
|
||||
* artwork url starts with `https` and
|
||||
* `artwork` (`DISCORD_ARTWORK`) is
|
||||
* `true` => always uses the url
|
||||
* a list of custom domains keywords an external artwork url should contain EX
|
||||
* ENV => `DISCORD_ARTWORK=mycdn,jellyfin`
|
||||
* File Config => `"artwork": ["mycdn","jellyfin"]`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Examples</summary>
|
||||
|
||||
None of the below URLs will **ever** be used because they are not `https`
|
||||
|
||||
```
|
||||
http://foobar.com/cool.jpg <-- NOT allowed, not http
|
||||
http://192.168.0.112/fun.png <-- NOT allowed, not http
|
||||
```
|
||||
|
||||
When `DISCORD_ARTWORK` is not set:
|
||||
|
||||
```
|
||||
https://archive.org/download/dir/myimage.jpg <-- allowed, is https and known service
|
||||
https://spotify.com/assets/cover.png?size=1000 <-- allowed, is https and known service
|
||||
https://example.com/cool.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
|
||||
https://anything.com/neat.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
|
||||
```
|
||||
|
||||
Setting `DISCORD_ARTWORK=true`
|
||||
|
||||
```
|
||||
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
|
||||
http://foobar.com/cool.jpg <-- NOT allowed, not http
|
||||
https://example.com/cool.jpg <-- allowed, is https
|
||||
https://anything.com/neat.jpg <-- allowed, is https
|
||||
```
|
||||
|
||||
Setting `DISCORD_ARTWORK=mycdn,jellyfin`
|
||||
|
||||
```
|
||||
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
|
||||
http://foobar.com/cool.jpg <-- NOT allowed, not http
|
||||
https://example.com/cool.jpg <-- NOT allowed, is https but does not contain "mycdn" or "jellyfin"
|
||||
https://jellyfin.mydomain.com/neat.jpg <-- allowed, is https and contains "jellyfin"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Musicbrainz/Cover Art Achive" value="caa">
|
||||
MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
MS will try to use Cover Art Archive if:
|
||||
|
||||
* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
|
||||
* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
|
||||
|
||||
Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
|
||||
|
||||
Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
|
||||
|
||||
For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
|
||||
</TabItem>
|
||||
<TabItem label="MS Default" value="ms-default">
|
||||
When these conditions are true:
|
||||
|
||||
* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
|
||||
* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
|
||||
* Play does not contain artwork information (no artwork shown in dashboard)
|
||||
|
||||
The status artwork will use a default URL instead of your artwork URL.
|
||||
|
||||
Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
|
||||
|
||||
<img src={require('/img/discord_presence_ms_default.png').default} height="127"/>
|
||||
|
||||
This default image can be customized:
|
||||
|
||||
* ENV Config => `DISCORD_ARTWORK_DEFAULT_URL=https://cooldomain.com/art.png`
|
||||
* File Config => `"artworkDefaultUrl": "https://cooldomain.com/art.png"`
|
||||
|
||||
Remember, this URL need to be accessible on the public internet.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="DiscordClientConfig" fileContent={JsonConfig} client name="discord">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :---------------------------- | --------- | ------- | :--------------------------------------------------------------------------------------------------------------- |
|
||||
| `DISCORD_APPLICATION_ID` | Yes | | Application ID |
|
||||
| `DISCORD_IPC_LOCATIONS` | No | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
|
||||
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
|
||||
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
|
||||
</Config>
|
||||
@@ -0,0 +1,334 @@
|
||||
---
|
||||
title: Discord (Headless)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/discord.json.example';
|
||||
|
||||
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
This integration is **headless**, it does not depend on any specific Discord application to be running.
|
||||
|
||||
:::tip
|
||||
|
||||
Check the [main Discord docs page](/configuration/clients/discord) for a comparison between **App** and **Headless**.
|
||||
|
||||
:::
|
||||
|
||||
<DetailsAdmo type="note" summary="How MS Uses Discord API">
|
||||
|
||||
Multi-scrobbler does its upmost to implement, to-spec, [Gateway API communication](https://docs.discord.com/developers/events/gateway) and respect usage. It requires no [permissions](https://docs.discord.com/developers/events/gateway#gateway-intents) and reads no data other than monitoring what other user sessions are active.
|
||||
|
||||
Aside from the on-paper violation of using a user token programmatically (on your behalf), there is no misuse of the API to achieve rich presence.
|
||||
|
||||
Additionally, MS uses the state of non-MS sessions to conservatively update presence. [By default it will not conflict](#configure-when-presence-is-used) with any other official presence activities and is only used when you are actually active on a device.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
<DetailsAdmo type="note" summary="REQUIRED Interaction after Starting Multi-Scrobbler">
|
||||
|
||||
Every time multi-scrobbler is started a discord session update needs to be triggered so that [MS can capture the signals it uses to determine if presence can be updated.](#configure-when-presence-is-used) This only needs to be *once*, after starting multi-scrobbler. But it does need to be *every time* multi-scrobbler is (re)started. Without these signals MS will not update presence.
|
||||
|
||||
This session update can happen automatically after *some* time so you may not need to do anything. If you want to force the update then do one of the following:
|
||||
|
||||
* Open discord on a device it was not recently active on
|
||||
* Exit discord on an active device
|
||||
* Change your status (online, idle, etc...)
|
||||
* Set a custom status
|
||||
* Cause a presence update with a different app (listening to..., playing..., etc.)
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
## Required Setup
|
||||
|
||||
### User Token
|
||||
|
||||
You must provide a **Discord User Token** for your account in order to authenticate with Discord.
|
||||
|
||||
<DetailsAdmo type="warning" summary="User Token usage violates Discord TOS">
|
||||
|
||||
This functionality requires you to use your own **Discord User Token** in a way that is **against Discord's policies/terms of service.** If you use this scrobbler you do so **at your risk**.
|
||||
|
||||
There is no precedent for Discord punishing users who use their own token for non-abuse/spam reasons but there is also no guarantee this lack of enforcement will continue.
|
||||
|
||||
Refer to the **How MS Uses Discord API** section above to gauge whether this risk is acceptable to you.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
|
||||
<DetailsAdmo type="warning" summary="User Token is a Sensitive Credential">
|
||||
|
||||
Treat your User Token like a login credential. Anyone who has your token can access Discord as you.
|
||||
|
||||
When finding instructions for obtaining this token only run commands, or follow steps, that you can understand and trust. Do not run commands you do not understand. Never give your token to someone else.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
No instructions will be provided for obtaining a User Token because it is against Discord's policies to use it. Instead, you can search online for "how to obtain discord user token".
|
||||
|
||||
## Optional Setup
|
||||
|
||||
### Configure When Presence Is Used
|
||||
|
||||
MS uses several signals from your "real" Discord sessions to determine if it should update presence. The default configuration is conservative to ensure that MS does not conflict with other apps using presence. Additionally, it only updates if a "real" user device is active on discord.
|
||||
|
||||
:::tip[TLDR]
|
||||
|
||||
Without any additional configuration, MS will only update presence if:
|
||||
|
||||
* you have discord open on a real device
|
||||
* your status is either: online, idle, or dnd (not invisible)
|
||||
* no other apps are broadcasting listening activities (listening to spotify, etc...)
|
||||
|
||||
:::
|
||||
|
||||
#### Online Status
|
||||
|
||||
Configure if MS is allowed to update presence based on your online status. If this setting is not defined MS will only update if your status is **online**, **idle**, or **dnd**.
|
||||
|
||||
Allowed Values: `online` `idle` `dnd` `invisible`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Only allow presence updating when all of your "real" sessions are either online or idle.
|
||||
|
||||
|
||||
In File/AIO:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "MS",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
|
||||
"applicationId": "8190211179716453570",
|
||||
"statusOverrideAllow": ["online", "idle"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In ENV:
|
||||
|
||||
```
|
||||
DISCORD_STATUS_OVERRIDE_ALLOW=online,idle
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Other Listening Activities
|
||||
|
||||
Without any additional configuration, MS will not set Rich Presence if **other** apps are broadcasting **listening** activities EX "Listening to Spotify".
|
||||
|
||||
<details>
|
||||
|
||||
<summary>What about other activity types?</summary>
|
||||
|
||||
Discord uses an undocumented activity type priority to determine what activity is shown "first" for your profile. The Listening activity type has the *lowest priority*.
|
||||
|
||||
This means that if you start any other type of activity (streaming, watching, competing, etc...) it will take precedence over MS's listening activity.
|
||||
|
||||
Your listening activity will still be visible by clicking into your full profile.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Configure the names of **other** listening activities that MS **is allowed** to broadcast at the same time as.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
You want to allow MS to update presence even if Spotify is also broadcasting a listening activity.
|
||||
|
||||
In File/AIO:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "MS",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
|
||||
"applicationId": "8190211179716453570",
|
||||
"listeningActivityAllow": ["spotify"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In ENV:
|
||||
|
||||
```
|
||||
DISCORD_LISTENING_ACTIVITY_ALLOW=spotify
|
||||
```
|
||||
</details>
|
||||
|
||||
### Discord Application
|
||||
|
||||
Creating a [Discord App](https://docs.discord.com/developers/quick-start/overview-of-apps) enables you to use custom album cover art and secondary status images for more links. Creating an App is free and simple, it is recommended you do this to get the most functionality from Rich Presence.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Create An Application</summary>
|
||||
|
||||
Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
|
||||
* Click on "New Application" and give it a name
|
||||
* Copy the **Application Id** (Client Id) shown after creation
|
||||
* Add this Id to your Multi-Scrobbler config
|
||||
* Env Config => `DISCORD_APPLICATION_ID=12345678`
|
||||
* File Config => `"applicationId": "12345678"`
|
||||
|
||||
</details>
|
||||
|
||||
### Artwork
|
||||
|
||||
Multi-scrobbler can display Album Art in your Discord status. The image that is used differs based on if the image URL is publically accessible.
|
||||
|
||||
:::note
|
||||
|
||||
If no [**Application Id**](#discord-application) is configured the status art will **always** be the [Discord Default](./?artworkUrl=discord-default#artwork-url-types).
|
||||
|
||||
:::
|
||||
|
||||
The order in which multi-scrobbler determines what image to use is the same as the order of the tabs shown below.
|
||||
|
||||
#### Artwork URL Types
|
||||
|
||||
<Tabs groupId="artworkUrl" queryString>
|
||||
<TabItem label="External URLs" value="external">
|
||||
When [**Application Id**](#discord-application) **is** configured MS can use external URLs for artwork.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
External URLs that are not from known music services are **disabled by default.** This is to preserve *your* privacy because Discord does not host images, it only links to them and the URL is visible to other users.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Why Should I Care?</summary>
|
||||
|
||||
There are some scenarios where you may not want your artwork URLs to be visible, for example:
|
||||
|
||||
* A Source may be accessible only on your LAN (Jellyfin at `http://192.168.0.101`)
|
||||
* A Source may be internet-facing but you do not want to expose this information to discord/other users
|
||||
* A Source may be internet-facing but requires authentication to view the image
|
||||
</details>
|
||||
|
||||
MS will use your artwork URLs only if:
|
||||
|
||||
* artwork url is from a known music service/[Cover Art Archive](./?artworkUrl=caa#artwork-url-types) or
|
||||
* artwork url starts with `https` and
|
||||
* `artwork` (`DISCORD_ARTWORK`) is
|
||||
* `true` => always uses the url
|
||||
* a list of custom domains keywords an external artwork url should contain EX
|
||||
* ENV => `DISCORD_ARTWORK=mycdn,jellyfin`
|
||||
* File Config => `"artwork": ["mycdn","jellyfin"]`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Examples</summary>
|
||||
|
||||
None of the below URLs will **ever** be used because they are not `https`
|
||||
|
||||
```
|
||||
http://foobar.com/cool.jpg <-- NOT allowed, not http
|
||||
http://192.168.0.112/fun.png <-- NOT allowed, not http
|
||||
```
|
||||
|
||||
When `DISCORD_ARTWORK` is not set:
|
||||
|
||||
```
|
||||
https://archive.org/download/dir/myimage.jpg <-- allowed, is https and known service
|
||||
https://spotify.com/assets/cover.png?size=1000 <-- allowed, is https and known service
|
||||
https://example.com/cool.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
|
||||
https://anything.com/neat.jpg <-- NOT allowed, is https but not a known service and DISCORD_ARTWORK is not set
|
||||
```
|
||||
|
||||
Setting `DISCORD_ARTWORK=true`
|
||||
|
||||
```
|
||||
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
|
||||
http://foobar.com/cool.jpg <-- NOT allowed, not http
|
||||
https://example.com/cool.jpg <-- allowed, is https
|
||||
https://anything.com/neat.jpg <-- allowed, is https
|
||||
```
|
||||
|
||||
Setting `DISCORD_ARTWORK=mycdn,jellyfin`
|
||||
|
||||
```
|
||||
https://archive.org/my/dir/myimage.jpg <-- allowed, is https and known service
|
||||
http://foobar.com/cool.jpg <-- NOT allowed, not http
|
||||
https://example.com/cool.jpg <-- NOT allowed, is https but does not contain "mycdn" or "jellyfin"
|
||||
https://jellyfin.mydomain.com/neat.jpg <-- allowed, is https and contains "jellyfin"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
</TabItem>
|
||||
<TabItem label="Musicbrainz/Cover Art Achive" value="caa">
|
||||
When [**Application Id**](#discord-application) **is** configured MS can try to use [Cover Art Archive](http://coverartarchive.org/) to get album art. The URL it retrieves is public which means it acts like an External URL that is always available for you.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
MS will try to use Cover Art Archive if:
|
||||
|
||||
* The scrobble data does not contain an album art url (no art in dashboard) or the existing url has failed an [External URL](./?artworkUrl=external#artwork-url-types) condition
|
||||
* and **the scrobble data contains a [Musicbrainz](/configuration/transforms/musicbrainz) Release (Album) MBID**
|
||||
|
||||
Basically, if MS would normally use the [MS Default](./?artworkUrl=ms-default#artwork-url-types) art, it will first try to use Cover Art Archive if the scrobble has a Release MBID.
|
||||
|
||||
Some Source, like [Jellyfin](/configuration/sources/jellyfin) and [Plex](/configuration/sources/plex), can automatically provide this ID if the music is "matched" in your library.
|
||||
|
||||
For all others, you can [**configure MS to use the Musicbrainz Stage**](/configuration/transforms/musicbrainz#use-metadata-for-discord-album-art) for Discord to try to get this MBID.
|
||||
</TabItem>
|
||||
<TabItem label="MS Default" value="ms-default">
|
||||
When [**Application Id**](#discord-application) **is** configured and these conditions are true:
|
||||
|
||||
* Cannot use an [External URL](./?artworkUrl=external#artwork-url-types) and
|
||||
* Cannot get album art from [Cover Art Archive](./?artworkUrl=caa#artwork-url-types) and
|
||||
* Play does not contain artwork information (no artwork shown in dashboard)
|
||||
|
||||
The status artwork will use a default URL instead of your artwork URL.
|
||||
|
||||
Without any other configuration, the default URL is an icon from the [multi-scrobbler repository](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/default-artwork.png):
|
||||
|
||||
<img src={require('/img/discord_presence_ms_default.png').default} height="127"/>
|
||||
|
||||
This default image can be customized:
|
||||
|
||||
* ENV Config => `DISCORD_ARTWORK_DEFAULT_URL=https://cooldomain.com/art.png`
|
||||
* File Config => `"artworkDefaultUrl": "https://cooldomain.com/art.png"`
|
||||
|
||||
Remember, this URL need to be accessible on the public internet.
|
||||
</TabItem>
|
||||
<TabItem label="Discord Default" value="discord-default">
|
||||
When [**Application Id**](#discord-application) is **not** configured Discord will show its own image. No custom artwork can be used in this configuration.
|
||||
|
||||
This is also the image Discord will display if the artwork URL cannot be fetched.
|
||||
|
||||
<img src={require('/img/discord_presence_discord_default.png').default} height="127"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="DiscordClientConfig" fileContent={JsonConfig} client name="discord">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------------------- | --------- | ----------------- | :---------------------------------------------------------------------------------------------------------------- |
|
||||
| `DISCORD_TOKEN` | Yes | | User Token acquired from an active Discord sessions |
|
||||
| `DISCORD_APPLICATION_ID` | No | | Application ID used to display album art |
|
||||
| `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
|
||||
| `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
|
||||
| `DISCORD_STATUS_OVERRIDE_ALLOW` | No | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
|
||||
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | No | | A commera-seperated list of listening activity names MS **is allowed** to broadcast presence at the same time as. |
|
||||
</Config>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Discord
|
||||
description: Discord Client Overview
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
There are two types of Discord integrations to choose from:
|
||||
|
||||
## App
|
||||
|
||||
The **App** integration uses an **existing** Discord application on a desktop computer to set Rich Presence.
|
||||
|
||||
* Uses "permitted" access to Discord, no TOS issues
|
||||
* More restricted customization compared to Headless
|
||||
* Only active when your existing Discord application is open
|
||||
* For most scenarios it requires additional, OS-specific steps to setup correctly
|
||||
* May require an additional program on your machine to relay the Discord connection
|
||||
* This is potentially insecure, or less secure, then Headless
|
||||
* **Can use [discord in a container](/configuration/clients/discord/discord-app#discord-connection) as an easy alternative**
|
||||
* This is a Headless-like experience
|
||||
|
||||
[**App Docs**](/configuration/clients/discord/discord-app)
|
||||
|
||||
## Headless
|
||||
|
||||
The **Headless** integration uses Discord cloud Gateway/API, directly, to set Rich Presence.
|
||||
|
||||
* Does not require an existing Discord application (uses Discord cloud API)
|
||||
* Using this **Violates Discord TOS** because it requires a user token
|
||||
* More customization than App integration
|
||||
* Active if **any** Discord application is online
|
||||
|
||||
[**Headless Docs**](/configuration/clients/discord/discord-headless)
|
||||
@@ -45,7 +45,7 @@ Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/g
|
||||
|
||||
:::tip
|
||||
|
||||
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
|
||||
For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLOWED_HOSTS` set correctly in your Koito environment/container. Ensure it is set to the IP address or domain name that you use to access Koito with e.g. `koito.mydomain.com`.
|
||||
|
||||
:::
|
||||
|
||||
@@ -54,7 +54,7 @@ Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito
|
||||
<Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | --------------------------------- |
|
||||
| KOITO_TOKEN | Yes | | API Key from your Koito Account |
|
||||
| KOITO_USER | Yes | | Your Koito username |
|
||||
| KOITO_URL | No | | The base URL for the Koito server |
|
||||
</Config>
|
||||
| `KOITO_TOKEN` | Yes | | API Key from your Koito Account |
|
||||
| `KOITO_USER` | Yes | | Your Koito username |
|
||||
| `KOITO_URL` | No | | The base URL for the Koito server |
|
||||
</Config>
|
||||
|
||||
@@ -11,24 +11,46 @@ import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
* To monitor and re-scrobble activity from your Last.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
|
||||
* To monitor and re-scrobble activity from your Last.fm/Libre.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
|
||||
|
||||
:::
|
||||
|
||||
[Register for an API account here.](https://www.last.fm/api/account/create)
|
||||
You will need a [Last.fm](https://www.last.fm) account to use the Last.fm API.
|
||||
|
||||
After creating your account, [register for an API account here.](https://www.last.fm/api/account/create)
|
||||
|
||||
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callbackredirect-url) section.
|
||||
|
||||
**Take note of your API Key and Secret after account creation.**
|
||||
|
||||
### Callback/Redirect URL
|
||||
|
||||
The Redirect (Callback) URL is used to redirect your **browser** to multi-scrobbler after authentication has completed.
|
||||
|
||||
So, the *base of the URL* you use should be the *same* as the URL you would use to navigate to the multi-scrobbler dashboard.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
If you can visit the multi-scrobbler dashboard by navigating to http://192.168.0.100:9078 in your web browser...
|
||||
|
||||
then the ENV `LASTFM_REDIRECT_URI`, or `redirectUri` for last.fm/libre.fm json config, should be
|
||||
|
||||
The Callback URL is actually specified by multi-scrobbler but to keep things consistent you should use
|
||||
```
|
||||
http://localhost:9078/lastfm/callback
|
||||
http://192.168.0.100:9078/lastfm/callback
|
||||
```
|
||||
or replace `localhost:9078` with your own base URL.
|
||||
|
||||
</details>
|
||||
|
||||
The Redirect URL can be explicitly specified in the configuration below, or by using the global [Base URL](/configuration#base-url) setting.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="LastfmClientConfig" fileContent={JsonConfig} client name="lastfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|-----------------------------------------|-------------------------------------------------------------------------------|
|
||||
| ---------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
|
||||
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
|
||||
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Libre.fm
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
* To monitor and re-scrobble activity from your Libre.fm account create a [Libre.fm (Source)](/configuration/sources/librefm-source)
|
||||
|
||||
:::
|
||||
|
||||
To use the official [Libre.fm](https://libre.fm/) instance you will need an account. Or, you can run your own Libre.fm instance [using the legacy source code](https://github.com/foocorp/gnu-fm).
|
||||
|
||||
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is needed to use the Libre.fm API. Instead, use **any values** for API Key/Secret in the configuration. Authentication is done by allowing access from your account on redirect.
|
||||
|
||||
### Callback/Redirect URL
|
||||
|
||||
The Redirect (Callback) URL is used to redirect your **browser** to multi-scrobbler after authentication has completed.
|
||||
|
||||
So, the *base of the URL* you use should be the *same* as the URL you would use to navigate to the multi-scrobbler dashboard.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
If you can visit the multi-scrobbler dashboard by navigating to http://192.168.0.100:9078 in your web browser...
|
||||
|
||||
then the ENV `LIBREFM_REDIRECT_URI`, or `redirectUri` for libre.fm json config, should be
|
||||
|
||||
```
|
||||
http://192.168.0.100:9078/librefm/callback
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
The Redirect URL can be explicitly specified in the configuration below, or by using the global [Base URL](/configuration#base-url) setting.
|
||||
|
||||
### Custom Libre.fm instance
|
||||
|
||||
If you are **not** using the official Libre.fm instance then specify your custom instance using ENV `LIBREFM_URLBASE` or `urlBase` property in the `data` section in File/AIO config.
|
||||
|
||||
The value should be `protocol://libre.tld/path/prefix` EX:
|
||||
|
||||
```
|
||||
https://my.cool.libre.com/2.0/
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client name="librefm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
|
||||
| `LIBREFM_API_KEY` | No | `anything` | Optional API Key |
|
||||
| `LIBREFM_SECRET` | No | `anything` | Optional shared secret |
|
||||
| `LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
|
||||
| `LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
|
||||
| `LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
|
||||
</Config>
|
||||
@@ -22,10 +22,10 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|-------------------------------|---------------------------------|
|
||||
| LZ_TOKEN | Yes | | User token from your LZ profile |
|
||||
| LZ_USER | Yes | | Your LZ username |
|
||||
| LZ_URL | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
|
||||
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
|
||||
| `LZ_TOKEN` | Yes | | User token from your LZ profile |
|
||||
| `LZ_USER` | Yes | | Your LZ username |
|
||||
| `LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
|
||||
</Config>
|
||||
@@ -23,13 +23,13 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
|
||||
|
||||
</details>
|
||||
|
||||
:::warning[Public Data]
|
||||
<DetailsAdmo type="warning" summary="Your Data Is Public">
|
||||
|
||||
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
:::warning[No Scrobbling Guarantee]
|
||||
<DetailsAdmo type="warning" summary="No Scrobbling Guarantee">
|
||||
|
||||
Rocksky **does not guarantee** that every scrobble you send will be persisted. Rocksky's design goal is to store scrobbles with **metdata-complete, 3rd party verifiable track information** so that it can be a metadata-rich discovery platform, rather than an exhaustive scrobble data source-of-truth.
|
||||
|
||||
@@ -37,7 +37,7 @@ To do this it *asynchronously* matches your scrobble data against Musicbrainz's
|
||||
|
||||
If you need your scrobbles to be 100% captured then you should scrobble to an *additional* [Scrobble Client](/configuration/clients).
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -64,9 +64,9 @@ The handle used with multi-scrobbler should be your **full** ATProto handle, inc
|
||||
|
||||
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky">
|
||||
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client name="rocksky">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| ROCKSKY_HANDLE | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| ROCKSKY_KEY | Yes | | API Key obtained from Rocksky developer dashboard |
|
||||
| `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| `ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard |
|
||||
</Config>
|
||||
@@ -26,23 +26,23 @@ To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (So
|
||||
|
||||
</details>
|
||||
|
||||
:::warning
|
||||
<DetailsAdmo type="warning" summary="Your Data Is Public">
|
||||
|
||||
The default Bluesky PDS is **public**, meaning your scrobbles are also public (read-only). This isn't any different than using Last.fm or Listenbrainz, in terms of privacy.
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
:::note
|
||||
<DetailsAdmo type="note" summary="teal.fm Is Still In Development">
|
||||
|
||||
[teal.fm is still in developement](https://notes.teal.fm/3lzlwe6puis2l) and does not have a public website, yet.
|
||||
[teal.fm is still in developement](https://notes.teal.fm/3lzlwe6puis2l) and does not have a true production website, yet.
|
||||
|
||||
To view your teal.fm scrobbles you can:
|
||||
To view your teal.fm scrobble data you can:
|
||||
|
||||
* use [wrapped.baileytownsend.dev](https://wrapped.baileytownsend.dev/) which gives a simple stats overview of your scrobbles
|
||||
* use [teal-slice.wisp.place](https://tealfm-slice.wisp.place/) for a preview of teal.fm
|
||||
* use [atproto.at](https://atproto.at) to view the raw data (convenience URLs are generated in multi-scrobbler for you to do this)
|
||||
* use [teal-relay-production.up.railway.app](https://teal-relay-production.up.railway.app) or [teal-slice.wisp.place](https://tealfm-slice.wisp.place/) for a preview of teal.fm
|
||||
* use [yearinmusic.teal.fm](https://yearinmusic.teal.fm) or [wrapped.baileytownsend.dev](https://wrapped.baileytownsend.dev/) for wrapped-style stats of your teal.fm scrobbles
|
||||
* use [atproto.at](https://atproto.at) to view the raw data (convenience URLs are generated in multi-scrobbler, per scrobble, for you to do this)
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -71,8 +71,8 @@ If using a handle:
|
||||
If your identifier does not look like a DID and does not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| TEALFM_IDENTIFIER | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| TEALFM_APP_PW | Yes | | Bluesky/ATProto network App Password |
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
|
||||
| `TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| `TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
|
||||
</Config>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Configuration
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
import Tabs from '@theme/Tabs';
|
||||
@@ -168,7 +169,7 @@ Each entry for a Source/Client includes a **Configuration** section that describ
|
||||
|
||||
## Secrets Interpolation
|
||||
|
||||
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configurations/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
|
||||
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configuration/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
|
||||
|
||||
Multi-scrobbler will look for patterns in text fields within _all_ of your json files:
|
||||
|
||||
@@ -179,7 +180,7 @@ Multi-scrobbler will look for patterns in text fields within _all_ of your json
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Given this [Last.fm](/configurations/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
Given this [Last.fm](/configuration/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
|
||||
```json title="lastfm.json"
|
||||
[
|
||||
@@ -220,14 +221,15 @@ The resulting json multi-scrobbler would use:
|
||||
|
||||
</details>
|
||||
|
||||
:::note[Caveats]
|
||||
|
||||
<DetailsAdmo type="note" summary="Caveats">
|
||||
|
||||
* ENV variable names/interpolation keys are case-insensitive
|
||||
* Interpolation only works for **string** values within json. This cannot be used for numbers, booleans, objects, etc...
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
:::warning[Missing ENVS]
|
||||
<DetailsAdmo type="warning" summary="Missing ENVs">
|
||||
|
||||
Multi-scrobbler **will not** throw an error if the environmental value is not found. Instead, it will leave the string as-is and log a warning (`WARN` level) with the names of the missing environmental variable names like so:
|
||||
|
||||
@@ -235,19 +237,16 @@ Multi-scrobbler **will not** throw an error if the environmental value is not fo
|
||||
WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECRET
|
||||
```
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
:::warning[ENV Name Collisions]
|
||||
<DetailsAdmo type="warning" summary="ENV Name Collisions">
|
||||
|
||||
**Verify that interpolation keys/environmental variable names you will use do not collide with existing ENV names used by multi-scrobbler.** Use the docs search to verify the name you want to use is not already used elsewhere by multi-scrobbler.
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
## Application Options
|
||||
|
||||
These options affect multi-scrobbler's behavior and are not specific to any source/client.
|
||||
|
||||
### Base URL
|
||||
## Base URL
|
||||
|
||||
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
|
||||
|
||||
@@ -278,79 +277,51 @@ Useful when running with [docker](../installation/installation.mdx#docker) so th
|
||||
|
||||
</details>
|
||||
|
||||
### Caching
|
||||
## Caching
|
||||
|
||||
Multi-scrobbler caches some activities to persist important data across restarts, reduce external API calls, and make some actions faster.
|
||||
Multi-scrobbler implements caching to persist important data across restarts, reduce external API calls, and make some actions faster.
|
||||
|
||||
All of the activities below are **always** cached **in-memory** with an optional, configurable [**secondary** store](#secondary-caching-configuration) for persistence.
|
||||
A default **in-memory** cache store is used so that you always benefit from some caching. An optional, [**secondary** store](#secondary-caching) can be configured for greater caching capabilities.
|
||||
|
||||
<Tabs groupId="cachedThings" queryString>
|
||||
<TabItem value="scrobbles" label="Scrobbles">
|
||||
|
||||
**Queued** and **Failed** Scrobbles are cached so that any un-scrobbled data you have is persisted across restarts of multi-scrobbler.
|
||||
|
||||
:::tip
|
||||
|
||||
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
|
||||
|
||||
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
|
||||
:::
|
||||
|
||||
##### Configuration
|
||||
|
||||
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | --------- | --------------------- |
|
||||
| `CACHE_SCROBBLE` | No | `file` | The cache type to use |
|
||||
| `CACHE_SCROBBLE_CONN` | No | `/config` | |
|
||||
<DetailsAdmo type="note" summary="What is Cached?">
|
||||
|
||||
<Tabs groupId="cachedThings">
|
||||
<TabItem value="auth" label="Auth Data">
|
||||
Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts.
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
<TabItem value="transform" label="Scrobble Transforms">
|
||||
The results of [transform rules](/configuration/transforms) are cached so that if a scrobble with identical data (track/artists/album) is identified and it has the same set of transform rules then the cached transform results can be applied.
|
||||
</TabItem>
|
||||
<TabItem value="metadata" label="Transform API Calls">
|
||||
API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="auth" label="Auth Data">
|
||||
</DetailsAdmo>
|
||||
|
||||
Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts.
|
||||
<DetailsAdmo type="important" summary="Auth Cache Configuration">
|
||||
|
||||
:::tip
|
||||
Auth caching defaults to a **file** that is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-auth.cache`.
|
||||
|
||||
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
|
||||
This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)).
|
||||
|
||||
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
If you wish to use the [secondary store](#secondary-caching) for caching Auth you must explicitly configure it. This is because valkey can potentially be *ephemeral* if you do not provide a volume for its data directory.
|
||||
|
||||
:::
|
||||
To explicitly configure auth to use the secondary store:
|
||||
|
||||
##### Configuration
|
||||
|
||||
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
|
||||
|
||||
<Tabs>
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | --------- | --------------------- |
|
||||
| `CACHE_AUTH` | No | `file` | The cache type to use |
|
||||
| `CACHE_AUTH_CONN` | No | `/config` | |
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
// highlight-start
|
||||
- CACHE_AUTH=valkey
|
||||
// highlight-end
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -360,8 +331,7 @@ Use any [Secondary Cache](#secondary-caching-configuration), the config examples
|
||||
{
|
||||
"cache": {
|
||||
"auth": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
"provider": "valkey"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
@@ -370,93 +340,19 @@ Use any [Secondary Cache](#secondary-caching-configuration), the config examples
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="metadata" label="Transform API Calls">
|
||||
</DetailsAdmo>
|
||||
|
||||
API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations.
|
||||
### Secondary Caching Store {#secondary-caching}
|
||||
|
||||
By default, these calls are only cached in memory. If you wish for cached calls to be persisted across restarts then setup [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration).
|
||||
Using a secondary store enables:
|
||||
|
||||
##### Configuration
|
||||
* persistence of cached data across restarts
|
||||
* a larger store (more data is saved)
|
||||
* a longer time-to-live in the store (cached data is fetchable for a longer period)
|
||||
|
||||
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
|
||||
These benefits are particularly beneficial when using [transforms](/configuration/transforms) like Musicbrainz and it is **strongly recommended** for these scenarios.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | --------------------- |
|
||||
| `CACHE_METADATA` | No | | The cache type to use |
|
||||
| `CACHE_METADATA_CONN` | No | | |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"metadata": {
|
||||
"provider": "valkey",
|
||||
"connection": "yourConnectionStringHere"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Secondary Caching Configuration
|
||||
|
||||
The type of cache used, and its connection properties, can be configured through **ENV** or **AIO** config.
|
||||
|
||||
<Tabs groupId="cacheType" queryString>
|
||||
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
**File** cache is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-[cacheName].cache`.
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Example
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | --------- | ------------------------------------------------------------ |
|
||||
| `CACHE_SCROBBLE` | No | `file` | The cache type to use |
|
||||
| `CACHE_SCROBBLE_CONN` | No | `/config` | The directory, within the container, to store the cache file |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Example
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="valkey" label="Valkey">
|
||||
|
||||
[**Valkey**](https://valkey.io/) is an open-source fork of Redis.
|
||||
Currently, Multi-scrobbler only supports [**Valkey**](https://valkey.io/), an open-source fork of Redis, as a secondary store.
|
||||
|
||||
<details>
|
||||
|
||||
@@ -464,11 +360,13 @@ Example
|
||||
|
||||
A valkey container can be added to the [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker):
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="named" label="Named Volume">
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
# adding everything below
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
@@ -481,6 +379,27 @@ volumes:
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="bind" label="Bind Mount">
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- ./valkeyData:/data
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Use `redis://valkey:6379` as the connection string in the configurations below.
|
||||
|
||||
</details>
|
||||
|
||||
The connection string used by multi-scrobbler to connect to your Valkey instance must be in the form:
|
||||
@@ -493,12 +412,16 @@ redis://HOST_IP:HOST_PORT
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Example
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | -------- | ------------------------------------------------------------------- |
|
||||
| `CACHE_METADATA` | Yes | `valkey` | The cache type to use |
|
||||
| `CACHE_METADATA_CONN` | Yes | | The host/IP and port to connect to EX: `redis://192.168.0.120:6379` |
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
// highlight-start
|
||||
- CACHE_VALKEY=redis://192.168.0.120:6379
|
||||
// highlight-end
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -509,10 +432,7 @@ Example
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"metadata": {
|
||||
"provider": "valkey",
|
||||
"connection": "redis://192.168.0.120:6379"
|
||||
}
|
||||
"valkey": "redis://192.168.0.120:6379"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
@@ -521,11 +441,8 @@ Example
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Debug Mode
|
||||
## Debug Mode
|
||||
|
||||
Turning on Debug Mode will
|
||||
|
||||
@@ -546,13 +463,13 @@ To set debug mode either add it to [AIO `config.json`](./?configType=aio#configu
|
||||
|
||||
or set the [ENV](./?configType=env#configuration-types) `DEBUG_MODE=true`
|
||||
|
||||
### Disable Web
|
||||
## Disable Web
|
||||
|
||||
If you do not need the dashboard and/or ingress sources, or have security concerns about ingress and cannot control your hosting environment, the web server and API can be disabled.
|
||||
|
||||
:::warning
|
||||
|
||||
Any **[ingress-based sources](/configuration/sources#ingress) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
Any **[ingress-based sources](/configuration/sources?sourceComm=ingress#by-communication-method) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
|
||||
:::
|
||||
|
||||
@@ -563,7 +480,19 @@ Disable using either:
|
||||
|
||||
## Monitoring
|
||||
|
||||
Multi-scrobbler supports some common webhooks and a healthcheck endpoint in order to monitor Sources and Clients for errors.
|
||||
### Upstream Services Status
|
||||
|
||||
A status page for monitoring the reachability of public services used by some Sources/Clients is available at
|
||||
|
||||
**https://status.multi-scrobbler.app**
|
||||
|
||||
This monitor runs on a VPS and checks the uptime of *actual* APIs for these services, not just landing pages. On a ~minute interval it checks:
|
||||
|
||||
* Listenbrainz API
|
||||
* Musicbrainz API
|
||||
* CoverArtArchive API
|
||||
* Last.fm API
|
||||
* A few MS-hosted support services
|
||||
|
||||
### Webhook Configurations
|
||||
|
||||
@@ -691,11 +620,56 @@ multi-scrobbler supports [stateless](https://github.com/caronc/apprise-api?tab=r
|
||||
|
||||
### Health Endpoint
|
||||
|
||||
An endpoint for monitoring the health of sources/clients is available at GET `http://YourMultiScrobblerDomain/health`
|
||||
An endpoint for monitoring the health of sources/clients is available at GET `http://YourMultiScrobblerDomain/api/health`
|
||||
|
||||
* Returns `200 OK` when **everything** is working or `500 Internal Server Error` if **anything** is not
|
||||
* The plain url (`/health`) aggregates status of **all clients/sources** -- so any failing client/source will make status return 500
|
||||
* Use query params `type` or `name` to restrict client/sources aggregated IE `/health?type=spotify` or `/health?name=MyMaloja`
|
||||
* The plain url (`/api/health`) aggregates status of **all clients/sources** -- so any failing client/source will make status return 500
|
||||
* Use query params `type` or `name` to restrict client/sources aggregated IE `/api/health?type=spotify` or `/api/health?name=MyMaloja`
|
||||
* On 500 the response returns a JSON payload with `messages` array that describes any issues
|
||||
* For any clients/sources that require authentication `/health` will return 500 if they are **not authenticated**
|
||||
* For sources that poll (spotify, yt music, subsonic) `/health` will 500 if they are **not polling**
|
||||
* For any clients/sources that require authentication `/api/health` will return 500 if they are **not authenticated**
|
||||
* For sources that poll (spotify, yt music, subsonic) `/api/health` will 500 if they are **not polling**
|
||||
|
||||
### Prometheus
|
||||
|
||||
A [Prometheus](https://prometheus.io/) export endpoint is available at GET `http://YourMultiScrobblerDomain/api/metrics`
|
||||
|
||||
It includes metrics for:
|
||||
|
||||
* Count of discovered plays per [Source](/configuration/sources)
|
||||
* Count of Queued/Scrobbled/Deadletter scrobbles per [Client](/configuration/clients)
|
||||
* Number of issues per Source/Client
|
||||
* If any of these metrics is > 0 it means your Source/Client is not operating normally
|
||||
* Caching metrics
|
||||
|
||||
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example Usage</summary>
|
||||
|
||||
Update your [Prometheus Configuration file](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file) (usually at `/etc/prometheus.yml`) to add a job to the `scrape_configs`[section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) like this:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
#
|
||||
# ...your other jobs are here
|
||||
#
|
||||
- job_name: multi-scrobbler
|
||||
scrape_interval: 30s
|
||||
metrics_path: '/api/metrics'
|
||||
static_configs:
|
||||
- targets: ['myMultiscrobblerIp:9078']
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo status="tip" summary="Grafana Dashboard">
|
||||
|
||||
Github user [4rft5](https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295) created a Grafana dashboard that uses the Prometheus metrics from above.
|
||||
|
||||
<img src={require('/img/grafana.png').default} height="800"/>
|
||||
|
||||
Download the json dashboard here: [`multiscrobbler-dashboard.json`](/multiscrobbler-dashboard.json)
|
||||
|
||||
</DetailsAdmo>
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: Duplicate Scrobble Guidance
|
||||
description: Common Scenarios and Solutions for Duplicate Scrobbles
|
||||
---
|
||||
|
||||
Multi-scrobbler's flexibility in configuration allows you to build some complex pipelines for scrobbling but it can also be a footgun for duplicating scrobbles.
|
||||
|
||||
## How Duplicates Are Detected
|
||||
|
||||
Multi-scrobbler uses multiple techniques for evaluating the **similarity** of text in scrobbles against existing data.
|
||||
|
||||
These techniques are used on all text from a scrobble (title, artist, album...):
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Character and string normalization</summary>
|
||||
|
||||
Symbols and multiple whitespaces are removed. All characters are converted to lowercase. Non ascii characters are converted to their ascii counterparts.
|
||||
|
||||
```
|
||||
this string! is the. same => this string is the same
|
||||
this string is the same => this string is the same
|
||||
ThIs STRING iS THe SAMe => this string is the same
|
||||
|
||||
Dina Ögon => Dina Ogon
|
||||
Nanã => Nana
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Word-order invariant text similarity</summary>
|
||||
|
||||
Strings with multiple words (tokens) are re-arranged so that tokens are ordered to be as close to other, similar tokens are possible:
|
||||
|
||||
Example 1
|
||||
|
||||
```
|
||||
String A: there change is => there change is
|
||||
String B: there is change => there change is
|
||||
```
|
||||
|
||||
Example 2
|
||||
|
||||
```
|
||||
String A: you, doin hwat are => you doin hwat are
|
||||
String B: what are you doing => you doing what are
|
||||
```
|
||||
|
||||
Then, the strings are independently compared using [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [Dice's Coefficient](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient). The algorithm that gives the highest score for similarity is used so that matching is aggressive.
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
For **Artists**, Multi-scrobbler attempts to extract unique Artists from common patterns in **single-string** artist values and track **titles**.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Matching Common Artist Patterns</summary>
|
||||
|
||||
Common joiners and patterns are detected in artist/track strings and the artists are extracted. Example of common joiners are: `ft.` `feat` `vs.` etc...
|
||||
|
||||
Example 1
|
||||
|
||||
```
|
||||
Melendi, Ryan Lewis, The Righteous Brothers (featuring Joan Jett & The Blackhearts, Robin Schulz)
|
||||
|
||||
becomes
|
||||
|
||||
Melendi, Ryan Lewis, The Righteous Brothers, Joan Jett, The Blackhearts, Robin Schulz
|
||||
```
|
||||
|
||||
Example 2
|
||||
|
||||
```
|
||||
Childish Gambino - 12.38 (feat. 21 Savage, Ink & Kadhja Bonet)
|
||||
|
||||
becomes
|
||||
|
||||
Childish Gambino, 21 Savage, Ink, Kadhja Bonet - 12.38
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
* MS uses "smart" heuristics to prevent artists with actual punctuation (`Tyler, The Creator`) from accidentally being separated
|
||||
* The extraction examples above are done **only for matching** other scrobble data (which is also parsed like the above), the resulting scrobble data is never transformed unless the user explicitly configures it via a [native transform](/configuration/transforms/native)
|
||||
|
||||
:::
|
||||
|
||||
</details>
|
||||
|
||||
The **timestamps** for Scrobbles are compared at different levels of granularity based on where they originate from.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Temporally Comparing Scrobbles</summary>
|
||||
|
||||
Timestamp closeness for two scrobbles can be classified in one of these states:
|
||||
|
||||
**Exact**
|
||||
|
||||
Timestamps differ by 1 seconds or less.
|
||||
|
||||
**Close**
|
||||
|
||||
Timestamps differ by less than the known granularity of the Source they are from. For most Sources (and all Clients) this is 10 seconds or less. For some sources (subsonic) the granularity is 60 seconds because they only report "now playing" changes every 60 seconds.
|
||||
|
||||
**During**
|
||||
|
||||
If the Play was monitoring in real-time by Multi-scrobbler then it knows the time range during which the Play was listened to. If an existing scrobble with similar artist/track/album has a timestamp that occurred *during* the known listening time then it is classified as during.
|
||||
|
||||
**Fuzzy**
|
||||
|
||||
If the timestamp occurs within a few seconds +/- of (existing scrobble + track duration) then, potentially, the Source timestamped the existing scrobble at the *end* of when the user listened to it, rather than when the user *started* listening to it. This is common for Spotify plays.
|
||||
|
||||
___
|
||||
|
||||
Each of the above classification results in a different score that is *roughly* descending from the order given above. Exact score is based on the Souce of the scrobble and if it was monitored in real-time by Multi-Scrobbler.
|
||||
|
||||
</details>
|
||||
|
||||
Finally, all of the above comparison/normalization techniques are combined when comparing a "new" Play/Scrobble against existing data. Each comparison is given a weighted score. If the score is above the match threshold then it is marked as a duplicated.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Match Scoring Example</summary>
|
||||
|
||||
```
|
||||
Frédéric Chopin - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:51-04:00 <-- New scrobble
|
||||
Frédéric Chopin / Krystian Zimerman - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:48-04:00 <-- Existing scrobble from client
|
||||
```
|
||||
|
||||
After comparing against all existing scrobbles from the client, in a timerange around the new scrobble, MS finds a match and logs the match along with the score breakdown of the consistuent parts:
|
||||
|
||||
```
|
||||
Source:
|
||||
Frédéric Chopin - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:51-04:00 (S)
|
||||
|
||||
Closest Scrobble:
|
||||
Frédéric Chopin / Krystian Zimerman - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:48-04:00 (S)
|
||||
|
||||
Artist: (0.30 + Whole Match Bonus 0.52) * (0.3 + Whole Match Bonus 0.05) = 0.29
|
||||
Title: 1.00 * 0.4 = 0.40
|
||||
Time: (Close) 1 * 0.5 = 0.50
|
||||
Time Detail => Existing: 13:01:48-04:00 - Candidate: 13:01:51-04:00 | Temporal Sameness: Close | Play Diff: 3s (Needed <10s) | Range Comparison N/A
|
||||
Score 1.19 => Matched!
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
All of the above techniques and scoring gives Multi-scrobbler a *very good chance* of detecting duplicate scrobbles, regardless of where they were originally from. Of Multi-Scrobbler's [400+ unit tests](https://github.com/FoxxMD/multi-scrobbler/tree/master/src/backend/tests), 70+ are related to testing scoring, artist extraction, and [fuzzing](https://en.wikipedia.org/wiki/Fuzzing) string normalization/simiarlity.
|
||||
|
||||
The dupe detection is so good that it is virtually gauranteed for [most configurations](#valid-configs) and is used for *backlogging* missed scrobbles for Sources that support fetching history.
|
||||
|
||||
**However**, it's not possible to gaurantee this for *all* possible Multi-scrobbler configurations. See the below sections to learn more.
|
||||
|
||||
## Configurations Where Duplicates DO NOT Occur {#valid-configs}
|
||||
|
||||
* Any Source/Client configuration where there is only one Source and one Client and they are not the same service/account
|
||||
* Any Source/Client configuration where scrobbles only flow in **one direction**
|
||||
* IE you *do not* have a Last.fm **Client** Account A that is *also* a Last.fm **Source** Account A
|
||||
|
||||
:::tip
|
||||
|
||||
**The majority of simple Multi-scrobbler configurations fall into these categories. If your workflow is straightforward you do not need to worry about duplicates at all.**
|
||||
|
||||
:::
|
||||
|
||||
Multi-scrobbler can handle recieving the same data from a Source multiple times. This is fine because the data is *always the same* and *always unique* to each Source. Since MS scrobbled the data to your clients you can be pretty confident that the *same* cleanup/matching process for that data will result in an indentical/similar match to the existing scrobble.
|
||||
|
||||
IE Same inputs in -> same inputs out.
|
||||
|
||||
**Even this scenario with multiple, mixed Sources is OK:**
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/unidirectional-flow.png').default}/>
|
||||
|
||||
Because the scrobbles coming from each **Source** are **independent** of each other. And they end up in Clients that are not *also* used as Sources.
|
||||
|
||||
:::tip
|
||||
|
||||
This is a common scenario when scrobbles are *backlogged* from a Source IE on startup MS fetches the last 50 Plays from Spotify/Lastfm/Listenbrainz.
|
||||
|
||||
:::
|
||||
|
||||
## Configurations Where Duplicates CAN Occur {#invalid-configs}
|
||||
|
||||
### Upstream Scrobbles Sent to Multiple Sources
|
||||
|
||||
In this scenario your device using Spotify is configured in MS as a Spotify **Source** but it **also** is scrobbling to Last.fm (via Pano Scrobbler, as an example).
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/dupe sources.png').default}/>
|
||||
|
||||
|
||||
❌ **Time Race Condition**: Potentially, the scrobbles arrive in Multi-scrobbler at exactly the same time. There are no existing scrobbles in Koito to check against, yet. So Multi-scrobbler scrobbles both to Koito, resulting in duplicate scrobbles.
|
||||
|
||||
❌ **Different Scrobble Data**: Pano Scrobbler, or Last.fm, [may modify the scrobble data.](#lastfm) When MS recieves the scrobble from Last.fm it may be *just different enough* that it does not get detected as a duplicate.
|
||||
|
||||
### Sources and Clients are Dependent {#dependent-source-client}
|
||||
|
||||
In this scenario, you have a **Client** that is also a **Source** and that Source is feeding scrobbles to a different Client that receives scrobbles from everywhere (or has some overlap with the original Client).
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/duped client source.png').default}/>
|
||||
|
||||
❌ **Different Scrobble Data**: [Last.fm may modify the scrobble data.](#lastfm) When MS recieves the scrobble from Last.fm it may be *just different enough* that it does not get detected as a duplicate.
|
||||
|
||||
❌ **Consistency Issues**: If the Koito Client has a backed up queue or is not currently scrobbling (imagine it was Last.fm or Listenbrainz, and they had upstream availability issues), then there is no existing scrobble yet and nothing to match against.
|
||||
|
||||
## Configuration Solutions to Avoid Duplicates
|
||||
|
||||
### Independent Sources
|
||||
|
||||
Use a configuration mentioned in [Configurations Where Duplicates DO NOT Occur](#valid-configs): make sure all of your Sources are **independent**. The activity/scrobbles from each Source do not have any upstream duplicates of activity.
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/independent source.png').default}/>
|
||||
|
||||
### Alternative Sources
|
||||
|
||||
Imagine you have a scenario where a device/player can **only** scrobble to Last.fm. But you want to use MS for all other devices/sources and still have all your scrobbles end up in your "main" Last.fm account for social reasons/stats.
|
||||
|
||||
Instead of using the same Last.fm account for all activity and potentially having [dependent client/sources](#dependent-source-client), create an *additional* last.fm account that is used *only for the device that needs it*. This ensures the scrobbles from that device are isolated from the main account and do not prevent duplicates, so you can continue to use the main account as an MS Client:
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/alt source.png').default}/>
|
||||
|
||||
### Mirror-Only Clients or Limiting Scrobble Destination {#isolated-sources}
|
||||
|
||||
In Multi-scrobbler, by default, all Sources scrobble to all Clients. Using [Limit Scrobble Destinations](/configuration/sources/#limiting-scrobble-destination) configuration can ensure that your dependent Source/Clients only scrobble to destinations where the scrobbles won't be duplicated. And/or limit other Sources to *not* scrobble to dependent Sources/Clients.
|
||||
|
||||
Here is an example where all Sources only scrobble to a *dependent* Last.fm Client, and then the *dependent* Last.fm Source only scrobbles to a "mirror-only" (backup) Koito Client:
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/mirror source.png').default}/>
|
||||
|
||||
### Normalize All Scrobbles
|
||||
|
||||
If using one of the above solutions is not possible you can also try to *aggressively normalize all scrobble data* using [the Musicbrainz transform stage](/configuration/transforms/musicbrainz).
|
||||
|
||||
In this scenario you still have [dependent Source/Clients](#dependent-source-client) that are [not isolated](#isolated-sources) but you can *potentially* improve duplicate detection by first using a transform stage to match scrobbles against the Musibrainz database and normalize their track/artist/album data.
|
||||
|
||||
:::warning
|
||||
|
||||
**There is no gaurantee this will work** but it is better than nothing. You should attempt to re-configure MS to use any of the other solutions before using this one.
|
||||
|
||||
:::
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/normalized dupes.png').default}/>
|
||||
|
||||
## The Last.fm Problem {#lastfm}
|
||||
|
||||
Last.fm will happily accept almost any scrobble data you throw at it.
|
||||
|
||||
It can then decide, opaquely, to match or modify that scrobble to some more "official" data it has server-side. This could be modifying the artists, title, or album sent in the scrobble. It can also mean modifying the [`mbid` (unique identifier)](https://musicbrainz.org/doc/MusicBrainz_Identifier) or adding what it _thinks_ is the correct one.
|
||||
|
||||
There is nothing in the scrobble history API response from Last.fm that identifies it has done any of this. And [it's not super great at being right about these modifications](https://github.com/FoxxMD/multi-scrobbler/issues/333#issuecomment-3145161339).
|
||||
|
||||
What this means for you, the user, is that this is very plausible scenario:
|
||||
|
||||
* You scrobble `Artist Foo, Artist Bar - My Cool Track` to Last.fm via Pano Scrobbler (outside of Multi-scrobbler)
|
||||
* Last.fm modifies the data to `The Artist Foo - My Cool Track ft. Artist Bar (Album Version)`
|
||||
* You _also_ scrobble `Artist Foo, Artist Bar - My Cool Track` to Multi-scrobbler via Pano LFM endpoint **Source**
|
||||
* MS forwards this to Koito Client as-is
|
||||
* Soon after, MS polls your Last.fm **Source** for new scrobbles and finds `The Artist Foo - My Cool Track ft. Artist Bar (Album Version)`
|
||||
* It queries Koito to see if this was scrobbled
|
||||
* It sees that a scrobble did happen at almost the same time BUT the track titles and artist strings are _just different enough_ that it decides it's not a duplicate
|
||||
* MS forwards the scrobble to Koito and you now have a duplicate
|
||||
|
||||
This behavior is unique to Last.fm. It's truly the **worst** out of all the scrobbler services in terms of preserving your data as-is, or providing a unqiue identifier for your data.
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_position: 5
|
||||
title: Kitchen Sink
|
||||
description: Kitchen Sink Example
|
||||
---
|
||||
|
||||
# Example Complex Scenario
|
||||
@@ -11,13 +12,13 @@ Scenario:
|
||||
* Each person has their own [Maloja](/configuration/clients/maloja) server
|
||||
* Each person has their own Spotify account
|
||||
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
|
||||
* You have your own [Youtube Music](/configuration/soures/youtube-mysic) account you want to scrobble from
|
||||
* You have your own [Youtube Music](/configuration/sources/youtube-music) account you want to scrobble from
|
||||
* Mary has her own [Last.fm](/configuration/clients/lastfm) account she also wants to scrobble to
|
||||
* Fred has his own [Spotify](/configuration/sources/spotify) application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
|
||||
* Fred has a Plex server and wants to scrobble everything he plays
|
||||
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
|
||||
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
|
||||
* Fred also has his own [Jellyfin server](/configurion/sources/jellyfin) and wants to scrobble everything he plays
|
||||
* Fred also has his own [Jellyfin server](/configuration/sources/jellyfin) and wants to scrobble everything he plays
|
||||
* You have an android music app that can scrobble to a [custom listenbrainz server](/configuration/sources/listenbrainz-endpoint)
|
||||
|
||||
### All-in-one Config
|
||||
|
||||
@@ -8,7 +8,7 @@ import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example';
|
||||
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
|
||||
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
|
||||
|
||||
:::tip[Scrobbling Threshold]
|
||||
|
||||
@@ -36,14 +36,8 @@ URL From Config => MS Uses
|
||||
'wss://mydomain.com/custom/azura/ws' => wss://mydomain.com/custom/azura/ws
|
||||
```
|
||||
|
||||
#### Scrobble Behavior
|
||||
|
||||
:::info
|
||||
|
||||
<ShouldScrobble filename="azuracast"/>
|
||||
|
||||
:::
|
||||
|
||||
Multi-scrobbler will set the default **scrobbling behavior** based on if either of these settings is configured:
|
||||
|
||||
* live status (`AZURA_LIVE` or `monitorWhenLive`) or
|
||||
@@ -55,28 +49,11 @@ If neither is configured automatic scrobble defaults to **off** and you should e
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<Config config="AzuracastSourceConfig" fileContent={AzuracastConfig} name="azuracast">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `AZ_URL` | Yes | | Azuracast URL *without station name* |
|
||||
| `AZ_STATION` | Yes | | The station name shown on the public page |
|
||||
| `AZURA_LIVE` | No | Yes | Only scrobble when station status is ONLINE |
|
||||
| `AZURA_LISTENERS_NUM` | No | `true` | Only scrobble if station has any listeners (`true`) or listeners are equal-to/greater-than `X` |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/azuracast.json" language="json5">{AzuracastConfig}</CodeBlock>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={AzuracastConfig} name="azuracast"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</Config>
|
||||
@@ -44,28 +44,6 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={DeezerConfig} name="deezer"/>
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Duplicate detection
|
||||
|
||||
Third party integrations with Deezer (Sonos) may cause Deezer to [report the same track many times in listening history.](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2922374738) If you experience this issue modify your Deezer Source config (file-based only) to include the option `"fuzzyDiscoveryIgnore": "aggressive"`
|
||||
@@ -91,6 +69,76 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
|
||||
|
||||
This option comes with some trade-offs: MS will aggressively detect repeated tracks within a window of time that should eliminate all duplicates. However, this will also prevent *intentionally* repeated tracks from being scrobbled. See [this thread](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2970417070) for more information on how this works.
|
||||
|
||||
### Family account
|
||||
|
||||
Multi-scrobbler can monitor listening history for accounts linked to a [Deezer Family Account](https://www.deezer.com/en/offers/family).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Instructions</summary>
|
||||
|
||||
Start multi-scrobbler using the [ARL](#retrieve-arl) for the **main account**.
|
||||
|
||||
In the logs look for **Linked Accounts** associated with the Deezer Source. It will look something like this:
|
||||
|
||||
```
|
||||
VERBOSE: [App] [Sources] [Deezer - MyDeezer] Linked Accounts:
|
||||
Name: Joe | ID: 166276334 | Private?: Yes
|
||||
Name: Mary | ID: 48475231 | Private?: Yes
|
||||
Name: FoxxMD | ID: 896225281 | Private?: No
|
||||
Name: Cool Guy | ID: 128522478 | Private?: Yes
|
||||
```
|
||||
|
||||
The Account **ID** you want to monitor should be set:
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
Set in your docker compose `environment` section:
|
||||
|
||||
```yaml
|
||||
- DEEZER_ACCOUNT_ID=896225281
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File or AIO">
|
||||
Add an `accountId` property to the `data` section
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "DeezerARL",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"arl": "UOsRPjT3U5Dhaaup3xQ30D...",
|
||||
"accountId": "896225281"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Restart multi-scrobbler to start monitoring that account.
|
||||
|
||||
:::warning[Restrictions]
|
||||
|
||||
Deezer may not return the correct listening history under these circumstances:
|
||||
|
||||
* The ARL used is **not** for the main account
|
||||
* If you can get an ARL specifically for the linked account (login as the linked account), use that **instead** of family `accountId`
|
||||
* The ARL account is private
|
||||
* The linked account to be monitored ( using `accountId`) is private
|
||||
|
||||
:::
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="DeezerCompatConfig" fileContent={DeezerConfig} name="deezer">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
|
||||
</Config>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="official" label="Official API">
|
||||
|
||||
|
||||
@@ -144,8 +144,7 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<Config config="ChromecastSourceConfig" fileContent={ChromecastConfig} name="chromecast">
|
||||
:::note
|
||||
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
|
||||
:::
|
||||
@@ -157,28 +156,4 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
|
||||
| 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 |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/chromecast.json" language="json5">{ChromecastConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<AIOExample data={ChromecastConfig} name="chromecast"/>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Config>
|
||||
@@ -2,13 +2,14 @@
|
||||
title: Icecast
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
description: Icecast Source Configuration
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import IcecastConfig from '!!raw-loader!@site/../config/icecast.json.example';
|
||||
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
|
||||
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx"
|
||||
|
||||
Parse metadata from any Icecast Station (streams) to use for scrobbling.
|
||||
|
||||
@@ -20,13 +21,9 @@ Multi-scrobbler will do its best to parse Artist/Album/Track information from th
|
||||
|
||||
:::
|
||||
|
||||
:::note[Scrobbling Behavior]
|
||||
Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, **automatic scrobbling for Icecast Sources is off by default.** Use **Should Scrobble Behavior** to initiate scrobbling or configure automatic scrobble.
|
||||
|
||||
Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, **automatic scrobbling for Icecast Sources is off by default.**
|
||||
|
||||
Use [Should Scrobble Behavior](/configuration/sources#should-scrobble-behavior) to initiate scrobbling or configure automatic scrobble.
|
||||
|
||||
:::
|
||||
<ShouldScrobble filename="icecast"/>
|
||||
|
||||
:::tip[Scrobbling Threshold]
|
||||
|
||||
@@ -66,32 +63,8 @@ If no `sources` are configured for multi-scrobbler it will automatically try to
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<Config config="IcecastSourceConfig" fileContent={IcecastConfig} name="icecast">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | :------ | ----------- |
|
||||
| `ICECAST_URL` | Yes | | Icecast URL |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/icecast.json" language="json5">{IcecastConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="IcecastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<AIOExample data={IcecastConfig} name="icecast"/>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="IcecastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Config>
|
||||
@@ -19,7 +19,7 @@ Must be using Jellyfin 10.7 or greater
|
||||
|
||||
It is **recommended** to use API Key + username but if you are not an admin for your Jellyfin instance you can also authenticate with your Jellyfin username and **password.**
|
||||
|
||||
:::tip[Important Defaults]
|
||||
<DetailsAdmo type="tip" summary="Important Defaults">
|
||||
|
||||
By default...
|
||||
|
||||
@@ -27,41 +27,43 @@ By default...
|
||||
* Allowed Users (`usersAllow` or `JELLYFIN_USERS_ALLOW`) are only necessary if you want to scrobble for additional users.
|
||||
* multi-scrobbler will **only** scrobble media found in Jellyfin libraries that were labelled as **Music.**
|
||||
* `librariesAllow` or `JELLYFIN_LIBRARIES_ALLOW` will override this
|
||||
* OR use `additionalAllowedLibraryTypes` to allow more types (like `mixed` or `book` for audiobooks)
|
||||
* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio.**
|
||||
* To force multi-scrobbler to scrobble when media is detected as **Unknown** use `"allowUnknown": true` in file/aio configuration.
|
||||
* OR use `additionalAllowedLibraryTypes` to allow more *library* types (like those labelled as `mixed` or `book` for audiobooks)
|
||||
* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio**.
|
||||
* To allow other media types to be scrobbled set `allowMediaTypes` or `JELLYFIN_MEDIATYPES_ALLOW` with a list of [official types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22).
|
||||
* One or more of: `Unknown`, `Video`, `Audio`, `Photo`, `Book`
|
||||
* If you use this setting **you must explicitly specify `Audio`**, it will no longer be provided as a default
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
<DetailsAdmo type="tip" summary="External Asset URL">
|
||||
|
||||
If you use MS and Jellyfin on an internal or otherwise inaccessible network but still want album art assets used in the UI or for [Discord](/configuration/clients/discord) to be accessible you can specify a URL to replace your internal url.
|
||||
|
||||
Use (file) `frontendUrlOverride` or (env) `JELLYFIN_FRONTEND_URL_OVERRIDE` to replace `url`/`JELLYFIN_URL`, respectively, when rendering links for the dashboard UI or other resources.
|
||||
|
||||
Example:
|
||||
|
||||
Your `JELLYFIN_URL` is `http://jellyfin:8096` but your Jellyfin instance is externally accessible (by MS or in general) at `https://jellyfin.mydomain.com`.
|
||||
|
||||
Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for the MS dashboard to use `https://jellyfin.mydomain.com/art/1234.jpg` when linking to album art.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| -------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
|
||||
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
|
||||
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
|
||||
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
|
||||
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
|
||||
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
|
||||
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/jellyfin.json" language="json5">{JellyfinConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={JellyfinConfig} name="jellyfin"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
<Config config="JellyApiSourceConfig" fileContent={JellyfinConfig} name="jellyfin">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| -------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
|
||||
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
|
||||
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
|
||||
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
|
||||
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
|
||||
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
|
||||
| `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` |
|
||||
| `JELLYFIN_FRONTEND_URL_OVERRIDE` | No | | The external URL to use for album art assets instead of `JELLYFIN_URL` |
|
||||
</Config>
|
||||
@@ -49,28 +49,10 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<EnvType>
|
||||
<Config config="JRiverSourceConfig" fileContent={JriverConfig} name="jriver">
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|---------------------------------|------------------------------------------------|
|
||||
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
|
||||
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
|
||||
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
|
||||
</EnvType>
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<FileType>
|
||||
<CodeBlock title="CONFIG_DIR/jriver.json" language="json5">{JriverConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="JRiverSourceConfig"/>
|
||||
</FileType>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<AIOType>
|
||||
<AIOExample data={JriverConfig} name="jriver"/>
|
||||
|
||||
or <SchemaLink lower objectName="JRiverSourceConfig"/>
|
||||
</AIOType>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Config>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Koito
|
||||
description: Koito (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -19,13 +20,14 @@ To _scrobble to_ a Koito server, create a [Koito (Client)](/configuration/clien
|
||||
|
||||
See the [Koito (Client)](/configuration/clients/koito) configuration for general setup. The only difference for **Source** configuration:
|
||||
|
||||
* Cannot be setup with ENV config
|
||||
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Koito Client config](/configuration/clients/koito) -- multi-scrobbler assumes Koito ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Koito as a Source.
|
||||
:::
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | --------------------------------- |
|
||||
| `SOURCE_KOITO_TOKEN` | Yes | | API Key from your Koito Account |
|
||||
| `SOURCE_KOITO_USER` | Yes | | Your Koito username |
|
||||
| `SOURCE_KOITO_URL` | No | | The base URL for the Koito server |
|
||||
</Config>
|
||||
@@ -41,6 +41,6 @@ http://localhost:9078/api/lastfm/mySlug
|
||||
<Config config="LastFMEndpointSourceConfig" fileContent={LastfmEndpointConfig} name="endpointlfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `LFMENDPOINT_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
|
||||
| `LFM_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
|
||||
| `LFM_SLUG` | No | | (Optional) The URL suffix to use for accepting LFM scrobbles |
|
||||
</Config>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Lastfm
|
||||
title: Last.fm
|
||||
description: Last.fm (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -13,7 +14,7 @@ This Source **monitors your Last.fm scrobble history** and re-scrobbles new acti
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
|
||||
* To **scrobble to** Last.fm/Libre.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
|
||||
|
||||
:::
|
||||
@@ -23,7 +24,10 @@ To configure this Source use the same registration instructions and configuratio
|
||||
## Configuration
|
||||
|
||||
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Last.fm Client config](/configuration/clients/lastfm) -- multi-scrobbler assumes Last.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Last.fm as a Source.
|
||||
:::
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `SOURCE_LASTFM_API_KEY` | Yes | | Api Key from your API Account |
|
||||
| `SOURCE_LASTFM_SECRET` | Yes | | Shared secret from your API Account |
|
||||
| `SOURCE_LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
|
||||
| `SOURCE_LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
|
||||
</Config>
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Libre.fm
|
||||
description: Libre.fm (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
|
||||
|
||||
This Source **monitors your Libre.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
* To **scrobble to** Libre.fm, create a [Libre.fm (Client)](/configuration/clients/librefm)
|
||||
|
||||
:::
|
||||
|
||||
To configure this Source use the same registration instructions and configuration data shown in [Libre.fm (Client)](/configuration/clients/librefm) with the difference being `"configureAs": "source"` defined in the File/AIO configuration below.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="LibrefmSourceConfig" fileContent={JsonConfig} name="librefm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ----------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `SOURCE_LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables |
|
||||
| `SOURCE_LIBREFM_API_KEY` | No | `anything` | Optional API Key |
|
||||
| `SOURCE_LIBREFM_SECRET` | No | `anything` | Optional shared secret |
|
||||
| `SOURCE_LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it |
|
||||
| `SOURCE_LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
|
||||
| `SOURCE_LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` |
|
||||
</Config>
|
||||
@@ -9,7 +9,9 @@ import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ListenbrainzEndpointConfig from '!!raw-loader!@site/../config/endpointlz.json.example';
|
||||
|
||||
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.
|
||||
This Source enables multi-scrobbler to accept scrobbles from outside applications **as if it was a Listenbrainz server.**
|
||||
|
||||
If your service/player has a "Scrobble to Listenbrainz" feature and there is an option to set the Listenbrainz URL then multi-scrobbler can accept scrobbles from that service/player by imitating a real Listenbrainz server.
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
@@ -18,51 +20,99 @@ This Source enables multi-scrobbler to accept scrobbles from outside application
|
||||
|
||||
:::
|
||||
|
||||
## Setup
|
||||
|
||||
### URL
|
||||
|
||||
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Listenbrainz scrobbles at
|
||||
In general, you should use one of the following as the **base URL** when configuring your application to scrobble to multi-scrobbler:
|
||||
|
||||
* `http://myMultiScrobblerIP:9078`
|
||||
* `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
<DetailsAdmo type="tip" summary="Default Scrobble Submit URL">
|
||||
|
||||
If you are not using a [slug for multiple endpoints](#multiple-endpoints) then multi-scrobbler will accept Listenbrainz scrobbles at
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/1/submit-listens
|
||||
```
|
||||
|
||||
which is the "standard" Listenbrainz server path for scrobbling.
|
||||
which is the **standard** Listenbrainz server path for scrobbling.
|
||||
|
||||
In general, you should use `http://myMultiScrobblerIP:9078/1/` as the **base path** when configuring your application to scrobble to multi-scrobbler.
|
||||
</DetailsAdmo>
|
||||
|
||||
<details>
|
||||
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
|
||||
|
||||
<summary>URL Setup for Popular Apps</summary>
|
||||
Some services that can scrobble to a custom Listenbrainz URL may require the URL to have a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
|
||||
|
||||
##### Navidrome
|
||||
|
||||
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
</details>
|
||||
|
||||
Use a slug only if you need to setup multiple Listenbrainz Endpoint sources and cannot use different tokens.
|
||||
|
||||
If a slug is used then the URL will be:
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/api/listenbrainz/mySlug
|
||||
```
|
||||
|
||||
:::note
|
||||
Some Listenbrainz applications may require custom Listenbrainz URLs to be a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
### Token
|
||||
|
||||
Most Listenbrainz clients require a token (Authentication Token) to be provided during setup. This value can be anything you want, just make sure to use the same value for `token` in your multi-scrobbler configuration for the endpoint.
|
||||
|
||||
### Multiple Endpoints
|
||||
|
||||
If you are setting up multiple [Listenbrainz (Endpoint) Sources](/configuration/sources/listenbrainz-endpoint) MS can differentiate scrobbles based on [Token](#token) so that you can use the same endpoint for many users.
|
||||
|
||||
However, if you cannot configure multiple tokens for your scenario you may also use a URL **Slug** to tell MS which Source belongs to a specific user.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Using a Slug</summary>
|
||||
|
||||
When a **slug** is defined in ENV/File config MS will listen for listenbrainz routes using the slug as a the route path. Example:
|
||||
|
||||
```
|
||||
LZE_SLUG=foobar
|
||||
```
|
||||
|
||||
MS will accept calls to
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/api/listenbrainz/foobar
|
||||
```
|
||||
|
||||
for scrobble submission.
|
||||
|
||||
</details>
|
||||
|
||||
## Setup for Popular Apps
|
||||
|
||||
### Navidrome
|
||||
|
||||
<details>
|
||||
|
||||
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
</details>
|
||||
|
||||
### Music Assistant
|
||||
|
||||
<details>
|
||||
|
||||
[Music Assistant Docs](https://www.music-assistant.io/plugins/listenbrainz_scrobble/)
|
||||
|
||||
* In your Music Assistant UI navigate to **Settings** -> **Plugins**
|
||||
* Click **Add a plugin**
|
||||
* Add **ListenBrainz Scrobbler**
|
||||
* In the ListenBrainz Scrobbler configuration screen:
|
||||
* User Token is the [Token](#token) you configured
|
||||
* Toggle **Show advanced settings**
|
||||
* Base URL is the [URL](#url) you can configured, or the equivalent of `http://myMultiScrobblerIP:9078` for your MS instance
|
||||
* Save the configuration and you are ready to scrobble
|
||||
|
||||
To troubleshoot any errors, and assuming you are using Home Assistant, view the Logs in the Music Assistant Add-ons. Please include these logs in any reported issue.
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenbrainzEndpointSourceConfig" fileContent={ListenbrainzEndpointConfig} name="endpointlz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `LZENDPOINT_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
|
||||
| `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application |
|
||||
| `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles |
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `LZE_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
|
||||
| `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application |
|
||||
| `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles |
|
||||
| `LZE_USERNAME` | No | | (Optional) A fake username that will be returned for token validation and on now-playing responses. If none is provided the Source's name is used instead. |
|
||||
</Config>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Listenbrainz
|
||||
description: Listenbrainz (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -25,7 +26,9 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Listenbrainz Client config](/configuration/clients/listenbrainz) -- multi-scrobbler assumes Listenbrainz ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Listenbrainz as a Source.
|
||||
:::
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
|
||||
| `SOURCE_LZ_TOKEN` | Yes | | User token from your LZ profile |
|
||||
| `SOURCE_LZ_USER` | Yes | | Your LZ username |
|
||||
| `SOURCE_LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
|
||||
</Config>
|
||||
@@ -17,13 +17,13 @@ To _scrobble to_ a Maloja server, create a [Maloja (Client)](/configuration/cli
|
||||
|
||||
See the [Maloja (Client)](/configuration/clients/maloja) configuration for general setup. The only difference for **Source** configuration:
|
||||
|
||||
* Cannot be setup with ENV config
|
||||
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="MalojaSourceConfig" fileContent={MalojaConfig} name="maloja">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Maloja Client config](/configuration/clients/maloja) -- multi-scrobbler assumes Maloja ENVs are always used for the **client** configuration. You must use the [[File or AIO](/configuration?configType=file#configuration-types) config to setup Maloja as a Source.
|
||||
:::
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ----------------------- | --------- | ------- | ----------------------------- |
|
||||
| `SOURCE_MALOJA_URL` | Yes | | Base URL of your installation |
|
||||
| `SOURCE_MALOJA_API_KEY` | Yes | | Api Key |
|
||||
</Config>
|
||||
@@ -18,6 +18,8 @@ bind_to_address "any" # or a specific ipv4/v6 address
|
||||
port "6600"
|
||||
```
|
||||
|
||||
Multi-scrobbler requires only the [`read`](https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords) permission in order to monitor your activity.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="MPDSourceConfig" fileContent={MPDConfig} name="mpd">
|
||||
|
||||
@@ -32,7 +32,7 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d
|
||||
|
||||
##### Local Unix Socket
|
||||
|
||||
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation/#linux-host) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
|
||||
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation?dockerSetting=linuxHost#recommended-settings) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
|
||||
@@ -63,6 +63,134 @@ By default multi-scrobbler will only scrobble media found in Plex libraries that
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo type="tip" summary="Use Plex metadata with Musicbrainz for better scrobbling">
|
||||
|
||||
<h3>Matching With Plex</h3>
|
||||
|
||||
In Plex, if the **Plex Music** Agent is set on a Music Library then Plex attempts to match your music with Musicbrainz when it is first imported.
|
||||
|
||||
<img src={require('/img/plex_music_agent.jpg').default} height="400"/>
|
||||
|
||||
Plex uses ID3/metdata tags to match or falls back to matching based on file name and folder names. If it cannot find a match it can be fixed through a **Fix Match** option in the album/track context menu. See [plex documentation for more information](https://support.plex.tv/articles/correcting-your-music-content-matches/).
|
||||
|
||||
You can verify if an Item has been matched by:
|
||||
|
||||
* Clicking on the Item context menu (...)
|
||||
* Get Info
|
||||
* View XML
|
||||
|
||||
If a node like this is present then it has been matched: `<Guid id="mbid://4e631558-13b0..."/>`
|
||||
|
||||
<h3>Using Musicbrainz With Multi-Scrobbler</h3>
|
||||
|
||||
The MBID from the above `<Guid>` node is a [Track](https://musicbrainz.org/doc/Track), a special type of ID that specifically identifies a song on a specific album. If this is present then Multi-scrobbler can use it to gaurantee your scrobble, and all its metadata, is matched 1:1 with what is in your Plex library when using the [Musicbrainz Stage](/configuration/transforms/musicbrainz) to enrich your scrobble.
|
||||
|
||||
To configure MS to use Musicbrainz with your Plex metadata use one of the methods below:
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
Add to your multi-scrobbler docker compose file `environment` section for [Musicbrainz ENV Configuration](/configuration/transforms/musicbrainz#env-configuration):
|
||||
```yaml
|
||||
# a real email that Musicbrainz can use to contact you in case of issues
|
||||
- MB_CONTACT=myEmail@myEmailProvider.com
|
||||
- MB_PRESETS=id,default
|
||||
- PLEX_TRANSFORMS=musicbrainz
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="file" label="File">
|
||||
Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
// ...
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
// a real email that Musicbrainz can use to contact you in case of issues
|
||||
"contact": "myEmail@myEmailProvider.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In your [File Config](/configuration?configType=file#configuration-types):
|
||||
|
||||
```json5 title="plex.json"
|
||||
[
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB"
|
||||
"searchOrder": ["mbidrecording", "basicOrIds", "basic"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="File AIO">
|
||||
Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
// ...
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
// a real email that Musicbrainz can use to contact you in case of issues
|
||||
"contact": "myEmail@myEmailProvider.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
// ...
|
||||
"sources": [
|
||||
// ...
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"type": "plex",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"searchOrder": ["mbidrecording", "basicOrIds", "basic"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</DetailsAdmo>
|
||||
|
||||
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Config config="PlexApiSourceConfig" fileContent={PlexConfig} name="plex">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Rocksky
|
||||
description: Rocksky (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -29,11 +30,11 @@ Follow the **Setup** instructions in the [Rocksky (Client)](/configuration/clien
|
||||
|
||||
See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration for general setup. The only difference for **Source** configuration:
|
||||
|
||||
* Cannot be setup with ENV config
|
||||
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
|
||||
|
||||
<Config config="RockskyClientConfig" fileContent={JsonConfig} name="rocksky">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Rocksky Client config](/configuration/clients/rocksky#configuration) -- multi-scrobbler assumes Rocksky ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Rocksky as a Source.
|
||||
:::
|
||||
<Config config="RockskySourceConfig" fileContent={JsonConfig} name="rocksky">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :---------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `SOURCE_ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| `SOURCE_ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard |
|
||||
</Config>
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Sonos
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/sonos.json.example';
|
||||
|
||||
Multi-scrobbler can use the [Sonos](https://www.sonos.com) [local API](https://docs.sonos.com/docs/control) to monitor what your Sonos system is playing.
|
||||
|
||||
MS supports scrobbling simultaneous, different Plays based on what each Group/Zone is playing.
|
||||
|
||||
<DetailsAdmo type="tip" summary="Use Musicbrainz for better scrobbling">
|
||||
|
||||
For some services, including Spotify, Sonos only reports the **first** artist for a Track. You will most likely want to enable [Musicbrainz scrobble enhancements](/configuration/transforms/musicbrainz) so that your plays have correct information, including all artists, when scrobbled.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
<DetailsAdmo type="note" summary="Avoid Sonos activity through other Sources">
|
||||
|
||||
Activity reporting for **other [Sources](/configuration/sources)** does not work well **when played through Sonos.**
|
||||
|
||||
If you have other Sources configured for Multi-Scrobbler that you may play *through Sonos* you should try to add Sonos to the "deny" list for that Source so that it does not try to monitor Sonos activity. Check the documentation for each individual Source to see how to configure this exclusion.
|
||||
|
||||
For example, to prevent the [Plex](/configuration/sources/plex) Source from monitoring audio played through Sonos, add Sonos to the "block" list ENV EX: `PLEX_DEVICES_BLOCK=sonos`
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How to report issues</summary>
|
||||
|
||||
If your Sonos Source is logging errors or the scrobble data is not as expected please [create a bug report](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=01-bug-report.yml) **after** enabling the following debug logging options based on the config type you have:
|
||||
|
||||
<Tabs groupdId="sonosBugConfig">
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Enable [Debug Mode](/configuration#debug-mode) using `DEBUG_MODE=true`
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
Enable `logPayload` and `logFitlerFailure` in your `sonos.json`, or AIO file, config:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MySonos",
|
||||
"data": {
|
||||
"host": "192.168.0.150"
|
||||
},
|
||||
"options": {
|
||||
"logFilterFailure": "warn",
|
||||
"logPayload": true
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Additional logging for the Sonos library may be enabled by including this environmental variable (regardless of config type):
|
||||
|
||||
```
|
||||
DEBUG=sonos:*
|
||||
```
|
||||
|
||||
The above changes:
|
||||
|
||||
* Triggers multi-scrobbler to log the raw data recieved from Sonos when a new track/device/service is seen for the first time
|
||||
* Trigger multi-scrobbler to log the reason it considers a play from Sonos as invalid (and will not monitor it)
|
||||
* Logs low-level activity from the Sonos library
|
||||
|
||||
Please include these additional logs when creating a bug report. They are essential to troubleshooting your issue.
|
||||
|
||||
</details>
|
||||
|
||||
### Filtering Activity
|
||||
|
||||
The **optional** `devices` and `groups` related properties found in the config below can be used to include or exclude Sonos devices/groups by name, case-insensitive.
|
||||
|
||||
## Configuration
|
||||
|
||||
You **must** define the IP address of at least one Sonos/accessory in order for Multi-scrobbler to connect to your Sonos system.
|
||||
|
||||
<Config config="SonosSourceConfig" fileContent={JsonConfig} name="sonos">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------- | --------- | ------- | :--------------------------------------------------------------------- |
|
||||
| `SONOS_HOST` | Yes | | The IP address of any active Sonos device/accessory EX `192.168.0.150` |
|
||||
| `SONOS_DEVICES_ALLOW` | No | | Comma-separated list of Sonos device names to scrobble from |
|
||||
| `SONOS_DEVICES_BLOCK` | No | | Comma-separated list of Sonos device names to disallow scrobbles from |
|
||||
| `SONOS_GROUPS_ALLOW` | No | | Comma-separated list of device group names to scrobble from |
|
||||
| `SONOS_GROUPS_BLOCK` | No | | Comma-separated list of devices group names to disallow scrobbles from |
|
||||
</Config>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Scrobble Sources
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
@@ -23,6 +24,7 @@ A **Source** is a data source that contains information about tracks you are pla
|
||||
| [Kodi](/configuration/sources/kodi) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
|
||||
| [Koito](/configuration/sources/koito-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [Last.fm](/configuration/sources/lastfm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [Libre.fm](/configuration/sources/librefm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint) | [Ingress](./?sourceComm=ingress#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [ListenBrainz](/configuration/sources/listenbrainz-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [ListenBrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint) | [Ingress](./?sourceComm=ingress#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
@@ -32,12 +34,14 @@ A **Source** is a data source that contains information about tracks you are pla
|
||||
| [Musikcube](/configuration/sources/musikcube) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
|
||||
| [Plex](/configuration/sources/plex) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
|
||||
| [Rocksky](/configuration/sources/rocksky-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [Sonos](/configuration/sources/sonos) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
|
||||
| [Spotify](/configuration/sources/spotify) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
|
||||
| [Subsonic-compatible APIs](/configuration/sources/subsonic) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ✅ | ✅ | ✅ | ❌ |
|
||||
| [teal.fm](/configuration/sources/tealfm-source) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [WebScrobbler](/configuration/sources/webscrobbler) | [Ingress](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [VLC](/configuration/sources/vlc) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
|
||||
| [Yamaha MusicCast](/configuration/sources/yamaha-musiccast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
|
||||
| [Yandex Music](/configuration/sources/yandex-music) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | | ✅ | ✅ | ❌ |
|
||||
| [Youtube Music](/configuration/sources/youtube-music) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
|
||||
## Features
|
||||
|
||||
@@ -8,17 +8,31 @@ import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import SpotifyConfig from '!!raw-loader!@site/../config/spotify.json.example';
|
||||
import IconWarning from '@theme/Admonition/Icon/Warning';
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Spotify and Automix</summary>
|
||||
|
||||
<DetailsAdmo type="note" summary="Spotify and Automix">
|
||||
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.
|
||||
|
||||
</details>
|
||||
</DetailsAdmo>
|
||||
|
||||
#### Authenticate Spotify with Multi-Scrobbler
|
||||
|
||||
<DetailsAdmo type="warning" summary="Premium Account Required">
|
||||
|
||||
Starting [March 9th 2026](https://developer.spotify.com/blog/2026-02-06-update-on-developer-access-and-platform-security) all applications on the Spotify Developer platform will only continue to work if they are **owned** by a **Spotify Premium** account.
|
||||
|
||||
If your App is not created by a Premium account you will likely recieve a 403 error when trying to use MS with Spotify.
|
||||
|
||||
If you do not have a Spotify Premium account *you can still use Spotify with MS* but it will require you to coordinate with a (friend?) user who *does* have Spotify Premium:
|
||||
|
||||
* Follow the steps below **logged in as the Spotify Premium account user**
|
||||
* After completing App creation (step 4 or 5), navigate to the **User Management** page for the created App
|
||||
* Add **your** (non-premium) account here using your account email/name
|
||||
* Complete the rest of the authentication/configuration process normally
|
||||
|
||||
Assure your friend: **Creating a Spotify Developer app does not give anyone access to the owner's account.** The authentication process within MS is where authorization is given: it requires clicking the link in MS's dashboard and following a separate auth flow. This auth flow only authorizes access to the specific account used in the flow, unrelated to the owner of the developer app.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
To access your Spotify history you must [create a Spotify App](https://developer.spotify.com/dashboard) to get a
|
||||
Client ID/Secret.
|
||||
|
||||
@@ -51,7 +65,7 @@ Use **one** of the following methods to specify a valid Redirect URI and complet
|
||||
|
||||
In this scenario you can use `http://127.0.0.1:9078/callback` as the Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV.
|
||||
|
||||
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation#storage) to the final location of where multi-scrobbler will be run.
|
||||
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation?dockerSetting=storage#recommended-settings) to the final location of where multi-scrobbler will be run.
|
||||
</TabItem>
|
||||
<TabItem value="echo" label="Echo">
|
||||
**Use a docker container on the same machine your browser is on to "intercept" the callback URL from Spotify authentication, then manually finish the process by changing the domain to your multi-scrobbler instance.**
|
||||
|
||||
@@ -11,26 +11,26 @@ import SubsonicConfig from '!!raw-loader!@site/../config/subsonic.json.example';
|
||||
|
||||
This Source can be used 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/))
|
||||
|
||||
:::warning[Known Issues]
|
||||
<DetailsAdmo type="warning" summary="Known Issues">
|
||||
|
||||
* Multiple artists are reported as one value and cannot be separated
|
||||
* If using [Airsonic Advanced](https://github.com/airsonic-advanced/airsonic-advanced) the password used (under **Credentials**) must be **Decodable**
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
:::tip
|
||||
<DetailsAdmo type="tip" summary="Recommended Alternative Sources">
|
||||
|
||||
If your serivce supports scrobbling to **Listenbrainz** (such as Navidrome), considering using a [**Listenbrainz (Endpoint)** Source](/configuration/sources/listenbrainz-endpoint) instead of this one.
|
||||
|
||||
Service-specific scrobble implementations tend to be more accurate and provide more information (multiple artists) compared to the Subsonic API.
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
Use the optional `usersAllow` property with **File** or **AIO** configuration to restrict scrobbling to a list of defined users.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="SubsonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
|
||||
<Config config="SubSonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------|
|
||||
| `SUBSONIC_USER` | Yes | | |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Teal.fm
|
||||
description: Teal.fm (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -27,13 +28,84 @@ Follow the **Setup** instructions in the [teal.fm (Client)](/configuration/clien
|
||||
|
||||
## Configuration
|
||||
|
||||
See the [teal.fm (Client)](/configuration/clients/tealfm#setup) configuration for general setup. The only difference for **Source** configuration:
|
||||
See the [teal.fm (Client)](/configuration/clients/tealfm#setup) configuration for general setup. The main differences for **Source** configuration:
|
||||
|
||||
* Cannot be setup with ENV config
|
||||
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
|
||||
|
||||
### Service Filtering
|
||||
|
||||
`serviceAllow` and `serviceDeny` are lists that can be used to filter what Plays are scrobbles from teal.fm. These correspond to the base domain of the music service where the Play is from, originally. The values in the list can be a subset of the actual service name (they do not need to be exact matches).
|
||||
|
||||
Only one filter can be used at a time. If `serviceAllow` is configured then `serviceDeny` will be ignored.
|
||||
|
||||
If a Play does not have an associated music service it can be allowed/denied using the special keyword `unknown`.
|
||||
|
||||
If you are unsure what services are being used for your scrobbles you can check this by:
|
||||
|
||||
* use [atproto.at](https://atproto.at/) to lookup your DID/handle
|
||||
* View the **Raw Data** for the PDS Collection `fm.teal.alpha.feed.play`
|
||||
* Check the `musicServiceBaseDomain` property for each record
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Examples</summary>
|
||||
|
||||
Allowing only scrobbles from [plyr.fm](https://plyr.fm/):
|
||||
|
||||
```json title="tealfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3",
|
||||
|
||||
"serviceAllow": ["plyr"],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Deny scrobbles from last.fm:
|
||||
|
||||
```json title="tealfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3",
|
||||
|
||||
"serviceDeny": ["last"],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Allow scrobbles from plyr.fm and unknown services:
|
||||
|
||||
```json title="tealfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3",
|
||||
|
||||
"serviceAllow": ["plyr", "unknown"],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<Config config="TealClientConfig" fileContent={JsonConfig} name="tealfm">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [teal.fm Client config](/configuration/clients/tealfm#configuration) -- multi-scrobbler assumes teal.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup teal.fm as a Source.
|
||||
:::
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
|
||||
| `SOURCE_TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| `SOURCE_TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
|
||||
</Config>
|
||||
@@ -42,6 +42,10 @@ If the file being played in VLC does not have ID3 tags/metadata information mult
|
||||
|
||||
To extract this information use [named capture groups](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group) in your expression matching the field that should be extracted: `artist` `title` and/or `album`. MS accepts multiple expressions, the first one that matches a filename will be used.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Example usage in [a file-based config:](/configuration?configType=file#configuration-types)
|
||||
|
||||
```json5 title="config/vlc.json" {10-15}
|
||||
@@ -65,10 +69,16 @@ Example usage in [a file-based config:](/configuration?configType=file#configura
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Vlc Information Reporting
|
||||
|
||||
If you find that VLC is incorrectly reporting track information (in its interface) but multi-scrobbler is not parsing it correctly it may be due to the _name_ of the fields VLC is reporting. mutli-scrobbler uses the most common field name but does not cover all use cases. Before reporting an issue please turn on metadata logging and include output from your logs while it is turned on:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Metadata Logging Configuration</summary>
|
||||
|
||||
```json5 title="config/vlc.json" {9-9}
|
||||
[
|
||||
{
|
||||
@@ -84,6 +94,8 @@ If you find that VLC is incorrectly reporting track information (in its interfac
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="VLCSourceConfig" fileContent={JsonConfig} name="vlc">
|
||||
|
||||
@@ -18,19 +18,19 @@ After installing the [Webscrobbler extension](https://web-scrobbler.com/) open t
|
||||
|
||||
Reload the extension after adding the webhook.
|
||||
|
||||
:::note
|
||||
<DetailsAdmo type="note" summary="URL Types Supported by Webscrobbler">
|
||||
|
||||
* **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
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
:::warning
|
||||
<DetailsAdmo type="warning" summary="Public-Facing Usage">
|
||||
|
||||
Multi-scrobbler is **not** designed to be publicly accessible from the internet. To use Webscrobbler outside your local network you should be accessing MS through a VPN or through a reverse proxy with some kind of authentication (though this is still not recommended).
|
||||
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
### Multiple Users
|
||||
|
||||
@@ -86,7 +86,7 @@ This affects **only** MS's behavior and does not affect the general connector be
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="WebscrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
|
||||
<Config config="WebScrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------------------------------------------|
|
||||
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Yandex Music
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/ymbridge.json.example';
|
||||
|
||||
Monitor your [**Yandex Music**](https://music.yandex.com) listening activity in real-time.
|
||||
|
||||
This Source requires a third party docker container to communicate with Yandex Music, [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example Docker Compose with `yandex-music-bridge`</summary>
|
||||
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT
|
||||
# ...your other sources/clients here
|
||||
|
||||
# add for yandex music
|
||||
// highlight-start
|
||||
- YMBRIDGE_URL=http://yandex-music-bridge:9980
|
||||
- YMBRIDGE_API_KEY=change-me
|
||||
// highlight-end
|
||||
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
ports:
|
||||
- "9078:9078"
|
||||
restart: unless-stopped
|
||||
|
||||
// highlight-start
|
||||
yandex-music-bridge:
|
||||
image: ghcr.io/druidblack/yandex-music-bridge:latest
|
||||
environment:
|
||||
- TZ=Europe/Moscow
|
||||
- YM_TOKEN=AgAAAAACO3_345345
|
||||
- YM_API_KEY=change-me
|
||||
- YM_PORT=9980
|
||||
- YM_LANGUAGE=ru
|
||||
- YM_ENABLE_YNISON=true
|
||||
- YM_PUSH_TTL=45
|
||||
- YM_QUEUE_CACHE_TTL=15
|
||||
- YM_LOG_LEVEL=INFO
|
||||
ports:
|
||||
- "9980:9980"
|
||||
restart: unless-stopped
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
You will need to acquire your own token for `YM_TOKEN` used in [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge) using the instructions provided there.
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo type="tip" summary="Need Help?">
|
||||
|
||||
If your issue is specifically related to [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge) (errors in the container, usage instructions, general Yandex setup like token etc...) please open an issue on **that repository** instead of Multi-Scrobbler's.
|
||||
|
||||
If you have issues specifically with the MS Source please mention [**Druidblack**](https://github.com/Druidblack) (`@Druidblack`) when opening an [issue](https://github.com/FoxxMD/multi-scrobbler/issues/new/choose) or [discussion](https://github.com/FoxxMD/multi-scrobbler/discussions/new/choose). Yandex Music is [not available](https://en.wikipedia.org/wiki/Yandex_Music) in the country of the main Multi-Scrobbler developer (FoxxMD) so they can only help troubleshooting general MS issues related to Yandex Music.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="YandexMusicBridgeSourceConfig" fileContent={JsonConfig} name="ymbridge">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | :------------------------------------------------------------------------ |
|
||||
| `YMBRIDGE_URL` | Yes | | `http://URL:PORT` for the `yandex-music-bridge` container |
|
||||
| `YMBRIDGE_API_KEY` | No | | The same key used for `YM_API_KEY` on the `yandex-music-bridge` container |
|
||||
</Config>
|
||||
@@ -115,7 +115,7 @@ Only one of these methods needs to be used.
|
||||
|
||||
If the beginning of the Redirect URI (before `api`) is EXACTLY how you would reach the MS dashboard from your browser (EX `http://localhost:9078`) then edit your google oauth client section for `Authorized redirect URIs` and add the URL MS has displayed.
|
||||
|
||||
If it is NOT EXACTLY the same you either need to set MS's [base url](https://foxxmd.github.io/multi-scrobbler/docs/configuration/#base-url) or you can provide your own (Custom) Redirect URI for MS to use by setting it in `ytmusic.json` or ENV.
|
||||
If it is NOT EXACTLY the same you either need to set MS's [base url](/configuration#base-url) or you can provide your own (Custom) Redirect URI for MS to use by setting it in `ytmusic.json` or ENV.
|
||||
|
||||
<details>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Musicbrainz Stage
|
||||
description: Enhancing Scrobbles with Musicbrainz
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -50,7 +51,7 @@ Set up [Valkey Caching](/configuration?cachedThings=metadata#caching) to cache M
|
||||
|
||||
### API Setup
|
||||
|
||||
To avoid rate limiting, MusicBrainz requires API users to identify themself using a [User-Agent that contains an email or URL.](https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?) You must set this in the `data` for your [stage configuration](http://localhost:3000/docs/configuration/transforms/#configuring-stages) in your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
To avoid rate limiting, MusicBrainz requires API users to identify themself using a [User-Agent that contains an email or URL.](https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?) You must set this in the `data` for your [stage configuration](/configuration/transforms/#configuring-stages) in your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
@@ -129,7 +130,6 @@ Example:
|
||||
],
|
||||
"defaults": {
|
||||
"releaseStatusPriority": ["official"],
|
||||
"fallbackArtistSearch": "native",
|
||||
"releaseAllowEmpty": true
|
||||
}
|
||||
},
|
||||
@@ -138,25 +138,97 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
### Rules
|
||||
### Rules and Hooks
|
||||
|
||||
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-moditication):
|
||||
[Add your Stage](/configuration/transforms/#stage) to a Source or Client by specifying it in a [Hook](/configuration/transforms/#hook):
|
||||
|
||||
```json5
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB"
|
||||
// ...
|
||||
"title": false, // will not apply any changes to Play title
|
||||
"artists": {
|
||||
"when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied
|
||||
/* ... */
|
||||
},
|
||||
"album": true // will always apply changes to Play album
|
||||
"meta": true // adds MusicBrainz MBIDs to scrobble data
|
||||
}
|
||||
```json5 title="subsonic.json"
|
||||
[
|
||||
{
|
||||
"name": "MySubsonic",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Each [**Stage Rule**](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-modification):
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="subsonic.json"
|
||||
[
|
||||
{
|
||||
"name": "MySubsonic",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB"
|
||||
// ...
|
||||
"title": false, // will not apply any changes to Play title
|
||||
"artists": {
|
||||
"when": {/* ... */}, // will only apply changes to Play artists if "when" is satisfied
|
||||
/* ... */
|
||||
},
|
||||
"album": true // will always apply changes to Play album
|
||||
"meta": true // adds MusicBrainz MBIDs to scrobble data
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
:::tip[Per Component Override]
|
||||
|
||||
The `defaults` you set in [Stage Configuration](#stage-configuration) [can be overriden/added to](/configuration/transforms/#overriding-configuration) (per property) in each Hook.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="subsonic.json"
|
||||
[
|
||||
{
|
||||
"name": "MySubsonic",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"releaseStatusPriority": ["psuedo-release"], // override from defaults
|
||||
"searchOrder": ["freetext"] // add new property, combined with defaults
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
:::
|
||||
|
||||
## ENV Configuration
|
||||
|
||||
The general configuration shown above can also be configured from a selection of *presets* using [ENV Config](/configuration?configType=env#configuration-types) for individual Sources/Clients.
|
||||
@@ -169,16 +241,18 @@ MB_CONTACT=contact@mydomain.com
|
||||
|
||||
To configure [stage defaults](#stage-configuration) use `MB_PRESETS` with a comma-delimited list of presets you wish to apply. More than one preset can be applied, in which case they combine. You must choose at least one of the presets below:
|
||||
|
||||
* `default` - Applies no defaults. This is the same as using no options from [Matching With Musicbrainz](#matching-with-musicbrainz)
|
||||
* `sensible` - Applies the [Sensible Default](#sensible-default) configuration for sorting releases
|
||||
* `native` - Applies `fallbackArtistSearch: native` for [Artist Extraction](#artist-extraction) if the first search fails to find matches
|
||||
* `aggressive` - Applies [Free Text Search](#free-text) `fallbackFreeTextSearch: true` if all other searches fail
|
||||
* `default` - Adds the default [`searchOrder`](#search-methods) `isrc`+`basic` (same as using no presets that affect `searchOrder`)
|
||||
* `id` - Adds [`searchOrder`](#search-methods) methods that priorites searching by ID: `isrc` + `mbidrecording` + `basicOrIds` + `basic`
|
||||
* `native` - Adds **Extracted Artists** (`artist`) in `native` mode to [`searchOrder`](#search-methods)
|
||||
* `aggressive` - Adds **Free Text Search** (`freetext`) to [`searchOrder`](#search-methods)
|
||||
* `fields` - Applies [Field Scoring](#field-scoring) to match sorting so match text is baised towards similarity to original scrobble
|
||||
* `sensible` - Applies the [Sensible Default](#sensible-default) configuration for [sorting releases](#sorting-releases)
|
||||
|
||||
Finally, use ENV `*_TRANSFORMS=musicbrainz` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preTransform` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules) enabled.
|
||||
Finally, use ENV `*_TRANSFORMS=musicbrainz` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preTransform` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules-and-hooks) enabled.
|
||||
|
||||
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example:
|
||||
|
||||
* All [Subsonic Source](http://localhost:3000/docs/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* All [Subsonic Source](/configuration/sources/subsonic) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* Use `SUBSONIC_TRANSFORMS=musicbrainz`
|
||||
|
||||
<details>
|
||||
@@ -195,16 +269,20 @@ services:
|
||||
environment:
|
||||
// highlight-start
|
||||
- MB_CONTACT=contact@mydomain.com
|
||||
- MB_PRESETS=sensible,native # creates sensible defaults with native fallback searching
|
||||
# searches with sensible release sorting with the searchOrder
|
||||
# isrc => basic => artist (native mode)
|
||||
- MB_PRESETS=default,sensible,native
|
||||
// highlight-end
|
||||
- JELLYFIN_URL=192.168.0.110:8096
|
||||
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
|
||||
- JELLYFIN_USER=MyUser
|
||||
// highlight-start
|
||||
- JELLYFIN_TRANSFORMS=musicbrainz # applies musicbrainz Stage to preTransform of Jellyfin source
|
||||
# applies musicbrainz Stage to preTransform of Jellyfin source
|
||||
- JELLYFIN_TRANSFORMS=musicbrainz
|
||||
// highlight-end
|
||||
|
||||
- MALOJA_URL=http://192.168.0.100:42010 # maloja receives enhanced scrobble from Jellyfin
|
||||
# maloja receives enhanced scrobble from Jellyfin
|
||||
- MALOJA_URL=http://192.168.0.100:42010
|
||||
- MALOJA_API_KEY=myApiKey
|
||||
|
||||
|
||||
@@ -236,7 +314,9 @@ Both steps have separate configuration.
|
||||
|
||||
### Searching
|
||||
|
||||
Before MS begins a search it checks Scrobble data it checks if your data already contains:
|
||||
#### Should MS Search?
|
||||
|
||||
Before MS begins a search it checks if your Scrobble data already contains:
|
||||
|
||||
* [MBIDs](https://musicbrainz.org/doc/MusicBrainz_Identifier) for [artists](https://musicbrainz.org/doc/Artist), [album](https://musicbrainz.org/doc/Release), and specific [track/recording](https://musicbrainz.org/doc/Recording)
|
||||
* and duration
|
||||
@@ -258,7 +338,7 @@ Define which data types are required using:
|
||||
// ...
|
||||
"defaults": {
|
||||
// only search if MBIDs for artists or album is missing
|
||||
"searchWithMissing": ['artists', 'album']
|
||||
"searchWithMissing": ["artists", "album"]
|
||||
|
||||
// uncomment to make the stage always search, even if above data is present
|
||||
//forceSearch: true
|
||||
@@ -267,65 +347,50 @@ Define which data types are required using:
|
||||
|
||||
</details>
|
||||
|
||||
If required data is missing then the next step is making [search queries to the Musicbrainz database](https://musicbrainz.org/doc/MusicBrainz_API/Search) to try to get potential candidates. Multi-scrobbler uses the **title, artist(s), and album** from your Scrobble data to query for matches.
|
||||
If required data is missing then the next step is making [search queries to the Musicbrainz database](https://musicbrainz.org/doc/MusicBrainz_API/Search) to try to get potential candidates.
|
||||
|
||||
:::note
|
||||
#### Search Methods
|
||||
|
||||
If your Scrobble data contains an [ISRC](https://musicbrainz.org/doc/ISRC) (usually from Spotify) then Multi-scrobbler will search using this ID first, before the above criteria. If no match is found with the ISRC then it automatically retries with the title, artist(s), and album.
|
||||
Multi-scrobbler can search for Musicbrainz matches using **multiple** methods. You control:
|
||||
|
||||
* the search methods that are used
|
||||
* **the order** in which search methods are executed
|
||||
|
||||
All search methods will be run in the order they were defined until one method returns at least one match.
|
||||
|
||||
**The methods you choose, and the order they run in, can drastically change how many API calls are needed to get a match, and what types of matches are returned.** See [**Search Considerations**](#search-considerations) for guidance on how to decide on methods based on Source.
|
||||
|
||||
To set search methods and their order use the `searchOrder` option in your [Stage Configuration](/configuration/transforms#configuring-stages):
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// only searches using these methods, in the order they are listed here
|
||||
"searchOrder": ["isrc", "mbidrecording", "basic","freetext"]
|
||||
}
|
||||
```
|
||||
|
||||
:::tip[Default Search Methods]
|
||||
|
||||
If `searchOrder` is undefined then Multi-scrobbler will default to using `isrc` then `basic` method.
|
||||
|
||||
:::
|
||||
|
||||
Musicbrainz agressively normalizes these fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like [Subsonic](/configuration/sources/subsonic) and [Last.fm](/configuration/sources/lastfm).
|
||||
<h4>Available Methods</h4>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Examples of Bad Data</summary>
|
||||
|
||||
Last.fm Scrobble returns an Artist in the title and combines two artists into one string:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Endless Possibility (feat. Wheatus)",
|
||||
"artists": ["Bowling For Soup & Punk Rock Factory"]
|
||||
}
|
||||
```
|
||||
|
||||
The corresponding [Musicbrainz Recording](https://musicbrainz.org/release/85bf284b-8b39-414e-8ad9-61f593072588) has all artists separated and no artist in the title:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Endless Possibility",
|
||||
"artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
:::note
|
||||
|
||||
If the initial search(es), and any [retried searches](#retrying-search), do not return any results then the stage is marked as [**failed** (`onFailure`) for **Flow Control**](/configuration/transforms/#flow-control).
|
||||
|
||||
:::
|
||||
|
||||
#### Retrying Search
|
||||
|
||||
If normal searches using all the fields mentioned above does not return any matches Multi-scrobbler can try additional searches with modified queries.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Album Only</summary>
|
||||
|
||||
If your Scrobble data contains a title, artist(s), and an album (all three fields) then Multi-scrobbler can retry the search using only title and album, if `fallbackAlbumSearch` is set in [Stage Configuration](/configuration/transforms#configuring-stages):
|
||||
<summary>ISRC (`isrc`)</summary>
|
||||
|
||||
If your Scrobble data contains an [ISRC](https://musicbrainz.org/doc/ISRC) (usually from Spotify) then Multi-scrobbler can search using this ID, only.
|
||||
|
||||
[Stage Configuration](/configuration/transforms#configuring-stages) example:
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// will try an additional search using only title + album name
|
||||
"fallbackAlbumSearch": "true"
|
||||
// will only search using isrc
|
||||
"searchOrder": ["isrc"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -333,19 +398,111 @@ If your Scrobble data contains a title, artist(s), and an album (all three field
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Artist Extraction</summary>
|
||||
<summary>Track MBID (`mbidrecording`)</summary>
|
||||
|
||||
If...
|
||||
If your Scrobble data contains an [Track (Recording) MBID](https://musicbrainz.org/doc/Recording) then Multi-scrobbler can search using this MBID, only.
|
||||
|
||||
* [Album Only](#album-only) returns no results or did not run
|
||||
* Your scrobble data contains only **one** artist string
|
||||
* `fallbackArtistSearch` is set in [Stage Configuration](/configuration/transforms#configuring-stages)
|
||||
[Stage Configuration](/configuration/transforms#configuring-stages) example:
|
||||
|
||||
Then Multi-scrobbler will attempt to extract multiple artists from your artist and track string. **This is not automatic. You must set `fallbackArtistSearch` to enable this additional search.**
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// will only search using recording mbid
|
||||
"searchOrder": ["mbidrecording"]
|
||||
}
|
||||
```
|
||||
|
||||
`fallbackArtistSearch` can be set to **Native** or **Naive** mode:
|
||||
</details>
|
||||
|
||||
<Tabs groupId="fallbackArtist" queryString>
|
||||
<details>
|
||||
|
||||
<summary>Album, Artist, and Title Fields (`basic`)</summary>
|
||||
|
||||
Will search Musicbrainz using any/all available text fields from your scrobble: Album, Artists(s), and Title. Does not leverage any existing MBIDs.
|
||||
|
||||
[Stage Configuration](/configuration/transforms#configuring-stages) example:
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// will only search using album, artist(s), and title
|
||||
"searchOrder": ["basic"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Album/Artist/Title OR MBIDs (`basicOrIds`)</summary>
|
||||
|
||||
This is the same as the above **Album, Artist, and Title Fields** search except that if the scrobble data contains any corresponding MBIDs it will use those instead of the plain text field.
|
||||
|
||||
[Stage Configuration](/configuration/transforms#configuring-stages) example:
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// will only search using album, artist(s), and title
|
||||
"searchOrder": ["basicOrIds"]
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Endless Possibility (feat. Wheatus)",
|
||||
"artists": ["Bowling For Soup"]
|
||||
"albumMbid": "c9f91cdc-984e-4303-9a51-4ac0dfa2348f",
|
||||
"album": "My Cool Album"
|
||||
}
|
||||
```
|
||||
|
||||
Multi-scrobbler will search Musicbrainz using `title`, `artists`, and `albumMbid` but NOT `album`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Album And Title Only (`album`)</summary>
|
||||
|
||||
If your Scrobble data contains a title, artist(s), and an album (all three fields) then Multi-scrobbler can search using only title and album by using `album` for `searchOrder`:
|
||||
|
||||
[Stage Configuration](/configuration/transforms#configuring-stages) example:
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// will only search using only title + album name
|
||||
"searchOrder": ["album"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Extracted Artists (`artist`)</summary>
|
||||
|
||||
If your scrobble data contains only **one** artist string then Multi-scrobbler can attempt to extract multiple artists from your artist and track string.
|
||||
|
||||
Use `artist` with `searchOrder` and optionally specify the extraction mode with `searchArtistMethod`:
|
||||
|
||||
[Stage Configuration](/configuration/transforms#configuring-stages) example:
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// will only search using title/album + extracted artists
|
||||
"searchOrder": ["artist"],
|
||||
//searchArtistMethod: 'native' // optional
|
||||
}
|
||||
```
|
||||
|
||||
`searchArtistMethod` can be set to **Native** or **Naive** mode:
|
||||
|
||||
<Tabs groupId="searchArtistMethod" queryString>
|
||||
<TabItem value="native" label="Native (Recommended)">
|
||||
|
||||
**Native** (`native`) mode uses an aggressive configuration of the [Native Stage](/configuration/transforms/native) to extract artists using common delimiters and common "joined" artist patterns from the artist and title string of your Scrobble data.
|
||||
@@ -388,8 +545,9 @@ Then Multi-scrobbler will attempt to extract multiple artists from your artist a
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// ...
|
||||
"fallbackArtistSearch": "native"
|
||||
// will only search using title/album + native extracted artists
|
||||
"searchOrder": ["artist"],
|
||||
"searchArtistMethod": "native"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -426,8 +584,9 @@ Then Multi-scrobbler will attempt to extract multiple artists from your artist a
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// ...
|
||||
"fallbackArtistSearch": "naive"
|
||||
// will only search using title/album + naive extracted artists
|
||||
"searchOrder": ["artist"],
|
||||
"searchArtistMethod": "naive"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -438,9 +597,9 @@ Then Multi-scrobbler will attempt to extract multiple artists from your artist a
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Free Text</summary>
|
||||
<summary>Free Text (`freetext`)</summary>
|
||||
|
||||
If both [Album Only](#album-only) and [Artist Extraction](#artist-extraction) searches fail to return results, or were not run, you can additionally enable **Free Text** search. This will search Musicbrainz for **all** text of your Scrobble data artist/title/album, without constraint.
|
||||
This will search Musicbrainz for **all** text of your Scrobble data artist/title/album, without constraint.
|
||||
|
||||
:::warning
|
||||
|
||||
@@ -461,12 +620,19 @@ If you know your Source's Play data is well organized you **should not** enable
|
||||
// ...
|
||||
"defaults": {
|
||||
// ...
|
||||
"fallbackFreeTextSearch": true
|
||||
// will only search using free text search of all artist/album/title
|
||||
"searchOrder": ["freetext"],
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
:::note
|
||||
|
||||
If all defined search methods do not return any results then the stage is marked as [**failed** (`onFailure`) for **Flow Control**](/configuration/transforms/#flow-control).
|
||||
|
||||
:::
|
||||
|
||||
### Refining
|
||||
|
||||
### Score
|
||||
@@ -532,14 +698,17 @@ You may want to filter the majority of your matches by releases but allow matchi
|
||||
|
||||
### Sorting
|
||||
|
||||
Each [Release Attribute](#release-attribute) has one additional property that can be used to **rank** releases based on the order of the values you give it. This is the `priority` property.
|
||||
|
||||
*After* matches have been [filtered](#filtering), the remaining matches will have their releases sorted. Release with an attribute that does not match are **not** removed, but they are sorted lower than releases that do match.
|
||||
*After* matches have been [filtered](#filtering), the remaining matches *and their releases** can be sorted. Matches/Releases with an attribute that does not match are **not** removed, but they are sorted lower than matches that do match.
|
||||
|
||||
Sorting can be a good alternative to filtering: with filters there is a possibility your filters may eliminate all matches; if you want to ensure that **some** match will be used then sorting can ensure that the **best choice** out of those given will always be used, without accidentally ending up with **no choice**.
|
||||
|
||||
Sorting can be used instead of filters, or in conjunction with filters, it's your choice.
|
||||
|
||||
#### Release Attributes {#sorting-releases}
|
||||
|
||||
Each [Release Attribute](#release-attributes) has one additional property that can be used to **rank** releases based on the order of the values you give it. This is the `priority` property.
|
||||
|
||||
In configuration:
|
||||
|
||||
```
|
||||
@@ -554,10 +723,65 @@ EX to prefer albums, then singles, over everything else
|
||||
"releaseGroupPrimaryTypePriority": ["album", "single"]
|
||||
```
|
||||
|
||||
If a rule is not present then multi-scrobbler defaults it to `true`.
|
||||
#### Field Scoring
|
||||
|
||||
Matches can additionally be sorted/ranked based on how similar their fields are to the original Scrobble. Use these properties for sorting when you want the final Musicbrainz match to be as closely matched to the original Scrobble, as opposed to what may be the most "correct" match based on the Musicbrainz score or suggested Release.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example Scenario</summary>
|
||||
|
||||
You scrobble [this track from spotify](https://open.spotify.com/track/21trRQA61afzljSK9V0SHX), which has an English title, Album name, and Artist name.
|
||||
|
||||
```json
|
||||
{
|
||||
"track": "Price",
|
||||
"artists": ["ATLUS Sound Team"],
|
||||
"album": "PERSONA5 ORIGINAL SOUNDTRACK"
|
||||
}
|
||||
```
|
||||
|
||||
However, the [Recording found by Musicbrainz](https://beta.musicbrainz.org/search?query=isrc%3AJPK651601515&type=recording&limit=25&method=advanced) has multiple Releases (albums). The first suggested release is a *related* Album with similar name (`Persona 25th Anniversary Deluxe Vinyl Box Set`) but it's not the same as your album. Additionally, the artists for this album are in Japanese, rather than English.
|
||||
|
||||
You have used `releaseStatusPriority` to prioritize `psuedo-release` which does work but there are still multiple releases chosen, and some are non-English (`『ペルソナ5』オリジナル・サウンドトラック`).
|
||||
|
||||
Using `"albumWeight": 0.4` and `"artistWeight": 0.3` ensures that the releases that have an album name, and artist names, more similar to your original scrobble are ranked higher. Resulting in [the correct Release (`PERSONA5 ORIGINAL SOUNDTRACK`)](https://beta.musicbrainz.org/release/82de33b1-1cd6-4236-b116-561d0ecc8acf) being chosen as the final match.
|
||||
|
||||
</details>
|
||||
|
||||
Add one or more of these weight properties to your [Stage Configuration](/configuration/transforms#configuring-stages) to enable Field Scoring:
|
||||
|
||||
```json5
|
||||
// ...
|
||||
"defaults": {
|
||||
// ...
|
||||
// enables album text similiarity scoring
|
||||
"albumWeight": 0.33,
|
||||
// enables title text similiarity scoring
|
||||
"titleWeight": 0.33,
|
||||
// enables artist text similiarity scoring
|
||||
"artistWeight": 0.33
|
||||
}
|
||||
```
|
||||
|
||||
While not enforced, it's a good idea to keep these weights under `1`. And if using more than one weight, they should be add up to `1`.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How Scoring is Combined</summary>
|
||||
|
||||
The score similarity from `albumWeight` will be added to the score accumulated by [Release Attributes](#sorting-releases) to affect the final ranking of each Release for a Match/Recording.
|
||||
|
||||
In the final sorting of Recordings, title score + artist score is combined with the top ranked release album score. The top ranked score is then used to choose the matched Recording.
|
||||
|
||||
</details>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Caching
|
||||
|
||||
You **should** setup [metadata caching](/configuration/transforms#caching) to reduce API calls, improve transform performance, and reduce memory usage when using this stage.
|
||||
|
||||
### Sensible Default
|
||||
|
||||
Generally, the Musicbrainz Stage can be used without any of the [optional configuration](#configuration) and you should still see good results from matches. The top [scored](#score) match is, anecdotally, good enough for correcting and filling in surface-level play data like Title and Artist names.
|
||||
@@ -575,16 +799,70 @@ For a more opinionated match that will mirror what you would expect from data fr
|
||||
}
|
||||
```
|
||||
|
||||
Consider adding [Artist Extraction](./?fallbackArtist=native#artist-extraction) in **Native Mode** if any of your Sources do not support multiple artists or your music collection is not tagged well.
|
||||
Consider adding [Artist Extraction (`artist`)](#search-methods) in **Native Mode** if any of your Sources do not support multiple artists or your music collection is not tagged well.
|
||||
|
||||
<details>
|
||||
|
||||
```json5
|
||||
{
|
||||
// add the line below to the sensible defaults above
|
||||
// (don't forget commas)
|
||||
"fallbackArtistSearch": "native"
|
||||
// use official release over anything else
|
||||
"releaseStatusPriority": ["official"],
|
||||
// prefer album, then single, then ep
|
||||
"releaseGroupPrimaryTypePriority": ["album", "single", "ep"],
|
||||
// prefer worldwide release
|
||||
"releaseCountryPriority": ["XW"],
|
||||
// search using extracted artist as last search attempt
|
||||
"searchOrder": ["isrc", "basic", "artist"],
|
||||
"searchArtistMethod": "native"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Search Considerations
|
||||
|
||||
How complete, and accurate, your scrobble data is from a certain Source should dictate what [Search Methods](#search-methods) you should use for matching.
|
||||
|
||||
Musicbrainz agressively normalizes **artist, album, and title** fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like [Subsonic](/configuration/sources/subsonic) and [Last.fm](/configuration/sources/lastfm-source).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Examples of Bad Data</summary>
|
||||
|
||||
Last.fm Scrobble returns an Artist in the title and combines two artists into one string:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Endless Possibility (feat. Wheatus)",
|
||||
"artists": ["Bowling For Soup & Punk Rock Factory"]
|
||||
}
|
||||
```
|
||||
|
||||
The corresponding [Musicbrainz Recording](https://musicbrainz.org/release/85bf284b-8b39-414e-8ad9-61f593072588) has all artists separated and no artist in the title:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Endless Possibility",
|
||||
"artists": ["Bowling For Soup", "Punk Rock Factory", "Wheatus"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**In general, you should prioritize methods that search by unique identifiers** (`isrc` `mbidRecording` `basicOrIds`) if you know your Sources may contain *accurate* data for these. Such as: Spotify, Jellyfin, Plex, and Listenbrainz (Source).
|
||||
|
||||
Use field-based methods if your Sources do not return IDs consistently (or at all). Add more field-based methods the more inaccurate your scrobble data may be, but be aware that more aggressive/broad searches may return less accurate results.
|
||||
|
||||
<DetailsAdmo type="tip" summary="Matching Scrobbles from Last.fm">
|
||||
|
||||
Last.fm is inconsistent when it comes to matching your scrobbles. It will accept (almost) any fields for track/title/album and aggressively attempt to match/correct these, regardless of your intentions.
|
||||
|
||||
Unless the client scrobbling to Last.fm is also sending an `mbid`, it's likely your scrobble will not be **consistently** matched correctly and so the `mbid` supplied by last.fm in its scrobble data will be wrong.
|
||||
|
||||
Therefore, you should *avoid* using `mbidRecording` and `basicOrIds` as the primary search methods (or understand the risks of using them). Instead, prefer to use field-based methods *before* falling back to id-based methods.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
### Filter Considerations
|
||||
|
||||
When using your own filters consider:
|
||||
@@ -596,7 +874,7 @@ When using your own filters consider:
|
||||
|
||||
### Using Partial Match
|
||||
|
||||
Use [Rules](#rules) to apply MusicBrainz match data selectively.
|
||||
Use [Rules](#rules-and-hooks) to apply MusicBrainz match data selectively.
|
||||
|
||||
If you know that your music collection is well organized and you do not want to change the artists/title/album etc... sent to a [Client](/configuration/clients), you can still benefit from matches by only applying MBIDs using `meta` so that any Client that supports Musicbrainz data ([Koito](/configuration/clients/koito), [Tealfm](/configuration/clients/tealfm), [Listenbrainz](/configuration/clients/listenbrainz), [Rocksky](/configuration/clients/rocksky)) can still get that data.
|
||||
|
||||
@@ -640,7 +918,7 @@ If
|
||||
|
||||
Enable logging by turning on [**Debug Mode**](/configuration#debug-mode) to help diagnose any issues with the Musicbrainz API and Scrobble enhancement. **Before creating an issue** please enable logging and include any logs with your issue as this is needed to debug.
|
||||
|
||||
If you have multiple Modification Stages and need to see the diff for your Play between each Stage, enable `"log": "all"` in the individual [Modification Stage](http://localhost:3000/docs/configuration/transforms/musicbrainz/?fallbackArtist=native#artist-extraction).
|
||||
If you have multiple Modification Stages and need to see the diff for your Play between each Stage, enable `"log": "all"` in the individual [Modification Stage](#rules-and-hooks).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -751,7 +1029,8 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
"releaseStatusPriority": ["official"],
|
||||
"releaseGroupPrimaryTypePriority": ["album", "single", "ep"],
|
||||
"releaseCountryPriority": ["XW"],
|
||||
"fallbackArtistSearch": "native"
|
||||
"searchArtistMethod": "native"
|
||||
"searchOrder": ["isrc", "basic", "artist"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -812,7 +1091,7 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
}
|
||||
```
|
||||
|
||||
In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
In a [Jellyfin](/configuration/sources/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
|
||||
```json5 title="jellyfin.json"
|
||||
[
|
||||
@@ -843,9 +1122,9 @@ In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?co
|
||||
|
||||
* Musicbrainz Stage configured with
|
||||
* [Sensible Defaults](#sensible-default)
|
||||
* Fallback search with [Artist Extraction](#retrying-search)
|
||||
* Fallback search with [Artist Extraction](#search-methods)
|
||||
* [Default Native Stage](/configuration/transforms/native/#default-stage)
|
||||
* Uses [Flow Control](/configurations/transforms#flow-control) to run Native only if there are no Musicbrainz matches
|
||||
* Uses [Flow Control](/configuration/transforms#flow-control) to run Native only if there are no Musicbrainz matches
|
||||
|
||||
<details>
|
||||
|
||||
@@ -871,14 +1150,15 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
"releaseStatusPriority": ["official"],
|
||||
"releaseGroupPrimaryTypePriority": ["album", "single", "ep"],
|
||||
"releaseCountryPriority": ["XW"],
|
||||
"fallbackArtistSearch": "native",
|
||||
"searchArtistMethod": "native",
|
||||
"searchOrder": ["isrc","basic", "artist"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
In a [Jellyfin](/configuration/sources/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
|
||||
```json5 title="jellyfin.json"
|
||||
[
|
||||
@@ -905,4 +1185,208 @@ In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?co
|
||||
}
|
||||
]
|
||||
```
|
||||
</details>
|
||||
|
||||
### Different Search Methods Based on Source (Last.fm and Spotify) {#override-stage-configuration-example}
|
||||
|
||||
[Last.fm has unreliable MBIDs](#search-considerations) and Spotify provides an [ISRC](https://musicbrainz.org/doc/ISRC) and consistent formatting.
|
||||
|
||||
* For Spotify, we want to rely on ID [searches](#search-methods) and matches that are [close to original scrobble text](#field-scoring)
|
||||
* For Last.fm, we want to use only text searches and take whatever correction is given to us
|
||||
|
||||
Use [**Per Component Overrides**](#rules-and-hooks) with the default (or [sensible](#sensible-default)) [Stage Configuration](#stage-configuration) to specify different search behavior for each Source.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Stage Configuration
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
// ...
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
"contact": "contact@mydomain.com"
|
||||
}
|
||||
],
|
||||
"defaults": {} // maybe use sensible?
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json5 title="lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "MyLFM",
|
||||
"configureAs": "source"
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"searchArtistMethod": "native"
|
||||
// does not use any ID searches
|
||||
// fallback to more aggressive/broad searches
|
||||
"searchOrder": ["basic", "artist", "freetext"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="spotify.json"
|
||||
[
|
||||
{
|
||||
"name": "MySpotify",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
// use ISRC as first search
|
||||
"searchOrder": ["isrc", "basic"]
|
||||
// bias matches towards similarity to original data
|
||||
"albumWeight": 0.33,
|
||||
"titleWeight": 0.33,
|
||||
"artistWeight": 0.33
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Use Metadata for Discord Album Art
|
||||
|
||||
The [Discord](/configuration/clients/discord) Client can fetch album art images from [Cover Art Archive](http://coverartarchive.org/) if the Scrobble data contains a Release MBID. Use the Musicbrainz Stage on the `preCompare` hook for your Discord client so that missing Musicbrainz IDs are populated for this functionality.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
Using the [ENV Configuration](#env-configuration) from above, add these fields to your docker compose `environment:`
|
||||
|
||||
```yaml
|
||||
- MB_CONTACT=contact@mydomain.com
|
||||
- MB_PRESETS=id,native,sensible
|
||||
- DISCORD_TRANSFORMS=musicbrainz
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
// ...
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
"contact": "contact@mydomain.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaults": {
|
||||
// sensible defaults
|
||||
"releaseStatusPriority": ["official"],
|
||||
"releaseGroupPrimaryTypePriority": ["album", "single", "ep"],
|
||||
"releaseCountryPriority": ["XW"],
|
||||
"searchArtistMethod": "native",
|
||||
"searchOrder": ["isrc","basic", "artist"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In your [Discord](/configuration/clients/discord) [File Config](/configuration?configType=file#configuration-types):
|
||||
|
||||
```json5 title="discord.json"
|
||||
[
|
||||
{
|
||||
"name": "MS",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
// ...
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
"contact": "contact@mydomain.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaults": {
|
||||
// sensible defaults
|
||||
"releaseStatusPriority": ["official"],
|
||||
"releaseGroupPrimaryTypePriority": ["album", "single", "ep"],
|
||||
"releaseCountryPriority": ["XW"],
|
||||
"searchArtistMethod": "native",
|
||||
"searchOrder": ["isrc","basic", "artist"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"name": "MS",
|
||||
"type": "discord",
|
||||
"data": { /* ... */},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Native Stage
|
||||
description: Enhancing Scrobbles with Built-in Parsing
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -75,7 +76,7 @@ Config => extracted artists
|
||||
|
||||
### Rules
|
||||
|
||||
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition.](/configuration/transforms#conditional-moditication):
|
||||
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition.](/configuration/transforms#conditional-modification):
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -96,7 +97,7 @@ If a rule is not present then multi-scrobbler defaults it to `true`.
|
||||
|
||||
For your convenience, if you do not define any Native [Stage Configurations](/configuration/transforms#configuring-stages) then multi-scrobbler automatically builds one using the defaults shown in [configuration](#configuration).
|
||||
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#modification-stage).
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#stage).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -132,7 +133,7 @@ Use ENV `*_TRANSFORMS=native` on each Source/Client you wish to apply this stage
|
||||
|
||||
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example
|
||||
|
||||
* All [Subsonic Source](http://localhost:3000/docs/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* All [Subsonic Source](/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* Use `SUBSONIC_TRANSFORMS=native`
|
||||
|
||||
<details>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
sidebar_position: 3
|
||||
title: Enhancing Scrobbles
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -36,7 +36,7 @@ Scrobble **Clients** listen for discovered Plays from Sources, then...
|
||||
* The Client **compares** this Play to all the other scrobbles it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
|
||||
* The Client **scrobbles** the Play downstream to the scrobble service and adds it as a Scrobble it has seen already
|
||||
|
||||
## Lifecyle Hooks
|
||||
## Lifecycle Hooks
|
||||
|
||||
You'll notice there is a pattern above that looks like this:
|
||||
|
||||
@@ -84,13 +84,14 @@ For **Clients**:
|
||||
* `compare` - temporarily modify Play data when it is being compared to see if it was already scrobbled
|
||||
* `postCompare` - modify Play data before scrobbling it to downstream service and adding to already seen scrobbles
|
||||
|
||||
:::tip
|
||||
<DetailsAdmo type="tip" summary="Transformed Scrobbles Are Permanently Modified">
|
||||
|
||||
Keep in mind that modifying Scrobble/Play data earlier in the lifecycle will affect that data at all times later in the lifecycle (except when using the **compare** hook).
|
||||
|
||||
For example, to modify the track so it's the same anywhere it is processed in multi-scrobbler you only need to modify it in the **Source's** `preCompare` hook because all later processes will receive the data with the modified track.
|
||||
|
||||
:::
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
:::tip[TLDR]
|
||||
|
||||
@@ -193,7 +194,7 @@ In a [Subsonic](/configuration/sources/subsonic) [File Config](/configuration?co
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "native"
|
||||
// when "name" is not defined, uses first found "native" transformer
|
||||
"name": "MyNativeTransformer" // when "name" is not defined, uses first found "native" transformer
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -363,7 +364,7 @@ The actual value of each property may be different for each Stage. Check the doc
|
||||
|
||||
Generically, though, each property may be some value **or** an object combining a [`when` condition](#conditional-modification) and that value.
|
||||
|
||||
If none of the properties are specified in the stage then it's assumed all transformed data should be used.
|
||||
**If none of the properties are specified in the stage then it's assumed all transformed data should be used.**
|
||||
|
||||
:::note
|
||||
|
||||
@@ -375,7 +376,7 @@ Specifying these Rules is **not** the same as [configuring the Stage](#configuri
|
||||
|
||||
Stages may be optionally configured to **continue** to the next Stage or **stop** (end early) all subsequent Stages, based on the outcome of the currently running Stage.
|
||||
|
||||
These three properties can be added to the [Modification Stage](#modification-stage) data, alongside [Rules](#rules-for-play-data):
|
||||
These three properties can be added to the [Modification Stage](#stage) data, alongside [Rules](#stage-rules):
|
||||
|
||||
* `onSuccess` (default `continue`) - If the Stage successfully finishes processing
|
||||
* `onFailure` (default `stop`) - If the Stage encounters an error while processing, or otherwise fails to achieve the transformation result
|
||||
@@ -584,6 +585,47 @@ The output shows the diff between the previous stage (or original Play) and the
|
||||
|
||||
</details>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Caching
|
||||
|
||||
MS uses [caching](/configuration/#caching) to reduce the number of API calls needed for stages like [Musicbrainz](/configuration/transforms/musicbrainz) and to speed up all transforms by caching steps and results. However, the default caching strategy uses a small cache size and very short [TTLs](https://en.wikipedia.org/wiki/Time_to_live) because it is *in-memory*.
|
||||
|
||||
**If you are using any Transform stages you should configure [secondary caching](/configuration#secondary-caching)** to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Add valkey service to your [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker) and configure MS to use it for metadata:
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
environment:
|
||||
# ...
|
||||
// highlight-start
|
||||
- CACHE_METADATA=valkey
|
||||
- CACHE_METADATA_CONN=redis://valkey:6379
|
||||
// highlight-end
|
||||
# ...
|
||||
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- valkeydata:/data
|
||||
|
||||
volumes:
|
||||
valkeydata:
|
||||
driver: local
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
See **Examples** sections in specific Stage docs (also in the sidebar):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: User Stage
|
||||
description: Enhancing Scrobbles with Regex Parsing
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -16,7 +17,7 @@ The user [Stage `type`](/configuration/transforms#stage) is `user`.
|
||||
|
||||
## Configuration
|
||||
|
||||
All [Stage Configuration](/configuration/transforms#configuring-stage) is done using [**search-and-replace** expressions](#search-and-replace-expression) inside individual [rules](#rules). Default configuration for all Rules can still be done using Stage `defaults`.
|
||||
All [Stage Configuration](/configuration/transforms#configuring-stages) is done using [**search-and-replace** expressions](#search-and-replace-expression) inside individual [rules](#rules). Default configuration for all Rules can still be done using Stage `defaults`.
|
||||
|
||||
<details>
|
||||
|
||||
@@ -46,9 +47,9 @@ All [Stage Configuration](/configuration/transforms#configuring-stage) is done u
|
||||
|
||||
#### Default Stage
|
||||
|
||||
For your convenience, if you do not define any User [Stage Configurations](/configuration/transforms#configuration) then multi-scrobbler automatically builds an empty one for you.
|
||||
For your convenience, if you do not define any User [Stage Configurations](/configuration/transforms#configuring-stages) then multi-scrobbler automatically builds an empty one for you.
|
||||
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#modification-stage).
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#stage).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -126,7 +127,7 @@ The `replace` property uses javascript's [`replace()` function and so can use an
|
||||
|
||||
### Rules
|
||||
|
||||
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expressions):
|
||||
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expression):
|
||||
|
||||
```json5 title="lastfm.json"
|
||||
[
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 3
|
||||
title: Client Development/Tutorial
|
||||
---
|
||||
|
||||
To do...
|
||||
@@ -3,7 +3,7 @@ toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 1
|
||||
title: Common Development
|
||||
description: Start here for MS development
|
||||
description: Common Development Knowledge
|
||||
---
|
||||
|
||||
# Development
|
||||
@@ -177,4 +177,16 @@ For more refer to the TS documentation for `PlayObject` or [`AmbPlayObject`](htt
|
||||
## Creating Clients and Sources
|
||||
|
||||
* [Source Development and Tutorial](dev-source.md)
|
||||
* [Client Development and Tutorial](dev-client.md)
|
||||
* Client Development and Tutorial (TBD)
|
||||
|
||||
## Profiling
|
||||
|
||||
Run tsx with inspect args
|
||||
|
||||
```
|
||||
NODE_ENV=production node node_modules/.bin/tsx --inspect --heap-prof src/backend/index.ts
|
||||
```
|
||||
|
||||
Use `chrome://inspect` from a chromium-based browser and attach to the running process, usually `localhost:9229`
|
||||
|
||||
From the opened DevTools window use Performance or Memory to profile the running process.
|
||||
@@ -3,6 +3,7 @@ toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 2
|
||||
title: Source Development/Tutorial
|
||||
description: Developing a new Source
|
||||
---
|
||||
|
||||
This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review [Common Development](dev-common.md#common-development).
|
||||
|
||||
@@ -3,7 +3,7 @@ toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 4
|
||||
title: Flatpak
|
||||
description: Building Flatpak App locally
|
||||
description: Building Flatpak App
|
||||
---
|
||||
|
||||
## Install Requirements
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Help
|
||||
---
|
||||
|
||||
## Getting Useful Data
|
||||
|
||||
### Logs
|
||||
|
||||
Multi-scrobbeler has a real-time log shown in the dashboard. These are the same logs you would find in docker logs. In the dashboard, logs can be filtered by LEVELS to give you a better view of errors/warnings.
|
||||
|
||||
When creating an issue please include all logs at the lowest level possible (DEBUG) to help provide context for your issue. You should try to include logs that occurred *before* and *after* your issue/error occurs as this can help determine if MS was behaving normally.
|
||||
|
||||
### Debug Mode
|
||||
|
||||
If possible, turn on [**Debug Mode**](/configuration#debug-mode) and replicate your issue. Debug Mode enables *much more* logging that can reveal additional data for troubleshooting.
|
||||
|
||||
This mode will make your logs very noisy, though, so it's recommended to turn it off after collecting logs.
|
||||
|
||||
### Copy Play Debug Data
|
||||
|
||||
From the dashboard, each Source/Client has a few details page links:
|
||||
|
||||
<img src={require('/img/details.jpg').default} height="400"/>
|
||||
|
||||
Opening the details page for the Source/Client reveals a lists all of the Plays that were discovered (Sources) or scrobbled (Clients).
|
||||
|
||||
Click on the Debug icon to copy debugging data relevant to that Play. This data should be included in any issue you create.
|
||||
|
||||
<img src={require('/img/debug_button.jpg').default} height="400"/>
|
||||
|
||||
To include this data in an issue either paste the copied data into a text file and save as `debugdata.json`, attaching to the issue.
|
||||
|
||||
Or directly paste the copied data into an issue/comment using [**fenced code blocks**](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#fenced-code-blocks) markdown to preserve the structure like:
|
||||
|
||||
````
|
||||
This is the contents of my comment/issue text on github.
|
||||
|
||||
I am explaining my issue here. Here is the debug data:
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"aProperty: 123,
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Thank you for your time.
|
||||
|
||||
````
|
||||
|
||||
## Finding Help
|
||||
|
||||
### FAQ
|
||||
|
||||
Check the [FAQ](/FAQ) to see if your issue is a known issue or a commonly-experienced issue with Multi-Scrobbler. The FAQ also includes fixes or ways to help troubleshoot these common issues.
|
||||
|
||||
### Existing Issues
|
||||
|
||||
On the github repository for Multi-Scrobbler, search [existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions](https://github.com/FoxxMD/multi-scrobbler/discussions) to see if anyone else has had this issue and if it has already been solved.
|
||||
|
||||
## Reporting an Issue
|
||||
|
||||
Before reporting an issue please consider:
|
||||
|
||||
* If this a how-do-I question, troublehooting, or generally **not** a bug/error with multi-scrobbler please consider [**starting a discussion**](https://github.com/FoxxMD/multi-scrobbler/discussions) instead of opening an issue
|
||||
* If you want to request new functionality use the [**Feature Request issue** template](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=02-feature-request.yml)
|
||||
* If you want to request a new Source/Client implementation use the [**New Source/Client issue** template](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=03-new-source-client.yml)
|
||||
|
||||
If none of the above apply to you then create a new [**bug report**](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=01-bug-report.yml). Please include all [logs](#logs) and [play debug data](#copy-play-debug-data) relevant to your issue. The more logs the better. If possible, replicate the issue with [**debug mode** turned on](#debug-mode).
|
||||
+18
-4
@@ -10,9 +10,9 @@ sidebar_position: 1
|
||||
|
||||
<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), [ListenBrainz](https://listenbrainz.org), and [Koito](https://koito.io/).
|
||||
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
|
||||
|
||||
* Supports scrobbling from many [**Sources**](/configuration/sources)
|
||||
* Supports monitoring activity from many [**Sources**](/configuration/sources)
|
||||
* [Azuracast](/configuration/sources/azuracast)
|
||||
* [Deezer](/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](/configuration/sources/google-cast)
|
||||
@@ -22,6 +22,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
|
||||
* [Kodi](/configuration/sources/kodi)
|
||||
* [Koito](/configuration/sources/koito-source)
|
||||
* [Last.fm](/configuration/sources/lastfm-source)
|
||||
* [Libre.fm](/configuration/sources/librefm-source)
|
||||
* [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
|
||||
* [ListenBrainz](/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
|
||||
@@ -30,18 +31,23 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
|
||||
* [MPD (Music Player Daemon)](/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](/configuration/sources/mpris)
|
||||
* [Musikcube](/configuration/sources/musikcube)
|
||||
* [Music Assistant](/configuration/sources/listenbrainz-endpoint#setup-for-popular-apps)
|
||||
* [Plex](/configuration/sources/plex)
|
||||
* [Rocksky](/configuration/sources/rocksky-source)
|
||||
* [Sonos](/configuration/sources/sonos)
|
||||
* [Spotify](/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](/configuration/sources/webscrobbler)
|
||||
* [VLC](/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](/configuration/sources/yamaha-musiccast)
|
||||
* [Yandex Music](/configuration/sources/yandex-music)
|
||||
* [Youtube Music](/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](/configuration/clients)
|
||||
* [Discord](/configuration/clients/discord) (Now Playing)
|
||||
* [Koito](/configuration/clients/koito)
|
||||
* [Last.fm](/configuration/clients/lastfm)
|
||||
* [Libre.fm](/configuration/clients/librefm)
|
||||
* [ListenBrainz](/configuration/clients/listenbrainz)
|
||||
* [Maloja](/configuration/clients/maloja)
|
||||
* [Rocksky](/configuration/clients/rocksky)
|
||||
@@ -78,9 +84,17 @@ Use the **Last.fm** tab in the [Quickstart Sources](/quickstart/#setup-sources)
|
||||
|
||||
You set up [configurations](/configuration) for one or more [**Sources**](/configuration/sources) and one or more [**Clients**](/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Diagram</summary>
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/ms-visual.png').default}/>
|
||||
|
||||
</details>
|
||||
|
||||
### Source
|
||||
|
||||
A [**Source**](/configuration/sources) is a data source that contains information about music you are playing or have listened to, like a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
A [**Source**](/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
@@ -90,7 +104,7 @@ Source configurations consist of:
|
||||
|
||||
### Client
|
||||
|
||||
A [**Client**](/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
A [**Client**](/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 'Overview'
|
||||
title: Overview
|
||||
description: Installation
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
@@ -37,81 +38,39 @@ Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
|
||||
|
||||
:::
|
||||
|
||||
Or use the example `docker-compose.yml`, found in the repo, after modifying it to fit your configuration:
|
||||
### Recommended Settings
|
||||
|
||||
<details>
|
||||
<Tabs groupId="dockerSetting" queryString>
|
||||
<TabItem value="storage" label="Storage">
|
||||
You **should** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
|
||||
|
||||
<summary>`docker-compose.yml`</summary>
|
||||
|
||||
<CodeBlock language="yaml">{ComposeStack}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
Recommended configuration steps for docker or docker-compose usage:
|
||||
|
||||
#### Storage
|
||||
|
||||
You should bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
[Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount):
|
||||
```shell
|
||||
docker run ... -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
[Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3):
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
#### Networking
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
// highlight-start
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="networking" label="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 (Webscrobbler, LFM/LZ Endpoints). The default container port is `9078`.
|
||||
|
||||
<details>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
// highlight-start
|
||||
ports:
|
||||
- "9078:9078"
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
[Docker `run` publish options](https://docs.docker.com/engine/reference/commandline/run/#publish):
|
||||
```shell
|
||||
docker run ... -p 9078:9078 foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
ports:
|
||||
- "9078:9078"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
##### Base URL
|
||||
|
||||
Optionally, when
|
||||
</TabItem>
|
||||
<TabItem value="baseUrl" label="Base URL">
|
||||
**Optionally**, when
|
||||
|
||||
* using a [Source or Client](../configuration/configuration.mdx) that has a "Redirect URI" that you have not explicitly defined
|
||||
* and
|
||||
@@ -120,47 +79,87 @@ Optionally, when
|
||||
|
||||
set the [Base URL](../configuration/configuration.mdx#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
// highlight-start
|
||||
- BASE_URL=http://hostMachineIP
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
```shell
|
||||
docker run ... -e BASE_URL="http://hostMachineIP" foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
|
||||
</TabItem>
|
||||
<TabItem value="tz" label="Timezone">
|
||||
Set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ`.
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
- BASE_URL=http://hostMachineIP
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
# Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
// highlight-start
|
||||
- TZ=America/New_York
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Other
|
||||
|
||||
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
|
||||
|
||||
#### Linux Host
|
||||
|
||||
::::info
|
||||
|
||||
If you are running this container with **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)
|
||||
</TabItem>
|
||||
<TabItem value="linuxHost" label="Linux Host">
|
||||
If you are running this container with **Docker** on a **Linux Host** you **should** 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
|
||||
</TabItem>
|
||||
<TabItem value="ro" label="Read Only">
|
||||
MS can be hardended by setting the file system as read only with [`read_only`](https://docs.docker.com/reference/compose-file/services/#read_only), with a few exceptions:
|
||||
|
||||
::::
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
// highlight-start
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run:exec
|
||||
- /tmp:noexec
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="caching" label="Caching">
|
||||
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration#secondary-caching) to take advantage of faster performance and reduced memory usage.
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
environment:
|
||||
# ...
|
||||
// highlight-start
|
||||
CACHE_VALKEY=redis://valkey:6379
|
||||
// highlight-end
|
||||
# ...
|
||||
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- valkeydata:/data
|
||||
|
||||
volumes:
|
||||
valkeydata:
|
||||
driver: local
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Docker Usage Example
|
||||
|
||||
@@ -178,59 +177,53 @@ The example scenario:
|
||||
* Docker container located on a different IP (`192.168.0.100`) so use [Base URL](/configuration#base-url)
|
||||
* Config/data directory on host machine in a directory next to `docker-compose.yml`
|
||||
* Linux uid/gid is `1000:1000`
|
||||
* Optional caching using valkey (after uncommenting)
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
```bash
|
||||
docker run --name multi-scrobbler -e "JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c" -e "JELLYFIN_URL=192.168.0.101:8096" -e "JELLYFIN_USER=MyUser" -e "BASE_URL=192.168.0.100" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /home/myUser/ms:/config foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
```bash
|
||||
docker run --name multi-scrobbler -e "PUID=1000" -e "PGID=1000" -e "BASE_URL=192.168.0.100" -p 9078:9078 -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
|
||||
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
|
||||
- JELLYFIN_URL=192.168.0.101:8096
|
||||
- JELLYFIN_USER=MyUser
|
||||
- BASE_URL=http://192.168.0.100:9078
|
||||
- MALOJA_URL=http://domain.tld:42010
|
||||
- MALOJA_API_KEY=1234
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
ports:
|
||||
- "9078:9078"
|
||||
restart: unless-stopped
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
|
||||
- JELLYFIN_URL=192.168.0.101:8096
|
||||
- JELLYFIN_USER=MyUser
|
||||
- BASE_URL=http://192.168.0.100:9078
|
||||
- MALOJA_URL=http://domain.tld:42010
|
||||
- MALOJA_API_KEY=1234
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
# uncomment along with valkey service/volume below for better caching
|
||||
#- CACHE_METADATA=valkey
|
||||
#- CACHE_METADATA_CONN=redis://valkey:6379
|
||||
volumes:
|
||||
- ./config:/config
|
||||
ports:
|
||||
- 9078:9078
|
||||
restart: unless-stopped
|
||||
|
||||
#valkey:
|
||||
# image: valkey/valkey
|
||||
# volumes:
|
||||
# - valkeydata:/data
|
||||
|
||||
#volumes:
|
||||
# valkeydata:
|
||||
# driver: local
|
||||
```
|
||||
## Local Installation
|
||||
|
||||
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
|
||||
|
||||
### Nodejs
|
||||
|
||||
Clone this repository somewhere and then install from the working directory
|
||||
Clone this repository somewhere using the latest [git tag](/updating#git) and then install from the working directory.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/FoxxMD/multi-scrobbler.git .
|
||||
git clone --branch <LATEST_RELEASE_TAG> https://github.com/FoxxMD/multi-scrobbler.git
|
||||
cd multi-scrobbler
|
||||
nvm use # optional, to set correct Node version
|
||||
npm install
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 'As a Service'
|
||||
title: As a Service
|
||||
description: Local Install as a Service
|
||||
---
|
||||
|
||||
If you have multi-scrobbler installed [locally](installation.mdx#nodejs) you can enable it to run as a background service when you login.
|
||||
|
||||
+56
-62
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'Quickstart'
|
||||
title: Quickstart
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ This guide will get you up and running with multi-scrobbler using [Docker](https
|
||||
|
||||
* the dashboard served on port `9078` of a host machine which has an IP of `192.168.0.100`
|
||||
* data saved to the same directory as the `docker-compose.yml` file
|
||||
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify), [Jellyfin](/configuration/sources/jellyfin), Navidrome, [Plex](/configuration/sources/plex), and/or [Last.fm](/configurations/sources/lastfm) for listening activity
|
||||
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify), [Jellyfin](/configuration/sources/jellyfin), Navidrome, [Plex](/configuration/sources/plex), and/or [Last.fm](/configuration/sources/lastfm-source) for listening activity
|
||||
* multi-scrobbler scrobbling to [Lastfm](/configuration/clients/lastfm), [Maloja](/configuration/clients/maloja), and/or [Koito](/configuration/clients/koito)
|
||||
|
||||
:::note
|
||||
@@ -126,20 +126,12 @@ services:
|
||||
|
||||
After account creation make note of the **Api Key** and **Secret** that are displayed.
|
||||
|
||||
Create a new [File](/configuration?configType=file#configuration-types) named `lastfm.json` in the `./config` directory that will be used with your [`docker-compose.yml` stack](#create-docker-compose-file) and copy the below contents, replacing placeholders with the data you got from Last.fm API account creation:
|
||||
Add these values to the `environment` section in the [`docker-compose.yml` you created.](#create-docker-compose-file)
|
||||
|
||||
```json title="~/msData/config/lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myLastFmSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"apiKey": "MY_API_KEY_PLACEDHOLDER",
|
||||
"secret": "MY_SECRET_PLACEHOLDER",
|
||||
"redirectUri": "http://192.168.0.100:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
- SOURCE_LASTFM_API_KEY=MY_API_KEY_PLACEDHOLDER
|
||||
- SOURCE_LASTFM_SECRET=MY_SECRET_PLACEHOLDER
|
||||
- SOURCE_LASTFM_REDIRECT_URI=http://192.168.0.100:9078/lastfm/callback
|
||||
```
|
||||
|
||||
Later, after [starting multi-scrobbler](#start-multi-scrobbler), visit the dashboard at `http://192.168.0.100:9078` and click **(Re)authenticate** on the **Last.fm (Source)** card to authorize multi-scrobbler to use your account. Monitoring will begin automatically after authorization is complete.
|
||||
@@ -170,10 +162,11 @@ Add these value to the `environment` section in the [`docker-compose.yml` you cr
|
||||
# an email or personal website musicbrainz can use to contact you
|
||||
- MB_CONTACT=myCoolName@emailProvider.com
|
||||
# how musicbrainz searches and selects matches
|
||||
- MB_PRESETS=sensible,native
|
||||
# isrc => then tries basic text fields => then tries extracted artists
|
||||
- MB_PRESETS=default,sensible,native
|
||||
```
|
||||
|
||||
**Presets** are found [here](http://localhost:3000/docs/configuration/transforms/musicbrainz/#env-configuration).
|
||||
**Presets** are found [here](/configuration/transforms/musicbrainz/#env-configuration).
|
||||
|
||||
<h3>Enable Musicbrainz For A Source</h3>
|
||||
|
||||
@@ -239,6 +232,50 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
|
||||
[**Clients**](/configuration/clients) are services that store scrobbles. Multi-scrobbler will scrobble all listening activity from the **Sources** you configured to all **Clients** you configure here.
|
||||
|
||||
<Tabs groupId="qsClient">
|
||||
<TabItem value="koito" label="Koito">
|
||||
Setup a [Koito server](https://koito.io/guides/installation/) if you have not already done this.
|
||||
|
||||
* [Create (or get) an API Key](https://koito.io/guides/scrobbler/)
|
||||
* From your Koito server dashboard navigate to **Settings** (bottom-left icon) => **API Keys**
|
||||
* Create a new Key (or use an existing) and Copy the key using the copy icon to the right of the key
|
||||
* EX `pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL`
|
||||
* Take note of your Koito username (used below as `myUser`)
|
||||
* Determine your Koito URL
|
||||
* This is the URL you use to access your Koito dashboard OR whatever URL is accessible from your multi-scrobbler instance.
|
||||
* EX dashboard at `http://192.168.0.100:4110` => replace `myURL` below with `http://192.168.0.100:4110`
|
||||
* [More info on the URL to use is the full docs](/configuration/clients/koito)
|
||||
|
||||
Add/Substitute values above into these examples values and add them to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
|
||||
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
- KOITO_TOKEN=myToken
|
||||
- KOITO_USER=myUser
|
||||
- KOITO_URL=myURL
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLOWED_HOSTS` set correctly in your Koito environment/container. Ensure it is set to the IP address or domain name that you use to access Koito with e.g. `koito.mydomain.com`.
|
||||
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="lastfm" label="Last.fm">
|
||||
[Register for an API account at Last.fm.](https://www.last.fm/api/account/create)
|
||||
|
||||
Use the following for **Callback URL**:
|
||||
|
||||
```
|
||||
http://192.168.0.100:9078/lastfm/callback
|
||||
```
|
||||
|
||||
After account creation use the displayed information and add these values to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
|
||||
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
- LASTFM_API_KEY=myApiKey
|
||||
- LASTFM_SECRET=myApiSecret
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="maloja" label="Maloja">
|
||||
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
|
||||
|
||||
@@ -271,57 +308,13 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
|
||||
- MALOJA_API_KEY=myApiKey
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="lastfm" label="Last.fm">
|
||||
[Register for an API account at Last.fm.](https://www.last.fm/api/account/create)
|
||||
|
||||
Use the following for **Callback URL**:
|
||||
|
||||
```
|
||||
http://192.168.0.100:9078/lastfm/callback
|
||||
```
|
||||
|
||||
After account creation use the displayed information and add these values to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
|
||||
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
- LASTFM_API_KEY=myApiKey
|
||||
- LASTFM_SECRET=myApiSecret
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="koito" label="Koito">
|
||||
Setup a [Koito server](https://koito.io/guides/installation/) if you have not already done this.
|
||||
|
||||
* [Create (or get) an API Key](https://koito.io/guides/scrobbler/)
|
||||
* From your Koito server dashboard navigate to **Settings** (bottom-left icon) => **API Keys**
|
||||
* Create a new Key (or use an existing) and Copy the key using the copy icon to the right of the key
|
||||
* EX `pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL`
|
||||
* Take note of your Koito username (used below as `myUser`)
|
||||
* Determine your Koito URL
|
||||
* This is the URL you use to access your Koito dashboard OR whatever URL is accessible from your multi-scrobbler instance.
|
||||
* EX dashboard at `http://192.168.0.100:4110` => replace `myURL` below with `http://192.168.0.100:4110`
|
||||
* [More info on the URL to use is the full docs](/configuration/clients/koito)
|
||||
|
||||
Add/Substitute values above into these examples values and add them to the `environment` section in the [`docker-compose.yml` you created for multi-scrobbler earlier.](#create-docker-compose-file)
|
||||
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
- KOTIO_TOKEN=myToken
|
||||
- KOTIO_USER=myUser
|
||||
- KOITO_URL=myURL
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
|
||||
|
||||
:::
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Start Multi-Scrobbler
|
||||
|
||||
:::tip
|
||||
|
||||
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation#linux-host)
|
||||
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation?dockerSetting=linuxHost#recommended-settings)
|
||||
|
||||
:::
|
||||
|
||||
@@ -343,6 +336,7 @@ Visit `http://192.168.0.100:9078` to see the dashboard where
|
||||
## Next Steps
|
||||
|
||||
* See more advanced docker options as well as other install methods in the [**Installation**](/installation#docker) docs
|
||||
* Setup [secondary caching](/configuration#secondary-caching) with [valkey](/installation?dockerSetting=caching#recommended-settings) for increased performance and reduced memory usage
|
||||
* Review the [**Configuration**](/configuration) docs
|
||||
* Learn about how to configure multi-scrobbler using files for more complicated Source/Client scenarios
|
||||
* See all available [**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) alongside configuration examples
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"label": "Updating",
|
||||
"position": 4,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Way to update Multi-Scrobbler"
|
||||
"type": "doc",
|
||||
"id": "updating/updating"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user