Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e5105b1bb2 | ||
|
|
e998a6d95a | ||
|
|
98f045a70f | ||
|
|
419f7cf657 | ||
|
|
9bef0791c4 | ||
|
|
15d667f653 | ||
|
|
939ae6f892 | ||
|
|
0cd36e57d6 | ||
|
|
5acb92e696 | ||
|
|
10a90c7d48 | ||
|
|
f0c5686b4a | ||
|
|
cae914b21b | ||
|
|
344083a3a9 | ||
|
|
36ed40f628 | ||
|
|
c650c4a1e0 | ||
|
|
f50c3b40d5 | ||
|
|
c1318f7efc | ||
|
|
615947b4d2 | ||
|
|
7c0a9fcd9a | ||
|
|
72572c9397 | ||
|
|
0e3874f35f | ||
|
|
db8a289517 | ||
|
|
698f0b91fc | ||
|
|
fb08a63e6f | ||
|
|
a320aba4da | ||
|
|
088e26ce4f | ||
|
|
e273159ecb | ||
|
|
f0ac53ffff | ||
|
|
4a364ce766 | ||
|
|
6c4cd27aa0 | ||
|
|
861578a417 | ||
|
|
7f62aa79d0 | ||
|
|
7223d51690 | ||
|
|
7eabd7f4df | ||
|
|
88b81dcd12 | ||
|
|
5d6b3d55cc | ||
|
|
a5f4f1be6e | ||
|
|
ac329cf538 | ||
|
|
fafd6a09a0 | ||
|
|
759029107a | ||
|
|
802484c1bb | ||
|
|
4dfb485c87 | ||
|
|
57c1812547 | ||
|
|
c261699f3e | ||
|
|
039deca865 | ||
|
|
f8a18dd1ee | ||
|
|
de93fe166b | ||
|
|
1956484295 | ||
|
|
f76604aca5 | ||
|
|
1f01d3e7be | ||
|
|
eae1a84832 | ||
|
|
17baebdbf2 | ||
|
|
023bd74f12 | ||
|
|
021ce4d03d | ||
|
|
761c0f79fb | ||
|
|
5e63936dda | ||
|
|
a7134e2dae | ||
|
|
1289c53941 | ||
|
|
b4a36a42cd | ||
|
|
ea4d9b3764 | ||
|
|
5c2819a70c | ||
|
|
83eb7a1b65 | ||
|
|
8976bb99f5 | ||
|
|
40b5ef594a | ||
|
|
5c6e2b8cad | ||
|
|
8083696469 | ||
|
|
c07b572abb | ||
|
|
0972a41962 | ||
|
|
aeae449abf | ||
|
|
fa6cc13c86 | ||
|
|
3e57b006d2 | ||
|
|
949c1f9176 | ||
|
|
2bcd0aa858 | ||
|
|
8ba8a054a6 | ||
|
|
08301d8664 | ||
|
|
c36ffa5ffd | ||
|
|
0d02215f71 | ||
|
|
dadea4b90c | ||
|
|
9c89142c47 | ||
|
|
e055e0bd53 | ||
|
|
14c8730435 | ||
|
|
1410e3a952 | ||
|
|
1e558de5e1 | ||
|
|
dab2414a92 | ||
|
|
03bb1e0552 | ||
|
|
1eab06c51d | ||
|
|
d217c91a30 | ||
|
|
b09cbb4bb5 | ||
|
|
d6d0ce515f | ||
|
|
db34c33c42 | ||
|
|
2024454f5b | ||
|
|
3718def8b4 | ||
|
|
094071b0ba | ||
|
|
ca8d397653 | ||
|
|
6ac44397d0 | ||
|
|
d6c4d7cf25 | ||
|
|
04ae651b15 | ||
|
|
ef6e2b0f88 | ||
|
|
b7271ea4c2 | ||
|
|
b5e835a0c6 | ||
|
|
a7df5a8d30 | ||
|
|
883050e0b4 | ||
|
|
b6417ed780 | ||
|
|
affb16fc0e | ||
|
|
56409dd1d7 | ||
|
|
1af0193636 | ||
|
|
53e766318c | ||
|
|
c5269791af | ||
|
|
fb5ffd053d | ||
|
|
0c04e7b79f | ||
|
|
dc5fab6f9a | ||
|
|
128c48947e | ||
|
|
4cab02e88d | ||
|
|
a360687264 | ||
|
|
6a677b6fe1 | ||
|
|
5c418d5618 | ||
|
|
e6e782dec8 | ||
|
|
ab7d20d7d3 | ||
|
|
ec4c2cdd56 | ||
|
|
88d3e13b52 | ||
|
|
7bf70d1973 | ||
|
|
8eae2dc69d | ||
|
|
aa44a08e9e | ||
|
|
6071abca49 | ||
|
|
86a719ba4a | ||
|
|
39b6f6e602 | ||
|
|
bfce5dcc54 | ||
|
|
18537e4e08 | ||
|
|
30db8712fc | ||
|
|
1701a5e5f1 | ||
|
|
406c39b6b2 | ||
|
|
ce7ad36db4 | ||
|
|
fa4bf5ab10 | ||
|
|
83d9af2aea | ||
|
|
433761525c | ||
|
|
f7a7a36d8b | ||
|
|
7e375f1531 | ||
|
|
357397f225 | ||
|
|
78bf507226 | ||
|
|
f892851b60 | ||
|
|
4244ddb8c2 | ||
|
|
3e67e9189f | ||
|
|
de1cbb2aae | ||
|
|
db17455b2e | ||
|
|
a7729fbfa6 | ||
|
|
771fdc0686 | ||
|
|
8cad2d4efd | ||
|
|
a094a1aa65 | ||
|
|
f2d45c5622 | ||
|
|
a429bccf07 | ||
|
|
04d950980d | ||
|
|
7ec90b97d6 | ||
|
|
a4f989c5e4 | ||
|
|
da4fbd2f27 | ||
|
|
b9e6f32500 | ||
|
|
5c5c653c3b | ||
|
|
a9e7a4bf16 | ||
|
|
1b4baafa67 | ||
|
|
794ef08aa2 | ||
|
|
fa6ed323ff | ||
|
|
8110e8e9fa | ||
|
|
12a6eec740 | ||
|
|
472021dd2c | ||
|
|
ebb74d1b5c | ||
|
|
ea23a89101 | ||
|
|
9062619d38 | ||
|
|
3fb64be54f | ||
|
|
3e083b19f7 | ||
|
|
e197c71bbb | ||
|
|
7b36d95dfc | ||
|
|
0427b6e945 | ||
|
|
326d9e58d7 | ||
|
|
63c079f08e | ||
|
|
ff74fd7e4c | ||
|
|
680f862ee8 | ||
|
|
abdd7f0b55 | ||
|
|
c017dd734b | ||
|
|
ad478adefd | ||
|
|
b11bd52963 | ||
|
|
538df6b6d0 | ||
|
|
819afcdb8a | ||
|
|
a5693f7c03 | ||
|
|
22eb5b1168 | ||
|
|
9402caa4ae | ||
|
|
0aff56e891 | ||
|
|
a5f6aaa7c0 | ||
|
|
4ee503655c | ||
|
|
e53a82782c | ||
|
|
020c6815ba | ||
|
|
5281a92e81 | ||
|
|
8577eb44af | ||
|
|
edd3cf25b5 | ||
|
|
b17719c868 | ||
|
|
971ec9d4db | ||
|
|
6607654e8a | ||
|
|
891e591ceb | ||
|
|
2540ed3945 | ||
|
|
7ac3f33cd4 | ||
|
|
9b91f6897e | ||
|
|
bc62b99401 | ||
|
|
a5d1cea910 | ||
|
|
3e3473f6a8 | ||
|
|
a0bdd15d15 | ||
|
|
920eb10c05 | ||
|
|
ae7a436758 | ||
|
|
92d246b45a | ||
|
|
0af480ac1a | ||
|
|
9f52c2ec67 | ||
|
|
cfb278087e | ||
|
|
402b0d2cc7 | ||
|
|
67c79f2eb7 | ||
|
|
d759b00c7e | ||
|
|
cac8163e58 | ||
|
|
11dbdaf6cc | ||
|
|
ac6b04f492 | ||
|
|
0790da2e0d | ||
|
|
dffb1c79ec | ||
|
|
46cc9f0b6e | ||
|
|
140e9f0b20 | ||
|
|
0fe5779c4b | ||
|
|
06b1475b63 | ||
|
|
6bc415120d | ||
|
|
e061540e6b | ||
|
|
38bf31a67f | ||
|
|
c8c3b4888f | ||
|
|
70bb30406a | ||
|
|
d9e71f6770 | ||
|
|
7cee9f04cf | ||
|
|
0a673fd333 | ||
|
|
fd316a8e84 | ||
|
|
c1e1de2255 | ||
|
|
9bea31e1b2 | ||
|
|
351edd8102 | ||
|
|
10dcb7f30d | ||
|
|
ffb41b4e30 | ||
|
|
d78bc48e6f | ||
|
|
b4154bb0d5 | ||
|
|
48467b08d3 | ||
|
|
682d507683 | ||
|
|
814e609efc | ||
|
|
652fa5a14c | ||
|
|
39bc9cd410 | ||
|
|
63b6012488 | ||
|
|
32e2aa6974 | ||
|
|
78f4767dd2 | ||
|
|
05a9ca15a9 | ||
|
|
e2c8158aec | ||
|
|
4eee6d4322 | ||
|
|
56f5431dd9 | ||
|
|
a00f94f9b1 | ||
|
|
df4fe87178 | ||
|
|
89c39d2d25 | ||
|
|
b60422c26c | ||
|
|
59fce8271e | ||
|
|
506faf39dc | ||
|
|
adf5401fff | ||
|
|
dbf5cba7cb | ||
|
|
b3a6beb8f4 | ||
|
|
1291f60e62 | ||
|
|
e86945355c | ||
|
|
8dd9958b30 | ||
|
|
869c7dcfe1 | ||
|
|
7068da5fc2 | ||
|
|
3704fba1f8 | ||
|
|
6d99790f7d | ||
|
|
3e7f1965e7 | ||
|
|
87d05b7c75 | ||
|
|
aca23ed90d | ||
|
|
a01c8e3f17 | ||
|
|
eba9bc1b6b | ||
|
|
34be8add14 | ||
|
|
bd44fa5609 | ||
|
|
c81bdf608f | ||
|
|
e9e5987330 | ||
|
|
ce4b0f7aa7 | ||
|
|
eb6e469dc9 | ||
|
|
4f6fb4ffea | ||
|
|
24df22781f | ||
|
|
33fda80d0a | ||
|
|
a3d8372f48 | ||
|
|
b5dbde66c4 | ||
|
|
abf532c6bb | ||
|
|
976c7c0e82 | ||
|
|
06b23d6287 | ||
|
|
38a15d6426 | ||
|
|
c2c86033cb | ||
|
|
72c94ca1e3 | ||
|
|
bf6bd4be55 | ||
|
|
53f78b02fb | ||
|
|
6f5a8a7bc7 | ||
|
|
0d5dd34597 | ||
|
|
a93ec8dd7e | ||
|
|
1e488b576c | ||
|
|
d3d8eb40b5 | ||
|
|
db54761422 | ||
|
|
aa9f20f544 | ||
|
|
60371f52f8 | ||
|
|
87394fbce7 | ||
|
|
72341ff1de | ||
|
|
63cebf3784 | ||
|
|
60f3f05f67 | ||
|
|
ce39065c80 | ||
|
|
3f39e9efa1 | ||
|
|
f37dbd75c6 | ||
|
|
47ad7a3b74 | ||
|
|
91747c0682 | ||
|
|
80f8495f8e | ||
|
|
c6bb258bf4 | ||
|
|
e70abc593c | ||
|
|
22d73c6a7f | ||
|
|
82afe4195c | ||
|
|
5e9409ab66 | ||
|
|
e64854eb14 | ||
|
|
333f70bd7e | ||
|
|
ace7d5efc9 | ||
|
|
cd14f82753 | ||
|
|
aba718aef1 | ||
|
|
c87497071d | ||
|
|
2a35e7cf5c | ||
|
|
3b98036687 | ||
|
|
3fc935b96d | ||
|
|
7586f226ba | ||
|
|
1a0e600db5 | ||
|
|
1dba5a6c6f | ||
|
|
3ec0652fbb | ||
|
|
e71d5983a4 | ||
|
|
bb9b5fc5cd | ||
|
|
89fde5a6fe | ||
|
|
24ff8f2b0f | ||
|
|
f103563cb7 | ||
|
|
1767268a48 | ||
|
|
8ab2d61742 | ||
|
|
132580af6f | ||
|
|
375c0123c6 | ||
|
|
769002412d | ||
|
|
7fa567c1e4 | ||
|
|
8dfc9a6d55 | ||
|
|
0efc85c516 | ||
|
|
dbb8e3c5e0 | ||
|
|
f4f1eed645 | ||
|
|
0a31f51567 | ||
|
|
c255138a6e | ||
|
|
856a05a357 | ||
|
|
18915167f4 | ||
|
|
34b0995d6b | ||
|
|
4e5f38378e | ||
|
|
5d2c9261ae | ||
|
|
e039d4512c | ||
|
|
51509a3531 | ||
|
|
8f032f2379 | ||
|
|
ff11e26219 | ||
|
|
f004aa4cfd | ||
|
|
2104f2a99d | ||
|
|
389129aadf | ||
|
|
f885726ed1 | ||
|
|
3cc452323d | ||
|
|
780acb32d7 | ||
|
|
bf5fdb95f4 | ||
|
|
7d7ccf53bb | ||
|
|
94837b7e03 | ||
|
|
a9c1c7083d | ||
|
|
8dd6fc15cc | ||
|
|
32ca57fc65 | ||
|
|
9d748f26c0 | ||
|
|
5af1454f36 | ||
|
|
bf7a660633 | ||
|
|
f0723f23e8 | ||
|
|
36357135cb | ||
|
|
8be08a5c29 | ||
|
|
9fe5b9b671 | ||
|
|
720c07b008 | ||
|
|
7fdf93c5d3 | ||
|
|
a0fd466bcf | ||
|
|
35d62c913a | ||
|
|
ede2d9f71a | ||
|
|
acccc32813 | ||
|
|
69f73ff09f | ||
|
|
305f3c42ae | ||
|
|
d6ac4c34c5 | ||
|
|
9c6eeaafdb | ||
|
|
6bd841fea2 | ||
|
|
e05c46e9ce | ||
|
|
e94109dc75 | ||
|
|
a68dea5193 | ||
|
|
b5d56b75d7 | ||
|
|
c8daffb935 | ||
|
|
18a7f26c54 | ||
|
|
e7395e79a5 | ||
|
|
08ee6d379f | ||
|
|
9c692539ce | ||
|
|
5cd5061f20 | ||
|
|
46bcf6d6a6 | ||
|
|
4da931269c | ||
|
|
d19d40d479 | ||
|
|
b3e1c7f660 | ||
|
|
1242a710df | ||
|
|
b724eca9be | ||
|
|
376d4936cc | ||
|
|
a5da0573c5 | ||
|
|
4694ce45a7 | ||
|
|
7c7d710cd3 | ||
|
|
e2cd7e2991 | ||
|
|
92d8eb8d6f | ||
|
|
d14e7d9b2e | ||
|
|
e4befae55a | ||
|
|
0e00b425b4 | ||
|
|
fcf6c8eb4c | ||
|
|
242bacc1be | ||
|
|
cbe28f97a0 | ||
|
|
336972f586 | ||
|
|
81edb31e7b | ||
|
|
dbde66ad77 | ||
|
|
efbf2da816 | ||
|
|
38d1db2f3f | ||
|
|
265ae82663 | ||
|
|
2bd2972af6 | ||
|
|
1bdf083e28 | ||
|
|
4c233fdded | ||
|
|
e0060fe3e5 | ||
|
|
2adb3bd052 | ||
|
|
0dca49ab43 | ||
|
|
6a71d4a495 | ||
|
|
230ddf4a48 | ||
|
|
e4e1797ac5 | ||
|
|
1dd3185715 | ||
|
|
4fee231a7b | ||
|
|
b5ead39792 | ||
|
|
190431b9d4 | ||
|
|
210fa35cb6 | ||
|
|
479af1cd5e | ||
|
|
c72831cd12 | ||
|
|
f68a9493ce | ||
|
|
78a93d0809 | ||
|
|
3680ac8af1 | ||
|
|
e74ba52c47 | ||
|
|
d3cd07c5c3 | ||
|
|
2b890d4704 | ||
|
|
47eead2a18 | ||
|
|
38ea3115f6 | ||
|
|
f11a2b5442 | ||
|
|
818f67d6da | ||
|
|
84f353220d | ||
|
|
47541a449b | ||
|
|
1a62edab37 | ||
|
|
5927450840 | ||
|
|
ec9bea70fd | ||
|
|
2f9a45b1e4 | ||
|
|
3c649df2e9 | ||
|
|
ab1766ca0a | ||
|
|
a42de78ad3 | ||
|
|
81bde3a771 | ||
|
|
982e6655ef | ||
|
|
44ce0cba3c | ||
|
|
c1e1c69b5e | ||
|
|
142defdd20 | ||
|
|
aad9f2b4de | ||
|
|
8e92407182 | ||
|
|
94210e29d1 | ||
|
|
d2c6194314 | ||
|
|
11ca7a724d | ||
|
|
3057908074 | ||
|
|
6a9e538845 | ||
|
|
130538cd9a | ||
|
|
1486613744 | ||
|
|
f2516206c0 | ||
|
|
f8902e05a6 | ||
|
|
c59b47c801 | ||
|
|
0e4845f2e2 | ||
|
|
e2775cd9de | ||
|
|
816bec88c8 | ||
|
|
d724bef4e4 | ||
|
|
cac851f120 | ||
|
|
c1df74ee27 | ||
|
|
0c7b51c622 | ||
|
|
56ecad24d4 | ||
|
|
6bb600648a | ||
|
|
fe2610c35a | ||
|
|
84bcbc9cea | ||
|
|
d741377e5a | ||
|
|
1ea106970d | ||
|
|
a879b7aded | ||
|
|
30ee000676 | ||
|
|
703cf80d5a | ||
|
|
34289f0176 | ||
|
|
8b8f32de02 | ||
|
|
095e5cc0d9 | ||
|
|
0c46c0a079 | ||
|
|
dd4b2ebb8c | ||
|
|
d30ee0c35a | ||
|
|
484fb16bba | ||
|
|
f320e6cef0 | ||
|
|
ea2421bc6d | ||
|
|
f43c31fb9a | ||
|
|
6825ad55e3 | ||
|
|
92841f8bb3 | ||
|
|
38e5e4c9a9 | ||
|
|
1bcfed05af | ||
|
|
e00ec4098a | ||
|
|
5d44953435 | ||
|
|
80ad5a3518 | ||
|
|
b5683ba4b8 | ||
|
|
884b3963d3 | ||
|
|
8de2296aaf | ||
|
|
c1869f469c | ||
|
|
86196b3122 | ||
|
|
fd03e42240 | ||
|
|
3e230b0d68 | ||
|
|
133d7f56e5 | ||
|
|
78d048ba62 | ||
|
|
323004a730 | ||
|
|
626bca9951 | ||
|
|
3ae0d4ff45 | ||
|
|
9d85df54e1 | ||
|
|
0ff1c95fd1 | ||
|
|
b93274fd53 | ||
|
|
0f0d1d64f9 | ||
|
|
d77e6c6f22 | ||
|
|
83ff5348c6 | ||
|
|
65012d40ed | ||
|
|
feb02bfa85 | ||
|
|
276e51b8ea | ||
|
|
c83d77c02a | ||
|
|
0cdb48a71a | ||
|
|
1556d7ac69 | ||
|
|
1e65abd962 | ||
|
|
9f11d799ac | ||
|
|
5c0f419e7e | ||
|
|
c33d42808a | ||
|
|
e8716a45b2 | ||
|
|
570dee30e3 | ||
|
|
97222ed233 | ||
|
|
e2a7fff6f9 | ||
|
|
efc8246a76 | ||
|
|
b4a95e0cf9 | ||
|
|
c66d5d4627 | ||
|
|
4de28e0390 | ||
|
|
f74721b366 | ||
|
|
4fb16e7098 | ||
|
|
a76c351206 | ||
|
|
7faf94cfab | ||
|
|
3a1e29f682 | ||
|
|
88cb287df8 | ||
|
|
eb26ee0cd4 | ||
|
|
844833f348 | ||
|
|
38b02cf771 | ||
|
|
1706d5cab9 | ||
|
|
640d2ff28b | ||
|
|
312b6ce987 | ||
|
|
489f70929e | ||
|
|
904ddfa5c6 | ||
|
|
3ed6840607 | ||
|
|
93125dd7c8 | ||
|
|
34356c6c56 | ||
|
|
05466fed2b | ||
|
|
535b818a22 | ||
|
|
e62e04dd14 | ||
|
|
61cd5fcf7b | ||
|
|
f308f71b5d | ||
|
|
c498bde1fd | ||
|
|
0c9b496137 | ||
|
|
9682d4f8f9 | ||
|
|
da76945f5e | ||
|
|
51074a58f4 | ||
|
|
3cc735b9c8 | ||
|
|
b74f31c26d | ||
|
|
4a96cbfb0e | ||
|
|
77a3c352b1 | ||
|
|
598ae01082 | ||
|
|
54e11899a0 | ||
|
|
dc4962ade1 | ||
|
|
ccf317c06f | ||
|
|
aff5717f19 | ||
|
|
fa39209272 | ||
|
|
d63082a889 | ||
|
|
a28d46ecbe | ||
|
|
3bc72ce881 | ||
|
|
314210117b | ||
|
|
57a8e48d3a | ||
|
|
2d3ab86238 | ||
|
|
b76edbf7de | ||
|
|
b2efc34b80 | ||
|
|
dee8d6bc61 | ||
|
|
3e5576481d | ||
|
|
ffd74aea6f | ||
|
|
145af33239 | ||
|
|
3851b2226a | ||
|
|
bb132101b5 | ||
|
|
1e6251edfb | ||
|
|
e251da9b8e | ||
|
|
335ee0cd60 | ||
|
|
dc9f6c22ad | ||
|
|
5568b54c99 | ||
|
|
e6e35e052a | ||
|
|
9c21db8172 | ||
|
|
77c9141d92 | ||
|
|
68e86f4f90 | ||
|
|
5e95f60461 | ||
|
|
908d7d4177 | ||
|
|
de1a9c35ec | ||
|
|
f7fa9cd330 | ||
|
|
ee8b371850 | ||
|
|
601228a369 | ||
|
|
5dcaefc460 | ||
|
|
026e94fc1b | ||
|
|
fd7c8467b4 | ||
|
|
06e651a844 | ||
|
|
1b0e7a3dfd | ||
|
|
abc5d1861a | ||
|
|
4b8129fd19 | ||
|
|
f610f385bd | ||
|
|
6994635fe4 | ||
|
|
61525e1351 | ||
|
|
a50070e133 | ||
|
|
db632424b1 | ||
|
|
6b92920f50 | ||
|
|
a30d362695 | ||
|
|
d105e62f18 | ||
|
|
d5ad796339 | ||
|
|
c0aabefc72 | ||
|
|
259d321f3d | ||
|
|
d2375d92e6 | ||
|
|
f8a63c5df1 | ||
|
|
5004d99473 | ||
|
|
5b26cc73e8 | ||
|
|
72a4c4542b | ||
|
|
fdaeb8f6b8 | ||
|
|
f2f550dae7 | ||
|
|
6780d72296 | ||
|
|
f1784f3c21 | ||
|
|
22d17ba621 | ||
|
|
4b4cc1c438 | ||
|
|
7aac664916 | ||
|
|
12c4a014f2 | ||
|
|
1eada12714 | ||
|
|
5adb831136 | ||
|
|
5b8a8a7449 | ||
|
|
005dae76ab | ||
|
|
cb2c7276dd | ||
|
|
54fe61c51c | ||
|
|
bdf27ec421 | ||
|
|
3addd39fc5 | ||
|
|
19bf456e47 | ||
|
|
66bf889e9f | ||
|
|
09ea158734 | ||
|
|
88c1b553e3 | ||
|
|
1d1fd1b54d | ||
|
|
9d3267b1fc | ||
|
|
22a04adde0 | ||
|
|
1dba90ceb5 | ||
|
|
4e9794d574 | ||
|
|
23a747d3a9 | ||
|
|
4678c35453 | ||
|
|
c153e53c5b | ||
|
|
e20ee09bbb | ||
|
|
619687472d | ||
|
|
b74f23f6d1 | ||
|
|
d073b4c402 | ||
|
|
258883d88b | ||
|
|
29c8d6236a | ||
|
|
a1443594f7 | ||
|
|
ac2e9570e5 | ||
|
|
983b1c67ac | ||
|
|
764493f5c9 | ||
|
|
d7981e7cc0 | ||
|
|
11fbe97325 | ||
|
|
3f37a63439 | ||
|
|
ee78b95617 | ||
|
|
d7091f8a8e | ||
|
|
4b5cdade77 | ||
|
|
cb67d1ae40 | ||
|
|
676af4a2bf | ||
|
|
0feb02babe | ||
|
|
90c481b12b | ||
|
|
59fed488ba | ||
|
|
8431e7b6b2 | ||
|
|
be30cdd540 | ||
|
|
54927ed77c | ||
|
|
97f974d931 | ||
|
|
0de2cc0922 | ||
|
|
e80ad81f05 | ||
|
|
a3960421d7 | ||
|
|
2f2eeeb13a | ||
|
|
eeb026abf0 | ||
|
|
0fed96f59c | ||
|
|
8369939135 | ||
|
|
08056a650f | ||
|
|
5b53b7490c | ||
|
|
ce95c3ae8c | ||
|
|
5e0c4e3e77 | ||
|
|
10bf48df40 | ||
|
|
ae935798f3 | ||
|
|
bf71f8cf86 | ||
|
|
4206696f0d | ||
|
|
2903662f8b | ||
|
|
469847e4b7 | ||
|
|
d7a86b0401 | ||
|
|
4fdd346c00 | ||
|
|
4f26b63ab1 | ||
|
|
0fed91302c | ||
|
|
831cefd9a7 | ||
|
|
114a3dda8b | ||
|
|
55ddd2c5a0 | ||
|
|
1d276186f8 | ||
|
|
fb3886b981 | ||
|
|
2bed7be654 | ||
|
|
5c460ad747 | ||
|
|
32220d7509 | ||
|
|
b0265001f8 | ||
|
|
9ede4634cb | ||
|
|
0e008ecd5c | ||
|
|
2ae82fdd9e | ||
|
|
3b430b95ed | ||
|
|
e907892c4a | ||
|
|
d9cd3764b9 | ||
|
|
9c2bf05a24 | ||
|
|
91e52c4c5d | ||
|
|
b41d7f539f | ||
|
|
9735b980d7 | ||
|
|
706483ed06 | ||
|
|
7760776a5f | ||
|
|
8b9a7893d3 | ||
|
|
729250a2cc | ||
|
|
f14203f85e | ||
|
|
60e880f7f2 | ||
|
|
16cef0ae65 | ||
|
|
12ff1f421e | ||
|
|
61c33da2b0 | ||
|
|
8b0c7205b7 | ||
|
|
b02c96ada8 | ||
|
|
52b20e4c8e | ||
|
|
a94ac8284f | ||
|
|
ee399136a9 | ||
|
|
ab54422acf | ||
|
|
d56c5720eb | ||
|
|
040de4b000 | ||
|
|
815a774e3d | ||
|
|
b2e7f58e14 | ||
|
|
8d124a20ac | ||
|
|
d1a4a54d2a | ||
|
|
049bb06762 | ||
|
|
e5dd473981 | ||
|
|
ca5cc3bb30 | ||
|
|
444d75c803 | ||
|
|
ccda3a6098 | ||
|
|
ce1c70a4e1 | ||
|
|
5fcfa979c5 | ||
|
|
ec4d609ba1 | ||
|
|
b42278a372 | ||
|
|
1f96035ba9 | ||
|
|
332edb5dca | ||
|
|
c5681d31eb | ||
|
|
685d70c639 | ||
|
|
848878bc5e | ||
|
|
088d063d38 | ||
|
|
4e493db987 | ||
|
|
6d017d4b27 | ||
|
|
a34066d650 | ||
|
|
0e34d63c84 | ||
|
|
9dca8ebfa0 | ||
|
|
c908aea1ae | ||
|
|
c7a58bbce6 | ||
|
|
b26f303479 | ||
|
|
13d64d2ce2 | ||
|
|
ddb7fb3d6e | ||
|
|
457967bfb2 | ||
|
|
734742ecbe | ||
|
|
26dab3870a | ||
|
|
ca9acbb089 | ||
|
|
f78d6fec1d | ||
|
|
bafb852545 | ||
|
|
a755db45b9 | ||
|
|
b9715f2159 | ||
|
|
e90dbaf691 | ||
|
|
08ac55f718 | ||
|
|
5bf831e236 | ||
|
|
1905804f2a | ||
|
|
afc931ac8c | ||
|
|
9fb032e030 | ||
|
|
09b0c96263 | ||
|
|
7c66816023 | ||
|
|
3e7e5df5de | ||
|
|
1c84a74034 | ||
|
|
09969dd9d7 | ||
|
|
63b57e1113 | ||
|
|
585677e515 | ||
|
|
57f2b5f7e5 | ||
|
|
3597735556 | ||
|
|
2442cf2286 | ||
|
|
97438e13d2 | ||
|
|
57dff9d122 | ||
|
|
963fc792a0 | ||
|
|
35a6418f38 | ||
|
|
3c0b951132 | ||
|
|
3dc9e7d8b4 | ||
|
|
ab372b8357 | ||
|
|
c79f04581f | ||
|
|
0dcd049453 | ||
|
|
7170b09586 | ||
|
|
3d893b126f | ||
|
|
fb73b8b106 | ||
|
|
cede77e3fa | ||
|
|
0572d6a308 | ||
|
|
5967e3df46 | ||
|
|
6b1ee83654 | ||
|
|
0af1496a0f | ||
|
|
52b08efd88 | ||
|
|
7a3e4e56e2 | ||
|
|
abe6638f66 | ||
|
|
eb62c2cea9 | ||
|
|
7e6eddce3e | ||
|
|
bc517e0cb0 | ||
|
|
41900b0048 | ||
|
|
9ea71b931c | ||
|
|
64f222b5b7 | ||
|
|
6c27406803 | ||
|
|
2f51e7ad99 | ||
|
|
d86dd63236 | ||
|
|
f794133915 | ||
|
|
c7d2e7acc2 | ||
|
|
2dde3840c6 | ||
|
|
4675847fec | ||
|
|
830d1a2243 | ||
|
|
8a43f0ded5 | ||
|
|
af24e0ffbe | ||
|
|
d3a70bf2c1 | ||
|
|
a62fe705f9 | ||
|
|
1c6407238c | ||
|
|
ec897b185c | ||
|
|
69e15b3797 | ||
|
|
dae4af070f | ||
|
|
beaf0f3bfd | ||
|
|
8057982184 | ||
|
|
9a5a808780 | ||
|
|
d2d5f4a705 | ||
|
|
4aa875660f | ||
|
|
5ab8f00aeb | ||
|
|
1e1b5f56ce | ||
|
|
49b9ee96f3 | ||
|
|
aef7716634 | ||
|
|
57f2e1d500 | ||
|
|
41c696992a | ||
|
|
f231d4e1e4 | ||
|
|
0efbe41b7e | ||
|
|
be9675bb10 | ||
|
|
4e45d35925 | ||
|
|
a5e293554c | ||
|
|
c76d4822fe | ||
|
|
0f8fd5cc0e | ||
|
|
8b27890254 | ||
|
|
8747c8375e | ||
|
|
50f27c110c | ||
|
|
c1f8997c01 | ||
|
|
12ed767961 | ||
|
|
335808217b | ||
|
|
b5bb7024fc | ||
|
|
ac2806dc66 | ||
|
|
9d149dab49 | ||
|
|
451404c0f9 | ||
|
|
a3f1f077ce | ||
|
|
dcd13833ae | ||
|
|
78b732b75c | ||
|
|
3152933937 | ||
|
|
6c6126bb27 | ||
|
|
ad13b7687d | ||
|
|
633e366e82 | ||
|
|
69b1786859 | ||
|
|
41839a961a | ||
|
|
ccb50c64bb | ||
|
|
d88ac1b424 | ||
|
|
c807611e55 | ||
|
|
155ec1403e | ||
|
|
ecde657c3e | ||
|
|
6aa8374f25 | ||
|
|
c2f01473fd | ||
|
|
41e607c995 | ||
|
|
fee754f15f | ||
|
|
d62d18232d | ||
|
|
a1acab0fed | ||
|
|
d39a7099c7 | ||
|
|
7b2c4f99f3 | ||
|
|
b941ff08dc | ||
|
|
210a74c75f | ||
|
|
eafce71cd0 | ||
|
|
d5777b501d | ||
|
|
dd60e5eb21 | ||
|
|
c57dbf23e1 | ||
|
|
cb52adb4de | ||
|
|
a75b0fc549 | ||
|
|
1a546b72d2 | ||
|
|
fd13d268d0 | ||
|
|
5837fc11a5 | ||
|
|
efdbbe40e8 | ||
|
|
5103cf1c12 | ||
|
|
e855a3de7d | ||
|
|
b90e38d595 | ||
|
|
1864f39be0 | ||
|
|
89e80b6f8b | ||
|
|
564295ee64 | ||
|
|
61c7f26007 | ||
|
|
54b9ece5b6 | ||
|
|
335136561a | ||
|
|
56f1cacd07 | ||
|
|
558e358d0b | ||
|
|
290e6eecc4 | ||
|
|
f512f55782 | ||
|
|
ad6eba58d2 | ||
|
|
b675aee8ff | ||
|
|
c29ea9c89c | ||
|
|
50016a31a8 | ||
|
|
1cecffc30f | ||
|
|
833273c19d | ||
|
|
35603d0caf | ||
|
|
de1377afdd | ||
|
|
a22ddd10cc | ||
|
|
280add7107 | ||
|
|
b34abcab56 | ||
|
|
788b26949e | ||
|
|
c11da3df3d | ||
|
|
0d68fea0eb | ||
|
|
c98672bcef | ||
|
|
bb0c079757 | ||
|
|
891144546e | ||
|
|
86611d764d | ||
|
|
e604268004 | ||
|
|
396c3da277 | ||
|
|
0591b90089 | ||
|
|
6763be53ac | ||
|
|
aee23f7550 | ||
|
|
57b5528d43 | ||
|
|
bd8cf18a1a | ||
|
|
84acdd901a | ||
|
|
e213a95996 | ||
|
|
78cd2eecf9 | ||
|
|
ebfeea1087 | ||
|
|
ddca623792 | ||
|
|
f0da35d115 | ||
|
|
10eb355f92 | ||
|
|
85702c534a | ||
|
|
e2dfe317a9 | ||
|
|
7008ae8672 | ||
|
|
10d9fa3581 | ||
|
|
2b85927739 | ||
|
|
8c402238ed | ||
|
|
758770d278 | ||
|
|
ee7ebb159b | ||
|
|
f47a66060f | ||
|
|
8929a630ed | ||
|
|
077f5d7547 | ||
|
|
369ad42946 | ||
|
|
5ab6b85b30 |
@@ -0,0 +1,25 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||
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"
|
||||
# array). The sample below assumes your primary file is in the root of your project.
|
||||
#
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
- ..:/workspaces:cached
|
||||
command: sleep infinity
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
@@ -0,0 +1,48 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||
{
|
||||
"name": "Node.js",
|
||||
// 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": "multi-scrobbler",
|
||||
|
||||
"workspaceFolder": "/workspaces",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [9078,3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"hbenl.vscode-mocha-test-adapter",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"TakumiI.markdowntable"
|
||||
]
|
||||
}
|
||||
},
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Docusaurus"
|
||||
},
|
||||
"9078": {
|
||||
"label": "App"
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
*.Dockerfile
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.github
|
||||
@@ -10,13 +11,21 @@ Dockerfile
|
||||
.idea
|
||||
config/*.json
|
||||
config/*.bak
|
||||
config/*.p8
|
||||
/docs
|
||||
/logs
|
||||
.flatpak-builder
|
||||
flatpak/generated-sources.json
|
||||
**/generated-sources.*
|
||||
flatpak/.flatpak-builder
|
||||
docsite/build
|
||||
docsite/node_modules
|
||||
docsite/.docusaurus
|
||||
docsite/.cache-loader
|
||||
build
|
||||
*.secrets
|
||||
.devcontainer
|
||||
.vscode
|
||||
**/act/*
|
||||
tmp-*
|
||||
.env
|
||||
*.env
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible problem
|
||||
title: "bug: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- 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
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: Describe what you expected to happen and what happened instead. Please provide as much detail as possible.
|
||||
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:
|
||||
label: Platform
|
||||
description: What platform are you running multi-scrobbler on?
|
||||
options:
|
||||
- Docker
|
||||
- Local (NodeJS)
|
||||
- Flatpak
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Versions
|
||||
description: What version of multi-scrobbler and issue-related sources/clients are running?
|
||||
placeholder: |
|
||||
- multi-scrobbler: 0.8.3 on docker
|
||||
- maloja 3.1.4
|
||||
- jellyfin 10.8.9
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
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 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
|
||||
[2025-02-14 12:47:57.236 -0500] INFO : [Init] Debug Mode: NO
|
||||
[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
|
||||
description: Add any other context about the problem here.
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or functionality
|
||||
title: "feat: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- 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
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the new functionality
|
||||
description: Please describe the feature or functionality you want multi-scrobbler to have. Use as much detail as possible about the exact behavior you wish to see.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Configuration Example
|
||||
description: |
|
||||
If the functionality involves changes to configuration files please provide an example here
|
||||
render: json5
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Will you help implement it?
|
||||
options:
|
||||
- label: I am willing to contribute code towards completing this feature
|
||||
- label: I am willing to help test preview builds for this feature
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Request a new Source or Client
|
||||
description: Request a new Source or Client to be implemented
|
||||
title: "feat: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- 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
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What Type?
|
||||
description: Is this a Source or a Client?
|
||||
options:
|
||||
- Source
|
||||
- Client
|
||||
default: 1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Name
|
||||
description: "What is the name of the new service/software?"
|
||||
placeholder: "My Cool Player"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Website
|
||||
description: "Website for the service/software. Preferably a github repo or other source-code homepage"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: API Available?
|
||||
description: Does the service have an API or other protocol that multi-scrobbler can use to communicate? **Without this the likelyhood of implementation is very low.**
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
default: 1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Free To Use?
|
||||
description: Is this service/software freely available or does it require purchasing/subscription?
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
default: 1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Willing to finance development?
|
||||
description: If the software is **non-free** are you will to provide a paid account/subscription to multi-scrobbler developer's in order to facilitate development/testing?
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Will you help implement it?
|
||||
options:
|
||||
- label: I am willing to contribute code towards completing this feature
|
||||
- label: I am willing to help test preview builds for this feature
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the new Source/Client here
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
|
||||
|
||||
```
|
||||
Copy and paste as much log data as possible related to this issue here.
|
||||
```
|
||||
|
||||
**Versions (please complete the following information):**
|
||||
Provide version information for any related sources/clients.
|
||||
|
||||
- multi-scrobbler: [e.g. 0.4.0 on docker]
|
||||
- maloja [e.g. 3.1.4]
|
||||
- jellyfin [e.g. 10.8.9]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,5 @@
|
||||
--directory ../../
|
||||
--env-file .github/act/.env
|
||||
--secret-file .github/act/.secrets
|
||||
--artifact-server-path out
|
||||
#--action-offline-mode
|
||||
@@ -0,0 +1,5 @@
|
||||
NO_DOCKER_BUILD=true
|
||||
#https://github.com/nektos/act/issues/329#issuecomment-1905955589
|
||||
ACTIONS_RUNTIME_URL=http://YOUR_HOST_IP:8082/
|
||||
ACTIONS_RUNTIME_TOKEN=foo
|
||||
ACTIONS_CACHE_URL=http://YOUR_HOST_IP:8082/
|
||||
@@ -0,0 +1,3 @@
|
||||
DOCKER_PASSWORD=
|
||||
DOCKER_USERNAME=
|
||||
GITHUB_TOKEN=
|
||||
@@ -0,0 +1,53 @@
|
||||
Testing GH Actions with ACT
|
||||
|
||||
Need to have credentials in a [`.secrets` file](https://nektosact.com/usage/index.html#secrets), copy and rename [`.secrets.example`](./secrets.example) to `.secrets`, then fill out blank fields. Required for docker/metadata-action to read...something. Fails with `Parameter token or opts.auth is required` if they are not supplied.
|
||||
|
||||
An ENV file can also be made by copy and renaming [`.env.example`](./env.example). Set `NO_DOCKER_BUILD=true` if you only want to test APP_VERSION and docker tags output.
|
||||
|
||||
If running a full docker build for multi-runner workflows you will need to create an [artifact server](https://github.com/nektos/act/issues/329#issuecomment-1905955589) for ACT to work:
|
||||
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/jefuller/artifact-server:latest
|
||||
docker run -d --name artifact-server -p 8082:8080 --add-host host.docker.internal:host-gateway -e AUTH_KEY=foo ghcr.io/jefuller/artifact-server:latest
|
||||
```
|
||||
|
||||
Run the following **from this directory** to make use of `.actrc` and proper working directory.
|
||||
|
||||
### Test Branch Test Suite
|
||||
|
||||
```shell
|
||||
act -W '.github/act/testSuite.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
|
||||
### Test Branch Push
|
||||
|
||||
```shell
|
||||
act -W '.github/act/actTest.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
|
||||
|
||||
### Test Tag (Release) Push
|
||||
|
||||
```shell
|
||||
act -W '.github/act/actTest.yml' -e '.github/act/actTagEvent.json'
|
||||
```
|
||||
|
||||
|
||||
### Test Tag (Pre-Release) Push
|
||||
|
||||
```shell
|
||||
act -W '.github/act/actTest.yml' -e '.github/act/actTagPreEvent.json'
|
||||
```
|
||||
|
||||
### Test Multi-Runner Push (WIP)
|
||||
|
||||
```shell
|
||||
act -W '.github/act/multiRunnerTest.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
|
||||
### Test Multi-Runner Bake Push (WIP)
|
||||
|
||||
```shell
|
||||
act -W '.github/act/multiRunnerBakeTest.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ref": "refs/heads/master",
|
||||
"sha": "14d5eb3313110f2cfd1aa8fe9c229c21901c1eeb"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"action": "synchronize",
|
||||
"number": 210,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/pulls/210",
|
||||
"id": 2123337348,
|
||||
"node_id": "PR_kwDOEpP-x85-j46E",
|
||||
"number": 210,
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"merge_commit_sha": "349bea1f4580b9e61aa22446eae73f8b1225bbb7",
|
||||
"labels": [
|
||||
{
|
||||
"id": 2494982061,
|
||||
"node_id": "MDU6TGFiZWwyNDk0OTgyMDYx",
|
||||
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/labels/bug",
|
||||
"name": "bug",
|
||||
"color": "d73a4a",
|
||||
"default": true,
|
||||
"description": "Something isn't working"
|
||||
},
|
||||
{
|
||||
"id": 7131135993,
|
||||
"node_id": "LA_kwDOEpP-x88AAAABqQx_-Q",
|
||||
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/labels/safe%20to%20test",
|
||||
"name": "safe to test",
|
||||
"color": "D4C5F9",
|
||||
"default": false,
|
||||
"description": "trusted to build image"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"draft": false,
|
||||
"head": {
|
||||
"label": "FoxxMD:GH-209/jellyfinCleanup",
|
||||
"ref": "GH-209/jellyfinCleanup",
|
||||
"sha": "e251da9b8eecfcb3aba8756393e5075d0092794b",
|
||||
"user": {
|
||||
"login": "FoxxMD",
|
||||
"id": 4663766,
|
||||
"node_id": "MDQ6VXNlcjQ2NjM3NjY=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4663766?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/FoxxMD",
|
||||
"html_url": "https://github.com/FoxxMD",
|
||||
"followers_url": "https://api.github.com/users/FoxxMD/followers",
|
||||
"following_url": "https://api.github.com/users/FoxxMD/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/FoxxMD/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/FoxxMD/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/FoxxMD/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/FoxxMD/orgs",
|
||||
"repos_url": "https://api.github.com/users/FoxxMD/repos",
|
||||
"events_url": "https://api.github.com/users/FoxxMD/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/FoxxMD/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"repo": {
|
||||
"id": 311688903,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkzMTE2ODg5MDM=",
|
||||
"name": "multi-scrobbler",
|
||||
"full_name": "FoxxMD/multi-scrobbler",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "FoxxMD",
|
||||
"id": 4663766,
|
||||
"node_id": "MDQ6VXNlcjQ2NjM3NjY=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4663766?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/FoxxMD",
|
||||
"html_url": "https://github.com/FoxxMD",
|
||||
"followers_url": "https://api.github.com/users/FoxxMD/followers",
|
||||
"following_url": "https://api.github.com/users/FoxxMD/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/FoxxMD/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/FoxxMD/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/FoxxMD/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/FoxxMD/orgs",
|
||||
"repos_url": "https://api.github.com/users/FoxxMD/repos",
|
||||
"events_url": "https://api.github.com/users/FoxxMD/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/FoxxMD/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/FoxxMD/multi-scrobbler",
|
||||
"description": "Scrobble plays from multiple sources to multiple clients",
|
||||
"fork": false,
|
||||
"created_at": "2020-11-10T14:45:38Z",
|
||||
"updated_at": "2024-10-14T09:21:37Z",
|
||||
"pushed_at": "2024-10-14T15:43:00Z",
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"visibility": "public",
|
||||
"forks": 16,
|
||||
"open_issues": 19,
|
||||
"watchers": 369,
|
||||
"default_branch": "master",
|
||||
"allow_squash_merge": true,
|
||||
"allow_merge_commit": true,
|
||||
"allow_rebase_merge": true,
|
||||
"allow_auto_merge": false,
|
||||
"delete_branch_on_merge": false,
|
||||
"allow_update_branch": false,
|
||||
"use_squash_pr_title_as_default": false,
|
||||
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
||||
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
|
||||
"merge_commit_message": "PR_TITLE",
|
||||
"merge_commit_title": "MERGE_MESSAGE"
|
||||
}
|
||||
},
|
||||
"base": {
|
||||
"label": "FoxxMD:master",
|
||||
"ref": "master",
|
||||
"sha": "9c21db8172b6beff9d321e831e9353bf17e92a37",
|
||||
"repo": {
|
||||
"id": 311688903,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkzMTE2ODg5MDM=",
|
||||
"name": "multi-scrobbler",
|
||||
"full_name": "FoxxMD/multi-scrobbler",
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"created_at": "2020-11-10T14:45:38Z",
|
||||
"updated_at": "2024-10-14T09:21:37Z",
|
||||
"pushed_at": "2024-10-14T15:43:00Z",
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"visibility": "public",
|
||||
"forks": 16,
|
||||
"open_issues": 19,
|
||||
"watchers": 369,
|
||||
"default_branch": "master",
|
||||
"allow_squash_merge": true,
|
||||
"allow_merge_commit": true,
|
||||
"allow_rebase_merge": true,
|
||||
"allow_auto_merge": false,
|
||||
"delete_branch_on_merge": false,
|
||||
"allow_update_branch": false,
|
||||
"use_squash_pr_title_as_default": false,
|
||||
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
||||
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
|
||||
"merge_commit_message": "PR_TITLE",
|
||||
"merge_commit_title": "MERGE_MESSAGE"
|
||||
}
|
||||
},
|
||||
"author_association": "OWNER",
|
||||
"auto_merge": null,
|
||||
"active_lock_reason": null,
|
||||
"merged": false,
|
||||
"mergeable": null,
|
||||
"rebaseable": null,
|
||||
"mergeable_state": "unknown",
|
||||
"merged_by": null,
|
||||
"comments": 0,
|
||||
"review_comments": 0,
|
||||
"maintainer_can_modify": false,
|
||||
"commits": 5,
|
||||
"additions": 84,
|
||||
"deletions": 28,
|
||||
"changed_files": 5
|
||||
},
|
||||
"before": "335ee0cd601f5a82e36643c812507b2b53e7daf0",
|
||||
"after": "e251da9b8eecfcb3aba8756393e5075d0092794b"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ref": "refs/tags/0.8.4",
|
||||
"ref_name": "0.8.4",
|
||||
"before": "0000000000000000000000000000000000000000",
|
||||
"after": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"sha": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"base_ref": "refs/heads/master"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ref": "refs/tags/0.8.5-gh-runner",
|
||||
"ref_name": "0.8.5-gh-runner",
|
||||
"before": "0000000000000000000000000000000000000000",
|
||||
"after": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"sha": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"base_ref": "refs/heads/master"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Publish Docker image to Dockerhub
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
# don't trigger if just updating docs
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
# use release instead of tags once version is correctly parsed
|
||||
# https://github.com/docker/metadata-action/issues/422
|
||||
# https://github.com/docker/metadata-action/issues/240
|
||||
# release:
|
||||
# types: [ published ]
|
||||
|
||||
jobs:
|
||||
|
||||
push_to_registry:
|
||||
name: Build and push container images
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Debug
|
||||
run: echo $JSON
|
||||
env:
|
||||
JSON: ${{ toJSON(github) }}
|
||||
|
||||
- name: Set git state to ENV
|
||||
id: vars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Check App Version
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo $APP_VERSION
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=edge
|
||||
|
||||
# maybe re-enable branch-named tags in the futures
|
||||
#type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
# flavor: |
|
||||
# latest=false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
id: qemu
|
||||
if: ${{ !env.NO_DOCKER_BUILD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
if: ${{ steps.qemu.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: ${{ steps.qemu.outcome == 'success' }}
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
# https://github.com/docker/build-push-action/issues/1026#issue-2041857786
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{env.APP_VERSION}}
|
||||
push: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -0,0 +1,30 @@
|
||||
variable "DEFAULT_TAG" {
|
||||
default = "multi-scrobbler:local"
|
||||
}
|
||||
|
||||
// Special target: https://github.com/docker/metadata-action#bake-definition
|
||||
target "docker-metadata-action" {
|
||||
tags = ["${DEFAULT_TAG}"]
|
||||
}
|
||||
|
||||
// Default target if none specified
|
||||
group "default" {
|
||||
targets = ["image-local"]
|
||||
}
|
||||
|
||||
target "image" {
|
||||
inherits = ["docker-metadata-action"]
|
||||
}
|
||||
|
||||
target "image-local" {
|
||||
inherits = ["image"]
|
||||
output = ["type=docker"]
|
||||
}
|
||||
|
||||
target "image-all" {
|
||||
inherits = ["image"]
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#with-bake (https://github.com/docker/build-push-action/issues/671#issuecomment-1609106171)
|
||||
# with example from here https://github.com/crazy-max/docker-linguist/blob/master/.github/workflows/build.yml that uses caching
|
||||
#
|
||||
# may have to use this to enable pushing to multiple registries? https://github.com/docker/build-push-action/discussions/1067#discussioncomment-8677682
|
||||
|
||||
name: build
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
|
||||
env:
|
||||
DOCKERHUB_SLUG: foxxmd/multi-scrobbler
|
||||
GHCR_SLUG: ghcr.io/foxxmd/multi-scrobbler
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: appvars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
run: |
|
||||
echo "matrix=$(docker buildx bake --file ./docker-bake.hcl image-all --print | jq -cr '.target."image-all".platforms')" >>${GITHUB_OUTPUT}
|
||||
-
|
||||
name: Show matrix
|
||||
run: |
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_SLUG }}
|
||||
${{ env.GHCR_SLUG }}
|
||||
tags: |
|
||||
type=edge
|
||||
|
||||
# maybe re-enable branch-named tags in the futures
|
||||
#type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
-
|
||||
name: Rename meta bake definition file
|
||||
run: |
|
||||
mv "${{ steps.meta.outputs.bake-file }}" "/tmp/bake-meta.json"
|
||||
-
|
||||
name: Upload meta bake definition
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: bake-meta
|
||||
path: /tmp/bake-meta.json
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
||||
steps:
|
||||
-
|
||||
name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Test App Version Transfer
|
||||
run: |
|
||||
echo "App Version in Build ${{ needs.prepare.appversion.outputs.appversion }}"
|
||||
-
|
||||
name: Download meta bake definition
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: bake-meta
|
||||
path: /tmp
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
buildkitd-flags: "--debug"
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build
|
||||
id: bake
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
files: |
|
||||
./.github/act/docker-bake.hcl
|
||||
/tmp/bake-meta.json
|
||||
targets: image
|
||||
set: |
|
||||
*.args.APP_BUILD_VERSION=${{ needs.prepare.appversion.outputs.appversion }}
|
||||
*.tags=
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.cache-from=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
*.cache-to=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
*.output=type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
-
|
||||
name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
-
|
||||
name: Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
-
|
||||
name: Download meta bake definition
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: bake-meta
|
||||
path: /tmp
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.DOCKERHUB_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
|
||||
$(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
|
||||
docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.GHCR_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
|
||||
$(printf '${{ env.GHCR_SLUG }}@sha256:%s ' *)
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
tag=$(jq -r '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
|
||||
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${tag}
|
||||
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${tag}
|
||||
@@ -0,0 +1,198 @@
|
||||
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||
|
||||
name: build
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
|
||||
env:
|
||||
DOCKERHUB_SLUG: foxxmd/multi-scrobbler
|
||||
GHCR_SLUG: ghcr.io/foxxmd/multi-scrobbler
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: appvars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
# - name: Extract metadata (tags, labels) for Docker
|
||||
# id: meta
|
||||
# uses: docker/metadata-action@v5
|
||||
# with:
|
||||
# images: |
|
||||
# ${{ env.DOCKERHUB_SLUG }}
|
||||
# ${{ env.GHCR_SLUG }}
|
||||
# labels: |
|
||||
# org.opencontainers.image.title=Multi-Scrobbler
|
||||
# org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
# org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
|
||||
# for caching
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
#tags: ${{ steps.meta.outputs.tags }}
|
||||
outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_SLUG }}
|
||||
${{ env.GHCR_SLUG }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=edge
|
||||
|
||||
# maybe re-enable branch-named tags in the futures
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
# flavor: |
|
||||
# latest=false
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_SLUG }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
|
||||
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
|
||||
@@ -0,0 +1,10 @@
|
||||
name: PR Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
@@ -1,6 +1,5 @@
|
||||
## Checklist before requesting a review
|
||||
|
||||
- [ ] **I am opening this PR for the [`develop` branch](https://github.com/FoxxMD/multi-scrobbler/tree/develop) and NOT `master`.**
|
||||
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
|
||||
|
||||
## Type of change
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Test Dependabot PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- synchronize
|
||||
- reopened
|
||||
- opened
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests / Build / Sanity Run
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
@@ -1,9 +1,9 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [released]
|
||||
# Review gh actions docs if you want to further define triggers, paths, etc
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
|
||||
|
||||
@@ -12,23 +12,38 @@ jobs:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
with:
|
||||
node-version: 18
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install app dev dependencies
|
||||
run: npm ci
|
||||
- name: Generate schema files
|
||||
run: npm run schema
|
||||
|
||||
- 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
|
||||
|
||||
# Popular action to deploy to GitHub Pages:
|
||||
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build output to publish to the `gh-pages` branch:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Delete Untagged Packages
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * 0'
|
||||
workflow_run:
|
||||
workflows: ["Publish Docker image to Dockerhub"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
delete-untagged:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 #1.0.16
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# maybe set up for PR close later
|
||||
# https://github.com/dataaxiom/ghcr-cleanup-action?tab=readme-ov-file#delete-image-when-pull-request-is-closed
|
||||
@@ -0,0 +1,235 @@
|
||||
name: PR Workflow
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
- reopened
|
||||
- opened
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
build-snapshot:
|
||||
name: Build OCI snapshot
|
||||
needs: test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
platform: linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
# list all registries to push to and join all non-empty with comma
|
||||
# https://unix.stackexchange.com/a/693165/116849
|
||||
# https://stackoverflow.com/a/9429887/1469797
|
||||
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
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
|
||||
# do not use here!
|
||||
# instead we can specify repo#sha in build-push-action so we only deal with foreign SHA for actually building image
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set short git commit SHA
|
||||
run: |
|
||||
short=$(echo "${{ github.event.pull_request.head.sha }}" | head -c7)
|
||||
echo "COMMIT_SHORT_SHA=$short" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
APP_VERSION: ${{ format('pr{0}-{1}', github.event.number, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# metadata extract for docker labels/image names is done in merge job
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
|
||||
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
|
||||
# for caching
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
# specify foreign repo#sha from PR that is used to build image
|
||||
context: ${{github.event.pull_request.head.repo.clone_url}}#${{github.event.pull_request.head.sha}}
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release-snapshot:
|
||||
name: Merge OCI Images and Push
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
needs:
|
||||
- build-snapshot
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
${{ vars.GHCR_SLUG }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
flavor: |
|
||||
latest=false
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
- name: Create manifest list and push dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
|
||||
- name: Create manifest list and push gchr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
|
||||
- name: Inspect image dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Inspect image ghcr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
|
||||
|
||||
combine-and-comment:
|
||||
name: Leave comment
|
||||
runs-on: ubuntu-latest
|
||||
needs: release-snapshot
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #2.9.4
|
||||
with:
|
||||
recreate: true
|
||||
header: "pr-release"
|
||||
message: |
|
||||
#### :package: A new release has been made for this pull request.
|
||||
|
||||
To play around with this PR, pull an image:
|
||||
* `foxxmd/multi-scrobbler:pr-${{ github.event.number }}`
|
||||
|
||||
Images are available for x86_64 and ARM64.
|
||||
|
||||
> Latest commit: ${{ github.event.pull_request.head.sha }}
|
||||
@@ -1,69 +1,245 @@
|
||||
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||
|
||||
name: Publish Docker image to Dockerhub
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
# don't trigger if just updating docs
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
# use release instead of tags once version is correctly parsed
|
||||
# https://github.com/docker/metadata-action/issues/422
|
||||
# https://github.com/docker/metadata-action/issues/240
|
||||
# release:
|
||||
# types: [ published ]
|
||||
|
||||
# define in GH Repository -> Actions -> Variables (or act .variables) to enable pushing to registries
|
||||
# -- will only push to registries that are defined
|
||||
# EX
|
||||
# DOCKERHUB_SLUG=foxxmd/multi-scrobbler
|
||||
# GHCR_SLUG=ghcr.io/foxxmd/multi-scrobbler
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
||||
test:
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
|
||||
build:
|
||||
name: Build OCI Images
|
||||
if: ${{ github.event_name != 'pull_request' && (vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '') }}
|
||||
needs: test
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
platform: linux/arm64
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
# list all registries to push to and join all non-empty with comma
|
||||
# https://unix.stackexchange.com/a/693165/116849
|
||||
# https://stackoverflow.com/a/9429887/1469797
|
||||
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
|
||||
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
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: appvars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
# metadata extract for docker labels/image names is done in merge job
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
|
||||
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
|
||||
# for caching
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${VARS_DIGEST}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
env:
|
||||
VARS_DIGEST: ${{ steps.build.outputs.digest }}
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Merge OCI Images and Push
|
||||
if: ${{ github.event_name != 'pull_request' && (vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '') }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
needs:
|
||||
- build
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
with:
|
||||
images: |
|
||||
foxxmd/multi-scrobbler
|
||||
ghcr.io/foxxmd/multi-scrobbler
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
${{ vars.GHCR_SLUG }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ endsWith(github.ref, 'master') }}
|
||||
type=edge
|
||||
|
||||
# push with branch name as tag if not master/main
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
flavor: |
|
||||
latest=false
|
||||
# flavor: |
|
||||
# latest=false
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
- name: Create manifest list and push dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
|
||||
- name: Create manifest list and push gchr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
|
||||
- name: Inspect image dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Inspect image ghcr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Tests and Sanity Run
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node-version:
|
||||
description: "Node version"
|
||||
required: false
|
||||
default: '20.x'
|
||||
type: string
|
||||
ref:
|
||||
description: "Ref to checkout"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests / Build / Sanity Run
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install concurrently
|
||||
run: npm install -g concurrently
|
||||
|
||||
- name: Install dev dependencies
|
||||
run: npm run install:parallel
|
||||
|
||||
- name: Test Backend
|
||||
run: npm run test
|
||||
|
||||
- name: Build
|
||||
run: npm run build:parallel
|
||||
|
||||
# remove modules that might include dev stuff
|
||||
# so that in the next step we are sure that prod-only runs work correctly
|
||||
- name: Install Prod Deps
|
||||
run: |
|
||||
rm -rf node_modules && \
|
||||
rm -rf docsite/node_modules && \
|
||||
NODE_ENV=production npm ci --omit=dev
|
||||
|
||||
# run app for 10 seconds as sanity check to see if it errors for any reason
|
||||
# easy testcase for missing packages and init errors
|
||||
- name: Sanity Run
|
||||
run: |
|
||||
set +e
|
||||
export NODE_ENV=production
|
||||
timeout --preserve-status 10s node node_modules/.bin/tsx src/backend/index.ts
|
||||
exitcode="$?"
|
||||
if [[ "$exitcode" -eq 143 ]] || [[ "$exitcode" -eq 137 ]]; then
|
||||
echo "App stayed up long enough and exited with expected status"
|
||||
exit 0
|
||||
else
|
||||
echo "App exited with unexpected code $exitcode"
|
||||
exit "$exitcode"
|
||||
fi
|
||||
@@ -73,6 +73,7 @@ web_modules/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
*.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
@@ -118,15 +119,32 @@ dist
|
||||
.pnp.*
|
||||
|
||||
config/*.json
|
||||
config/mscache
|
||||
config/yti-*
|
||||
config/*.cache
|
||||
*.txt
|
||||
!robots.txt
|
||||
.idea/
|
||||
|
||||
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
|
||||
.flatpak-builder
|
||||
flatpak/generated-sources.json
|
||||
*.secrets
|
||||
|
||||
build
|
||||
!setupProxy.js
|
||||
**/generated-sources.*
|
||||
.github/act/out
|
||||
tmp-*
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://lnav.org/schemas/format-v1.schema.json",
|
||||
"error_log": {
|
||||
"title": "Multi-Scrobbler Log",
|
||||
"description": "Logs from Multi Scrobbler",
|
||||
"regex": {
|
||||
"std": {
|
||||
"pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?)(?:.*?)\\] (?<level>[A-Z]*) : (?<body>.*)"
|
||||
}
|
||||
},
|
||||
"level-field": "level",
|
||||
"level": {
|
||||
"info": "^(?i)(?:info)$",
|
||||
"debug": "^(?i)(?:debug)$",
|
||||
"error": "^(?i)(error)$",
|
||||
"warning": "^(?i)(warn)$",
|
||||
"trace": "^(?i)verbose$",
|
||||
"critical": "^(?i)(?:crit|Cr)$",
|
||||
"fatal": "^(?i)(?:alert|fatal|panic|Al|Em)$"
|
||||
},
|
||||
"sample": [
|
||||
{
|
||||
"line": "[2024-12-26 01:58:48.261 -0500] DEBUG : [App] [Sources] [Jellyfin - unnamed] Last activity was at 01:58:48-05:00 | Next check in 10.00s | No new tracks discovered",
|
||||
"level": "debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"require": ["./register.js", "source-map-support/register"],
|
||||
"reporter": "dot"
|
||||
"reporter": "dot",
|
||||
"extension": "ts",
|
||||
"import": "tsx/esm",
|
||||
"spec": "./src/backend/tests/**/*.test.ts",
|
||||
"file": [
|
||||
"./src/backend/tests/setup.ts"
|
||||
],
|
||||
"exit": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/index.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "dev (ENV Only)",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/index.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"CONFIG_DIR": "./not-real"
|
||||
},
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "tsx",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug current file in VSCode
|
||||
"program": "${file}",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "schema",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/utils/SchemaStaticUtil.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
//"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/src/**/*",
|
||||
"${workspaceFolder}/node_modules/**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"-u",
|
||||
"bdd",
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--reporter",
|
||||
"spec",
|
||||
"--colors",
|
||||
"--config",
|
||||
"${workspaceRoot}/.mocharc.json",
|
||||
"--recursive",
|
||||
"${workspaceFolder}/src/backend/tests/**/*.test.ts"
|
||||
],
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"name": "Mocha Tests",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"-u",
|
||||
"bdd",
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--reporter",
|
||||
"spec",
|
||||
"--colors",
|
||||
"--config", "${workspaceRoot}/.mocharc.json",
|
||||
"${file}"
|
||||
],
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"name": "Mocha Test on File",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"mochaExplorer.require": "tsx/esm",
|
||||
"mochaExplorer.timeout": 1200000,
|
||||
"mochaExplorer.exit": true,
|
||||
"search.exclude": {
|
||||
"**/.docusaurus": true,
|
||||
"**/build": true,
|
||||
"**/schema": true
|
||||
},
|
||||
"tailwindCSS.files.exclude": [
|
||||
"docsite/**"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": "src/client/index.css"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "src/backend/tsconfig.json",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "tsc: build - src/backend/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "compose-up-dev",
|
||||
"command": "docker-compose -f docker-compose.dev.yml up",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "compose-build-dev",
|
||||
"command": "docker-compose -f docker-compose.dev.yml build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
Please follow these guidelines when contributing code to this repository:
|
||||
|
||||
* The PR **must be for the [`develop` branch.](https://github.com/FoxxMD/multi-scrobbler/tree/develop)** The `master` branch is for releases only.
|
||||
* Use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format when creating commits.
|
||||
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `develop`.
|
||||
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `master`.
|
||||
* Ensure that if your code is covered by [an existing test](./src/backend/tests) that you have updated the test accordingly
|
||||
|
||||
@@ -1,83 +1,119 @@
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
ENV NODE_VERSION=20.18.1
|
||||
|
||||
# borrowing openssl header removal trick from offical docker-node
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
|
||||
RUN \
|
||||
ARCH= OPENSSL_ARCH= && dpkgArch="$(dpkg --print-architecture)" \
|
||||
&& case "${dpkgArch##*-}" in \
|
||||
amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \
|
||||
ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \
|
||||
s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \
|
||||
arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \
|
||||
armhf) ARCH='armv7l' OPENSSL_ARCH='linux-armv4';; \
|
||||
i386) ARCH='x86' OPENSSL_ARCH='linux-elf';; \
|
||||
*) echo "unsupported architecture"; exit 1 ;; \
|
||||
esac && \
|
||||
set -ex && \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
alpine-base \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
#yarn \
|
||||
openssh && \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
#ca-certificates \
|
||||
xz-utils \
|
||||
avahi-utils \
|
||||
curl && \
|
||||
echo "**** Fetch and install node****" && \
|
||||
# get node/npm directly from nodejs dist \
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L41
|
||||
curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
|
||||
tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr --strip-components=1 --no-same-owner && \
|
||||
rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
|
||||
ln -s /usr/bin/node /usr/bin/nodejs && \
|
||||
npm update -g npm && \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/tmp/*
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L49
|
||||
# Remove unused OpenSSL headers to save ~34MB
|
||||
# (does not affect arm64 issue below)
|
||||
find /usr/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; && \
|
||||
apt-get purge --auto-remove -y perl xz-utils && \
|
||||
apt-get autoclean && \
|
||||
apt-get autoremove && \
|
||||
rm -rf \
|
||||
/config/.cache \
|
||||
/root/cache \
|
||||
/var/lib/apt/lists/* \
|
||||
/var/tmp/* \
|
||||
/tmp/*
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN npm install -g concurrently
|
||||
|
||||
ARG data_dir=/config
|
||||
VOLUME $data_dir
|
||||
ENV CONFIG_DIR=$data_dir
|
||||
|
||||
COPY docker/root/ /
|
||||
|
||||
RUN npm install -g patch-package \
|
||||
&& chown -R root:root /usr/local/lib/node_modules/patch-package
|
||||
COPY docker/root /
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as build
|
||||
FROM base AS build
|
||||
|
||||
# would very much like to use YARN for dependency management but its got show-stopping bad design for prod dependencies
|
||||
# https://github.com/yarnpkg/yarn/issues/6323 -- always downloads devDependencies -- and im not about to migrate to v2 since alpine doesn't support it yet
|
||||
|
||||
# copy dep/TS config and install dev dependencies
|
||||
#COPY --chown=abc:abc package.json tsconfig.json yarn.lock ./
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --chown=abc:abc docsite/package*.json tsconfig.json ./docsite/
|
||||
|
||||
# for debugging, so the build fails faster when timing out (arm64)
|
||||
#RUN npm config set fetch-retries 1 && \
|
||||
# npm config set fetch-retry-mintimeout 5000 && \
|
||||
# npm config set fetch-retry-maxtimeout 5000
|
||||
|
||||
RUN npm install \
|
||||
&& chown -R root:root node_modules
|
||||
#RUN yarn install
|
||||
# https://www.npmjs.com/package/tls-test
|
||||
# used to test that the OS supports downloading packages over HTTPS with TLS 1.2 enforced
|
||||
# -- this always succeeds but a good sanity check
|
||||
#RUN npm install -g https://tls-test.npmjs.com/tls-test-1.0.0.tgz
|
||||
|
||||
# This FAILED for node < 20 when building arm64 but not amd64 (and alpine-based Dockerfile has no issues building arm64)
|
||||
# see https://github.com/FoxxMD/multi-scrobbler/issues/126
|
||||
RUN npm run install:parallel \
|
||||
&& chown -R root:root node_modules \
|
||||
&& chown -R root:root docsite/node_modules
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
|
||||
# need to set before build so server/client build is optimized and has constants (if needed)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run build && rm -rf node_modules
|
||||
#RUN yarn run build && rm -rf node_modules
|
||||
RUN npm run build:parallel && rm -rf node_modules && rm -rf docsite/node_modules
|
||||
|
||||
FROM base as app
|
||||
FROM base AS app
|
||||
|
||||
#COPY --chown=abc:abc package.json yarn.lock ./
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
LABEL org.opencontainers.image.source="https://github.com/FoxxMD/multi-scrobbler"
|
||||
|
||||
COPY --chown=abc:abc *.json *.js *.ts index.html ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --from=build --chown=abc:abc /app/build /app/build
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
|
||||
COPY --from=base /usr/bin /usr/bin
|
||||
COPY --from=base /usr/lib /usr/lib
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV IS_DOCKER=true
|
||||
#
|
||||
#RUN yarn global add patch-package \
|
||||
# && yarn install --production=true \
|
||||
# && yarn global remove patch-package \
|
||||
# && yarn cache clean --mirror \
|
||||
# && chown abc:abc node_modules \
|
||||
# && rm -rf node_modules/ts-node \
|
||||
# && rm -rf node_modules/typescript
|
||||
|
||||
RUN npm install --omit=dev \
|
||||
# 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 \
|
||||
&& rm -rf node_modules/ts-node \
|
||||
&& rm -rf node_modules/typescript \
|
||||
&& rm -rf node_modules/@types
|
||||
&& rm -rf /root/.cache
|
||||
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
|
||||
@@ -3,37 +3,65 @@
|
||||
[](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/)
|
||||
|
||||
|
||||
<img src="/assets/icon.png" align="right"
|
||||
alt="multi-scrobbler logo" width="180" height="180">
|
||||
|
||||
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
|
||||
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
|
||||
|
||||
* Supports scrobbling from many **Sources**
|
||||
* [Spotify](/docsite/docs/configuration/configuration.md#spotify)
|
||||
* [Plex](/docsite/docs/configuration/configuration.md#plex) or [Tautulli](/docsite/docs/configuration/configuration.md#tautulli)
|
||||
* [Subsonic-compatible APIs](/docsite/docs/configuration/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
|
||||
* [WebScrobbler](/docsite/docs/configuration/configuration.md#webscrobbler)
|
||||
* [Youtube Music](/docsite/docs/configuration/configuration.md#youtube-music)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm-source)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz--source-)
|
||||
* [Deezer](/docsite/docs/configuration/configuration.md#deezer)
|
||||
* [MPRIS (Linux Desktop)](/docsite/docs/configuration/configuration.md#mpris)
|
||||
* [Mopidy](/docsite/docs/configuration/configuration.md#mopidy)
|
||||
* [JRiver](/docsite/docs/configuration/configuration.md#jriver)
|
||||
* [Kodi](/docsite/docs/configuration/configuration.md#kodi)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](/docsite/docs/configuration/configuration.md#maloja)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
|
||||
* Supports 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)
|
||||
* [Libre.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/librefm-source)
|
||||
* [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)
|
||||
* [Sonos](https://foxxmd.github.io/multi-scrobbler/configuration/sources/sonos)
|
||||
* [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)
|
||||
* [Discord](https://foxxmd.github.io/multi-scrobbler/configuration/clients/discord) (Now Playing)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/clients/koito)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/lastfm)
|
||||
* [Libre.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/librefm)
|
||||
* [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), [healthcheck endpoints](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint), or [prometheus metrics](https://foxxmd.github.io/multi-scrobbler/configuration/#prometheus).
|
||||
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/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
|
||||
* Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
|
||||
* 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)
|
||||
|
||||
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
|
||||
|
||||
<img src="/docsite/static/img/status-ui.png" width="800">
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
@@ -44,17 +72,15 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
<img src="/assets/status-ui.png" width="800">
|
||||
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.
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
|
||||
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.
|
||||
|
||||
### Source
|
||||
|
||||
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
|
||||
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, 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:
|
||||
|
||||
@@ -64,20 +90,24 @@ Source configurations consist of:
|
||||
|
||||
### Client
|
||||
|
||||
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
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 **Koito, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
* A friendly name.
|
||||
* Any data needed to communicate or authenticate with the Client.
|
||||
|
||||
## Quick Start
|
||||
|
||||
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
|
||||
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
|
||||
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -92,7 +122,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
|
||||
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
|
||||
|
||||
## Development
|
||||
|
||||
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* Attribution for [`default-artwork.png`](/assets/default-artwork.png): https://www.flaticon.com/free-icon/music_15795471
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 527 KiB |
@@ -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"
|
||||
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import {API, Collection, FileInfo, Options} from 'jscodeshift';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
export type TransformFrom = 'none' | 'any' | string;
|
||||
export type TransformTo = 'none' | 'any' | string;
|
||||
export type ImportTypes = 'any' | 'relative' | 'alias';
|
||||
|
||||
const base = process.cwd();
|
||||
export default function transformer(
|
||||
file: FileInfo,
|
||||
{ jscodeshift: j }: API,
|
||||
options: Options,
|
||||
) {
|
||||
|
||||
const tf = options.transformFrom as string | undefined;
|
||||
if(tf === undefined) {
|
||||
throw new Error(`arg '--transformFrom' must be defined.`);
|
||||
}
|
||||
const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[];
|
||||
const tfIsAny = transformFrom.includes('any');
|
||||
|
||||
const tt = options.transformTo as string | undefined;
|
||||
if(tt === undefined) {
|
||||
throw new Error(`arg '--transformTo' must be defined.`);
|
||||
}
|
||||
const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[];
|
||||
|
||||
if(transformTo.length > 1 && transformFrom.length !== transformTo.length) {
|
||||
throw new Error('When more than one Transform To is specified then number of Transform From arguments must match');
|
||||
}
|
||||
|
||||
const itypes = options.importTypes as string | undefined;
|
||||
if(itypes === undefined) {
|
||||
throw new Error(`arg '--importTypes' must be defined.`);
|
||||
}
|
||||
const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[];
|
||||
const importsIsAny = importTypes.includes('any');
|
||||
|
||||
const fileDir = path.dirname(path.join(base, file.path));
|
||||
const processingFilePath = file.path;
|
||||
console.log(`Processing => ${processingFilePath}`);
|
||||
let source: Collection<any>;
|
||||
try {
|
||||
source = j(file.source);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse ${processingFilePath}, will skip`);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const imports = source.find(j.ImportDeclaration)
|
||||
imports.forEach((x) => {
|
||||
const importPath = x.value.source.value as string;
|
||||
const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`;
|
||||
|
||||
const pathInfo = path.parse(importPath);
|
||||
const hasNoExt = pathInfo.ext === '';
|
||||
let filenameFromDir: string;
|
||||
if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) {
|
||||
const pathFull = path.join(fileDir, importPath);
|
||||
const dir = path.dirname(pathFull);
|
||||
const normalExt = pathInfo.ext.replace('.', '');
|
||||
let isRelative: boolean | undefined;
|
||||
let dirExists: boolean | undefined;
|
||||
let fileExists: boolean | undefined;
|
||||
try {
|
||||
// is dir path real?
|
||||
fs.realpathSync(dir);
|
||||
dirExists = true;
|
||||
} catch (e) {
|
||||
isRelative = false;
|
||||
dirExists = false;
|
||||
}
|
||||
|
||||
if(isRelative === undefined) {
|
||||
// if extension is none we need to check for any file in dir with this name
|
||||
if(hasNoExt) {
|
||||
const dirFiles = fs.readdirSync(dir)
|
||||
filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name);
|
||||
if(filenameFromDir !== undefined) {
|
||||
fileExists = false;
|
||||
isRelative = false;
|
||||
} else {
|
||||
fileExists = true;
|
||||
isRelative = true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// does a file exist?
|
||||
fs.realpathSync(pathFull);
|
||||
isRelative = true;
|
||||
} catch (e) {
|
||||
isRelative = false;
|
||||
fileExists = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!importsIsAny) {
|
||||
if(!isRelative && !importTypes.includes('alias')) {
|
||||
console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`);
|
||||
return;
|
||||
} else if(isRelative && !importTypes.includes('relative')) {
|
||||
console.log(`${pathFull} => Import is relative but import types does not specify relative`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// determine transformTo
|
||||
|
||||
// if there is only one TO then use it
|
||||
let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined;
|
||||
if(derivedTT === undefined) {
|
||||
// otherwise we find the TO by using the same index as the matching FROM extension type
|
||||
const tfIndex = transformFrom.findIndex((x) => {
|
||||
if(x === 'any') {
|
||||
return true;
|
||||
}
|
||||
if(x === 'none' && hasNoExt) {
|
||||
return true;
|
||||
}
|
||||
if(x === normalExt) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(tfIndex === -1) {
|
||||
console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`);
|
||||
return;
|
||||
}
|
||||
derivedTT = transformTo[tfIndex];
|
||||
}
|
||||
|
||||
let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`;
|
||||
|
||||
let transformedImport: string;
|
||||
|
||||
switch(derivedTT) {
|
||||
case 'none':
|
||||
if(hasNoExt) {
|
||||
console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`);
|
||||
return;
|
||||
}
|
||||
transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name);
|
||||
break;
|
||||
case 'any':
|
||||
if(hasNoExt && filenameFromDir) {
|
||||
transformedImport = filenameFromDir;
|
||||
} else {
|
||||
const otherFilename = fs.readdirSync(dir).find(x => {
|
||||
const pinfo = path.parse(x);
|
||||
return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext;
|
||||
});
|
||||
if(otherFilename === undefined) {
|
||||
console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
|
||||
return;
|
||||
}
|
||||
transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename);
|
||||
transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})`
|
||||
}
|
||||
break;
|
||||
default:
|
||||
transformedImport = combineDirFile(pathInfo.dir, `.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`);
|
||||
j(x).replaceWith(
|
||||
j.importDeclaration(
|
||||
x.node.specifiers,
|
||||
j.stringLiteral(transformedImport)
|
||||
)
|
||||
);
|
||||
|
||||
} else {
|
||||
console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Early exit condition
|
||||
* -----
|
||||
* It is often good practice to exit early and return the original source file
|
||||
* if it does not contain code relevant to the codemod.
|
||||
* See this page for more information:
|
||||
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output
|
||||
*/
|
||||
// if (/* Some condition here */ true) {
|
||||
// return file.source;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Codemod logic goes here 👇
|
||||
* -----
|
||||
* This is where the core logic for your codemod will go,
|
||||
* consider grouping specific actions into 'motions' and running them in sequence
|
||||
*
|
||||
* See this page for more information:
|
||||
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions
|
||||
*/
|
||||
//source.findVariableDeclarators('foo').renameTo('bar');
|
||||
|
||||
/**
|
||||
* Return your modified AST here 👇
|
||||
* -----
|
||||
* This is where your modified AST will be transformed back into a string
|
||||
* and written back to the file.
|
||||
*/
|
||||
return source.toSource(options.printOptions);
|
||||
}
|
||||
|
||||
const combineDirFile = (dir: string, file: string) => {
|
||||
if(dir === '') {
|
||||
return file;
|
||||
}
|
||||
return `${dir}${path.sep}${file}`;
|
||||
}
|
||||
@@ -8,5 +8,5 @@ These are **NOT** exhaustive examples. You should consult the **configuration**
|
||||
|
||||
Documentation at
|
||||
|
||||
* [internal docs](../docsite/docs/configuration/configuration.md)
|
||||
* [internal docs](../docsite/docs/configuration/configuration.mdx)
|
||||
* External Link: https://foxxmd.github.io/multi-scrobbler/docs/configuration
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"type": "azuracast",
|
||||
"enable": true,
|
||||
"name": "azura",
|
||||
"data": {
|
||||
"url": "ws://192.168.0.101",
|
||||
"station": "my-station-name",
|
||||
"monitorWhenLive": true,
|
||||
"monitorWhenListeners": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"data": {
|
||||
"blacklistedDevices": [],
|
||||
"whitelistedDevices": [],
|
||||
"blacklistedApps": [],
|
||||
"whitelistedApps": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"debugMode": false,
|
||||
"disableWeb": false,
|
||||
"sourceDefaults": {
|
||||
"maxPollRetries": 1,
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5,
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn",
|
||||
"logPlayerState": false,
|
||||
"scrobbleThresholds": {
|
||||
"duration": 30,
|
||||
"percent": 50
|
||||
},
|
||||
"options": {
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn",
|
||||
"logPlayerState": false
|
||||
}
|
||||
"maxPollRetries": 1,
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5
|
||||
},
|
||||
"clientDefaults": {
|
||||
"maxRequestRetries": 1,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"name": "DeezerOfficalDeprecated",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/deezer/callback",
|
||||
"interval": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,13 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "FoxxMDeezer",
|
||||
"name": "DeezerARL",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/deezer/callback",
|
||||
"interval": 60
|
||||
"arl": "UOsRPjT3U5Dhaaup3xQ30DWFoSgqugdSYzL3QE743waaVy2eur2ckyJcZ4RV123Q3PsbKXkLwA6ahc9XmEfev6BtfSP8LujE3JswWRQogZCDUOwVzWO5sAig",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"name": "myLfm",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"slug": "mySlug"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"name": "myLz",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "myIcy",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://myIcecast.com/radio"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,12 +4,24 @@
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"users": ["FoxxMD"],
|
||||
"servers": ["myServer","anotherServer"]
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
|
||||
// everything below is optional
|
||||
"usersAllow": ["FoxxMD","SomeOtherUser"],
|
||||
"usersBlock": ["AnotherUser"],
|
||||
"devicesAllow": ["firefox"],
|
||||
"devicesBlock": ["google-home"],
|
||||
"librariesAllow": ["GoodMusic"],
|
||||
"librariesBlock": ["BadMusic"],
|
||||
"additionalAllowedLibraryTypes": ["musicvideos"],
|
||||
"allowMediaTypes": ["audio"],
|
||||
"frontendUrlOverride": "https://myJellyfinExternal.tld"
|
||||
},
|
||||
"options": {
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn"
|
||||
"logPayload": true,
|
||||
"logFilterFailure": "debug"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "koito-client",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "koito-source",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name": "myLastFm",
|
||||
"name": "myLastFmClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
@@ -8,5 +8,15 @@
|
||||
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myLastFmSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,11 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "brainz",
|
||||
"name": "brainzClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "FoxxMD"
|
||||
"username": "FoxxMD",
|
||||
"url": "https://api.listenbrainz.org"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "brainzSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "FoxxMD",
|
||||
"url": "https://api.listenbrainz.org"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "myMaloja",
|
||||
"name": "myMaloja-client",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myMaloja-source",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MyMPD",
|
||||
"data": {
|
||||
"url": "192.168.0.100:6600",
|
||||
"password": "MY_PASSWORD"
|
||||
},
|
||||
"options": {
|
||||
"disableDiscovery": false
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"name": "myYamaha",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "192.168.0.101"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"type": "musikcube",
|
||||
"enable": true,
|
||||
"name": "musikcube",
|
||||
"data": {
|
||||
"url": "ws://localhost:7905",
|
||||
"password": "MY_PASSWORD"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,15 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"name": "MyPlexApi",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
"token": "1234",
|
||||
"url": "http://192.168.0.120:32400",
|
||||
"usersAllow": ["FoxxMD","SomeOtherUser"],
|
||||
"usersBlock": ["AnotherUser"],
|
||||
"devicesAllow": ["firefox"],
|
||||
"devicesBlock": ["google-home"],
|
||||
"librariesAllow": ["GoodMusic"],
|
||||
"librariesBlock": ["BadMusic"]
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true,
|
||||
"logFilterFailure": "debug",
|
||||
"ignoreInvalidCert": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "myRockskyClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",
|
||||
"handle": "alice.bsky.social"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myRockskySource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",
|
||||
"handle": "alice.bsky.social"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MySonos",
|
||||
"data": {
|
||||
"host": "192.168.0.150",
|
||||
"devicesAllow": ["move","beam"],
|
||||
"devicesBlock": ["roam"],
|
||||
"groupsAllow": ["family"],
|
||||
"groupsBlock": ["bedroom"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -5,7 +5,8 @@
|
||||
"data": {
|
||||
"url": "http://localhost:4040/airsonic",
|
||||
"user": "yourUser",
|
||||
"password": "yourPassword"
|
||||
"password": "yourPassword",
|
||||
"usersAllow": ["yourUser"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "myTeal",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3",
|
||||
|
||||
// everything below is optional
|
||||
"serviceAllow": ["plyr"],
|
||||
"serviceDeny": ["last.fm"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MyVlc",
|
||||
"data": {
|
||||
"url": "localhost:8080",
|
||||
"password": "changeme"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "MyWebScrobbler",
|
||||
"data": {
|
||||
"slug": null,
|
||||
"slug": "MyOptionalSlug",
|
||||
"whitelist": [],
|
||||
"blacklist": []
|
||||
}
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"cookie": "VISITOR_INFO1_LIVE=jMDXz2_L8rY; __Secure-3PAPISID=3AxsXpSXGqOInSDn1jEKn; DEVICE_INFO=ChxOekU0TmTBpjek5EWZ0G; YSC=7gZdl3Zdl3; SID=TwhNsaZRXYTAtXxzGyu6rZdpg2HvGROeW8J4Ym_FhkhoZMUYEQ.; __Secure-1PSID=TwhNOsaZRXYTyRBe4rxAtXRIKsIEtk_Qot2VLBNfHQrQ.; __Secure-3PSID=ZRXYTAtXRIKsIEtk_Qot2yRBerZdpg2HvvZRXYTAtXRIKsIEtk_Qot2yRBerkuZICFQ.; HSID=A1UMmELW79; SSID=AKhomOs; APISID=IlHHmuzkPdQzZZDhHn3; SAPISID=3AxsXpy0u75Qb/n1jEKn; __Secure-1PAPISID=3AxsXpQb/AkSDn1jEKn; LOGIN_INFO=AFmP6vFpyVCZZAIgDwbkhWMBBhluaIWAPP:QUQ314UW5NWMjNmd2ZUJnYnJsakdIMjZoaE5zVVMjNmd2ZZUiHRlb3ZlV3ZIcUVyRVIMjNmdjNmd2ZZUivYlNqX2ZNZUiHdUNFNFdaYmJIW1NkJRX3hqdlU2YnFESkFuSS1uTldnZVRmLXNjWFc5OUJuR3dTd3JsZGZYa2EtZFQ2a0k2Ry1KQQ==; PREF=volume=26; SIDCC=AFvI_94PxXwls-ndqpGfPgFX3FWj80y_94PxXwls-ndqfSh15sP; __Secure-1PSIDCC=AFvIBnUbRr96I96UCIp2U4T8HRVk2B0HfKzhzxwsiP; __Secure-3PSIDCC=AFvIB3bINuUN0ETDR9gO91wpwWIVmpGki3BxT3bINuUN0ETDR9gO91wCH",
|
||||
"authUser": "0"
|
||||
"cookie": "__Secure-1PSIDTS=sidts-CjEB3EgAEvCd-......",
|
||||
// either cookie or id/secret needs to be provided
|
||||
"clientId": "891098404....apps.googleusercontent.com",
|
||||
"clientSecret": "GOCS..."
|
||||
|
||||
// optional
|
||||
//"redirectUri": "http://my.custom.tld/api/ytmusic/callback?name=MyYTMusic"
|
||||
},
|
||||
"options": {
|
||||
"logDiff": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,29 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
multi-scrobbler:
|
||||
build:
|
||||
context: .
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
#environment:
|
||||
#- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- SPOTIFY_CLIENT_ID=
|
||||
#- SPOTIFY_CLIENT_SECRET=
|
||||
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
|
||||
#- MALOJA_URL=http://maloja:42010
|
||||
#- MALOJA_API_KEY=
|
||||
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
|
||||
# set if using a source/client with redirect URI that you have not explicitly set and MS is NOT running on the same machine that you will view the dashboard from
|
||||
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
|
||||
#- BASE_URL=http://MyHostIP:9078
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
#networks:
|
||||
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
|
||||
ports:
|
||||
- 9078:9078 # first port is the HOST port multi-scrobbler will serve UI and API on
|
||||
restart: unless-stopped
|
||||
@@ -1,14 +1,12 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- SPOTIFY_CLIENT_ID=
|
||||
#- SPOTIFY_CLIENT_SECRET=
|
||||
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
|
||||
#- JELLYFIN_APIKEY=
|
||||
#- JELLYFIN_URL=
|
||||
#- JELLYFIN_USER=
|
||||
#- MALOJA_URL=http://maloja:42010
|
||||
#- MALOJA_API_KEY=
|
||||
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
@@ -18,7 +16,7 @@ services:
|
||||
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
|
||||
#- BASE_URL=http://MyHostIP:9078
|
||||
volumes:
|
||||
- /path_on_host/multi-scrobbler-config:/config
|
||||
- "./config:/config"
|
||||
#networks:
|
||||
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
|
||||
ports:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
# NODE_ARGS can be passed by ENV in docker command like "docker run foxxmd/multi-scrobbler -e NODE_ARGS=--optimize_for_size"
|
||||
echo -e "\nmulti-scrobbler is starting!"
|
||||
export NODE_ENV=production
|
||||
cd /app || exit
|
||||
exec \
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/build/server.js
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/node_modules/.bin/tsx /app/src/backend/index.ts
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,119 +1,53 @@
|
||||
* [Connection Issues](#connection-issues)
|
||||
* [Plex/Tautulli/Jellyfin don't connect](#plextautullijellyfin-dont-connect)
|
||||
* [Jellyfin has warnings about undefined or missing data](#jellyfin-has-warnings-about-undefined-or-missing-data)
|
||||
* [Jellyfin has warnings about missing headers](#jellyfin-has-warnings-about-missing-headers)
|
||||
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
|
||||
* [Configuration Issues](#configuration-issues)
|
||||
* [Config could not be parsed](#config-could-not-be-parsed)
|
||||
* [Scrobbling Issues](#scrobbling-issues)
|
||||
* [Last.fm does not scrobble tracks with multiple artists correctly](#lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
* [Jellyfin does not scrobble tracks with multiple artists correctly](#jellyfin-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
# Connection Issues
|
||||
## Connection Issues
|
||||
|
||||
## Plex/Tautulli/Jellyfin don't connect
|
||||
### Webscrobbler/Listenbrainz Endpoint doesn't connect {#ingress-connection}
|
||||
|
||||
These three [sources](/#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
|
||||
These sources are **ingress-based** which means that multi-scrobbler waits for the Webscrobbler extension or app using Listenbrainz Endpoint to contact multi-scrobbler, as opposed to multi-scrobbler contacting them.
|
||||
|
||||
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
|
||||
multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:
|
||||
|
||||
```
|
||||
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] Received request from a new remote address: ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59)
|
||||
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) Received valid data from server examplePlex for the first time.
|
||||
2023-02-22T10:55:56-05:00 warn : [Plex Request ] Received valid Plex webhook payload but no Plex sources are configured
|
||||
[2025-07-10 12:32:21.417 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] Received request from a new remote address: ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0)
|
||||
[2025-07-10 12:32:21.418 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0) Received data for API URL slug '(none)' for the first time.
|
||||
```
|
||||
It also logs if a server tries to connect to a URL that it does not recognize:
|
||||
```
|
||||
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
|
||||
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.100 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
|
||||
```
|
||||
**So, if you do not see either of these in your logs then Plex/Tautulli/Jellyfin is not able to connect to your multi-scrobbler instance at all.**
|
||||
**So, if you do not see either of these in your logs then the Source is not able to connect to your multi-scrobbler instance at all.**
|
||||
|
||||
This is not something multi-scrobbler can fix and means you have an issue in your network.
|
||||
|
||||
### Troubleshooting
|
||||
#### Troubleshooting
|
||||
|
||||
Check or try all these steps before submitting an issue:
|
||||
|
||||
#### Turn on Debug Logging
|
||||
##### Turn on Debug Logging
|
||||
|
||||
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `LOG_LEVEL=debug`:
|
||||
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `DEBUG_MODE=true`. Then, check the output for any additional information.
|
||||
|
||||
* using node `LOG_LEVEL=debug ... node src/index.js`
|
||||
* using docker `docker run -e LOG_LEVEL=debug ... foxxmd/multi-scrobbler`
|
||||
##### Check Host name and URL
|
||||
|
||||
Check the output for any additional information.
|
||||
The URLs examples in the [configuration](/configuration#base-url) documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
|
||||
|
||||
#### Check Host name and URL
|
||||
EX `http://localhost:9078/api/webscrobbler` -> `http://192.168.0.140:9078/api/webscrobbler`
|
||||
|
||||
The URLs examples in the [configuration](configuration/configuration.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
|
||||
|
||||
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
|
||||
|
||||
#### Check Firewall and Port Forwarding
|
||||
##### Check Firewall and Port Forwarding
|
||||
|
||||
If the machine multi-scrobbler is running on has a firewall ensure that port **9078** is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine.
|
||||
|
||||
#### Check Source Service Logs
|
||||
##### Check Source Service Logs
|
||||
|
||||
Plex/Tautulli/Jellyfin all have logs that will log if they cannot connect to multi-scrobbler. Check these for further information.
|
||||
###### Webscrobbler
|
||||
|
||||
##### Plex
|
||||
See [Debugging the extension](https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension) to get logs which should have information about failed requests.
|
||||
|
||||
Settings -> Manage -> Console
|
||||
|
||||
##### Tautulli
|
||||
|
||||
Check the command-line output of the application or docker logs.
|
||||
|
||||
##### Jellyfin
|
||||
|
||||
Administration -> Dashboard -> Advanced -> Logs
|
||||
|
||||
## Jellyfin has warnings about undefined or missing data
|
||||
|
||||
Make sure you have
|
||||
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
|
||||
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
|
||||
|
||||
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
|
||||
|
||||
You can verify the payload sent from the webhook by modifying your jellyfin configuration to include `logPayload: true` which will output the raw payload to DEBUG level logging:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "MyJellyfin",
|
||||
"clients": [],
|
||||
"data": {
|
||||
"users": ["FoxxMD"],
|
||||
"options": {
|
||||
"logPayload": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
|
||||
|
||||
## Jellyfin has warnings about missing headers
|
||||
|
||||
If you experience issues trying to scrobble with Jellyfin and find this in your MS logs
|
||||
|
||||
```
|
||||
[API] Jellyfin is not sending a request with valid headers...
|
||||
```
|
||||
|
||||
A workaround that may fix this:
|
||||
|
||||
* In Webhook settings:
|
||||
* [In the webhook you have already configured...](configuration/configuration.md#jellyfin)
|
||||
* Add Request Header...
|
||||
* **Key:** `Content-Type`
|
||||
* **Value:** `application/json`
|
||||
* Then Save
|
||||
|
||||
## Spotify/Deezer/LastFM won't authenticate
|
||||
### Spotify/Deezer/LastFM won't authenticate
|
||||
|
||||
Ensure any **client id** or **secrets** are correct in your configuration.
|
||||
|
||||
@@ -126,9 +60,46 @@ If multi-scrobbler is not running on the same machine your browser is on then th
|
||||
|
||||
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
|
||||
|
||||
# Configuration Issues
|
||||
### I get errors with `ECONNRESET` `ETIMEDOUT` `ENOTFOUND` `EAI_AGAIN`
|
||||
|
||||
## Config could not be parsed
|
||||
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
|
||||
|
||||
```
|
||||
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#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
|
||||
|
||||
Refresh your authentication by using the **(Re)authenticate** link from MS's web dashboard.
|
||||
|
||||
#### When using Cookies Authentication
|
||||
|
||||
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#ytm-auth) and then restart MS to potentially resolve the problem.
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
### Config could not be parsed
|
||||
|
||||
If you see something like this in your logs:
|
||||
|
||||
@@ -140,14 +111,124 @@ If you see something like this in your logs:
|
||||
|
||||
It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like [JSONLint](https://jsonlint.com/) to find out where errors you have and fix them.
|
||||
|
||||
# Scrobbling Issues
|
||||
## Scrobbling Issues
|
||||
|
||||
## Last.fm does not scrobble tracks with multiple artists correctly
|
||||
### Last.fm does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
|
||||
|
||||
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
|
||||
|
||||
## Jellyfin does not scrobble tracks with multiple artists correctly
|
||||
### Google Cast track information is missing/incorrect or MS player has weird times
|
||||
|
||||
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
|
||||
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
|
||||
|
||||
To diagnose these issues you [**must enable payload logging**](/configuration/sources/google-cast#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
|
||||
|
||||
### Google Cast device does not track media
|
||||
|
||||
It is likely the app playing on the cast device is incorrectly reporting the media type as **not music**.
|
||||
|
||||
MS logs will tell you what type the media is reported as with lines like:
|
||||
|
||||
```
|
||||
My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track
|
||||
```
|
||||
|
||||
Refer to [Allow Unknown Media Type](/configuration/sources/google-cast#allow-unknown-media-type) section to fix this
|
||||
|
||||
```
|
||||
My Artist - Example Track has 'movie' media type so will not track
|
||||
```
|
||||
|
||||
Refer to [Force Media Tracking](/configuration/sources/google-cast#forcing-media-tracking) section to fix this
|
||||
|
||||
### VLC is not scrobbling fields correctly
|
||||
|
||||
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](/configuration/sources/vlc#vlc-information-reporting)
|
||||
|
||||
### Youtube Music misses or duplicates scrobbles
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How MS detects YTM history</summary>
|
||||
|
||||
In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs **a source of truth.** For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).
|
||||
|
||||
To compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.
|
||||
|
||||
Duplicate scrobbles can also occur if the change between two checks is technically consistent. For instance, if you listen to a track twice in some period, separated by other music, YTM will sometimes "remove" the track from the earlier time (further down in your history) and "re-add" it at the top of the history.
|
||||
|
||||
</details>
|
||||
|
||||
#### Reporting YTM scrobble issues
|
||||
|
||||
If you experience scrobble detection issues you can help improve MS's YTM heureistic by providing thorough feedback as [an issue.](https://github.com/FoxxMD/multi-scrobbler/issues/new?assignees=&labels=bug&projects=&template=01-bug-report.yml&title=bug%3A+) **Please do the following to provide the most useful report:**
|
||||
|
||||
##### Turn on Change Detection
|
||||
|
||||
In your YTM configuration (`ytmusic.json`) add `logDiff` under `options` like this:
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "MyYTM",
|
||||
"data": { ... },
|
||||
"options": {
|
||||
"logDiff": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or set either ENVs:
|
||||
|
||||
* `YTM_LOG_DIFF=true`
|
||||
* [`DEBUG_MODE=true`](/configuration#debug-mode)
|
||||
|
||||
This will cause MS to log YTM history changes similar to this:
|
||||
|
||||
```
|
||||
[Ytmusic - MyYTM] Changes from last seen list:
|
||||
1. (tuhe1CpHRxY) KNOWER - I’m The President --- undefined => Moved - Originally at 6
|
||||
2. (Mtg8V6Xa2nc) Vulfpeck - Romanian Drinking Song --- Schvitz => Moved - Originally at 1
|
||||
3. (rxbCaiyYSXM) Nightmares On Wax - You Wish --- In A Space Outta Sound => Moved - Originally at 2
|
||||
4. (tMt_YXr90AM) Gorillaz - O Green World --- undefined => Moved - Originally at 3
|
||||
...
|
||||
```
|
||||
|
||||
Which are essential to troubleshooting this behavior.
|
||||
|
||||
|
||||
##### Turn on File Logging
|
||||
|
||||
Optionally, if your logs are busy due to many other sources, you can log **only** YTM logs to a file. Turn this on with the `logToFile` config option:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "MyYTM",
|
||||
"data": { ... },
|
||||
"options": {
|
||||
"logDiff": true,
|
||||
"logToFile": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will cause MS to write YTM logs to a file in your `logs` folder named `ytmusic-MyYTM.log` (based on your source name).
|
||||
|
||||
##### Provide Detail and Context
|
||||
|
||||
Provide a detailed account of how you were using YTM when the issue occurred, including things like:
|
||||
|
||||
* the platform listening on (desktop, mobile, 3rd party client, etc...)
|
||||
* any changes in platform
|
||||
* > I switched from desktop to listening on my phone...
|
||||
* how you were listening to music
|
||||
* > I was playing an album start to finish
|
||||
* > I listened to two songs in a row, then browsed for a new song in library by artist, then went back to a song in the queue...
|
||||
|
||||
Explain the expected behavior (it should have scrobbled songs x, y, then z) and what actually happened (it scrobbled songs x, then y, then x again, then z)
|
||||
|
||||
Provide ALL logs from the time when the issue occurred including logs from BEFORE (ideally 2-3 minutes of logs) and AFTER the issue.
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"position": 2,
|
||||
"position": 5,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Configuring Multi-Scrobbler and examples"
|
||||
"type": "doc",
|
||||
"id": "configuration/configuration"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Scrobble Clients",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "configuration/clients/clients"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
|
||||
|
||||
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
|
||||
## Clients
|
||||
|
||||
| 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) | ❌ |
|
||||
| [Rocksky](/configuration/clients/rocksky) | ❌ |
|
||||
| [Teal.fm](/configuration/clients/tealfm) | ❌ |
|
||||
|
||||
## Features
|
||||
|
||||
### 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.
|
||||
|
||||
After it stops _automatically_ retrying the scrobble still exists and can be retried from the dashboard. Dead scrobbles persist between restart so you will never lose a scrobble that hasn't been successfully submitted yet.
|
||||
|
||||
### Now Playing
|
||||
|
||||
Multi-scrobbler can report the currently **playing** tracks it is monitoring to some Scrobble Clients via their individual **Now Playing** functionality.
|
||||
|
||||
Now Playing is **default enabled** for all Clients that support it.
|
||||
|
||||
The behavior multi-scrobbler uses for determining Now Playing reporting:
|
||||
|
||||
* Now Playing can be explicitly enabled or disabled globally using ENV `NOW_PLAYING=true` or `NOW_PLAYING=false`
|
||||
* This only affects Clients that don't have behavior set via File/AIO (below)
|
||||
* Now Playing will be only be reported to the same Clients a Source is configured to scrobble to
|
||||
* A Source's Player status must be **Playing**, IE **active**, for reporting to occur
|
||||
* This status can be verified via Player UI on the Dashboard
|
||||
* If multiple Sources having active Players then the scrobble Client will default to reporting the track based on Source **configuration** name, alphabetically
|
||||
* This is the `name` property set in [File or AIO source configs](/configuration?configType=file#configuration-types)
|
||||
|
||||
Clients can customize the Now Playing behavior individually using [File or AIO source configs:](/configuration?configType=file#configuration-types)
|
||||
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myLastFmClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
// ...
|
||||
},
|
||||
"options": {
|
||||
// disable or enable
|
||||
//"nowPlaying": true
|
||||
//
|
||||
// OR define a list of Source *config* names that should be allowed to report Now Playing
|
||||
// order of the list determines priority for reporting Now Playing
|
||||
//"nowPlaying": ["mySpotify1","myJellyfin2"]
|
||||
}
|
||||
},
|
||||
]
|
||||
```
|
||||
@@ -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,37 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Discord
|
||||
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)
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Koito
|
||||
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/koito.json.example';
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To monitor and re-scrobble activity from a Koito account create a [Koito (Source)](/configuration/sources/koito-source)
|
||||
|
||||
:::
|
||||
|
||||
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 `KOITO_USER`)
|
||||
* 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` => `KOITO_URL` is set to `http://192.168.0.100:4110`
|
||||
|
||||
:::note[Koito Base URL]
|
||||
|
||||
Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/guides/scrobbler/) **is not required.** The Koito URL used for multi-scrobbler should only be the **base** URL. Only include a URL path if your Koito **base** url is not at `host:port`. See the table below for examples.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Base URL Examples</summary>
|
||||
| `KOITO_URL` | MS Detected Base URL | Example Scrobble Api URL |
|
||||
| :-------------------------------------------- | :------------------------------ | ---------------------------------------------------- |
|
||||
| `http://192.168.0.100:4110` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
|
||||
| `https://koito.mydomain.com` | `https://koito.mydomain.com` | `https://koito.mydomain.com/apis/listenbrainz/1` |
|
||||
| `http://192.168.0.100:4110/apis/listenbrainz` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
|
||||
| `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito/apis/listenbrainz/1` |
|
||||
</details>
|
||||
:::
|
||||
|
||||
|
||||
:::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.
|
||||
|
||||
:::
|
||||
|
||||
## Configuration
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Last.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/lastfm.json.example';
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
* 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)
|
||||
|
||||
:::
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
http://192.168.0.100:9078/lastfm/callback
|
||||
```
|
||||
|
||||
</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 |
|
||||
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
|
||||
</Config>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Listenbrainz
|
||||
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/listenbrainz.json.example';
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
* To monitor and re-scrobble activity from a Listenbrainz account create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
|
||||
|
||||
:::
|
||||
|
||||
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
|
||||
|
||||
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Maloja
|
||||
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/maloja.json.example';
|
||||
|
||||
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Maloja Setup Intructions</summary>
|
||||
|
||||
Using Maloja's example `docker-compose.yml`:
|
||||
|
||||
```yaml reference title="~/malojaData/docker-compose.yml"
|
||||
https://github.com/krateng/maloja/blob/master/example-compose.yml
|
||||
```
|
||||
|
||||
Uncomment `environment` and add `MALOJA_FORCE_PASSWORD=CHANGE_ME` to set an admin password
|
||||
|
||||
Start the container:
|
||||
|
||||
```shell title="~/malojaData"
|
||||
docker compose up -d
|
||||
```
|
||||
</details>
|
||||
|
||||
* Navigate to the Admin Panel (Cog in upper-right corner) -> API Keys (or at http://myMalojaServerIP/admin_apikeys)
|
||||
* Create a **New Key** and then copy the generated key value
|
||||
|
||||
Finally, add the Maloja server URL and API Key to the configuration type you choose to use, below.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="MalojaClientConfig" fileContent={JsonConfig} client name="maloja">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|-------------------------------|
|
||||
| `MALOJA_URL` | Yes | | Base URL of your installation |
|
||||
| `MALOJA_API_KEY` | Yes | | Api Key |
|
||||
</Config>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Rocksky
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
|
||||
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)](/configuration/sources/rocksky-source)
|
||||
|
||||
:::
|
||||
|
||||
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How does that work?</summary>
|
||||
|
||||
<Bluesky name="Rocksky" lexicon="app.rocksky"/>
|
||||
|
||||
</details>
|
||||
|
||||
<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>
|
||||
|
||||
<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.
|
||||
|
||||
To do this it *asynchronously* matches your scrobble data against Musicbrainz's and/or Spotify's catalogs, *after the scrobble has been submitted.* **If the scrobble does not end up matching then it is not persisted.**
|
||||
|
||||
If you need your scrobbles to be 100% captured then you should scrobble to an *additional* [Scrobble Client](/configuration/clients).
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
## Setup
|
||||
|
||||
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
|
||||
|
||||
:::note
|
||||
|
||||
Usage is adapted from [Rocksky docs](https://docs.rocksky.app/migrating-to-rocksky-scrobble-api-957839m0) on [docs.rocksky.app](https://docs.rocksky.app)
|
||||
|
||||
:::
|
||||
|
||||
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Bluesky account
|
||||
* Then, navigate to the [RockSky developer dashboard](https://rocksky.app/apikeys) and obtain an API Key
|
||||
|
||||
## Configuration
|
||||
|
||||
### Handle
|
||||
|
||||
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
|
||||
|
||||
* For regular Bluesky account this will be like: `alice.bsky.social`
|
||||
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
|
||||
* For non-Bluesky users, you probably already know your handle
|
||||
|
||||
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">
|
||||
| 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 |
|
||||
</Config>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Teal.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/tealfm.json.example';
|
||||
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (Source)](/configuration/sources/tealfm-source)
|
||||
|
||||
:::
|
||||
|
||||
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How does that work?</summary>
|
||||
|
||||
<Bluesky name="teal.fm" lexicon="fm.teal"/>
|
||||
|
||||
</details>
|
||||
|
||||
<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>
|
||||
|
||||
<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 true production website, yet.
|
||||
|
||||
To view your teal.fm scrobble data you can:
|
||||
|
||||
* 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
|
||||
|
||||
#### Bluesky
|
||||
|
||||
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
|
||||
* Navigate to https://bsky.app/settings/app-passwords
|
||||
* Settings -> Privacy and Security -> App passwords
|
||||
* Create a new **App Password** and save it somewhere, it will not be shown again
|
||||
|
||||
## Configuration
|
||||
|
||||
### Identifier
|
||||
|
||||
The ATPRoto identifier used with multi-scrobbler should be either:
|
||||
|
||||
* A valid [DID](https://atproto.com/specs/did#at-protocol-did-identifier-syntax), starting with `did:plc:...` or `did:web:...`
|
||||
* Your **full** ATProto handle, including TLD
|
||||
|
||||
If using a handle:
|
||||
|
||||
* For regular Bluesky account this will be like: `alice.bsky.social`
|
||||
* For Bluesky accounts that [use their domain as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
|
||||
* For non-Bluesky-PDS users, you probably already know your 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 |
|
||||
</Config>
|
||||
@@ -1,854 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
* [Overview](#overview)
|
||||
* [ENV-Based Configuration](#env-based-configuration)
|
||||
* [File-Based Configuration](#file-based-configuration)
|
||||
* [All-in-One File Configuration](#all-in-one-file-configuration)
|
||||
* [Specific File Configuration](#specific-file-configuration)
|
||||
* [Source Configurations](#source-configurations)
|
||||
* [Spotify](#spotify)
|
||||
* [Plex](#plex)
|
||||
* [Tautulli](#tautulli)
|
||||
* [Subsonic](#subsonic)
|
||||
* [Jellyfin](#jellyfin)
|
||||
* [Last.fm (Source)](#lastfm--source-)
|
||||
* [Listenbrainz (Source)](#listenbrainz--source-)
|
||||
* [Deezer](#deezer)
|
||||
* [Youtube Music](#youtube-music)
|
||||
* [MPRIS (Linux Desktop)](#mpris)
|
||||
* [Mopidy](#mopidy)
|
||||
* [JRiver](#jriver)
|
||||
* [Kodi](#kodi)
|
||||
* [WebScrobbler](#webscrobbler)
|
||||
* [Client Configurations](#client-configurations)
|
||||
* [Maloja](#maloja)
|
||||
* [Last.fm](#lastfm)
|
||||
* [Listenbrainz](#listenbrainz)
|
||||
* [Monitoring](#monitoring)
|
||||
* [Webhooks](#webhook-configurations)
|
||||
* [Health Endpoint](#health-endpoint)
|
||||
|
||||
# Overview
|
||||
|
||||
[**Sources** and **Clients**](/#how-does-multi-scrobbler-ms-work) are configured using environmental (ENV) variables and/or json files.
|
||||
|
||||
**MS will parse configuration from both configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
|
||||
|
||||
TIP: Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
|
||||
## ENV-Based Configuration
|
||||
|
||||
This is done by passing environmental variables and so does not require any files to run MS.
|
||||
|
||||
* Using a docker container EX `docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...`
|
||||
* Using a local installations by exporting variables before running MS EX `SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret node index.js`
|
||||
|
||||
Use ENV-based configuration if:
|
||||
|
||||
* You are the only person for whom MS is scrobbling for
|
||||
* You have a very simple setup for MS such as one scrobble [Client](/#client) and one [Source](/#source) IE Plex -> Maloja
|
||||
|
||||
## File-Based Configuration
|
||||
|
||||
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
|
||||
|
||||
* Local installation -> `PROJECT_DIR/config`
|
||||
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.md#docker) for how to configure this correctly
|
||||
|
||||
Use File-based configuration if:
|
||||
|
||||
* You have many [Sources](/#source)
|
||||
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
|
||||
Funkwhale...
|
||||
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
|
||||
* You want only some **Sources** to scrobble to some **Clients** IE Fred's Spotify account scrobbles to Fred's Maloja
|
||||
server, but not Mary's Maloja server
|
||||
|
||||
File-based configurations located in the `CONFIG_DIR` directory can be parsed from
|
||||
|
||||
* an **all-in-one** config file named `config.json` that contains information for all Sources and Clients and/or
|
||||
* many **specific** files named based on the client/source to configure IE `plex.json` `spotify.json`
|
||||
|
||||
There are **example configurations** for all Source/Client types and AIO config located in the [/config](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
|
||||
For docker installations these examples are copied to your configuration directory on first-time use.
|
||||
|
||||
There is also a [**kitchensink example**](kitchensink.md) that provides examples of using all sources/clients in a complex configuration.
|
||||
|
||||
### All-in-One File Configuration
|
||||
|
||||
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
Example directory structure:
|
||||
|
||||
```
|
||||
/CONFIG_DIR
|
||||
config.json
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
//...
|
||||
"sources": [
|
||||
{
|
||||
"name": "myConfig",
|
||||
"type": "spotify",
|
||||
"clients": [
|
||||
"myMalojaClient"
|
||||
],
|
||||
"data": {
|
||||
"clientId": "anExample"
|
||||
//...
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"name": "myFirstMalojaClient",
|
||||
"type": "maloja",
|
||||
"data": {
|
||||
"url": "http://myMalojaServer.example",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
`config.json` can also be used to set default behavior for all sources/clients using `sourceDefaults` and `clientDefaults` properties.
|
||||
|
||||
See [config.json.example](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/config.json.example) for an annotated example or check out [the kitchen sink example](kitchensink.md).
|
||||
|
||||
### Specific File Configuration
|
||||
|
||||
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
|
||||
|
||||
Example directory structure:
|
||||
|
||||
```
|
||||
/CONFIG_DIR
|
||||
plex.json
|
||||
spotify.json
|
||||
maloja.json
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
```json5
|
||||
// in maloja.json
|
||||
[
|
||||
{
|
||||
"name": "myFirstMalojaClient",
|
||||
"data": {
|
||||
"url": "http://myMalojaServer.example",
|
||||
"apiKey": "myKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mySecondMalojaClient",
|
||||
"data": {
|
||||
"url": "http://my2ndMalojaServer.example",
|
||||
"apiKey": "myKey"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
See the [/config](https://github.com/FoxxMD/multi-scrobbler/blob/master/config) directory of this project for examples of each type of config file or reference specific files below.
|
||||
|
||||
# Application Options
|
||||
|
||||
These options affect multi-scrobbler's behavior and are not specific to any source/client.
|
||||
|
||||
#### Base URL
|
||||
|
||||
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](#spotify), [lastfm](#lastfm), and [deezer](#deezer) -- as well as some logging hints.
|
||||
|
||||
* Default => `http://localhost`
|
||||
* Set with [ENV](#env-based-configuration) `BASE_URL` or `baseUrl` [all-in-one configuration](#all-in-one-file-configuration)
|
||||
|
||||
EX: Lastfm Redirect Url is `BASE_URL:PORT/lastfm/callback` -- Set `BASE_URL=http://192.168.0.101` => Redirect URL is `http://192.168.0.101:9078/lastfm/callback` (when no other redirectUri is specified for [lastfm configuration](#lastfm))
|
||||
|
||||
Useful when running with [docker](../installation/installation.md#docker) so that you do not need to specify redirect URLs for each configuration.
|
||||
|
||||
# Source Configurations
|
||||
|
||||
## [Spotify](https://www.spotify.com)
|
||||
|
||||
To access your Spotify history you must [register an application](https://developer.spotify.com/dashboard) to get a
|
||||
Client ID/Secret. Make sure to also whitelist your redirect URI in the application settings.
|
||||
|
||||
**NOTE:** If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-is-Automix/ta-p/5257278) enabled and Spotify uses it for your playlist/queue then MS cannot accurately determine when a track will end. This is because the track is "mixed" in your queue with a shorter play time than its actual length and [Spotify does not report this modified play time in its API.](https://community.spotify.com/t5/Spotify-for-Developers/Wrong-duration-ms-of-track-with-Automix/m-p/5429147) This **does not affect MS's ability to scrobble** from Spotify but it will affect the accuracy of the duration MS reports was played.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|----------------------------|-----------|----------------------------------|----------------------------------------------------|
|
||||
| `SPOTIFY_CLIENT_ID` | Yes | | |
|
||||
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
|
||||
| `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`spotify.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/spotify.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSpotifySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Plex](https://plex.tv)
|
||||
|
||||
Check the [instructions](plex.md) on how to setup a [webhooks](https://support.plex.tv/articles/115002267687-webhooks) to scrobble your plays.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|---------|-------------------------------------------------|
|
||||
| `PLEX_USER` | No | | The a comma-delimited list of usernames to scrobble tracks for. No usernames specified means all tracks by all users will be scrobbled. |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`plex.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/plex.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FPlexSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Tautulli](https://tautulli.com)
|
||||
|
||||
Check the [instructions](plex.md) on how to setup a notification agent.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|---------|-------------------------------------------------|
|
||||
| `TAUTULLI_USER` | No | | The a comma-delimited list of usernames to scrobble tracks for. No usernames specified means all tracks by all users will be scrobbled. |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`tautulli.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/tautulli.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FTautulliSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Subsonic](http://www.subsonic.org/)
|
||||
|
||||
Can use this source for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) and supports the [`getNowPlaying`](http://www.subsonic.org/pages/api.jsp#getNowPlaying) endpoint (such as [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
|
||||
**Known Issues:**
|
||||
* "Time played at" is somewhat inaccurate since the api only reports "played X minutes ago" so...
|
||||
* All scrobble times are therefore "on the minute" and you may experience occasional duplicate scrobbles
|
||||
* "played X minutes ago" sometimes is also not reported correctly
|
||||
* Multiple artists are reported as one value and cannot be separated
|
||||
* If using [Airsonic Advanced](https://github.com/airsonic-advanced/airsonic-advanced) the password used (under **Credentials**) must be **Decodable**
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|----------------------------|-----------|----------------------------------|----------------------------------------------------|
|
||||
| `SUBSONIC_USER` | Yes | | |
|
||||
| `SUBSONIC_PASSWORD` | Yes | | |
|
||||
| `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`subsonic.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/subsonic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSubSonicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Jellyfin](https://jellyfin.org/)
|
||||
|
||||
Must be using Jellyfin 10.7 or greater
|
||||
|
||||
* In the Jellyfin desktop web UI Navigate to -> Administration -> Dashboard -> Plugins -> Catalog
|
||||
* Under Notifications -> **Webhook** -> Install, then restart your server
|
||||
* Navigate back to -> Administration -> Dashboard -> Plugins -> My Plugins -> Webhook
|
||||
* Click "..." -> Settings
|
||||
* In Webhook settings:
|
||||
* `Add Generic Destination`
|
||||
* In the new `Generic` dropdown:
|
||||
* Webhook Url: `http://localhost:9078/jellyfin`
|
||||
* Notification Type: `Playback Progress`
|
||||
* Item Type: `Songs`
|
||||
* Check `Send All Properties`
|
||||
* Save
|
||||
|
||||
If you see errors in the MS logs regarding `missing headers` when using Jellyfin [see this workaround.](../FAQ.md#jellyfin-has-warnings-about-missing-headers)
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|-------------------------------------------------------------------|
|
||||
| `JELLYFIN_USER` | | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
|
||||
| `JELLYFIN_SERVER` | | | Comma-separated list of Jellyfin server names to scrobble from |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`jellyfin.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/jellyfin.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FJellySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Last.fm (Source)](https://www.last.fm)
|
||||
|
||||
See the [Last.fm (Client)](#lastfm) setup for registration instructions. You may need to disable "Hide recent listening information" on your [privacy page](https://www.last.fm/settings/privacy) for this to work.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
No support for ENV based for Last.fm as a client (only source)
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`lastfm.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/lastfm.json.example), change `configureAs` to `source`. Or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Listenbrainz (Source)](https://listenbrainz.org)
|
||||
|
||||
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
|
||||
|
||||
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
|
||||
|
||||
**NOTE:** You cannot use ENV variables shown in the [Listenbrainz Client config](#listenbrainz) -- multi-scrobbler assumes Listenbrainz ENVs are always used for the **client** configuration. You must use the file-based config from below to setup Listenbrainz as a Source.
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`listenbrainz.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
**Change `configureAs` to `source`**
|
||||
|
||||
## [Deezer](https://deezer.com/)
|
||||
|
||||
Create a new application at [Deezer Developers](https://developers.deezer.com/myapps)
|
||||
|
||||
* Application Domain must be the same as your multi-scrobbler domain. Default is `localhost:9078`
|
||||
* Redirect URL must end in `deezer/callback`
|
||||
* Default would be `http://localhost:9078/deezer/callback`
|
||||
|
||||
After application creation you should have credentials displayed in the "My Apps" dashboard. You will need:
|
||||
|
||||
* **Application ID**
|
||||
* **Secret Key**
|
||||
* **Redirect URL** (if not the default)
|
||||
|
||||
**If no access token is provided...**
|
||||
|
||||
After starting multi-scrobbler with credentials in-place open the dashboard (`http://localhost:9078`) and find your Deezer source. Click **(Re)authenticate and (re)start polling** to start the login process. After login is complete polling will begin automatically.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|----------------------------|-----------|-----------------------------------------|----------------------------------------------------|
|
||||
| `DEEZER_CLIENT_ID` | Yes | | Your **Application ID** |
|
||||
| `DEEZER_CLIENT_SECRET` | Yes | | Your **Secret Key** |
|
||||
| `DEEZER_REDIRECT_URI` | No | `http://localhost:9078/deezer/callback` | URI must end in `deezer/callback` |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`deezer.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/deezer.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FDeezerSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Youtube Music](https://music.youtube.com)
|
||||
|
||||
Credentials for YT Music are obtained from a browser request to https://music.youtube.com **once you are logged in.** [Specific requirements are here and summarized below:](https://github.com/nickp10/youtube-music-ts-api/blob/master/DOCUMENTATION.md#authenticate)
|
||||
|
||||
* Open a new tab
|
||||
* Open the developer tools (Ctrl-Shift-I) and select the “Network” tab
|
||||
* Go to https://music.youtube.com and ensure you are logged in
|
||||
|
||||
Then...
|
||||
|
||||
1. Find and select an authenticated POST request. The simplest way is to filter by /browse using the search bar of the developer tools. If you don’t see the request, try scrolling down a bit or clicking on the library button in the top bar.
|
||||
2. **Make sure **Headers** pane is selected and open
|
||||
3. In the **Request Headers** section find and copy the **entire value** found after `Cookie:` and use this as the `cookie` value in your multi-scrobbler config
|
||||
4. If present, in the **Request Headers** section find and copy the number found in `X-google-AuthUser` and use this as the value for `authUser` in your multi-scrobbler config
|
||||
|
||||

|
||||
|
||||
NOTES:
|
||||
|
||||
* YT Music authentication is "browser based" which means your credentials may expire after a (long?) period of time OR if you log out of https://music.youtube.com. In the event this happens just repeat the steps above to get new credentials.
|
||||
* Communication to YT Music is **unofficial** and not supported or endorsed by Google. This means that **this integration may stop working at any time** if Google decides to change how YT Music works in the browser.
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`ytmusic.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/ytmusic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FYTMusicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/)
|
||||
|
||||
MPRIS is a standard interface for communicating with Music Players on **linux operating systems.**
|
||||
|
||||
If you run Linux and have a notification tray that shows what media you are listening to, you likely have access to MPRIS.
|
||||
|
||||

|
||||
|
||||
multi-scrobbler can listen to this interface and scrobble tracks played by **any media player** that communicates to the operating system with MPRIS.
|
||||
|
||||
**NOTE:** multi-scrobbler needs to be running as a [**Local Installation**](../installation/installation.md#local) in order to use MPRIS. This cannot be used from docker.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|----------------------------------------------------------------------------------|
|
||||
| MPRIS_ENABLE | No | | Use MPRIS as a Source (useful when you don't need any other options) |
|
||||
| MPRIS_BLACKLIST | No | | Comma-delimited list of player names not to scrobble from |
|
||||
| MPRIS_WHITELIST | No | | Comma-delimited list of players names to ONLY scrobble from. Overrides blacklist |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`mpris.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/mpris.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMPRISSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Mopidy](https://mopidy.com/)
|
||||
|
||||
Mopidy is a headless music server that supports playing music from many [standard and non-standard sources such as Pandora, Bandcamp, and Tunein.](https://mopidy.com/ext/)
|
||||
|
||||
multi-scrobbler can scrobble tracks played from any Mopidy backend source, regardless of where you listen to them.
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`mopidy.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/mopidy.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMopidySourceConfig/%23%2Fdefinitions%2FMopidyData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
Configuration Options:
|
||||
|
||||
##### `url`
|
||||
|
||||
The URL used to connect to the Mopidy server. You MUST have [Mopidy-HTTP extension](https://mopidy.com/ext/http) enabled.
|
||||
|
||||
If no `url` is provided a default is used which assumes Mopidy is installed on the same server as multi-scrobbler: `ws://localhost:6680/mopidy/ws/`
|
||||
|
||||
Make sure the hostname and port number match what is found in the Mopidy configuration file `mopidy.conf`:
|
||||
|
||||
```
|
||||
...
|
||||
|
||||
[http]
|
||||
hostname = localhost
|
||||
port = 6680
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
|
||||
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
|
||||
|
||||
Part => Default Value
|
||||
|
||||
* Protocol => `ws://`
|
||||
* Hostname => `localhost`
|
||||
* Port => `6680`
|
||||
* Path => `/mopidy/ws/`
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "mopidy.mydomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `ws://mopidy.mydomain.com:6680/mopidy/ws/`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "192.168.0.101:3456"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `ws://192.168.0.101:3456/mopidy/ws/`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "mopidy.mydomain.com:80/MOPWS"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `ws://mopidy.mydomain.com:80/MOPWS`
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
#### URI Blacklist/Whitelist
|
||||
|
||||
If you wish to disallow or only allow scrobbling from some sources played through Mopidy you can specify these using `uriBlacklist` or `uriWhitelist` in your config. multi-scrobbler will check the list to see if any string matches the START of the `uri` on a track. If whitelist is used then blacklist is ignored. All strings are case-insensitive.
|
||||
|
||||
EX:
|
||||
|
||||
```json
|
||||
{
|
||||
"uriBlacklist": ["soundcloud"]
|
||||
}
|
||||
```
|
||||
|
||||
Will prevent multi-scrobbler from scrobbling any Mopidy track that start with a `uri` like `soundcloud:song:MySong-1234`
|
||||
|
||||
#### Album Blacklist
|
||||
|
||||
For certain sources (Soundcloud) Mopidy does not have all track info (Album) and will instead use "Soundcloud" as the Album name. You can prevent multi-scrobbler from using this bad Album data by adding the fake Album name to this list. Multi-scrobbler will still scrobble the track, just without the bad data. All strings are case-insensitive.
|
||||
|
||||
EX:
|
||||
|
||||
```json
|
||||
{
|
||||
"albumBlacklist": ["SoundCloud", "Mixcloud"]
|
||||
}
|
||||
```
|
||||
|
||||
If a track would be scrobbled like `Album: Soundcloud, Track: My Cool Track, Artist: A Cool Artist`
|
||||
then multi-scrobbler will instead scrobble `Track: My Cool Track, Artist: A Cool Artist`
|
||||
|
||||
## [JRiver](https://jriver.com/)
|
||||
|
||||
In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:
|
||||
|
||||
* Tools -> Options -> Media Network
|
||||
* Check `Use Media Network to share this library...`
|
||||
* If you have `Authentication` checked you will need to provide the **Username** and **Password** in the ENV/File configuration below.
|
||||
|
||||
#### URL
|
||||
|
||||
If you do not provide a URL then a default is used which assumes JRiver is installed on the same server as multi-scrobbler: `http://localhost:52199/MCWS/v1/`
|
||||
|
||||
* Make sure the port number matches what is found in `Advanced` section in the [Media Network](#jriver) options.
|
||||
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
|
||||
|
||||
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
|
||||
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
|
||||
|
||||
Part => Default Value
|
||||
|
||||
* Protocol => `http://`
|
||||
* Hostname => `localhost`
|
||||
* Port => `52199`
|
||||
* Path => `/MCWS/v1/`
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "jriver.mydomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://jriver.mydomain.com:52199/MCWS/v1/`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "192.168.0.101:3456"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://192.168.0.101:3456/MCWS/v1/`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "mydomain.com:80/jriverReverse/MCWS/v1/"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://mydomain.com:80/jriverReverse/MCWS/v1/`
|
||||
|
||||
</details>
|
||||
|
||||
### ENV-Based
|
||||
|
||||
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|---------------------------------|------------------------------------------------|
|
||||
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
|
||||
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
|
||||
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
|
||||
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`jriver.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/jriver.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FJRiverSourceConfig/%23%2Fdefinitions%2FJRiverData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Kodi](https://kodi.tv/)
|
||||
|
||||
In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can can be in the Kodi GUI:
|
||||
|
||||
* Settings -> Services -> Control
|
||||
* Check `Allow remote control via HTTP`
|
||||
* Ensure you have a **Username** and **Password** set, you will need to provide them in the ENV/File configuration below.
|
||||
|
||||
#### URL
|
||||
|
||||
If you do not provide a URL then a default is used which assumes Kodi is installed on the same server as multi-scrobbler: `http://localhost:8080/jsonrpc`
|
||||
|
||||
* Make sure the port number matches what is found in **Port** in the [Control](#kodi) section mentioned above.
|
||||
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
|
||||
|
||||
The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
|
||||
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.
|
||||
|
||||
Part => Default Value
|
||||
|
||||
* Protocol => `http://`
|
||||
* Hostname => `localhost`
|
||||
* Port => `8080`
|
||||
* Path => `/jsonrpc`
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "kodi.mydomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://kodi.mydomain.com:8080/jsonrpc`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "192.168.0.101:3456"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://192.168.0.101:3456/jsonprc`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "mydomain.com:80/kodiReverse/jsonrpc"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://mydomain.com:80/kodiReverse/jsonrpc`
|
||||
|
||||
</details>
|
||||
|
||||
### ENV-Based
|
||||
|
||||
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|-------------------------------|----------------------------|
|
||||
| KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
|
||||
| KODI_USERNAME | No | | The username set |
|
||||
| KODI_PASSWORD | No | | The password set |
|
||||
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`kodi.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/kodi.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FKodiSourceConfig/%23%2Fdefinitions%2FKodiData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [WebScrobbler](https://web-scrobbler.com/)
|
||||
|
||||
After installing the extension open the preferences/settings for it:
|
||||
|
||||
* Under **Accounts**
|
||||
* **Add Webhook**
|
||||
* API URL: `http://localhost:9078/api/webscrobbler`
|
||||
* Application name: `(whatever you want)`
|
||||
|
||||
Reload the extension after adding the webhook.
|
||||
|
||||
* **On Firefox** - Only FQNs (domain.tld), `localhost`, and `127.0.0.1` are supported for API URL due to [firefox requiring https](https://github.com/web-scrobbler/web-scrobbler/issues/4183#issuecomment-1749222006)
|
||||
* **On Chromium-based Browsers** - Any domain will work for API URL
|
||||
* All Other browsers are untested
|
||||
|
||||
#### Multiple Users
|
||||
|
||||
If you would like use multiple WebScrobbler sources they can be matched using a **slug** at the end of the **API URL.** This requires using [a file-based config.](#file-based-configuration)
|
||||
|
||||
Example:
|
||||
|
||||
In `webscrobbler.json`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "aUserWS",
|
||||
"clients": [
|
||||
"client1Maloja"
|
||||
],
|
||||
"data": {
|
||||
"slug": "usera"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bUserWS",
|
||||
"clients": [
|
||||
"client2Maloja"
|
||||
],
|
||||
"data": {
|
||||
"slug": "userb"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
* To use `aUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/usera`
|
||||
* To use `bUserWS` source set **API URL** to `http://localhost:9078/api/webscrobbler/userb`
|
||||
|
||||
Note: `http://localhost:9078/api/webscrobbler` is matched with the first source that _that does not have a slug defined._
|
||||
|
||||
##### Connectors Black/Whitelist
|
||||
|
||||
MS can be configured to only scrobble, or NOT scrobble, from some WS connectors. Use the name of the website from the [supported websites](https://web-scrobbler.com/) or from the **Connectors** tab in the extension. Note that this **only** affects MS's behavior and does not affect the general connector behavior you have configured within the WebScrobbler extension.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------------------------------------------|
|
||||
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
|
||||
| WS_WHITELIST | No | | Only scrobble from these WebScrobbler Connectors. Comma-delimited list |
|
||||
| WS_BLACKLIST | No | | Do not scrobble from these WebScrobbler Connectors. Comma-delimited list |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`webscrobbler.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/webscrobbler.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FWebScrobblerSourceConfig/%23%2Fdefinitions%2FWebScrobblerData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
# Client Configurations
|
||||
|
||||
## [Maloja](https://github.com/krateng/maloja)
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|----------------------------|-----------|---------|-------------------------------|
|
||||
| `MALOJA_URL` | Yes | | Base URL of your installation |
|
||||
| `MALOJA_API_KEY` | Yes | | Api Key |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`maloja.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/maloja.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FMalojaClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
## [Last.fm](https://www.last.fm)
|
||||
|
||||
[Register for an API account here.](https://www.last.fm/api/account/create)
|
||||
|
||||
The Callback URL is actually specified by multi-scrobbler but to keep things consistent you should use
|
||||
```
|
||||
http://localhost:9078/lastfm/callback
|
||||
```
|
||||
or replace `localhost:9078` with your own base URL
|
||||
|
||||
### ENV-Based
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|----------------------------|-----------|-----------------------------------------|-------------------------------|
|
||||
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
|
||||
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
|
||||
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
|
||||
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`lastfm.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/lastfm.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
## [Listenbrainz](https://listenbrainz.org)
|
||||
|
||||
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
|
||||
|
||||
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|-------------------------------|---------------------------------|
|
||||
| LZ_TOKEN | Yes | | User token from your LZ profile |
|
||||
| LZ_USER | Yes | | Your LZ username |
|
||||
| LZ_URL | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`listenbrainz.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
# Monitoring
|
||||
|
||||
multi-scrobbler supports some common webhooks and a healthcheck endpoint in order to monitor Sources and Clients for errors.
|
||||
|
||||
## Webhook Configurations
|
||||
|
||||
Webhooks will **push** a notification to your configured servers on these events:
|
||||
|
||||
* Source polling started
|
||||
* Source polling retry
|
||||
* Source polling stopped on error
|
||||
* Scrobble client scrobble failure
|
||||
|
||||
Webhooks are configured in the main [config.json](#all-in-one-file-configuration) file under the `webhook` top-level property. Multiple webhooks may be configured for each webhook type. EX:
|
||||
|
||||
```json5
|
||||
{
|
||||
"sources": [
|
||||
//...
|
||||
],
|
||||
"clients": [
|
||||
//...
|
||||
],
|
||||
"webhooks": [
|
||||
{
|
||||
"name": "FirstGotifyServer",
|
||||
"type": "gotify",
|
||||
"url": "http://192.168.0.100:8070",
|
||||
"token": "abcd"
|
||||
},
|
||||
{
|
||||
"name": "SecondGotifyServer",
|
||||
"type": "gotify",
|
||||
//...
|
||||
},
|
||||
{
|
||||
"name": "NtfyServerOne",
|
||||
"type": "ntfy",
|
||||
//...
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [Gotify](https://gotify.net/)
|
||||
|
||||
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
EX
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "gotify",
|
||||
"name": "MyGotifyFriendlyNameForLogs",
|
||||
"url": "http://192.168.0.100:8070",
|
||||
"token": "AQZI58fA.rfSZbm",
|
||||
"priorities": {
|
||||
"info": 5,
|
||||
"warn": 7,
|
||||
"error": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### [Ntfy](https://ntfy.sh/)
|
||||
|
||||
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
EX
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ntfy",
|
||||
"name": "MyNtfyFriendlyNameForLogs",
|
||||
"url": "http://192.168.0.100:9991",
|
||||
"topic": "RvOwKJ1XtIVMXGLR",
|
||||
"username": "Optional",
|
||||
"password": "Optional",
|
||||
"priorities": {
|
||||
"info": 3,
|
||||
"warn": 4,
|
||||
"error": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Health Endpoint
|
||||
|
||||
An endpoint for monitoring the health of sources/clients is available at GET `http://YourMultiScrobblerDomain/health`
|
||||
|
||||
* Returns `200 OK` when **everything** is working or `500 Internal Server Error` if **anything** is not
|
||||
* The plain url (`/health`) aggregates status of **all clients/sources** -- so any failing client/source will make status return 500
|
||||
* Use query params `type` or `name` to restrict client/sources aggregated IE `/health?type=spotify` or `/health?name=MyMaloja`
|
||||
* On 500 the response returns a JSON payload with `messages` array that describes any issues
|
||||
* For any clients/sources that require authentication `/health` will return 500 if they are **not authenticated**
|
||||
* For sources that poll (spotify, yt music, subsonic) `/health` will 500 if they are **not polling**
|
||||
@@ -0,0 +1,729 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import SchemaLink from "../../src/components/SchemaLink";
|
||||
import AIOExample from "../../src/components/AIOExample";
|
||||
import FileExample from "../../src/components/FileExample";
|
||||
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
|
||||
|
||||
import AIOConfig from '!!raw-loader!../../../config/config.json.example';
|
||||
import SpotifyConfig from '!!raw-loader!../../../config/spotify.json.example';
|
||||
|
||||
:::tip
|
||||
|
||||
Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
|
||||
:::
|
||||
|
||||
## Configuration Types
|
||||
|
||||
[**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) are configured using:
|
||||
|
||||
* environmental (ENV) variables
|
||||
* client/source specific json config files
|
||||
* an all-in-one json config file
|
||||
|
||||
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
MS will parse environmental variables present in the OS/container when it is run. **This method means MS does not require files to run.**
|
||||
|
||||
<details>
|
||||
<summary>Use ENV-based configuration if...</summary>
|
||||
|
||||
* You are the only person for whom MS is scrobbling for
|
||||
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Plex -> Maloja
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
For Docker container...
|
||||
|
||||
```shell
|
||||
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...
|
||||
```
|
||||
|
||||
For Docker Compose
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
environment:
|
||||
- SPOTIFY_CLIENT_ID=yourId
|
||||
- SPOTIFY_CLIENT_SECRET=yourSecret
|
||||
- MALOJA_URL=http://domain.tld:42010
|
||||
- MALOJA_API_KEY=1234
|
||||
# ...
|
||||
# ...
|
||||
```
|
||||
|
||||
For a local/node installation export variables before running...
|
||||
|
||||
```shell
|
||||
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret npm run start
|
||||
```
|
||||
</details>
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
|
||||
|
||||
* Local installation -> `PROJECT_DIR/config`
|
||||
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Use File-based configuration if...</summary>
|
||||
|
||||
* You have many [Sources](/configuration/sources)
|
||||
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
|
||||
Funkwhale...
|
||||
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
|
||||
* You want only to scrobble to specific **Clients**
|
||||
* You need to setup more advanced configuration for a Source/Client
|
||||
* Most Source/Clients only support basic configuration through ENV, all configuration is possible using File/AIO
|
||||
|
||||
</details>
|
||||
|
||||
:::tip
|
||||
* There are **example configurations** for all Source/Client types and AIO config located in the [`/config`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
|
||||
* For docker installations these examples are copied to your configuration directory on first-time use.
|
||||
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
|
||||
:::
|
||||
|
||||
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
|
||||
|
||||
Example directory structure:
|
||||
|
||||
```
|
||||
/CONFIG_DIR
|
||||
plex.json
|
||||
spotify.json
|
||||
maloja.json
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/spotify.json" language="json5">{SpotifyConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="File AIO">
|
||||
MS will parse an **all-in-one** configuration file located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
|
||||
|
||||
* Local installation -> `PROJECT_DIR/config/config.json`
|
||||
* Docker -> `/config/config.json` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Use AIO-based configuration if...</summary>
|
||||
|
||||
* You have many [Sources](/configuration/sources)
|
||||
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
|
||||
Funkwhale...
|
||||
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
|
||||
* You want only to scrobble to specific **Clients**
|
||||
* You need to setup [monitoring/webhooks](#monitoring)
|
||||
* You want to setup defaults for all Sources/Clients
|
||||
|
||||
</details>
|
||||
|
||||
**The AIO config also enables setting default options for sources/clients as well as global options for MS itself.**
|
||||
|
||||
:::tip
|
||||
* An example AIO config files can be found in the project directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example)
|
||||
* For docker installations this example is copied to your configuration directory on first-time use.
|
||||
* There is also a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
|
||||
:::
|
||||
|
||||
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Config Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/config.json" language="json5">{AIOConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Setup Sources and Clients
|
||||
|
||||
See the [Configuration Types](#configuration-types) above for your options for creating Source and Client configurations.
|
||||
|
||||
* Reference [**Scrobble Sources**](/configuration/sources) for what Sources are available
|
||||
* Reference [**Scrobble Clients**](/configuration/clients) for what Clients are available
|
||||
|
||||
Each entry for a Source/Client includes a **Configuration** section that describes how to configure it using a [configuration type](#configuration-types).
|
||||
|
||||
## 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](/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:
|
||||
|
||||
* Some part of the text field matches: `[[MY_ENV]]`
|
||||
* Is replaced by the value of the Environmental Variable named `MY_ENV`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Given this [Last.fm](/configuration/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
|
||||
```json title="lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myLastFmClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "[[MY_APIKEY]]",
|
||||
"secret": "[[MY_SECRET]]",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
And these environmental variables, in this scenario set through `environment` in the [docker compose installation](/quickstart#create-docker-compose-file):
|
||||
|
||||
```
|
||||
MY_APIKEY=a89cba1569901a0671d5a9875fed4be1
|
||||
MY_SECRET=ec42e09d5ae0ee0f0816ca151008412a
|
||||
```
|
||||
|
||||
The resulting json multi-scrobbler would use:
|
||||
|
||||
```json title="lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myLastFmClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<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:
|
||||
|
||||
```
|
||||
WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECRET
|
||||
```
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
<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
|
||||
|
||||
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.
|
||||
|
||||
* Default => `http://localhost:9078`
|
||||
* Set with [ENV](./?configType=env#configuration-types) `BASE_URL` or `baseUrl` [all-in-one configuration](./?configType=aio#configuration-types)
|
||||
* If protocol is `http` or no protocol is specified MS will try to use port `9078` -- to override this explicitly set the port or use `https`
|
||||
|
||||
Useful when running with [docker](../installation/installation.mdx#docker) so that you do not need to specify redirect URLs for each configuration.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
EX Lastfm Redirect Url is `BASE_URL:PORT/lastfm/callback` (when no other redirectUri is specified for [lastfm configuration](/configuration/clients/lastfm))
|
||||
|
||||
| `BASE_URL` | Redirect URL |
|
||||
|-------------------------------------|----------------------------------------------------------|
|
||||
| `192.168.0.101` | `http://192.168.0.101:9078/lastfm/callback` |
|
||||
| `http://my.domain.local` | `http://my.domain.local:9078/lastfm/callback` |
|
||||
| `http://192.168.0.101/my/subfolder` | `http://192.168.0.101:9078/my/subfolder/lastfm/callback` |
|
||||
|
||||
| `BASE_URL` | Redirect URL |
|
||||
|-----------------------------------|----------------------------------------------------------|
|
||||
| `my.domain.local:80` | `http://192.168.0.101:80/lastfm/callback` |
|
||||
| `my.domain.local:9000` | `http://my.domain.local:9000/lastfm/callback` |
|
||||
| `192.168.0.101:4000/my/subfolder` | `http://192.168.0.101:4000/my/subfolder/lastfm/callback` |
|
||||
| `https://192.168.0.101` | `https://192.168.0.101:443/lastfm/callback` |
|
||||
|
||||
</details>
|
||||
|
||||
### Caching
|
||||
|
||||
Multi-scrobbler caches some activities 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.
|
||||
|
||||
<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` | |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="auth" label="Auth Data">
|
||||
|
||||
Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts.
|
||||
|
||||
:::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_AUTH` | No | `file` | The cache type to use |
|
||||
| `CACHE_AUTH_CONN` | No | `/config` | |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"auth": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</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.
|
||||
|
||||
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).
|
||||
|
||||
##### 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_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.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Adding Valkey container to multi-scrobbler</summary>
|
||||
|
||||
A valkey container can be added to the [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker):
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
# adding everything below
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- valkeydata:/data
|
||||
|
||||
volumes:
|
||||
valkeydata:
|
||||
driver: local
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
redis://HOST_IP:HOST_PORT
|
||||
```
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<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` |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Example
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"metadata": {
|
||||
"provider": "valkey",
|
||||
"connection": "redis://192.168.0.120:6379"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Turning on Debug Mode will
|
||||
|
||||
* override and enable all optional "debugging" options found in configuration
|
||||
* set log output to DEBUG
|
||||
|
||||
Use this as a shortcut for enabling output that can be used for troubleshooting and issue reporting. Be aware that logs will likely be VERY noisy while Debug Mode is on. You should only have this mode on while gathering logs for troubleshooting and then turn it off afterwards.
|
||||
|
||||
To set debug mode either add it to [AIO `config.json`](./?configType=aio#configuration-types)
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"debugMode": true,
|
||||
"sources": [...],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
or set the [ENV](./?configType=env#configuration-types) `DEBUG_MODE=true`
|
||||
|
||||
### 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?sourceComm=ingress#by-communication-method) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
|
||||
:::
|
||||
|
||||
Disable using either:
|
||||
|
||||
* ENV `DISABLE_WEB=true`
|
||||
* In [All-in-One File](./?configType=aio#configuration-types) use the top-level property `"disableWeb": true`
|
||||
|
||||
## Monitoring
|
||||
|
||||
### 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
|
||||
|
||||
Webhooks will **push** a notification to your configured servers on these events:
|
||||
|
||||
* Source polling started
|
||||
* Source polling retry
|
||||
* Source polling stopped on error
|
||||
* Scrobble client scrobble failure
|
||||
|
||||
Webhooks are configured in the AIO [config.json](#configuration-types) file under the `webhook` top-level property. Multiple webhooks may be configured for each webhook type.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"sources": [
|
||||
//...
|
||||
],
|
||||
"clients": [
|
||||
//...
|
||||
],
|
||||
"webhooks": [
|
||||
{
|
||||
"name": "FirstGotifyServer",
|
||||
"type": "gotify",
|
||||
"url": "http://192.168.0.100:8070",
|
||||
"token": "abcd"
|
||||
},
|
||||
{
|
||||
"name": "SecondGotifyServer",
|
||||
"type": "gotify",
|
||||
//...
|
||||
},
|
||||
{
|
||||
"name": "NtfyServerOne",
|
||||
"type": "ntfy",
|
||||
//...
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### [Gotify](https://gotify.net/)
|
||||
|
||||
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"type": "gotify",
|
||||
"name": "MyGotifyFriendlyNameForLogs",
|
||||
"url": "http://192.168.0.100:8070",
|
||||
"token": "AQZI58fA.rfSZbm",
|
||||
"priorities": {
|
||||
"info": 5,
|
||||
"warn": 7,
|
||||
"error": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### [Ntfy](https://ntfy.sh/)
|
||||
|
||||
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"type": "ntfy",
|
||||
"name": "MyNtfyFriendlyNameForLogs",
|
||||
"url": "http://192.168.0.100:9991",
|
||||
"topic": "RvOwKJ1XtIVMXGLR",
|
||||
"username": "Optional",
|
||||
"password": "Optional",
|
||||
"priorities": {
|
||||
"info": 3,
|
||||
"warn": 4,
|
||||
"error": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### [Apprise](https://github.com/caronc/apprise-api)
|
||||
|
||||
Refer to the [config schema for AppriseConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FAppriseConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler supports [stateless](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) and [persistent storage](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) endpoints as well as [tags](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging)/
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"type": "apprise",
|
||||
"name": "MyAppriseFriendlyNameForLogs",
|
||||
"host": "http://192.168.0.100:8080",
|
||||
"urls": ["gotify://192.168.0.101:8070/MyToken"], // stateless endpoints
|
||||
"keys": ["e90b20526808373353afad7fb98a201198c0c3e0555bea19f182df3388af7b17"], //persistent storage endpoints
|
||||
"tags": ["my","optional","tags"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Health Endpoint
|
||||
|
||||
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 (`/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 `/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
|
||||
|
||||
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
|
||||
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 226 KiB |
@@ -3,27 +3,26 @@ sidebar_position: 3
|
||||
title: Kitchen Sink
|
||||
---
|
||||
|
||||
# Example Config using all Possible Features
|
||||
# Example Complex Scenario
|
||||
|
||||
Scenario:
|
||||
|
||||
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
|
||||
* Each person has their own Maloja server
|
||||
* Each person has their own [Maloja](/configuration/clients/maloja) server
|
||||
* Each person has their own Spotify account
|
||||
* You have your own Airsonic (subsonic) server you to scrobble from
|
||||
* You have your own Youtube Music account you want to scrobble from
|
||||
* Mary has her own Last.fm account she also wants to scrobble to
|
||||
* Fred has his own Spotify application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
|
||||
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you 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 called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
|
||||
* Fred also has his own Jellyfin server and wants to scrobble everything he plays
|
||||
* 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](/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
|
||||
|
||||
Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/config.json"
|
||||
{
|
||||
"sourceDefaults": {
|
||||
"maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config
|
||||
@@ -42,6 +41,8 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"data": {
|
||||
"clientId": "foxxSpotifyAppId",
|
||||
"clientSecret": "foxxSpotifyAppSecret",
|
||||
},
|
||||
"options": {
|
||||
"maxRequestRetries": 2, // override default max retries because spotify can...spotty
|
||||
}
|
||||
},
|
||||
@@ -69,7 +70,9 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "fredPlex",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,8 +80,10 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "maryPlex",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"user": ["mary@email.com"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
|
||||
"libraries": ["podcasts"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["maryUser"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
|
||||
"librariesAllow": ["podcasts"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,7 +91,9 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "partyPlex",
|
||||
// omitting clients (or making it empty) will make this Source scrobble to all Clients
|
||||
"data": {
|
||||
"libraries": ["party"],
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"librariesAllow": ["party"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -94,7 +101,10 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "FredJelly",
|
||||
// omitting clients (or making it empty) will make this Source scrobble to all Clients
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
"usersAllow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -113,9 +123,16 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
|
||||
"authUser": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "endpointlz",
|
||||
"name": "listenbrainzfoxx",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
@@ -158,9 +175,7 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
|
||||
### Separate JSON files
|
||||
|
||||
In `CONFIG_DIR/spotify.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/spotify.json"
|
||||
[
|
||||
{
|
||||
// may omit 'type' property since app knows this is file is for spotify configs
|
||||
@@ -191,66 +206,76 @@ In `CONFIG_DIR/spotify.json`:
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/plex.json`
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/plex.json"
|
||||
[
|
||||
{
|
||||
"name": "fredPlex",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maryPlex",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"user": ["mary@email.com"],
|
||||
"libraries": ["podcasts"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["maryUser"],
|
||||
"librariesAllow": ["podcasts"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "partyPlex",
|
||||
"data": {
|
||||
"libraries": ["party"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"librariesAllow": ["party"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/jellyfin.json`
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/jellyfin.json"
|
||||
[
|
||||
{
|
||||
"name": "FredJelly",
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
"usersAllow": ["fredUser"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/ytmusic.json`
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/ytmusic.json"
|
||||
[
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "foxxYoutube",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
|
||||
"authUser": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
```json5 title="CONFIG_DIR/endpointlz.json"
|
||||
[
|
||||
{
|
||||
"name": "listenbrainzfoxx",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"token": "myToken",
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/maloja.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/maloja.json"
|
||||
[
|
||||
{
|
||||
"name": "foxxMaloja",
|
||||
@@ -276,9 +301,7 @@ In `CONFIG_DIR/maloja.json`:
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/lastfm.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "maryLFM",
|
||||
|
After Width: | Height: | Size: 52 KiB |