mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Compare commits
1051
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bdc9447b8 | ||
|
|
b8cd53f554 | ||
|
|
e7218074f8 | ||
|
|
a48ada9ac3 | ||
|
|
bc28de66b1 | ||
|
|
bc18993cd3 | ||
|
|
d280ca5dd2 | ||
|
|
155d8a3252 | ||
|
|
68d9417cf7 | ||
|
|
9b7483db91 | ||
|
|
7c0797b496 | ||
|
|
1b3cbe9ab3 | ||
|
|
15e0332ac2 | ||
|
|
d532c85c2a | ||
|
|
eecbea4cab | ||
|
|
823dd5d011 | ||
|
|
0fa9a8df10 | ||
|
|
a76213a7a0 | ||
|
|
a42ab3d8fd | ||
|
|
475a3f1276 | ||
|
|
6dcfd6028b | ||
|
|
54247e89c2 | ||
|
|
433ea532ae | ||
|
|
b615172ed1 | ||
|
|
aa2beb5846 | ||
|
|
06d3c6a3a8 | ||
|
|
c0cfc5fa9f | ||
|
|
2195ac7057 | ||
|
|
3fc67146c7 | ||
|
|
fe4e625824 | ||
|
|
b98756a262 | ||
|
|
2b7521ebac | ||
|
|
ecd48ca234 | ||
|
|
41aaaca530 | ||
|
|
ed42da6843 | ||
|
|
34044401d4 | ||
|
|
f61e50fd45 | ||
|
|
f5ad2ad29b | ||
|
|
2e4acd0f00 | ||
|
|
b9517bd066 | ||
|
|
335880249f | ||
|
|
5c2917f906 | ||
|
|
124e857d9f | ||
|
|
5dee499381 | ||
|
|
10325fd4e6 | ||
|
|
045890e243 | ||
|
|
685e48a7a6 | ||
|
|
7eb5104f0b | ||
|
|
4272e8ea58 | ||
|
|
8e6d35a85b | ||
|
|
917d05f295 | ||
|
|
287c994643 | ||
|
|
7980a0c856 | ||
|
|
9d2557cf6a | ||
|
|
6f8debc304 | ||
|
|
b53ae78d55 | ||
|
|
8723bc8890 | ||
|
|
eca8ad2ce1 | ||
|
|
b6b8d3423f | ||
|
|
fc027e8554 | ||
|
|
707da834bb | ||
|
|
002520cd8a | ||
|
|
7ca84ce27f | ||
|
|
8af56cb561 | ||
|
|
0e249b13a9 | ||
|
|
591b5c6618 | ||
|
|
3c5b9cc402 | ||
|
|
420a053e25 | ||
|
|
72f64953f6 | ||
|
|
8b8c2765c7 | ||
|
|
e1324921e9 | ||
|
|
9f71fbef97 | ||
|
|
dd22418c41 | ||
|
|
d296b825d6 | ||
|
|
792ccc3c09 | ||
|
|
6772e9934d | ||
|
|
4ce7219214 | ||
|
|
b2ce42fecf | ||
|
|
46a061a602 | ||
|
|
ff7360fced | ||
|
|
425a0b2807 | ||
|
|
ad632f38d8 | ||
|
|
1d1b3b6f56 | ||
|
|
7575e8bff7 | ||
|
|
31546ed4c1 | ||
|
|
bf9cdc141c | ||
|
|
02ab1571b6 | ||
|
|
699bd02782 | ||
|
|
0d45d06966 | ||
|
|
a93d02e1b7 | ||
|
|
513e2dde15 | ||
|
|
2fc90dcfda | ||
|
|
fd2a178808 | ||
|
|
9f54d45576 | ||
|
|
78aff32334 | ||
|
|
0115ccb862 | ||
|
|
8a71663c9f | ||
|
|
40eff0b901 | ||
|
|
13acc56424 | ||
|
|
09fc0ce15c | ||
|
|
4fbdd71588 | ||
|
|
7cf105935e | ||
|
|
1bd10326eb | ||
|
|
74605caf15 | ||
|
|
7dec9a39e6 | ||
|
|
90a8426748 | ||
|
|
8469d161e9 | ||
|
|
941f50bff2 | ||
|
|
f8d882cfcf | ||
|
|
0704cebe72 | ||
|
|
e43a18bcbb | ||
|
|
0baa399e9f | ||
|
|
05b2e1c116 | ||
|
|
a0cdaeb01f | ||
|
|
73c35751ed | ||
|
|
516a8d2178 | ||
|
|
d595bf9961 | ||
|
|
8345779030 | ||
|
|
b58d39c90e | ||
|
|
7074ece0a5 | ||
|
|
bfb0d6b392 | ||
|
|
5c3f49fb2f | ||
|
|
f70e680d55 | ||
|
|
c2fda1e7d9 | ||
|
|
70e4239e52 | ||
|
|
d96073282c | ||
|
|
2887600aa0 | ||
|
|
6f7aa6f567 | ||
|
|
857f44fa14 | ||
|
|
d764b0204e | ||
|
|
ee2f988357 | ||
|
|
de69977d7e | ||
|
|
6eab629168 | ||
|
|
f21572bed0 | ||
|
|
98084665ba | ||
|
|
e8a91bb6de | ||
|
|
206d1571ac | ||
|
|
ebf6ec6957 | ||
|
|
03fc3e3a09 | ||
|
|
aa50842444 | ||
|
|
03dbbdc2cf | ||
|
|
bf0b27924e | ||
|
|
e02a0cc6ae | ||
|
|
ff4dd25a97 | ||
|
|
28e0d03e6f | ||
|
|
d7f2d6fa8e | ||
|
|
8f33dc9f28 | ||
|
|
fe068a9216 | ||
|
|
06b6e200f5 | ||
|
|
afea13e681 | ||
|
|
f8401faf61 | ||
|
|
7c3590adf9 | ||
|
|
742025fecc | ||
|
|
b983de1a58 | ||
|
|
ea77ea435a | ||
|
|
ecf3273eb9 | ||
|
|
a79b697496 | ||
|
|
79874c6c20 | ||
|
|
aa91d2cb92 | ||
|
|
c727445f76 | ||
|
|
c25ca5297c | ||
|
|
956054f387 | ||
|
|
86ebd3d673 | ||
|
|
0679937b54 | ||
|
|
61a2ff5180 | ||
|
|
26db80d8c7 | ||
|
|
fc85790df5 | ||
|
|
212ac27dd3 | ||
|
|
a30d1ae6a5 | ||
|
|
5c586fb635 | ||
|
|
de0d336325 | ||
|
|
65d0c7c16b | ||
|
|
fc6cc68d59 | ||
|
|
8c83969e95 | ||
|
|
084dca73c1 | ||
|
|
7ac57ca757 | ||
|
|
1dc8c21716 | ||
|
|
6f8e7d3862 | ||
|
|
a6d33fc2ef | ||
|
|
26121ace10 | ||
|
|
27e5656a04 | ||
|
|
f3d7d59af6 | ||
|
|
2a4afefe64 | ||
|
|
1dba73ccb2 | ||
|
|
3e82e035ac | ||
|
|
b12153fba9 | ||
|
|
8c82412de4 | ||
|
|
e804ce33ef | ||
|
|
3907fa5474 | ||
|
|
a7b3593314 | ||
|
|
76ef994067 | ||
|
|
c3cb3cf3a1 | ||
|
|
8096219526 | ||
|
|
67c1823195 | ||
|
|
2014e78ade | ||
|
|
b3c332deeb | ||
|
|
93a1042e03 | ||
|
|
ba010240c0 | ||
|
|
7f491d6974 | ||
|
|
74e67e0e41 | ||
|
|
1853f9abba | ||
|
|
c13e7ebeec | ||
|
|
207354e799 | ||
|
|
d7f6b17308 | ||
|
|
c069020aac | ||
|
|
f84a0f190d | ||
|
|
d0f43fbd2d | ||
|
|
b38168975e | ||
|
|
a845ac10ce | ||
|
|
56fdb12683 | ||
|
|
1f40e1e414 | ||
|
|
c1a260dc56 | ||
|
|
5d34ea68af | ||
|
|
41ea9e0aa7 | ||
|
|
a9d91c2c8a | ||
|
|
a696436a75 | ||
|
|
4ae388676e | ||
|
|
89b698d48d | ||
|
|
b71a4cbbd6 | ||
|
|
a870cdc6f4 | ||
|
|
c404dd5396 | ||
|
|
cae845028d | ||
|
|
1698c5d71e | ||
|
|
765d86e02b | ||
|
|
f347b82d33 | ||
|
|
87aa9c461e | ||
|
|
417ce85bc7 | ||
|
|
c8d917cfe6 | ||
|
|
8c789123f9 | ||
|
|
06ed3f4388 | ||
|
|
fab2e4938d | ||
|
|
fbcd1e1b9a | ||
|
|
d48f2bd4ac | ||
|
|
e2dad463f6 | ||
|
|
642af13f3c | ||
|
|
4146b159bd | ||
|
|
3a3a97132d | ||
|
|
04a5b91694 | ||
|
|
945f494a2e | ||
|
|
4b09909b33 | ||
|
|
a5a1365b72 | ||
|
|
6bc618c481 | ||
|
|
85d1a0e648 | ||
|
|
1ae45c6060 | ||
|
|
7daa43a883 | ||
|
|
f8501b2a2e | ||
|
|
14c35edadf | ||
|
|
cbb9774a18 | ||
|
|
7b1eb7c2c1 | ||
|
|
8c8d1e1673 | ||
|
|
fbdd504453 | ||
|
|
612a90d7e7 | ||
|
|
de8ad60db8 | ||
|
|
43fd8ea807 | ||
|
|
ec3a7e2d20 | ||
|
|
981a211e56 | ||
|
|
c42b5cacaa | ||
|
|
919907f1cb | ||
|
|
9eac9fdfa1 | ||
|
|
86cea49ed4 | ||
|
|
8fc892a738 | ||
|
|
fd591be3d6 | ||
|
|
63b23689a4 | ||
|
|
3035d4c5e8 | ||
|
|
7891133002 | ||
|
|
66db4d017e | ||
|
|
15e55dc6c5 | ||
|
|
239fb5919e | ||
|
|
7e53553a8e | ||
|
|
4bd0345d1b | ||
|
|
b207109697 | ||
|
|
77de2a7526 | ||
|
|
69b19da6aa | ||
|
|
f2e2d67668 | ||
|
|
9c44ccb663 | ||
|
|
f431f4d5af | ||
|
|
be872d5ce1 | ||
|
|
0246f7dfa6 | ||
|
|
b57f325684 | ||
|
|
153f37e927 | ||
|
|
de4cb8a5c2 | ||
|
|
eb133e4f7e | ||
|
|
a3e056e7c5 | ||
|
|
7d8b00ab22 | ||
|
|
8b633dd901 | ||
|
|
f055cf683f | ||
|
|
baaa954f31 | ||
|
|
c727de9c40 | ||
|
|
e6f4a1dbd2 | ||
|
|
3d6e86f258 | ||
|
|
e6130eb5eb | ||
|
|
97b6042768 | ||
|
|
b3c1ce6d8a | ||
|
|
5daba47ac7 | ||
|
|
8d7760df6d | ||
|
|
75c95fdaa8 | ||
|
|
e874ccc2d3 | ||
|
|
17e97b3a6a | ||
|
|
c6389590fe | ||
|
|
7151d96b59 | ||
|
|
b2b2018764 | ||
|
|
655b97ce1c | ||
|
|
5c6edefc56 | ||
|
|
bf7fb4f558 | ||
|
|
32b64ff384 | ||
|
|
f6e0f4be4a | ||
|
|
fb66f0ca84 | ||
|
|
c4b1d72f3f | ||
|
|
fb452eb40b | ||
|
|
d3fd03a78c | ||
|
|
7b04e76c3b | ||
|
|
9f34c45192 | ||
|
|
7ab0f710ff | ||
|
|
ea1fb54de1 | ||
|
|
35fd84a1d0 | ||
|
|
e498db8ad3 | ||
|
|
145960ca04 | ||
|
|
91ba24e165 | ||
|
|
66fc7a2b09 | ||
|
|
97a7c7044c | ||
|
|
53d7de7e8d | ||
|
|
32da14c08e | ||
|
|
c7f370f121 | ||
|
|
9d105173da | ||
|
|
3e7bbab5b0 | ||
|
|
e04965eb35 | ||
|
|
f2c5e82665 | ||
|
|
4c41f0976d | ||
|
|
db333184cc | ||
|
|
a5963a2333 | ||
|
|
0fe81d6164 | ||
|
|
3ad9f5dcac | ||
|
|
d39abe40c6 | ||
|
|
6f8ca72ccf | ||
|
|
2ae32ee19f | ||
|
|
2ac89c5db3 | ||
|
|
b0db1dd8bc | ||
|
|
a603be6e22 | ||
|
|
7ad23a76de | ||
|
|
4481646153 | ||
|
|
5d2c9b557f | ||
|
|
eb69ae48f1 | ||
|
|
c7479b2f00 | ||
|
|
8ae7a8d338 | ||
|
|
7573c762c0 | ||
|
|
6d503a1a15 | ||
|
|
94d3ea5da8 | ||
|
|
e998006e6c | ||
|
|
234fc67ab2 | ||
|
|
37bb507359 | ||
|
|
1237056d59 | ||
|
|
7174702f7b | ||
|
|
3b721673c8 | ||
|
|
b64f0d2dd5 | ||
|
|
7738dc004a | ||
|
|
5c5563a917 | ||
|
|
7d08939a18 | ||
|
|
fabc50aa62 | ||
|
|
4c0615d427 | ||
|
|
b4dd729150 | ||
|
|
f336089755 | ||
|
|
4fc6285081 | ||
|
|
0cf3e33fe6 | ||
|
|
ab424aee0d | ||
|
|
9fb55d479d | ||
|
|
c8fb01bbab | ||
|
|
f008c4074d | ||
|
|
d66922c99a | ||
|
|
c7689199cc | ||
|
|
9a8b6bfa09 | ||
|
|
45c8141137 | ||
|
|
4e7b3e9ceb | ||
|
|
dfcc9c32a8 | ||
|
|
a3d891aa2f | ||
|
|
088ae4a26a | ||
|
|
f425b5a906 | ||
|
|
a4ddaeff61 | ||
|
|
8932224b57 | ||
|
|
ffe2203ba5 | ||
|
|
987819cc7c | ||
|
|
974a017e5a | ||
|
|
594a59347e | ||
|
|
b0970c6653 | ||
|
|
d8017dda62 | ||
|
|
a8c3e88ec9 | ||
|
|
7bf31a7fc7 | ||
|
|
deb3310e3f | ||
|
|
5450b48eb3 | ||
|
|
70a94cff9b | ||
|
|
ff31259092 | ||
|
|
8b4f1849f6 | ||
|
|
110ffad7d7 | ||
|
|
62a0670b4d | ||
|
|
7a1142fdd5 | ||
|
|
5def3cde99 | ||
|
|
075fa877ed | ||
|
|
6ffb46a65a | ||
|
|
ac5f34ea5f | ||
|
|
9975f377d0 | ||
|
|
76b4606d17 | ||
|
|
1244e0ee3e | ||
|
|
c56b16e2e6 | ||
|
|
831fd420ff | ||
|
|
2125f92bd8 | ||
|
|
42c9383dda | ||
|
|
f5eab006f5 | ||
|
|
5a2c664fe1 | ||
|
|
d7cbb7c9bd | ||
|
|
8afe30bfd4 | ||
|
|
24b67fc430 | ||
|
|
d01a00ad13 | ||
|
|
ac250eba39 | ||
|
|
778a2d6565 | ||
|
|
0b9a7b78df | ||
|
|
6b93e839fa | ||
|
|
7e6040ce7b | ||
|
|
d4b2057c41 | ||
|
|
bc04603fb4 | ||
|
|
ba2a23e039 | ||
|
|
6bfe203a34 | ||
|
|
8c6f34ebc7 | ||
|
|
18e20123c2 | ||
|
|
dc44f6c921 | ||
|
|
a19fd42a5b | ||
|
|
206fd62f70 | ||
|
|
6123503ee8 | ||
|
|
b751c6f5f9 | ||
|
|
d1ef09ba0d | ||
|
|
d073642203 | ||
|
|
9bb48fbf13 | ||
|
|
bf8f1175ce | ||
|
|
4bfcc40571 | ||
|
|
892d035d7a | ||
|
|
d4019e6dea | ||
|
|
3ce94f443a | ||
|
|
b863a9773a | ||
|
|
489d7d12cb | ||
|
|
63c7ea7809 | ||
|
|
a6a019e9fd | ||
|
|
dbd1c81c90 | ||
|
|
2b42b40c0a | ||
|
|
4230674c2c | ||
|
|
b0955270ba | ||
|
|
a194f752bb | ||
|
|
4180ae0c6b | ||
|
|
3227541f34 | ||
|
|
e949da3830 | ||
|
|
e13d86b1d0 | ||
|
|
f7577c43e0 | ||
|
|
f3531f41ec | ||
|
|
d9d37c68fc | ||
|
|
3bb887763f | ||
|
|
c65504ee9f | ||
|
|
373fdb0dc2 | ||
|
|
825d0da532 | ||
|
|
2efdada4c0 | ||
|
|
01afe9006a | ||
|
|
da9edb2116 | ||
|
|
d5c886d718 | ||
|
|
e99351407e | ||
|
|
d9184fa350 | ||
|
|
e4af1e3f18 | ||
|
|
9ca4f3fa62 | ||
|
|
20e3f22f3f | ||
|
|
bca9e195c9 | ||
|
|
8b1765cb03 | ||
|
|
0605cf1fd5 | ||
|
|
07d6487875 | ||
|
|
6222aa5899 | ||
|
|
47f61b491e | ||
|
|
42818c51c1 | ||
|
|
a0fee4184e | ||
|
|
470a70f951 | ||
|
|
2d2b3a6d48 | ||
|
|
6f40dd387e | ||
|
|
2445336ec0 | ||
|
|
41d181e02f | ||
|
|
ff158c99cd | ||
|
|
c1cdcd016a | ||
|
|
fcb9092f8a | ||
|
|
ba2bd0cc8a | ||
|
|
f224ce6b6a | ||
|
|
c74678e9ef | ||
|
|
1b1770ff29 | ||
|
|
41d3a63c42 | ||
|
|
52240aa13b | ||
|
|
95d7d276d7 | ||
|
|
18c520ce2f | ||
|
|
fb65cb222a | ||
|
|
c4f028fd2c | ||
|
|
b85452f5c6 | ||
|
|
6546ee3db2 | ||
|
|
f0e798c052 | ||
|
|
44424f2570 | ||
|
|
72df520300 | ||
|
|
25c783051d | ||
|
|
264bc31af6 | ||
|
|
e0bdb8fe3c | ||
|
|
4d7c93f7dc | ||
|
|
dd54075c52 | ||
|
|
ff08b6327d | ||
|
|
76392a9668 | ||
|
|
f47faabf7c | ||
|
|
120e280fc8 | ||
|
|
6ae80fd881 | ||
|
|
be70e7074a | ||
|
|
65841a8c6b | ||
|
|
82374fc806 | ||
|
|
004400d7ea | ||
|
|
d54e169a14 | ||
|
|
31c35647dd | ||
|
|
204ee159d6 | ||
|
|
4fe1f8ced9 | ||
|
|
c26e6f77f5 | ||
|
|
e2b59900e5 | ||
|
|
c9cde11871 | ||
|
|
ea0c75ccf3 | ||
|
|
13a64bd63a | ||
|
|
c3b23271d4 | ||
|
|
d047c600c2 | ||
|
|
b6cae4a538 | ||
|
|
1eed48456c | ||
|
|
20834e812e | ||
|
|
d1e5b11598 | ||
|
|
c7af346daf | ||
|
|
3975edeaf3 | ||
|
|
119c8d7fd7 | ||
|
|
3a96c28b2d | ||
|
|
6905853aa3 | ||
|
|
3f1a952e44 | ||
|
|
405ee4f6d7 | ||
|
|
34e0eaefd8 | ||
|
|
6d4b617409 | ||
|
|
4195bde081 | ||
|
|
925f3b916d | ||
|
|
770621ba41 | ||
|
|
a00e5d0dbc | ||
|
|
0d6d803df3 | ||
|
|
396e4b922e | ||
|
|
14ecb57210 | ||
|
|
14369836f6 | ||
|
|
70e5aaaad5 | ||
|
|
366e668c13 | ||
|
|
d0e842ede3 | ||
|
|
a0c1f71162 | ||
|
|
7ef159448e | ||
|
|
b4e6ce6e78 | ||
|
|
abe75869b1 | ||
|
|
38d7a9cfce | ||
|
|
83a425a3d4 | ||
|
|
7ae727a8fe | ||
|
|
fe4b947d26 | ||
|
|
c5cdc465f3 | ||
|
|
c86819b0b3 | ||
|
|
5228d4894c | ||
|
|
e6709c442f | ||
|
|
0a594d5f83 | ||
|
|
49be95a4d1 | ||
|
|
fbf3af8474 | ||
|
|
146916bc6d | ||
|
|
a410721acf | ||
|
|
af5aec2ed3 | ||
|
|
51b7b3fd40 | ||
|
|
c22867a9b1 | ||
|
|
f93c1fa4db | ||
|
|
1f199e28ef | ||
|
|
811615dd06 | ||
|
|
fc6ec840da | ||
|
|
eb2159de27 | ||
|
|
d0a490b200 | ||
|
|
53870ac1f2 | ||
|
|
d2f8717555 | ||
|
|
756d9b84e1 | ||
|
|
a1ad250dcc | ||
|
|
f36fd16517 | ||
|
|
28ade6ac28 | ||
|
|
fa1051b29a | ||
|
|
3cdbfc337e | ||
|
|
80474a96fe | ||
|
|
957db35c30 | ||
|
|
90f0bdb5ef | ||
|
|
640d944b4c | ||
|
|
c7fce06480 | ||
|
|
8cffd0d068 | ||
|
|
b8a9623def | ||
|
|
d922f37359 | ||
|
|
dcc53d1656 | ||
|
|
0cb20b83b4 | ||
|
|
e814cf58fd | ||
|
|
f211a4a4c4 | ||
|
|
4d7162a1a4 | ||
|
|
a5926d71c1 | ||
|
|
d95f323dc0 | ||
|
|
1b53d22e00 | ||
|
|
75c0230dae | ||
|
|
630edd7b3f | ||
|
|
092cf907a2 | ||
|
|
562b388e3e | ||
|
|
f788894494 | ||
|
|
d0379e1bd6 | ||
|
|
5eb967f61c | ||
|
|
0bf9d5e786 | ||
|
|
102f9795f8 | ||
|
|
7a7d7bb975 | ||
|
|
246e9ad7c5 | ||
|
|
d772c8c8cd | ||
|
|
48b6b555c1 | ||
|
|
8be56457e2 | ||
|
|
d9a5aa1ce0 | ||
|
|
f6799955a0 | ||
|
|
077c3d33b5 | ||
|
|
524afb60a9 | ||
|
|
12ba99437c | ||
|
|
8882e7ab87 | ||
|
|
8de382f581 | ||
|
|
691db9215c | ||
|
|
22c0916544 | ||
|
|
cbe996db95 | ||
|
|
8fd870b9ef | ||
|
|
272c3cc8be | ||
|
|
54da8b0add | ||
|
|
cfb4131d5d | ||
|
|
b8494986d5 | ||
|
|
f9f923041f | ||
|
|
5774be1d22 | ||
|
|
4bc460f598 | ||
|
|
0f6aa56fe5 | ||
|
|
439f0e05aa | ||
|
|
952ddbdcc3 | ||
|
|
765b13f910 | ||
|
|
47caf8de8c | ||
|
|
3c3faf22fc | ||
|
|
f94538ea1f | ||
|
|
e9b2990e15 | ||
|
|
3ef48547a7 | ||
|
|
047de49ee6 | ||
|
|
2a52d1e050 | ||
|
|
8f049a089b | ||
|
|
adf6b9ed58 | ||
|
|
59d1050b2b | ||
|
|
561c62b426 | ||
|
|
f38f10d2a0 | ||
|
|
7aa5595cc9 | ||
|
|
46389fd81d | ||
|
|
c0384317b1 | ||
|
|
6e66cb06a9 | ||
|
|
cb1ff7d030 | ||
|
|
3b5468499a | ||
|
|
c7361a8976 | ||
|
|
60a6ddbc41 | ||
|
|
92e027bfc4 | ||
|
|
b388ca6106 | ||
|
|
84b43d7c39 | ||
|
|
99ebb53fc6 | ||
|
|
cda33ba04f | ||
|
|
dda73cd96a | ||
|
|
5771fbf496 | ||
|
|
e9d1c81380 | ||
|
|
d59cb2668d | ||
|
|
3c51149d9a | ||
|
|
40234e64b3 | ||
|
|
2df8bfe6aa | ||
|
|
fec39b2e2e | ||
|
|
83f501df0d | ||
|
|
c40add7d70 | ||
|
|
569c5a8690 | ||
|
|
11f7299c0e | ||
|
|
8a6ae698a1 | ||
|
|
d6ed03028d | ||
|
|
2818d1d275 | ||
|
|
8127e4b6ce | ||
|
|
8e829a53b5 | ||
|
|
1562f8bffc | ||
|
|
e04514f6a1 | ||
|
|
00e9755fb1 | ||
|
|
5ba4729f83 | ||
|
|
e5df4f2752 | ||
|
|
d11db4cb8d | ||
|
|
14f5f60c11 | ||
|
|
4d7eadbd38 | ||
|
|
747b62289a | ||
|
|
1eeb5e6b08 | ||
|
|
ac01348266 | ||
|
|
3028f83d47 | ||
|
|
a02d43acea | ||
|
|
c7679a0a62 | ||
|
|
d11ac3d6ed | ||
|
|
8a09c91713 | ||
|
|
81d24f1753 | ||
|
|
5be12aab15 | ||
|
|
47c600ed8d | ||
|
|
5f84fefd5d | ||
|
|
a143aa4193 | ||
|
|
23a3dfa5d1 | ||
|
|
3005d36db0 | ||
|
|
ada9d70be9 | ||
|
|
9d839db2e1 | ||
|
|
ca20b23fbc | ||
|
|
577cff3abf | ||
|
|
c4786bdfe2 | ||
|
|
df0da7054c | ||
|
|
272a336a22 | ||
|
|
ec4a51074e | ||
|
|
5a4bcb2949 | ||
|
|
77387c292b | ||
|
|
4caf1c8cdb | ||
|
|
981805aa96 | ||
|
|
60e3242c44 | ||
|
|
e7e1417b0b | ||
|
|
bf765be174 | ||
|
|
e27c301932 | ||
|
|
fc4606585d | ||
|
|
fd80101a7a | ||
|
|
2cbc5542d1 | ||
|
|
6f15f15722 | ||
|
|
ff1d9248c6 | ||
|
|
218ece8f5c | ||
|
|
3adac7c6e0 | ||
|
|
1c78da7985 | ||
|
|
457279465c | ||
|
|
d551c8563f | ||
|
|
75bd4a04d6 | ||
|
|
339232c368 | ||
|
|
8741ff25ea | ||
|
|
8a952cb576 | ||
|
|
eeb55aa02e | ||
|
|
6a81fc1be2 | ||
|
|
a47e2a6242 | ||
|
|
dadaa6df35 | ||
|
|
fb19191209 | ||
|
|
2f53a60da1 | ||
|
|
ab2f397fab | ||
|
|
db2dd8fb8b | ||
|
|
135439f509 | ||
|
|
d743b849fe | ||
|
|
9885768cad | ||
|
|
c6e6bbb87b | ||
|
|
bbc2351ebc | ||
|
|
b547db9045 | ||
|
|
28c2e9873a | ||
|
|
6bcd3f9516 | ||
|
|
11b2632d02 | ||
|
|
fb6dbe307d | ||
|
|
2474b792c3 | ||
|
|
3007c949a0 | ||
|
|
63533534df | ||
|
|
5c03062976 | ||
|
|
dbe41fa5ab | ||
|
|
5454f817e5 | ||
|
|
8d8f8260b5 | ||
|
|
62bb00571f | ||
|
|
c10537d658 | ||
|
|
e9ccc319e9 | ||
|
|
54803c6ef9 | ||
|
|
a3d37a864a | ||
|
|
394570e5a3 | ||
|
|
2bbfdf424f | ||
|
|
9785ad6ba7 | ||
|
|
fe7d77c03d | ||
|
|
0e7aa92f5b | ||
|
|
2ff80283b9 | ||
|
|
4f34b1a055 | ||
|
|
4effdd6df5 | ||
|
|
af3ce9dea7 | ||
|
|
f0c5cb524f | ||
|
|
46883384b6 | ||
|
|
7bc5ab3d0f | ||
|
|
fd15d15124 | ||
|
|
14da0795f1 | ||
|
|
3faf0ee7f9 | ||
|
|
78ddb15dcf | ||
|
|
760373c227 | ||
|
|
6bb7b17e97 | ||
|
|
231156d452 | ||
|
|
d85628b4c9 | ||
|
|
db0e74aec3 | ||
|
|
a918a96d34 | ||
|
|
bb51915085 | ||
|
|
7c177dfe03 | ||
|
|
cf5e7ca1fe | ||
|
|
fd43ff86c8 | ||
|
|
194a0e78ff | ||
|
|
04027e87e7 | ||
|
|
a998060bca | ||
|
|
5b16e8857e | ||
|
|
c53ce7c517 | ||
|
|
7897dc9503 | ||
|
|
160ddac3e4 | ||
|
|
59b4f27294 | ||
|
|
21f42bf45b | ||
|
|
3e8f170f9a | ||
|
|
15fbed09a5 | ||
|
|
52b301e729 | ||
|
|
0bef197521 | ||
|
|
f16750609a | ||
|
|
c39ef29597 | ||
|
|
72639616b7 | ||
|
|
effe870104 | ||
|
|
ee753389f7 | ||
|
|
5f4a5cb815 | ||
|
|
0607aedf04 | ||
|
|
1f165096b3 | ||
|
|
fb6f6a18c8 | ||
|
|
03f8b20704 | ||
|
|
c49cdba718 | ||
|
|
de67879af5 | ||
|
|
9d0cf8f845 | ||
|
|
8d9fd11c79 | ||
|
|
745343c589 | ||
|
|
e5935c38da | ||
|
|
cac13fa3da | ||
|
|
ca4803713e | ||
|
|
e53dd55ad8 | ||
|
|
1f2772353a | ||
|
|
78b48107a0 | ||
|
|
bd00055ae5 | ||
|
|
b174b23b3b | ||
|
|
9e6319ee73 | ||
|
|
6b5b3990cd | ||
|
|
f2465f0f88 | ||
|
|
c68ec8dbf6 | ||
|
|
6e1cdba58c | ||
|
|
df4bb27d3a | ||
|
|
de7707b45a | ||
|
|
c884b4776c | ||
|
|
0893b62f3b | ||
|
|
7d071c763d | ||
|
|
664395a0ee | ||
|
|
007d553d59 | ||
|
|
7c8c185c4c | ||
|
|
972eccd0bf | ||
|
|
034cfe7a8e | ||
|
|
7a8cf7c004 | ||
|
|
30cdc9f553 | ||
|
|
3f19abb899 | ||
|
|
a601757759 | ||
|
|
fe03bcd651 | ||
|
|
1f319cb9f6 | ||
|
|
a0ac0f8e46 | ||
|
|
06945fda52 | ||
|
|
306c4c858e | ||
|
|
81885632a0 | ||
|
|
23467481c1 | ||
|
|
e716de2160 | ||
|
|
dda06a0873 | ||
|
|
eca64d68e7 | ||
|
|
1065643b23 | ||
|
|
d92153f85f | ||
|
|
22d76c459a | ||
|
|
ec87ab96d6 | ||
|
|
f62fe270fb | ||
|
|
677474beed | ||
|
|
cb9dd0fbc5 | ||
|
|
0685d4b06d | ||
|
|
82c0a4faec | ||
|
|
ccbd5e304a | ||
|
|
4f81a192fa | ||
|
|
22cf95e58c | ||
|
|
358355c612 | ||
|
|
c5f9600047 | ||
|
|
703731e654 | ||
|
|
0dfa4c7aad | ||
|
|
68e473ec02 | ||
|
|
a8d20734aa | ||
|
|
ebdb76cf6c | ||
|
|
e62e1ac2dd | ||
|
|
e68b46f647 | ||
|
|
7605cd3f1d | ||
|
|
2735c283ce | ||
|
|
0daaec81ca | ||
|
|
2085df4406 | ||
|
|
9b8664d3a3 | ||
|
|
e027a33e8d | ||
|
|
f04a19d34d | ||
|
|
4b27671a91 | ||
|
|
5f4943e025 | ||
|
|
66cdb94d57 | ||
|
|
5ebb0e6a9c | ||
|
|
dfb7f43c00 | ||
|
|
ed85d35117 | ||
|
|
7d69e77ac6 | ||
|
|
03973d215b | ||
|
|
1b182d89cd | ||
|
|
3d5c2c7450 | ||
|
|
d6264dd994 | ||
|
|
4c18123207 | ||
|
|
71e7f41ea5 | ||
|
|
60cf5d1019 | ||
|
|
3977057dfa | ||
|
|
943bdc3e88 | ||
|
|
44710048f4 | ||
|
|
9f9f5655b1 | ||
|
|
e621811364 | ||
|
|
685bc846e8 | ||
|
|
3a9d58fae9 | ||
|
|
33639c6e4c | ||
|
|
7c235e3179 | ||
|
|
559d749207 | ||
|
|
53feae0f8c | ||
|
|
0941ccc0ac | ||
|
|
82ecd96ba4 | ||
|
|
e118dc5db1 | ||
|
|
1a23491ac2 | ||
|
|
d916f167a5 | ||
|
|
0102249549 | ||
|
|
2ca96dab42 | ||
|
|
345e60b7a3 | ||
|
|
b9fb6c9d35 | ||
|
|
5d0ccd8867 | ||
|
|
b36f484f3b | ||
|
|
d2f7f986c7 | ||
|
|
9137d03b9f | ||
|
|
f6c0d13a8b | ||
|
|
c472b4e33a | ||
|
|
f6c3fc6b50 | ||
|
|
8ba146495e | ||
|
|
68c46fefb2 | ||
|
|
eddda41559 | ||
|
|
5945fdc2ef | ||
|
|
d110dcbdcf | ||
|
|
ebc0aa8a4e | ||
|
|
d4113b464a | ||
|
|
753dfe1fb3 | ||
|
|
013cccaac5 | ||
|
|
504f8ffbeb | ||
|
|
81d1eb2553 | ||
|
|
c9327b0e92 | ||
|
|
36aa236d82 | ||
|
|
3c37aa4449 | ||
|
|
f1bf3cbe64 | ||
|
|
17a9356040 | ||
|
|
97c799bf2c | ||
|
|
99c02daec7 | ||
|
|
4cb792641d | ||
|
|
fbc2950854 | ||
|
|
08e0bdb437 | ||
|
|
73dcab4de1 | ||
|
|
00594a14ed | ||
|
|
52d5bab0f5 | ||
|
|
a41f4c56f7 | ||
|
|
8ea83962d6 | ||
|
|
9d97cc6e4c | ||
|
|
023579de9a | ||
|
|
214d99f1c3 | ||
|
|
d6fc6b9b10 | ||
|
|
cbdab93f92 | ||
|
|
abf37f0687 | ||
|
|
e993a43399 | ||
|
|
bc79969fd0 | ||
|
|
801f9ed403 | ||
|
|
3d9f6f8f58 | ||
|
|
c2676fb620 | ||
|
|
cdb4ab23e9 | ||
|
|
b3217f2d9c | ||
|
|
7b61385ad5 | ||
|
|
b5c86e74ad | ||
|
|
a3031a7eb9 | ||
|
|
7c49a0535f | ||
|
|
797e5b0794 | ||
|
|
bc6cd78c86 | ||
|
|
98a5b40be5 | ||
|
|
87c174fe0f | ||
|
|
0cbb45e4ad | ||
|
|
b11226af94 | ||
|
|
27dc5b5e19 | ||
|
|
d892b83110 | ||
|
|
c28984524a | ||
|
|
5d4fb63baa | ||
|
|
ca2574d1e5 | ||
|
|
758038e278 | ||
|
|
cb803b1a24 | ||
|
|
c985af5722 | ||
|
|
1ede54acbb | ||
|
|
a73ca6a95d | ||
|
|
25f4095276 | ||
|
|
d242c133dd | ||
|
|
da3e8044b7 | ||
|
|
b6d5939fbf | ||
|
|
d23ef88647 | ||
|
|
f374f130de | ||
|
|
b0c2c8611b | ||
|
|
6f6f56ce64 | ||
|
|
4b28ec4b3a | ||
|
|
d189084c15 | ||
|
|
975d9540bc | ||
|
|
212bfba950 | ||
|
|
619e32cd83 | ||
|
|
ce9ff53ae1 | ||
|
|
d13fb33884 | ||
|
|
253e91250f | ||
|
|
b23668372b | ||
|
|
5b847c136e | ||
|
|
0d304a793d | ||
|
|
906e1a1a19 | ||
|
|
87f46f1e02 | ||
|
|
56b4be1fe1 | ||
|
|
a08dcf05f1 | ||
|
|
460882f6c1 | ||
|
|
71fc94ba63 | ||
|
|
b587d834bc | ||
|
|
9b63e022ed | ||
|
|
324cbd76ee | ||
|
|
ceab13f672 | ||
|
|
10e17ed239 | ||
|
|
4f84479513 | ||
|
|
216ad4ed32 | ||
|
|
85e4555322 | ||
|
|
a90e443a88 | ||
|
|
a9c54b5f91 | ||
|
|
6d21435aab | ||
|
|
9c3bb11a66 | ||
|
|
d983080584 | ||
|
|
852b96009c | ||
|
|
c82617d857 | ||
|
|
6f19256187 | ||
|
|
479ede334b | ||
|
|
e1349255de | ||
|
|
4a600dc01d | ||
|
|
315256ce96 | ||
|
|
bbead20723 | ||
|
|
bb77c1c0bb | ||
|
|
2a2fb9ad55 | ||
|
|
ea5b66cdae | ||
|
|
1a978c7ee3 | ||
|
|
cfd458f463 | ||
|
|
61b8277822 | ||
|
|
53c8036e34 | ||
|
|
f5c8ce5109 | ||
|
|
17b11f19a3 | ||
|
|
2993279d78 | ||
|
|
b95b7e3781 | ||
|
|
c57200132e | ||
|
|
f6d9921581 | ||
|
|
3aada673d5 | ||
|
|
1456070044 | ||
|
|
883797a51d | ||
|
|
1b93512b0a | ||
|
|
7c99262a79 | ||
|
|
d10ecb88bb | ||
|
|
bb0f8e1a9a | ||
|
|
8be1ace9a1 | ||
|
|
80d6879d2a | ||
|
|
0b8dfaa5b5 | ||
|
|
3e25be1e9d | ||
|
|
4e550d14af | ||
|
|
eab33559ca | ||
|
|
83076a0770 | ||
|
|
850f9f081a | ||
|
|
1110b6b047 | ||
|
|
3c99adc01c | ||
|
|
55cec9a16f |
@@ -1,7 +1,7 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||
app:
|
||||
multi-scrobbler:
|
||||
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:24-bookworm
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"dockerComposeFile": [
|
||||
"compose.yml"
|
||||
// uncomment to add kasm/discord service
|
||||
//"../deploy/docker-compose.discord.yml"
|
||||
],
|
||||
|
||||
"service": "app",
|
||||
"service": "multi-scrobbler",
|
||||
|
||||
"workspaceFolder": "/workspaces",
|
||||
|
||||
@@ -18,7 +20,7 @@
|
||||
"forwardPorts": [9078,3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
"postCreateCommand": ".devcontainer/postCreate.sh",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
@@ -28,7 +30,9 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"TakumiI.markdowntable"
|
||||
"TakumiI.markdowntable",
|
||||
"YoavBls.pretty-ts-errors",
|
||||
"bowlerr.sqlite-intelliview-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo 'Adding node_modules to path'
|
||||
echo 'export PATH=/workspaces/node_modules/.bin:/workspaces/docsite/node_modules/.bin:$PATH' >> /home/node/.bashrc
|
||||
|
||||
echo 'Updating npm...'
|
||||
npm install -g npm@11.12.1
|
||||
|
||||
echo 'Installing concurrently...'
|
||||
npm install -g concurrently
|
||||
|
||||
echo 'If you have freshly cloned this project or node_modules folder does not exist then you should run "npm run install:parallel" now'
|
||||
+6
-1
@@ -13,6 +13,7 @@ config/*.json
|
||||
config/*.bak
|
||||
config/*.p8
|
||||
/docs
|
||||
dist
|
||||
/logs
|
||||
.flatpak-builder
|
||||
**/generated-sources.*
|
||||
@@ -27,4 +28,8 @@ build
|
||||
.vscode
|
||||
**/act/*
|
||||
tmp-*
|
||||
.env
|
||||
.env
|
||||
*.env
|
||||
.storybook
|
||||
.stories
|
||||
**/lexicons/**/*.json
|
||||
@@ -0,0 +1,37 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
groups:
|
||||
sec-updates:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
directories:
|
||||
- "/"
|
||||
- "/docsite"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
# Look for a `Dockerfile` in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
groups:
|
||||
sec-updates:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
version-updates:
|
||||
applies-to: "version-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,6 +1,6 @@
|
||||
## Checklist before requesting a review
|
||||
|
||||
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
|
||||
- [ ] I have read the [contributing guidelines.](../blob/master/CONTRIBUTING.md)
|
||||
|
||||
## Type of change
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Install app dev dependencies
|
||||
@@ -28,22 +28,24 @@ jobs:
|
||||
- name: Install docusaurus dependencies
|
||||
run: npm ci
|
||||
working-directory: ./docsite
|
||||
|
||||
- name: Build website
|
||||
env:
|
||||
ANALYTICS: ${{ vars.ANALYTICS }}
|
||||
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
|
||||
RY_ANALYTICS: ${{ vars.RY_ANALYTICS }}
|
||||
RY_ANALYTICS_SITEID: ${{ vars.RY_ANALYTICS_SITEID }}
|
||||
RY_ANALYTICS_REPLAY: ${{ vars.RY_ANALYTICS_REPLAY }}
|
||||
GSITEVERIFICATION: ${{ vars.GSITEVERIFICATION }}
|
||||
DOCS_BASE: '/multi-scrobbler'
|
||||
run: npm run build
|
||||
working-directory: ./docsite
|
||||
|
||||
- name: Replace canonical
|
||||
working-directory: ./docsite
|
||||
run: node --experimental-strip-types canonical-replace.ts ./build
|
||||
|
||||
# Popular action to deploy to GitHub Pages:
|
||||
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #4.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build output to publish to the `gh-pages` branch:
|
||||
@@ -55,3 +57,24 @@ jobs:
|
||||
# You can swap them out with your own user credentials.
|
||||
user_name: github-actions[bot]
|
||||
user_email: 41898282+github-actions[bot]@users.noreply.github.com
|
||||
|
||||
- name: Trigger dependent build
|
||||
if: ${{ vars.DOCS_REPO != ''}}
|
||||
run: |
|
||||
# Trigger builds in dependent repositories
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ vars.DOCS_REPO }}/dispatches" \
|
||||
-d "{
|
||||
\"event_type\": \"release\",
|
||||
\"client_payload\": {
|
||||
\"commit\": \"${{ github.sha }}\"
|
||||
}
|
||||
}"
|
||||
- name: Trigger CF Pages build
|
||||
if: ${{ vars.CF_PAGES == 'true' }}
|
||||
run: |
|
||||
# trigger CF pages hook deployment
|
||||
curl -d "" \
|
||||
"${{ secrets.CF_PAGES_HOOK }}"
|
||||
@@ -51,7 +51,9 @@ jobs:
|
||||
strings=("${VARS_DOCKERHUB_SLUG}" "${VARS_GHCR_SLUG}")
|
||||
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
|
||||
joined_string=$(IFS=, ; echo "${strings[*]}")
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "BUILD_DATE=$date" >> $GITHUB_ENV
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
@@ -104,6 +106,7 @@ jobs:
|
||||
context: ${{github.event.pull_request.head.repo.clone_url}}#${{github.event.pull_request.head.sha}}
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
@@ -117,7 +120,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #7.0.0
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -136,7 +139,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -162,7 +165,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #6.0.0
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
@@ -220,7 +223,7 @@ jobs:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #2.9.4
|
||||
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff #3.0.3
|
||||
with:
|
||||
recreate: true
|
||||
header: "pr-release"
|
||||
|
||||
@@ -66,13 +66,15 @@ jobs:
|
||||
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
|
||||
joined_string=$(IFS=, ; echo "${strings[*]}")
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "BUILD_DATE=$date" >> $GITHUB_ENV
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -126,6 +128,7 @@ jobs:
|
||||
with:
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
@@ -140,7 +143,7 @@ jobs:
|
||||
env:
|
||||
VARS_DIGEST: ${{ steps.build.outputs.digest }}
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #7.0.0
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -159,7 +162,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -185,7 +188,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #6.0.0
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
node-version:
|
||||
description: "Node version"
|
||||
required: false
|
||||
default: '20.x'
|
||||
default: '24.x'
|
||||
type: string
|
||||
ref:
|
||||
description: "Ref to checkout"
|
||||
@@ -18,13 +18,18 @@ jobs:
|
||||
name: Tests / Build / Sanity Run
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
- name: Check out the repo (gha)
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out the repo (act)
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: 'npm'
|
||||
@@ -55,7 +60,7 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
export NODE_ENV=production
|
||||
timeout --preserve-status 10s node node_modules/.bin/tsx src/backend/index.ts
|
||||
timeout --preserve-status 10s node src/backend/index.ts
|
||||
exitcode="$?"
|
||||
if [[ "$exitcode" -eq 143 ]] || [[ "$exitcode" -eq 137 ]]; then
|
||||
echo "App stayed up long enough and exited with expected status"
|
||||
|
||||
+14
@@ -73,6 +73,7 @@ web_modules/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
*.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
@@ -133,6 +134,7 @@ src/backend/common/schema/*.json
|
||||
!src/backend/common/schema/aio.json
|
||||
!src/backend/common/schema/client.json
|
||||
!src/backend/common/schema/source.json
|
||||
docsite/static/schemas/*.json
|
||||
|
||||
|
||||
*.bak
|
||||
@@ -146,3 +148,15 @@ build
|
||||
**/generated-sources.*
|
||||
.github/act/out
|
||||
tmp-*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
*.db
|
||||
*.db*
|
||||
|
||||
**/lexicons/**/*.json
|
||||
|
||||
bun.lock
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"reporter": "dot",
|
||||
"extension": "ts",
|
||||
"import": "tsx/esm",
|
||||
"spec": "./src/backend/tests/**/*.test.ts",
|
||||
"file": [
|
||||
"./src/backend/tests/setup.ts"
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"exclude": [
|
||||
"node_modules/",
|
||||
"**/src/backend/common/schema/**",
|
||||
"**/src/backend/tests/**",
|
||||
"register.js",
|
||||
"**/src/server/**/*.d.ts",
|
||||
"**/src/client/**"
|
||||
],
|
||||
"include": [
|
||||
"**/src/backend/**/*.ts",
|
||||
"**/src/**/backend/*.js",
|
||||
"**/src/**/backend/*.js.map"
|
||||
],
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"reporter": [
|
||||
"text-summary",
|
||||
"html"
|
||||
],
|
||||
"report-dir": "./coverage"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { defineMain } from "@storybook/react-vite/node";
|
||||
import { mergeConfig } from "vite";
|
||||
|
||||
|
||||
export default defineMain({
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
"@chromatic-com/storybook",
|
||||
// "@storybook/addon-vitest",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-docs",
|
||||
'storybook-addon-remix-react-router'
|
||||
// "@storybook/addon-onboarding"
|
||||
],
|
||||
features: {
|
||||
// https://github.com/storybookjs/storybook/discussions/33279
|
||||
sidebarOnboardingChecklist: false,
|
||||
},
|
||||
staticDirs: ['../public'],
|
||||
"framework": "@storybook/react-vite",
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
optimizeDeps: {
|
||||
include: ["@storybook/addon-docs"],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "fix-mdx-react-shim",
|
||||
enforce: "pre",
|
||||
resolveId(source: any) {
|
||||
if (
|
||||
source.startsWith("file://") &&
|
||||
source.includes("mdx-react-shim.js")
|
||||
) {
|
||||
// Convert file:///... path to normal filesystem path for Vite
|
||||
return new URL(source).pathname;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { addons } from 'storybook/manager-api';
|
||||
import { themes } from 'storybook/theming';
|
||||
|
||||
addons.setConfig({
|
||||
theme: themes.normal,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import addonDocs from "@storybook/addon-docs";
|
||||
import addonA11y from "@storybook/addon-a11y";
|
||||
import { definePreview } from "@storybook/react-vite";
|
||||
|
||||
import { themes } from 'storybook/theming';
|
||||
import { initialize, mswLoader, getWorker } from 'msw-storybook-addon';
|
||||
|
||||
// https://github.com/mswjs/msw-storybook-addon/issues/82#issuecomment-3894302575
|
||||
try {
|
||||
getWorker();
|
||||
} catch (_) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
export default definePreview({
|
||||
loaders: [mswLoader],
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo'
|
||||
},
|
||||
docs: {
|
||||
theme: themes.normal,
|
||||
},
|
||||
},
|
||||
|
||||
addons: [addonA11y(), addonDocs()]
|
||||
});
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"sanjulaganepola.github-local-actions",
|
||||
"github.vscode-github-actions"
|
||||
]
|
||||
}
|
||||
Vendored
+37
-21
@@ -1,13 +1,44 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Zod Conversion",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/ts-zod.ts",
|
||||
"args": [
|
||||
"${file}"
|
||||
],
|
||||
"runtimeExecutable": "node",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Run Canonical Replace",
|
||||
"program": "${workspaceFolder}/docsite/canonical-replace.ts",
|
||||
"runtimeArgs": ["--experimental-strip-types"],
|
||||
"args": [
|
||||
"${workspaceFolder}/docsite/build"
|
||||
],
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"name": "dev",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/index.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"runtimeExecutable": "node",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
@@ -22,7 +53,7 @@
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/index.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"runtimeExecutable": "node",
|
||||
"console": "integratedTerminal",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
@@ -35,28 +66,13 @@
|
||||
"${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",
|
||||
"program": "${workspaceFolder}/src/backend/utils/SchemaDocsStaticUtils.ts",
|
||||
"runtimeExecutable": "node",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
@@ -83,7 +99,7 @@
|
||||
"--recursive",
|
||||
"${workspaceFolder}/src/backend/tests/**/*.test.ts"
|
||||
],
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"runtimeExecutable": "node",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"name": "Mocha Tests",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
@@ -107,7 +123,7 @@
|
||||
"--config", "${workspaceRoot}/.mocharc.json",
|
||||
"${file}"
|
||||
],
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"runtimeExecutable": "node",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"name": "Mocha Test on File",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
|
||||
Vendored
+6
-4
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"mochaExplorer.require": "tsx/esm",
|
||||
"mochaExplorer.timeout": 1200000,
|
||||
"mochaExplorer.exit": true,
|
||||
"search.exclude": {
|
||||
"**/.docusaurus": true,
|
||||
"**/build": true
|
||||
"**/build": true,
|
||||
"**/schema": true
|
||||
},
|
||||
"tailwindCSS.files.exclude": [
|
||||
"docsite/**"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
"*.css": "tailwindcss",
|
||||
"**/config/*.json": "jsonc"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": "src/client/index.css"
|
||||
"tailwindCSS.experimental.configFile": "src/client/index.css",
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
coc@foxxmd.dev.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+28
-7
@@ -1,7 +1,7 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
ENV NODE_VERSION=20.18.1
|
||||
ENV NODE_VERSION=24.14.0
|
||||
|
||||
# borrowing openssl header removal trick from offical docker-node
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
|
||||
@@ -49,11 +49,14 @@ RUN \
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN echo "Node: $(node -v)\nNPM: $(npm -v)"
|
||||
|
||||
RUN npm install -g concurrently
|
||||
|
||||
ARG data_dir=/config
|
||||
VOLUME $data_dir
|
||||
ENV CONFIG_DIR=$data_dir
|
||||
ENV DATA_DIR=$data_dir
|
||||
|
||||
COPY docker/root /
|
||||
|
||||
@@ -63,7 +66,8 @@ FROM base AS build
|
||||
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --chown=abc:abc docsite/package*.json tsconfig.json ./docsite/
|
||||
COPY --chown=abc:abc docsite/patches ./docsite/patches
|
||||
COPY --chown=abc:abc docsite/package*.json docsite/tsconfig* ./docsite/
|
||||
|
||||
# for debugging, so the build fails faster when timing out (arm64)
|
||||
#RUN npm config set fetch-retries 1 && \
|
||||
@@ -90,30 +94,47 @@ RUN npm run build:parallel && rm -rf node_modules && rm -rf docsite/node_modules
|
||||
|
||||
FROM base AS app
|
||||
|
||||
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
|
||||
# frontend build from vite/esbuild
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
# backend
|
||||
COPY --from=build --chown=abc:abc /app/src/backend /app/src/backend
|
||||
COPY --from=build --chown=abc:abc /app/src/core /app/src/core
|
||||
# docusaurus docs
|
||||
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
|
||||
COPY --from=base /usr/bin /usr/bin
|
||||
COPY --from=base /usr/lib /usr/lib
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV IS_DOCKER=true
|
||||
ENV COLORED_STD=true
|
||||
|
||||
# https://stackoverflow.com/a/63640896/1469797
|
||||
ARG APP_BUILD_VERSION
|
||||
ENV APP_VERSION=$APP_BUILD_VERSION
|
||||
|
||||
RUN npm ci --omit=dev --no-audit \
|
||||
# we used compiled schema json for production so this isn't needed
|
||||
&& npm uninstall ts-json-schema-generator \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules \
|
||||
## peerDependency of validbot and can(?) be removed for production
|
||||
&& rm -r node_modules/typescript \
|
||||
&& npx @usex/prune-mod -w \
|
||||
&& rm -rf /root/.cache
|
||||
|
||||
ARG BUILD_DATE=0
|
||||
|
||||
LABEL org.opencontainers.image.version="$APP_BUILD_VERSION" \
|
||||
org.opencontainers.image.source="https://github.com/FoxxMD/multi-scrobbler" \
|
||||
org.opencontainers.image.documentation="https://docs.multi-scrobbler.app" \
|
||||
org.opencontainers.image.description="Scrobble from multiple sources to multiple clients" \
|
||||
org.opencontainers.image.title="Multi-Scrobbler" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.authors="FoxxMD" \
|
||||
org.opencontainers.image.created="$BUILD_DATE" \
|
||||
org.opencontainers.image.url="https://docs.multi-scrobbler.app" \
|
||||
maintainer="FoxxMD" \
|
||||
build_version="$APP_BUILD_VERSION"
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://github.com/FoxxMD/multi-scrobbler/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://hub.docker.com/r/foxxmd/multi-scrobbler)
|
||||
[](https://foxxmd.github.io/multi-scrobbler/)
|
||||
[](https://docs.multi-scrobbler.app/)
|
||||
|
||||
|
||||
<img src="/assets/icon.png" align="right"
|
||||
@@ -11,56 +11,59 @@ alt="multi-scrobbler logo" width="180" height="180">
|
||||
|
||||
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
|
||||
|
||||
* Supports scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
|
||||
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/azuracast)
|
||||
* [Deezer](https://foxxmd.github.io/multi-scrobbler/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/google-cast)
|
||||
* [Icecast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/icecast)
|
||||
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jellyfin)
|
||||
* [JRiver](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jriver)
|
||||
* [Kodi](https://foxxmd.github.io/multi-scrobbler/configuration/sources/kodi)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/sources/koito-source)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-endpoint)
|
||||
* [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)
|
||||
* [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 scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
|
||||
* [Apple Music](https://docs.multi-scrobbler.app/configuration/sources/applemusic/?configType=file)
|
||||
* [Azuracast](https://docs.multi-scrobbler.app/configuration/sources/azuracast)
|
||||
* [Deezer](https://docs.multi-scrobbler.app/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://docs.multi-scrobbler.app/configuration/sources/google-cast)
|
||||
* [Icecast](https://docs.multi-scrobbler.app/configuration/sources/icecast)
|
||||
* [Jellyfin](https://docs.multi-scrobbler.app/configuration/sources/jellyfin)
|
||||
* [JRiver](https://docs.multi-scrobbler.app/configuration/sources/jriver)
|
||||
* [Kodi](https://docs.multi-scrobbler.app/configuration/sources/kodi)
|
||||
* [Koito](https://docs.multi-scrobbler.app/configuration/sources/koito-source)
|
||||
* [Last.fm](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/lastfm-endpoint)
|
||||
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/sources/librefm-source)
|
||||
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint)
|
||||
* [Maloja](https://docs.multi-scrobbler.app/configuration/sources/maloja)
|
||||
* [Mopidy](https://docs.multi-scrobbler.app/configuration/sources/mopidy)
|
||||
* [MPD (Music Player Daemon)](https://docs.multi-scrobbler.app/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](https://docs.multi-scrobbler.app/configuration/sources/mpris)
|
||||
* [Musikcube](https://docs.multi-scrobbler.app/configuration/sources/musikcube)
|
||||
* [Music Assistant](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint#music-assistant)
|
||||
* [Plex](https://docs.multi-scrobbler.app/configuration/sources/plex)
|
||||
* [Rocksky](https://docs.multi-scrobbler.app/configuration/sources/rocksky-source)
|
||||
* [Sonos](https://docs.multi-scrobbler.app/configuration/sources/sonos)
|
||||
* [Spotify](https://docs.multi-scrobbler.app/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](https://docs.multi-scrobbler.app/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](https://docs.multi-scrobbler.app/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](https://docs.multi-scrobbler.app/configuration/sources/webscrobbler)
|
||||
* [VLC](https://docs.multi-scrobbler.app/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](https://docs.multi-scrobbler.app/configuration/sources/yamaha-musiccast)
|
||||
* [Youtube Music](https://docs.multi-scrobbler.app/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](https://docs.multi-scrobbler.app/configuration/clients)
|
||||
* [Discord](https://docs.multi-scrobbler.app/configuration/clients/discord) (Now Playing)
|
||||
* [Koito](https://docs.multi-scrobbler.app/configuration/clients/koito)
|
||||
* [Last.fm](https://docs.multi-scrobbler.app/configuration/clients/lastfm)
|
||||
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/clients/librefm)
|
||||
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/clients/listenbrainz)
|
||||
* [Maloja](https://docs.multi-scrobbler.app/configuration/clients/maloja)
|
||||
* [Rocksky](https://docs.multi-scrobbler.app/configuration/clients/rocksky)
|
||||
* [teal.fm](https://docs.multi-scrobbler.app/configuration/clients/tealfm)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://docs.multi-scrobbler.app/configuration#webhook-configurations), [healthcheck endpoints](https://docs.multi-scrobbler.app/configuration#health-endpoint), or [prometheus metrics](https://docs.multi-scrobbler.app/configuration/#prometheus).
|
||||
* Supports [Now Playing](https://docs.multi-scrobbler.app/configuration/clients#now-playing) for scrobble Clients
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Graceful network and client failure handling (queued scrobbles that auto-retry)
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://docs.multi-scrobbler.app/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://docs.multi-scrobbler.app/installation#docker) or [locally with NodeJS](https://docs.multi-scrobbler.app/installation#nodejs)
|
||||
|
||||
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
|
||||
[**Quick Start Guide**](https://docs.multi-scrobbler.app//quickstart)
|
||||
|
||||
<img src="/assets/status-ui.png" width="800">
|
||||
<img src="/docsite/static/img/status-ui.png" width="800">
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
@@ -71,15 +74,15 @@ A dockerized app that monitors your music listening activity from *everywhere* a
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
Yes! You can use [Last.fm as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
You set up [configurations](https://docs.multi-scrobbler.app/configuration) for one or more [**Sources**](https://docs.multi-scrobbler.app/configuration/sources) and one or more [**Clients**](https://docs.multi-scrobbler.app/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
|
||||
### Source
|
||||
|
||||
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
A [**Source**](https://docs.multi-scrobbler.app/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
@@ -89,7 +92,7 @@ Source configurations consist of:
|
||||
|
||||
### Client
|
||||
|
||||
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
|
||||
A [**Client**](https://docs.multi-scrobbler.app/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
@@ -98,15 +101,15 @@ Client configurations consist of:
|
||||
|
||||
## Quick Start
|
||||
|
||||
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
|
||||
[See the **Quick Start Guide**](https://docs.multi-scrobbler.app/quickstart)
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
|
||||
[See the **Installation** documentation](https://docs.multi-scrobbler.app/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
|
||||
[See the **Configuration** documentation](https://docs.multi-scrobbler.app/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -121,11 +124,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
|
||||
Having issues with connections or configuration? Check the [FAQ](https://docs.multi-scrobbler.app/FAQ) before creating an issue!
|
||||
|
||||
## Development
|
||||
|
||||
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
|
||||
[Detailed architecture and development guides for Sources/Clients](https://docs.multi-scrobbler.app/development/dev-common)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* Attribution for [`default-artwork.png`](/assets/default-artwork.png): https://www.flaticon.com/free-icon/music_15795471
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 699 KiB |
+42
-29
@@ -1,18 +1,20 @@
|
||||
# configuration file for git-cliff (0.1.0)
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
# A Tera template to be rendered as the changelog's header.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
header = """
|
||||
# Changelog
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
# A Tera template to be rendered for each release in the changelog.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
## Unreleased
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
@@ -24,6 +26,7 @@ body = """
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
@@ -33,35 +36,45 @@ body = """
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespaces from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
# A Tera template to be rendered as the changelog's footer.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
# allow only conventional commits
|
||||
# https://www.conventionalcommits.org
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# regex for parsing and grouping commits
|
||||
# Exclude commits that do not match the conventional commits specification.
|
||||
filter_unconventional = true
|
||||
# An array of regex based parsers for extracting data from the commit message.
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features"},
|
||||
{ message = "^fix", group = "Bug Fixes"},
|
||||
{ message = "^doc", group = "Documentation"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^test", group = "Testing"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||
{ body = ".*security", group = "Security"},
|
||||
{ message = "^feat", group = "⛰️ Features" },
|
||||
{ message = "^fix", group = "🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "📚 Documentation" },
|
||||
{ message = "^perf", group = "⚡ Performance" },
|
||||
{ message = "^refactor", group = "🚜 Refactor" },
|
||||
{ message = "^style", group = "💅 Styling" },
|
||||
{ message = "^test", group = "🧪 Testing" },
|
||||
{ message = "^ci", group = "🧑🍳 Continuous Integration" },
|
||||
{ message = "^build", group = "🐋 Build" },
|
||||
{ message = "^chore\\(release\\)", skip = true },
|
||||
{ message = "^chore", group = "⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "🛡️ Security" },
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
# Exclude commits that are not matched by any commit parser.
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order = false
|
||||
# Order of commits in each group/release within the changelog.
|
||||
# Allowed values: newest, oldest
|
||||
sort_commits = "oldest"
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"id": "myAppleMusic",
|
||||
"name": "My Apple Music",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
// either key or token needs to be provided
|
||||
"key": {
|
||||
"id": "2HPSNJZ88N",
|
||||
"teamId": "SN6YASW8G4",
|
||||
"p8": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEG...-----END PRIVATE KEY-----"
|
||||
},
|
||||
"token": "your-jwt-token-here",
|
||||
"mediaUserToken": "your-media-user-token-here",
|
||||
|
||||
// optional: Origin header for API requests (required when using a browser token)
|
||||
"origin": "https://music.apple.com",
|
||||
|
||||
// optional: polling interval in seconds (default 60)
|
||||
"interval": 60
|
||||
},
|
||||
"options": {
|
||||
"logDiff": true,
|
||||
"recoverUnchangedTopHistory": true,
|
||||
"normalizeAlbum": true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2,7 +2,8 @@
|
||||
{
|
||||
"type": "azuracast",
|
||||
"enable": true,
|
||||
"name": "azura",
|
||||
"name": "Cool Station",
|
||||
"id": "myAzura",
|
||||
"data": {
|
||||
"url": "ws://192.168.0.101",
|
||||
"station": "my-station-name",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"name": "My Cast",
|
||||
"id": "myCast",
|
||||
"data": {
|
||||
"blacklistedDevices": [],
|
||||
"whitelistedDevices": [],
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"type": "spotify",
|
||||
"enable": true,
|
||||
"clients": ["myConfig"],
|
||||
"name": "mySpotifySource",
|
||||
"name": "Cool Spotify Name",
|
||||
"id": "mySpotifyId",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
@@ -34,7 +35,8 @@
|
||||
{
|
||||
"type": "maloja",
|
||||
"enable": true,
|
||||
"name": "myConfig",
|
||||
"name": "Foxx Maloja",
|
||||
"id": "myMalojaId",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "DeezerARL",
|
||||
"name": "Family Account",
|
||||
"id": "myDeezerId",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"name": "Desktop",
|
||||
"enable": true,
|
||||
"id": "myDiscordLocal",
|
||||
"data": {
|
||||
"applicationId": "8190211179716453570",
|
||||
"ipcLocations": ["192.168.0.105:6655"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "Discord",
|
||||
"enable": true,
|
||||
"id": "myDiscordRemote",
|
||||
"data": {
|
||||
"token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
|
||||
"applicationId": "8190211179716453570"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "myLfm",
|
||||
"name": "LFM For App",
|
||||
"enable": true,
|
||||
"id": "myLfmApp",
|
||||
"data": {
|
||||
"slug": "mySlug"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myLz",
|
||||
"name": "LZ for Navidrome",
|
||||
"id": "myLzNavidrome",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "myIcy",
|
||||
"name": "Station 1",
|
||||
"enable": true,
|
||||
"id": "myIceStation1",
|
||||
"data": {
|
||||
"url": "http://myIcecast.com/radio"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "MyJellyfin",
|
||||
"name": "Cool JF Server",
|
||||
"enable": true,
|
||||
"id": "myJellyfin",
|
||||
"clients": [],
|
||||
"data": {
|
||||
"url": "http://localhost:8096",
|
||||
@@ -16,7 +17,8 @@
|
||||
"librariesAllow": ["GoodMusic"],
|
||||
"librariesBlock": ["BadMusic"],
|
||||
"additionalAllowedLibraryTypes": ["musicvideos"],
|
||||
"allowUnknown": false,
|
||||
"allowMediaTypes": ["audio"],
|
||||
"frontendUrlOverride": "https://myJellyfinExternal.tld"
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyJriver",
|
||||
"name": "My JRiver Server",
|
||||
"id": "myRiver",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "0.0.0.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyKodi",
|
||||
"name": "My Kodi Server",
|
||||
"id": "myKodi",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://localhost:8080/jsonrpc",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "koito-client",
|
||||
{
|
||||
"name": "Koito Client A",
|
||||
"id": "myKoitoClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
@@ -9,7 +10,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "koito-source",
|
||||
"name": "Koito Source B",
|
||||
"id": "myKoitoSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myLastFmClient",
|
||||
"name": "Foxx LFM Client",
|
||||
"id": "myLastFmClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
@@ -10,7 +11,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myLastFmSource",
|
||||
"name": "Foxx LFM Source",
|
||||
"id": "myLastFmSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myLibreFmClient",
|
||||
"name": "Foxx Libre Client",
|
||||
"id": "myLibreFmClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
@@ -10,7 +11,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myLibreFmSource",
|
||||
"name": "Foxx Libre Source",
|
||||
"id": "myLibreFmSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "brainzClient",
|
||||
"name": "Foxx LZ Client",
|
||||
"id": "brainzClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
@@ -10,7 +11,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "brainzSource",
|
||||
"name": "Foxx LZ Source",
|
||||
"id": "brainzSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myMaloja-client",
|
||||
"name": "Maloja Client",
|
||||
"id": "myMalojaClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
@@ -9,7 +10,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myMaloja-source",
|
||||
"name": "Maloja Source",
|
||||
"id": "myMalojaSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyMopidy",
|
||||
"name": "Mopidy Desktop",
|
||||
"id": "MyMopidy",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "localhost",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MyMPD",
|
||||
"id": "myMPD",
|
||||
"name": "Desktop",
|
||||
"data": {
|
||||
"url": "192.168.0.100:6600",
|
||||
"password": "MY_PASSWORD"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "ubuntu",
|
||||
"name": "Desktop",
|
||||
"id": "myMprisUbuntu",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"whitelist": ["vlc", "mpd"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myYamaha",
|
||||
"name": "Livingroom Reciever",
|
||||
"id": "myYamaha",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "192.168.0.101"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
{
|
||||
"type": "musikcube",
|
||||
"enable": true,
|
||||
"name": "musikcube",
|
||||
"name": "Desktop",
|
||||
"id": "myMusikcube",
|
||||
"data": {
|
||||
"url": "ws://localhost:7905",
|
||||
"password": "MY_PASSWORD"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyPlexApi",
|
||||
"name": "Foxx Plex",
|
||||
"id": "MyPlexApi",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
[
|
||||
{
|
||||
"name": "myRockskyClient",
|
||||
"id": "myRockskyClient",
|
||||
"name": "FoxxMD Rocksky Client",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6cGxjOng0NjVwZmtyeW5zaDZ...",
|
||||
"handle": "alice.bsky.social"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myRockskySource",
|
||||
"id": "myRockskySource",
|
||||
"name": "FoxxMD Rocksky Source",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6cGxjOng0NjVwZmtyeW5zaDZ...",
|
||||
"handle": "alice.bsky.social"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MySonos",
|
||||
"id": "MySonos",
|
||||
"name": "Boom 2",
|
||||
"data": {
|
||||
"host": "192.168.0.150",
|
||||
"devicesAllow": ["move","beam"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MySpotify",
|
||||
"id": "MySpotify",
|
||||
"name": "Foxx Spotify",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
[
|
||||
{
|
||||
"name": "MySubsonic",
|
||||
"id": "mySubsonic",
|
||||
"name": "Airsonic Server",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://localhost:4040/airsonic",
|
||||
"user": "yourUser",
|
||||
"password": "yourPassword",
|
||||
"usersAllow": ["yourUser"]
|
||||
"usersAllow": ["yourUser"],
|
||||
"detectStaleNowPlayingFromMinutesAgo": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "myTeal",
|
||||
"id": "myTealClient",
|
||||
"name": "Foxx Teal Client",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
@@ -8,7 +9,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"id": "myTealSource",
|
||||
"name": "Foxx Teal Source",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MyVlc",
|
||||
"id": "myVlc",
|
||||
"name": "Desktop",
|
||||
"data": {
|
||||
"url": "localhost:8080",
|
||||
"password": "changeme"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyWebScrobbler",
|
||||
"id": "MyWebScrobbler",
|
||||
"name": "Firefox Laptop",
|
||||
"data": {
|
||||
"slug": "MyOptionalSlug",
|
||||
"whitelist": [],
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"id": "myYandex",
|
||||
"name": "Yandex Music",
|
||||
"type": "ymbridge",
|
||||
"data": {
|
||||
"url": "http://192.168.1.161:9980",
|
||||
"apiKey": "change-me",
|
||||
"interval": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "MyYTMusic",
|
||||
"id": "myYtMusic",
|
||||
"name": "Foxx YT Music",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
multi-scrobbler:
|
||||
volumes:
|
||||
- kasm_run:/run
|
||||
kasmcord:
|
||||
container_name: kasmcord
|
||||
image: kasmweb/discord:1.14.0
|
||||
ports:
|
||||
# VNC port to use in web browser
|
||||
- 6901:6901
|
||||
shm_size: 512m
|
||||
environment:
|
||||
# set a password for VNC access
|
||||
VNC_PW: ${KASM_PW:-hunter2}
|
||||
XDG_RUNTIME_DIR: /run/user/1000
|
||||
volumes:
|
||||
- kasm_run:/run/user/1000
|
||||
- kasm_config:/home/kasm-user/.config
|
||||
user: "0"
|
||||
entrypoint: sh -c "chmod 700 /run/user/1000 && chown -R kasm-user:kasm-user /run/user/1000 && chown -R kasm-user:kasm-user /home/kasm-user/.config && su kasm-user -c '/dockerstartup/kasm_default_profile.sh /dockerstartup/vnc_startup.sh /dockerstartup/kasm_startup.sh'"
|
||||
|
||||
volumes:
|
||||
kasm_run:
|
||||
name: kasm_run
|
||||
kasm_config:
|
||||
name: kasm_config
|
||||
@@ -4,6 +4,8 @@ services:
|
||||
multi-scrobbler:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
BUILD_DATE: "${BUILD_DATE}"
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
|
||||
@@ -1,33 +1,19 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# used https://github.com/linuxserver/docker-plex as a template
|
||||
|
||||
POPULATE_EXAMPLES=false
|
||||
|
||||
echo "-------------------------------------"
|
||||
echo -e "Setting up multi-scrobbler config directory based on CONFIG_DIR env: ${CONFIG_DIR}\n"
|
||||
echo -e "Verifying multi-scrobbler directory permissions based on CONFIG_DIR env: ${CONFIG_DIR} and DATA_DIR env: ${DATA_DIR}\n"
|
||||
|
||||
# make config folder if it does not exist
|
||||
if [ ! -d "${CONFIG_DIR}" ]; then
|
||||
echo "Directory does not exist! Creating..."
|
||||
POPULATE_EXAMPLES=true
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
else
|
||||
if [ "$(ls -A ${CONFIG_DIR})" ]; then
|
||||
echo "Directory is not empty, not creating examples."
|
||||
else
|
||||
POPULATE_EXAMPLES=true
|
||||
fi
|
||||
if [ -d "${CONFIG_DIR}" ]; then
|
||||
echo "chown'ing config directory to ensure correct permissions."
|
||||
chown -R abc:abc "${CONFIG_DIR}"
|
||||
echo "Done!"
|
||||
fi
|
||||
|
||||
# add example configs
|
||||
if [ "$POPULATE_EXAMPLES" = true ]; then
|
||||
echo "Directory is empty, adding examples..."
|
||||
cp -r /app/config/. "${CONFIG_DIR}"/
|
||||
if [ -d "${DATA_DIR}" ]; then
|
||||
echo "chown'ing data directory to ensure correct permissions."
|
||||
chown -R abc:abc "${DATA_DIR}"
|
||||
echo "Done!"
|
||||
fi
|
||||
|
||||
# permissions
|
||||
echo "chown'ing directory to ensure correct permissions."
|
||||
chown -R abc:abc "${CONFIG_DIR}"
|
||||
echo "Done!"
|
||||
echo -e "-------------------------------------\n"
|
||||
|
||||
@@ -7,4 +7,4 @@ echo -e "\nmulti-scrobbler is starting!"
|
||||
export NODE_ENV=production
|
||||
cd /app || exit
|
||||
exec \
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/node_modules/.bin/tsx /app/src/backend/index.ts
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/src/backend/index.ts
|
||||
|
||||
@@ -18,3 +18,4 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lib
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
var args = process.argv.slice(2);
|
||||
|
||||
const linkRegex = new RegExp(/<link data-rh="true" rel="\w+"\shref="(https:\/\/foxxmd\.github\.io\/multi-scrobbler)/g,);
|
||||
const replacement = process.env.CANONICAL_HREF ?? 'https://docs.multi-scrobbler.app';
|
||||
|
||||
/**
|
||||
* Replace canonical and alternative <link> nodes with the "real" domain of the site
|
||||
* so that SEO (google) chooses the correct domain when showing search results
|
||||
*
|
||||
*/
|
||||
|
||||
(async function () {
|
||||
|
||||
if(replacement === undefined || replacement === '') {
|
||||
console.warn('No replacement value found in process.env.CANONICAL_HREF');
|
||||
return;
|
||||
}
|
||||
|
||||
const buildDir = path.resolve(args[0]);
|
||||
console.log(`Reading dir recursively ${buildDir}`);
|
||||
const files = (await fs.readdir(buildDir, { recursive: true })).filter(x => x.includes('.html'));
|
||||
console.log(`Found ${files.length} files with .html extensions`);
|
||||
let modifications = 0;
|
||||
|
||||
await Promise.all(files.map(async (x) => {
|
||||
const filePath = path.resolve(path.join(buildDir, x));
|
||||
//console.log(`Replacing at ${filePath}`);
|
||||
await fs.writeFile(filePath, (await fs.readFile(filePath)).toString().replace(linkRegex, (match, capture, offset, string, groups) => {
|
||||
// this may not be fully accurate since we're mutating concurrently/async
|
||||
// but its a good enough signal for logging to tell if replacements happened
|
||||
modifications++;
|
||||
return match.replace(capture, replacement);
|
||||
}));
|
||||
}));
|
||||
|
||||
console.log(`Done with ${modifications} replacements`);
|
||||
|
||||
}());
|
||||
+24
-2
@@ -1,6 +1,7 @@
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
title: FAQ
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
@@ -60,6 +61,21 @@ If multi-scrobbler is not running on the same machine your browser is on then th
|
||||
|
||||
EX `http://localhost:9078/lastfm/callback` -> `http://192.168.0.220:9078/lastfm/callback`
|
||||
|
||||
### I get errors with `ECONNRESET` `ETIMEDOUT` `ENOTFOUND` `EAI_AGAIN`
|
||||
|
||||
These are errors indicating a network issue is occuring between you and the Source/Client.
|
||||
|
||||
* `ECONNRESET` means the upstream service unexpectedly closed the connection
|
||||
* `ETIMEDOUT` means the upstream service did not respond to a request MS
|
||||
* `ENOTFOUND` means that MS could not find a DNS record for the domain of the upstream service
|
||||
* `EAI_AGAIN` means that a DNS server did not respond to MS's request to lookup the record for a domain
|
||||
|
||||
`ECONNRESET` and timeout issues can occur when you have a poor connection or if the upstream service is overloaded or is generally not behaving well. This is a common occurrence for Listenbrainz/Musicbrainz related requests.
|
||||
|
||||
Please check the [Upstream Services Status Page](https://status.multi-scrobbler.app) to independently verify if the service is misbehaving. If the service is up and has good uptime then the issue may be in your network.
|
||||
|
||||
`ENOTFOUND` and `EAI_AGAIN` and generally caused by DNS caching (stale records) or misbehaving DNS servers. Though this can happen at the upstream service's end, it is *usually* an issue in your network. If you run a Pihole, AdGaurd, or other DNS solution try flushing the cache for the mishaving domain and check that your DNS application is working correctly.
|
||||
|
||||
### Youtube Music fails after some time
|
||||
|
||||
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
|
||||
@@ -70,7 +86,7 @@ Error: Could not send the specified request to browse. Status code: 401
|
||||
|
||||
then YTM has invalidated your authentication.
|
||||
|
||||
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#authentication) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
|
||||
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
|
||||
|
||||
#### When using OAuth Client Authentication
|
||||
|
||||
@@ -80,7 +96,7 @@ Refresh your authentication by using the **(Re)authenticate** link from MS's web
|
||||
|
||||
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
|
||||
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth) and then restart MS to potentially resolve the problem.
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
@@ -98,6 +114,12 @@ It means the JSON in your configuration file is not valid. Copy and paste your c
|
||||
|
||||
## Scrobbling Issues
|
||||
|
||||
### How do I prevent duplicate scrobbles?
|
||||
|
||||
Multi-scrobbler has a [robust duplicate scrobble detection system](/configuration/duplicates#how-duplicates-are-detected) that should handle this for you in the majority of usecases. If you have a [simple setup without any circular dependencies](/configuration/duplicates#valid-configs) then you are already protected against duplicates.
|
||||
|
||||
If your scenario is more complex or you have a service(s) acting as **both** a **Client** and **Source** you should refer to the [**Duplicate Scrobble Guidance**](/configuration/duplicates#valid-configs) page to determine if you can avoid a configuration that may result in duplicate scrobbles.
|
||||
|
||||
### Last.fm does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
Using [File Config](/configuration/?configType=file#configuration-types)
|
||||
|
||||
```json5 title="CONFIG_DIR/jellyin.json"
|
||||
[
|
||||
{
|
||||
"id": "fredJf",
|
||||
"name": "Freds Jellyfin",
|
||||
"clients": ["fredLFM"],
|
||||
"data": {
|
||||
"url": "https://jellyfin.fred.com",
|
||||
"user": "Fred",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "maryJf",
|
||||
"name": "Marys Jellyfin",
|
||||
"clients": ["maryLFM"],
|
||||
"data": {
|
||||
"url": "https://jellyfin.mary.com",
|
||||
"user": "Mary",
|
||||
"apiKey": "26fe9c5dc6374542adc4f4d5e8352838"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/plex.json"
|
||||
[
|
||||
{
|
||||
"id": "foxxPlex",
|
||||
"name": "Foxx Plex",
|
||||
"clients": ["foxxKoi"],
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"id": "maryLFM",
|
||||
"name": "Marys Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "maryApiKey",
|
||||
"secret": "marySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fredLFM",
|
||||
"name": "Freds Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "fredApiKey",
|
||||
"secret": "fredSecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/koito.json"
|
||||
[
|
||||
{
|
||||
"id": "foxxKoi",
|
||||
"name": "Foxx Koito",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Using [AIO Config](/configuration/?configType=aio#configuration-types)
|
||||
|
||||
```json5 title="CONFIG_DIR/config.json"
|
||||
{
|
||||
"sources": [
|
||||
{
|
||||
"type": "jellyfin",
|
||||
"id": "fredJf",
|
||||
"name": "Freds Jellyfin",
|
||||
"clients": [
|
||||
"fredLFM"
|
||||
],
|
||||
"data": {
|
||||
"url": "https://jellyfin.fred.com",
|
||||
"user": "Fred",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "jellyfin",
|
||||
"id": "maryJf",
|
||||
"name": "Marys Jellyfin",
|
||||
"clients": [
|
||||
"maryLFM"
|
||||
],
|
||||
"data": {
|
||||
"url": "https://jellyfin.mary.com",
|
||||
"user": "Mary",
|
||||
"apiKey": "26fe9c5dc6374542adc4f4d5e8352838"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "plex",
|
||||
"id": "foxxPlex",
|
||||
"name": "Foxx Plex",
|
||||
"clients": [
|
||||
"foxxKoi"
|
||||
],
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": [
|
||||
"foxx"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"type": "lastfm",
|
||||
"id": "maryLFM",
|
||||
"name": "Marys Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "maryApiKey",
|
||||
"secret": "marySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lastfm",
|
||||
"id": "fredLFM",
|
||||
"name": "Freds Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "fredApiKey",
|
||||
"secret": "fredSecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "koito",
|
||||
"id": "foxxKoi",
|
||||
"name": "Foxx Koito",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,196 @@
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Using [ENV Config](/configuration/?configType=env#configuration-types)
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
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
|
||||
- SPOTIFY_ID=foxxSpotify
|
||||
- SPOTIFY_CLIENT_ID=H2UvGv8okrFlv2OUoc1lAtK7Xe5EGDgs
|
||||
- SPOTIFY_CLIENT_SECRET=HvoFE5Ce2hdblNE6vaFBEx6dRVcDdo43
|
||||
- SPOTIFY_REDIRECT_URI=https://multiscrobbler.example.com/callback
|
||||
|
||||
# navidrome listenbrainz endpoint
|
||||
- LZE_ID=myLzNavidrome
|
||||
- LZE_TOKEN=myToken
|
||||
|
||||
# plex
|
||||
- PLEX_ID=foxxPlex
|
||||
- PLEX_URL=192.168.0.233:32400
|
||||
- PLEX_TOKEN=JtRnwQWD__XMJF8sT3jc
|
||||
- PLEX_USERS_ALLOW=foxx
|
||||
- PLEX_LIBRARIES_ALLOW=oldies
|
||||
|
||||
# lastfm
|
||||
- LASTFM_ID=myLFM
|
||||
- LASTFM_API_KEY=myApiKey
|
||||
- LASTFM_SECRET=mySecret
|
||||
- LASTFM_REDIRECT_URI=https://multiscrobbler.example.com/lastfm/callback
|
||||
|
||||
# koito
|
||||
- KOITO_ID=foxxKoi
|
||||
- KOITO_TOKEN=029b081ba-9156-4pe7-88e5-3be671f5ea2b
|
||||
- KOITO_USER=admin
|
||||
- KOITO_URL=http://192.168.0.100:4110
|
||||
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
ports:
|
||||
- "9078:9078"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
Using [File Config](/configuration/?configType=file#configuration-types)
|
||||
|
||||
```json5 title="CONFIG_DIR/spotify.json"
|
||||
[
|
||||
{
|
||||
"id": "foxxSpotify",
|
||||
"name": "Foxx Spotify",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/callback",
|
||||
"interval": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/endpointlz.json"
|
||||
[
|
||||
{
|
||||
"id": "myLzNavidrome",
|
||||
"name": "LZ for Navidrome",
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/plex.json"
|
||||
[
|
||||
{
|
||||
"id": "foxxPlex",
|
||||
"name": "Foxx Plex",
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["foxx"],
|
||||
"librariesAllow": ["oldies"],
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"id": "myLFM",
|
||||
"name": "Old Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "myApiKey",
|
||||
"secret": "mySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/koito.json"
|
||||
[
|
||||
{
|
||||
"id": "foxxKoi",
|
||||
"name": "Foxx Koito",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Using [AIO Config](/configuration/?configType=aio#configuration-types)
|
||||
|
||||
```json5 title="CONFIG_DIR/config.json"
|
||||
{
|
||||
"sources": [
|
||||
{
|
||||
"type": "spotify",
|
||||
"id": "foxxSpotify",
|
||||
"name": "Foxx Spotify",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/callback",
|
||||
"interval": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "endpointlz",
|
||||
"name": "LZ for Navidrome",
|
||||
"id": "myLzNavidrome",
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "plex",
|
||||
"id": "foxxPlex",
|
||||
"name": "Foxx Plex",
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["foxx"],
|
||||
"librariesAllow": ["oldies"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"type": "lastfm",
|
||||
"id": "myLFM",
|
||||
"name": "Old Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "myApiKey",
|
||||
"secret": "mySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "koito",
|
||||
"id": "foxxKoi",
|
||||
"name": "Foxx Koito",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,226 @@
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Using [ENV Config](/configuration/?configType=env#configuration-types)
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
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
|
||||
|
||||
# required setup for musicbrainz
|
||||
- MB_CONTACT=contact@mydomain.com # a real email
|
||||
# searches with sensible release sorting with the searchOrder
|
||||
# isrc => basic => artist (native mode)
|
||||
- MB_PRESETS=default,sensible,native
|
||||
|
||||
# spotify
|
||||
- SPOTIFY_ID=foxxSpotify
|
||||
- SPOTIFY_CLIENT_ID=H2UvGv8okrFlv2OUoc1lAtK7Xe5EGDgs
|
||||
- SPOTIFY_CLIENT_SECRET=HvoFE5Ce2hdblNE6vaFBEx6dRVcDdo43
|
||||
- SPOTIFY_REDIRECT_URI=https://multiscrobbler.example.com/callback
|
||||
|
||||
# navidrome listenbrainz endpoint
|
||||
- LZE_ID=myLzNavidrome
|
||||
- LZE_TOKEN=myToken
|
||||
# applies musicbrainz Stage to preTransform of navidrome source
|
||||
- LZE_TRANSFORMS=musicbrainz
|
||||
|
||||
# lastfm
|
||||
- LASTFM_ID=myLFM
|
||||
- LASTFM_API_KEY=myApiKey
|
||||
- LASTFM_SECRET=mySecret
|
||||
- LASTFM_REDIRECT_URI=https://multiscrobbler.example.com/lastfm/callback
|
||||
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
ports:
|
||||
- "9078:9078"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
Using [File Config](/configuration/?configType=file#configuration-types)
|
||||
|
||||
For non-ENV config, [Musicbrainz Transformer](/configuration/transforms/musicbrainz/#api-setup) must be setup using the [AIO Config](/configuration/?configType=aio#configuration-types) `config.json` file. This configuration uses the [sensible defaults](/configuration/transforms/musicbrainz/#sensible-default) in the `defaults` property. This is a suggestion and can be removed if not desired.
|
||||
|
||||
```json5 title="CONFIG_DIR/config.json"
|
||||
{
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
"contact": "contact@mydomain.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaults": {
|
||||
"releaseStatusPriority": ["official"],
|
||||
"releaseGroupPrimaryTypePriority": [
|
||||
"album",
|
||||
"single",
|
||||
"ep"
|
||||
],
|
||||
"releaseCountryPriority": ["XW"],
|
||||
"searchArtistMethod": "native",
|
||||
"searchOrder": [
|
||||
"isrc",
|
||||
"basic",
|
||||
"artist"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/spotify.json"
|
||||
[
|
||||
{
|
||||
"id": "foxxSpotify",
|
||||
"name": "Foxx Spotify",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/callback",
|
||||
"interval": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json5 title="CONFIG_DIR/endpointlz.json"
|
||||
[
|
||||
{
|
||||
"id": "myLzNavidrome",
|
||||
"name": "LZ for Navidrome",
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"id": "myLFM",
|
||||
"name": "Old Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "myApiKey",
|
||||
"secret": "mySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Using [AIO Config](/configuration/?configType=aio#configuration-types)
|
||||
|
||||
This configuration uses the [sensible defaults](/configuration/transforms/musicbrainz/#sensible-default) in the `defaults` property. This is a suggestion and can be removed if not desired.
|
||||
|
||||
```json5 title="CONFIG_DIR/config.json"
|
||||
{
|
||||
"transformers": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB",
|
||||
"data": {
|
||||
"apis": [
|
||||
{
|
||||
"contact": "contact@mydomain.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaults": {
|
||||
"releaseStatusPriority": ["official"],
|
||||
"releaseGroupPrimaryTypePriority": [
|
||||
"album",
|
||||
"single",
|
||||
"ep"
|
||||
],
|
||||
"releaseCountryPriority": ["XW"],
|
||||
"searchArtistMethod": "native",
|
||||
"searchOrder": [
|
||||
"isrc",
|
||||
"basic",
|
||||
"artist"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "spotify",
|
||||
"id": "foxxSpotify",
|
||||
"name": "Foxx Spotify",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/callback",
|
||||
"interval": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "endpointlz",
|
||||
"name": "LZ for Navidrome",
|
||||
"id": "myLzNavidrome",
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
},
|
||||
"options": {
|
||||
"playTransform": {
|
||||
"preCompare": [
|
||||
{
|
||||
"type": "musicbrainz",
|
||||
"name": "MyMB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"type": "lastfm",
|
||||
"id": "myLFM",
|
||||
"name": "Old Lastfm",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"apiKey": "myApiKey",
|
||||
"secret": "mySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 501 KiB |
@@ -0,0 +1,12 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------------------- | -------------------------- | --------------------- | --------------------------------------- |
|
||||
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
|
||||
| `DISCORD_ENABLE` | boolean | true | Should this component be used? |
|
||||
| `DISCORD_TOKEN` | string | | |
|
||||
| `DISCORD_ARTWORK` | boolean OR string OR array | | |
|
||||
| `DISCORD_APPLICATION_ID` | string | | |
|
||||
| `DISCORD_IPC_LOCATIONS` | string | | |
|
||||
| `DISCORD_ARTWORK_DEFAULT_URL` | string | | |
|
||||
| `DISCORD_STATUS_OVERRIDE_ALLOW` | string | | |
|
||||
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | string | | |
|
||||
@@ -0,0 +1,8 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | ------------------- | --------------------------------------- |
|
||||
| _**`KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name EX `My Cool Component` |
|
||||
| `KOITO_ENABLE` | boolean | true | Should this component be used? |
|
||||
| _**`KOITO_URL`**_ | string | | URL for the Koito server |
|
||||
| _**`KOITO_TOKEN`**_ | string | | User token for the user to scrobble for |
|
||||
| _**`KOITO_USER`**_ | string | | Username of the user to scrobble for |
|
||||
@@ -0,0 +1,9 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | ------------------------------------- | ------------------------------------------------------- |
|
||||
| _**`LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name EX `My Cool Component` |
|
||||
| `LASTFM_ENABLE` | boolean | true | Should this component be used? |
|
||||
| _**`LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account |
|
||||
| _**`LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account |
|
||||
| `LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. |
|
||||
| `LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow |
|
||||
@@ -0,0 +1,10 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- |
|
||||
| _**`LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name EX `My Cool Component` |
|
||||
| `LIBREFM_ENABLE` | boolean | true | Should this component be used? |
|
||||
| `LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account |
|
||||
| `LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account |
|
||||
| `LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. |
|
||||
| `LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow |
|
||||
| `LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance |
|
||||
@@ -0,0 +1,8 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- |
|
||||
| _**`LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `LZ_NAME` | string | Value of `LZ_ID` | A vanity name EX `My Cool Component` |
|
||||
| `LZ_ENABLE` | boolean | true | Should this component be used? |
|
||||
| `LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default |
|
||||
| _**`LZ_TOKEN`**_ | string | | User token for the user to scrobble for |
|
||||
| _**`LZ_USER`**_ | string | | Username of the user to scrobble for |
|
||||
@@ -0,0 +1,7 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | -------------------- | --------------------------------------- |
|
||||
| _**`MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name EX `My Cool Component` |
|
||||
| `MALOJA_ENABLE` | boolean | true | Should this component be used? |
|
||||
| _**`MALOJA_URL`**_ | string | | URL for maloja server |
|
||||
| _**`MALOJA_API_KEY`**_ | string | | API Key for Maloja server |
|
||||
@@ -0,0 +1,8 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| _**`ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name EX `My Cool Component` |
|
||||
| `ROCKSKY_ENABLE` | boolean | true | Should this component be used? |
|
||||
| `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account |
|
||||
| `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account |
|
||||
| _**`ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account |
|
||||
@@ -0,0 +1,7 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ------------------------- | ------- | -------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| _**`TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name EX `My Cool Component` |
|
||||
| `TEALFM_ENABLE` | boolean | true | Should this component be used? |
|
||||
| _**`TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as |
|
||||
| _**`TEALFM_APP_PW`**_ | string | | The [App Password](https://atproto.com/specs/xrpc#app-passwords) you created for your account |
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Scrobble Clients
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
@@ -12,16 +13,31 @@ A **Client** is an application that stores the historical information about what
|
||||
|
||||
| Name | Now Playing |
|
||||
| :-------------------------------------------------- | :---------- |
|
||||
| [Discord](/configuration/clients/discord) | ✅ |
|
||||
| [Last.fm](/configuration/clients/lastfm) | ✅ |
|
||||
| [Libre.fm](/configuration/clients/librefm) | ✅ |
|
||||
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
|
||||
| [Koito](/configuration/clients/koito) | ❌ |
|
||||
| [Koito](/configuration/clients/koito) | ✅ |
|
||||
| [Maloja](/configuration/clients/maloja) | ❌ |
|
||||
| [Rocksky](/configuration/clients/rocksky) | ❌ |
|
||||
| [Teal.fm](/configuration/clients/tealfm) | ❌ |
|
||||
| [Teal.fm](/configuration/clients/tealfm) | ✅ |
|
||||
|
||||
## Features
|
||||
|
||||
### Duplicate Detection
|
||||
|
||||
Before a Play is scrobbled to a client MS checks existing scrobbles from the Client's API to see if the Play has already been scrobbled.
|
||||
|
||||
For each Play, MS fetches (cached) scrobbles from the Client in a time range inclusive of the Play's listening timestamp and then scores existing scrobbles against the Play based on:
|
||||
|
||||
* Similarity of Title, Artists, and Album
|
||||
* Temporal closeness of the Play's timestamp to the existing scrobble's timestamp
|
||||
* Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)
|
||||
|
||||
Detailed scoring breakdowns against each existing scrobble are logged at the `TRACE` logging level or in the [debug data](/help#copy-play-debug-data) for a scrobble.
|
||||
|
||||
See the [**Duplicate Scrobble Guidance**](/configuration/duplicates#how-duplicates-are-detected) page to learn how duplicates are detected and how you can ensure your configuration avoids possible duplicates.
|
||||
|
||||
### Dead Scrobbles
|
||||
|
||||
If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Discord",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "configuration/clients/discord/discord"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
---
|
||||
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 | Type | Default | Description |
|
||||
| :----------------------------- | ----------------- | --------------------- | :--------------------------------------------------------------------------------------------------------------- |
|
||||
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
|
||||
| _**`DISCORD_APPLICATION_ID`**_ | string | | Application ID |
|
||||
| `DISCORD_IPC_LOCATIONS` | string | | A commera-separated list of `host:port` network locations or file paths to unix socket for Discord |
|
||||
| `DISCORD_ARTWORK` | boolean | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
|
||||
| `DISCORD_ARTWORK_DEFAULT_URL` | string or boolean | | 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,336 @@
|
||||
---
|
||||
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 utmost 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 | Type | Default | Description |
|
||||
| :--------------------------------- | ----------------- | --------------------- | :---------------------------------------------------------------------------------------------------------------- |
|
||||
| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` |
|
||||
| _**`DISCORD_TOKEN`**_ | string | | User Token acquired from an active Discord sessions |
|
||||
| `DISCORD_APPLICATION_ID` | string | | Application ID used to display album art |
|
||||
| `DISCORD_ARTWORK` | boolean | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
|
||||
| `DISCORD_ARTWORK_DEFAULT_URL` | string or boolean | | 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` | string | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
|
||||
| `DISCORD_LISTENING_ACTIVITY_ALLOW` | string | | A commera-seperated list of listening activity names MS **is allowed** to broadcast presence at the same time as. |
|
||||
</Config>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Discord
|
||||
description: Discord Client Overview
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
This scrobbler uses **[Now Playing](/configuration/clients#now-playing)** functionality to set your Discord [Rich Presence](https://docs.discord.com/developers/rich-presence/overview) to the music you are currently monitoring with multi-scrobbler.
|
||||
|
||||
<img src={require('/img/discord_presence_art.png').default} height="127"/>
|
||||
|
||||
There are two types of Discord integrations to choose from:
|
||||
|
||||
## App
|
||||
|
||||
The **App** integration uses an **existing** Discord application on a desktop computer to set Rich Presence.
|
||||
|
||||
* Uses "permitted" access to Discord, no TOS issues
|
||||
* More restricted customization compared to Headless
|
||||
* Only active when your existing Discord application is open
|
||||
* For most scenarios it requires additional, OS-specific steps to setup correctly
|
||||
* May require an additional program on your machine to relay the Discord connection
|
||||
* This is potentially insecure, or less secure, then Headless
|
||||
* **Can use [discord in a container](/configuration/clients/discord/discord-app#discord-connection) as an easy alternative**
|
||||
* This is a Headless-like experience
|
||||
|
||||
[**App Docs**](/configuration/clients/discord/discord-app)
|
||||
|
||||
## Headless
|
||||
|
||||
The **Headless** integration uses Discord cloud Gateway/API, directly, to set Rich Presence.
|
||||
|
||||
* Does not require an existing Discord application (uses Discord cloud API)
|
||||
* Using this **Violates Discord TOS** because it requires a user token
|
||||
* More customization than App integration
|
||||
* Active if **any** Discord application is online
|
||||
|
||||
[**Headless Docs**](/configuration/clients/discord/discord-headless)
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_koito.md"
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
@@ -45,16 +46,12 @@ Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/g
|
||||
|
||||
:::tip
|
||||
|
||||
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
|
||||
For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLOWED_HOSTS` set correctly in your Koito environment/container. Ensure it is set to the IP address or domain name that you use to access Koito with e.g. `koito.mydomain.com`.
|
||||
|
||||
:::
|
||||
|
||||
## 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>
|
||||
<Config config="KoitoClientConfig" fileContent={JsonConfig} client ambiguous name="koito">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_lastfm.md"
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
@@ -16,11 +17,25 @@ import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
|
||||
|
||||
:::
|
||||
|
||||
<DetailsAdmo type="warning" summary="Limits for Old Scrobbles">
|
||||
|
||||
Last.fm will not accept scrobbles that are timestamped as **older than 2 weeks** from the current time.
|
||||
|
||||
This is not officially documented but has been confirmed by multiple people/mods in the last.fm support forums: [[1]](https://support.last.fm/t/importing-listening-history-spotify-apple-music-and-itunes-wmp/1424/3) [[2]](https://support.last.fm/t/suggestion-allow-last-fm-pro-members-to-use-the-api-to-submit-scrobbles-older-than-2-weeks/69190/2) [[3]](https://support.last.fm/t/how-to-scrobble-complete-spotify-history/44558)
|
||||
|
||||
Multi-scrobbler can queue scrobbles for you if the Last.fm service is unavailable, or if there is an issue with a specific scrobble. However, you are responsible for making sure these scrobbles are eventually sent to Last.fm (by retrying in the UI or retarting Last.fm client in UI) before the two week window has passed or else they will not be scrobblable *at all* unless the timestamp is changed.
|
||||
|
||||
A future update to Multi-scrobbler will enable editing of unsent scrobble data and modifying the timestamp from the UI.
|
||||
|
||||
If you have issues with specific scrobbles not being sent and retries/restarts do not fix it, please make sure to [create a new issue](https://github.com/FoxxMD/multi-scrobbler/issues/new?template=01-bug-report.yml) in a timely manner so that it can be resolved before the window has passed.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
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](#callback-redirect-url) section.
|
||||
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.**
|
||||
|
||||
@@ -48,11 +63,6 @@ The Redirect URL can be explicitly specified in the configuration below, or by u
|
||||
|
||||
## 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 config="LastfmClientConfig" fileContent={JsonConfig} client ambiguous name="lastfm">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_librefm.md"
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
@@ -15,9 +16,9 @@ import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example';
|
||||
|
||||
:::
|
||||
|
||||
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).
|
||||
To use the official [Libre.fm](https://libre.fm/) website you will need an account. Or, you can run your own GNU FM instance [using the 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.
|
||||
No API account, [API Key, or Secret](https://github.com/libre-fm/developer/wiki/Libre.fm-fundamentals#api-keys) is currently 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
|
||||
|
||||
@@ -53,13 +54,6 @@ 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>
|
||||
<Config config="LibrefmClientConfig" fileContent={JsonConfig} client ambiguous name="librefm">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_listenbrainz.md"
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
@@ -22,10 +23,6 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
|
||||
| `LZ_TOKEN` | Yes | | User token from your LZ profile |
|
||||
| `LZ_USER` | Yes | | Your LZ username |
|
||||
| `LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
|
||||
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client ambiguous name="listenbrainz">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_maloja.md"
|
||||
|
||||
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
|
||||
|
||||
@@ -37,9 +38,6 @@ Finally, add the Maloja server URL and API Key to the configuration type you cho
|
||||
|
||||
## 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 config="MalojaClientConfig" fileContent={JsonConfig} client ambiguous name="maloja">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
@@ -6,6 +6,7 @@ toc_max_heading_level: 5
|
||||
|
||||
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
|
||||
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_rocksky.md"
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
@@ -13,7 +14,7 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
|
||||
|
||||
:::
|
||||
|
||||
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
|
||||
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol and your your [**Atmosphere Account**](https://atmosphereaccount.com/).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -23,13 +24,13 @@ To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)]
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo type="warning" summary="Your Data Is Public">
|
||||
<DetailsAdmo type="important" 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">
|
||||
<DetailsAdmo type="important" 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.
|
||||
|
||||
@@ -41,22 +42,16 @@ If you need your scrobbles to be 100% captured then you should scrobble to an *a
|
||||
|
||||
## Setup
|
||||
|
||||
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
|
||||
You will need an [**Atmosphere Account**](https://atmosphereaccount.com/) 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
|
||||
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Atmosphere Account
|
||||
* Then, navigate to the [Access Tokens](https://rocksky.app/access-tokens) in the Rocksky settings dropdown, and create a new token
|
||||
|
||||
## Configuration
|
||||
|
||||
### Handle
|
||||
|
||||
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
|
||||
The handle used with multi-scrobbler should be your **full** [**Atmosphere Account**](https://atmosphereaccount.com/) 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`
|
||||
@@ -64,9 +59,23 @@ The handle used with multi-scrobbler should be your **full** ATProto handle, inc
|
||||
|
||||
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky">
|
||||
| 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 |
|
||||
<DetailsAdmo type="warning" summary="API Applications Deprecated">
|
||||
|
||||
In < 0.14.1, Multi-Scrobber used Rocksky's API Appication "API Key" for communication with a Listenbrainz-compatible API from Rocksky.
|
||||
|
||||
Going forward, this should be replaced with the official guidance in the [Setup](#setup) section above: use an **Access Token**. This uses the native Rocksky API which has more functionality.
|
||||
|
||||
If your configuration is using either
|
||||
|
||||
* (File/AIO) `'key': ...`
|
||||
* (ENV) `ROCKSKY_KEY`
|
||||
|
||||
you should redo the [Setup](#setup) section and modify your configuration to use an Access Token instead.
|
||||
|
||||
Keys are now **deprecated** and functionality for them will be removed in a future version of Multi-Scrobbler.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client ambiguous name="rocksky">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
import ENVConfig from "@site/docs/configuration/clients/_env_configs/_tealfm.md"
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
@@ -16,7 +17,7 @@ To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (So
|
||||
|
||||
:::
|
||||
|
||||
[**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).
|
||||
[**teal.fm**](https://teal.fm/) is a social discovery and browsing app for scrobbles stored in your [**Atmosphere Account**](https://atmosphereaccount.com/).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -46,6 +47,13 @@ To view your teal.fm scrobble data you can:
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
:::note
|
||||
|
||||
Multi-scrobbler will work with *any* Atmosphere Account, not just Bluesky. See [Configuration](#configuration) below for provider-agnostic config.
|
||||
|
||||
:::
|
||||
|
||||
#### Bluesky
|
||||
|
||||
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
|
||||
@@ -57,10 +65,10 @@ To view your teal.fm scrobble data you can:
|
||||
|
||||
### Identifier
|
||||
|
||||
The ATPRoto identifier used with multi-scrobbler should be either:
|
||||
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
|
||||
* Your **full** [Atmosphere Account](https://atmosphereaccount.com/) ATProto handle, including TLD
|
||||
|
||||
If using a handle:
|
||||
|
||||
@@ -70,9 +78,6 @@ If using a handle:
|
||||
|
||||
If your identifier does not look like a DID and does not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
|
||||
| `TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| `TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password |
|
||||
<Config config="TealClientConfig" fileContent={JsonConfig} client ambiguous name="tealfm">
|
||||
<ENVConfig/>
|
||||
</Config>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Configuration
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
import Tabs from '@theme/Tabs';
|
||||
@@ -12,7 +13,7 @@ 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';
|
||||
import SingleConfig from '!!raw-loader!../../../config/jellyfin.json.example';
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -28,40 +29,35 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
* 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.
|
||||
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one type.
|
||||
|
||||
<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.**
|
||||
MS will parse environmental variables present in the OS/container when it is run.
|
||||
|
||||
This method means MS **does not require config files** but it will still create and use data files in the [local](/installation/#config-and-data-directories) or [docker](/installation/?dockerSetting=storage#recommended-settings) data directory.
|
||||
|
||||
<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
|
||||
* You are the only person for whom MS is scrobbling for or
|
||||
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Jellyfin -> Koito, or
|
||||
* You are not comfortable editing JSON files
|
||||
|
||||
</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
|
||||
- JELLYFIN_URL=192.168.0.110:8096
|
||||
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56b
|
||||
- JELLYFIN_USER=MyUser
|
||||
- JELLYFIN_ID=myJellyin
|
||||
# ...
|
||||
# ...
|
||||
```
|
||||
@@ -76,77 +72,114 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
<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
|
||||
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
|
||||
* Docker -> `/config` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) 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 are comfortable editing JSON files
|
||||
* You have more than one of the same type of **Source/Client** you want to use EX 2x Last.fm accounts, 3x Jellyfin accounts
|
||||
* You want activity from specific **Sources** 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
|
||||
* Most Source/Clients only support basic configuration through ENV, more configuration is possible using File. AIO enables complete configuration.
|
||||
|
||||
</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.
|
||||
* These are the same examples you will find in the **Configuration** section of each Source/Client
|
||||
* 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.
|
||||
Each config file is named after the **type** of the Client/Source. Within each file, a JSON **array** defines all of the instances for that type of Client/Source.
|
||||
|
||||
Example directory structure:
|
||||
|
||||
```
|
||||
/CONFIG_DIR
|
||||
plex.json
|
||||
spotify.json
|
||||
maloja.json
|
||||
jellyfin.json
|
||||
lastfm.json
|
||||
koito.json
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/spotify.json" language="json5">{SpotifyConfig}</CodeBlock>
|
||||
```yaml title="jellyfin.json"
|
||||
[
|
||||
{
|
||||
"name": "Foxx JF Server",
|
||||
"id": "foxxJf",
|
||||
"data": {
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>Multiple of Same Config Example</summary>
|
||||
|
||||
```yaml title="jellyfin.json"
|
||||
[
|
||||
{
|
||||
"name": "Foxx JF Server",
|
||||
"id": "foxxJf",
|
||||
"data": {
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
|
||||
}
|
||||
},
|
||||
// highlight-start
|
||||
{
|
||||
"name": "Foo's JF Server",
|
||||
"id": "fooJf",
|
||||
"data": {
|
||||
"url": "http://192.168.0.150:8096",
|
||||
"user": "foo",
|
||||
"apiKey": "9c5bb56bd6540c756fbf481ebd9c5c40"
|
||||
}
|
||||
}
|
||||
// highlight-end
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
You can find an example of the file name that should be used under the **File** Configuration for each Source/Client. Note: this is the same value that is used for `type` in AIO configs.
|
||||
</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:
|
||||
MS will parse an **all-in-one** configuration file (`config.json`) located in the directory specified by the `CONFIG_DIR` environmental variable. The file's location 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
|
||||
* Local installation -> See [Config and Data Directories](/installation/#config-and-data-directories)
|
||||
* Docker -> `/config/config.json` (in the container) -- see the [**Storage** tab in the Docker installation docs](/installation/?dockerSetting=storage#recommended-settings) 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
|
||||
* ...All of the reasons listed in [File](./?configType=file#configuration-types)
|
||||
* You want to setup [monitoring/webhooks](#monitoring), default settings for all Sources/Clients, or global config options
|
||||
* You want to use [Transformers](/configuration/transforms) (other than what is provided for [Musicbrainz ENV](/configuration/transforms/musicbrainz/#env-configuration))
|
||||
* You want complete control over all potential MS configuration
|
||||
|
||||
</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.
|
||||
* **AIO config is the "complete" way to configure MS.**
|
||||
* All settings available in File/ENV are available in AIO and AIO has *more* exclusive settings.
|
||||
* AIO enables configuring **default settings** for all Sources/Clients
|
||||
* There is a [**kitchensink example**](/configuration/kitchensink) that provides examples of using all sources/clients in a complex configuration.
|
||||
* Use the [**Config Playground**](/playground) to explore the entire config schema with an editable example
|
||||
* The example used can also be found in the repo directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example) in the example below
|
||||
:::
|
||||
|
||||
[**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>
|
||||
@@ -154,6 +187,32 @@ Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
<CodeBlock title="CONFIG_DIR/config.json" language="json5">{AIOConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo type="important" summary="Source/Client AIO Configs require a 'type' property">
|
||||
|
||||
Compared to their [File](./?configType=file#configuration-types) equivalent, Sources/Clients configured using AIO require an additional `type` property.
|
||||
|
||||
```yaml title="config.json"
|
||||
{
|
||||
"sources": [
|
||||
{
|
||||
"name": "Foxx JF Server",
|
||||
// highlight-start
|
||||
"type: "jellyfin",
|
||||
// highlight-end
|
||||
"id": "foxxJf",
|
||||
"data": {
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can find an example of the required `type` value by referring to the **AIO** tab in each Source/Client's Configuration section example. This is the same value that is used as the name of individual [File](./?configType=file#configuration-types) config files.
|
||||
</DetailsAdmo>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
@@ -168,7 +227,7 @@ Each entry for a Source/Client includes a **Configuration** section that describ
|
||||
|
||||
## Secrets Interpolation
|
||||
|
||||
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configurations/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
|
||||
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configuration/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
|
||||
|
||||
Multi-scrobbler will look for patterns in text fields within _all_ of your json files:
|
||||
|
||||
@@ -179,7 +238,7 @@ Multi-scrobbler will look for patterns in text fields within _all_ of your json
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Given this [Last.fm](/configurations/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
Given this [Last.fm](/configuration/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
|
||||
```json title="lastfm.json"
|
||||
[
|
||||
@@ -244,13 +303,10 @@ WARN : [App] [Sources] [spotify Secrets] Matched: None | Unmatched: SPOTIFY_SECR
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
## Application Options
|
||||
|
||||
These options affect multi-scrobbler's behavior and are not specific to any source/client.
|
||||
## Base URL
|
||||
|
||||
### Base URL
|
||||
|
||||
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
|
||||
Defines the URL that is used to generate default redirect URLs for authentication for services like [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)
|
||||
@@ -279,79 +335,51 @@ Useful when running with [docker](../installation/installation.mdx#docker) so th
|
||||
|
||||
</details>
|
||||
|
||||
### Caching
|
||||
## Caching
|
||||
|
||||
Multi-scrobbler caches some activities to persist important data across restarts, reduce external API calls, and make some actions faster.
|
||||
Multi-scrobbler implements caching to reduce external API calls and make some actions faster.
|
||||
|
||||
All of the activities below are **always** cached **in-memory** with an optional, configurable [**secondary** store](#secondary-caching-configuration) for persistence.
|
||||
A default **in-memory** cache store is used so that you always benefit from some caching. An optional, [**secondary** store](#secondary-caching) can be configured for greater caching capabilities.
|
||||
|
||||
<Tabs groupId="cachedThings" queryString>
|
||||
<TabItem value="scrobbles" label="Scrobbles">
|
||||
|
||||
**Queued** and **Failed** Scrobbles are cached so that any un-scrobbled data you have is persisted across restarts of multi-scrobbler.
|
||||
|
||||
:::tip
|
||||
|
||||
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
|
||||
|
||||
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
|
||||
:::
|
||||
|
||||
##### Configuration
|
||||
|
||||
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | --------- | --------------------- |
|
||||
| `CACHE_SCROBBLE` | No | `file` | The cache type to use |
|
||||
| `CACHE_SCROBBLE_CONN` | No | `/config` | |
|
||||
<DetailsAdmo type="note" summary="What is Cached?">
|
||||
|
||||
<Tabs groupId="cachedThings">
|
||||
<TabItem value="auth" label="Auth Data">
|
||||
Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts.
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
<TabItem value="transform" label="Scrobble Transforms">
|
||||
The results of [transform rules](/configuration/transforms) are cached so that if a scrobble with identical data (track/artists/album) is identified and it has the same set of transform rules then the cached transform results can be applied.
|
||||
</TabItem>
|
||||
<TabItem value="metadata" label="Transform API Calls">
|
||||
API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="auth" label="Auth Data">
|
||||
</DetailsAdmo>
|
||||
|
||||
Authentication sessions/tokens/etc... are cached for quicker requests and for persistence across restarts.
|
||||
<DetailsAdmo type="important" summary="Auth Cache Configuration">
|
||||
|
||||
:::tip
|
||||
Auth caching defaults to a **file** that is stored in the `DATA_DIR` directory using the pre-defined file name `ms-auth.cache`.
|
||||
|
||||
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
|
||||
This provides automatic persistence across restarts for long-lived auth data/credentials if you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)).
|
||||
|
||||
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
If you wish to use the [secondary store](#secondary-caching) for caching Auth you must explicitly configure it. This is because valkey can potentially be *ephemeral* if you do not provide a volume for its data directory.
|
||||
|
||||
:::
|
||||
To explicitly configure auth to use the secondary store:
|
||||
|
||||
##### Configuration
|
||||
|
||||
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
|
||||
|
||||
<Tabs>
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | --------- | --------------------- |
|
||||
| `CACHE_AUTH` | No | `file` | The cache type to use |
|
||||
| `CACHE_AUTH_CONN` | No | `/config` | |
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
// highlight-start
|
||||
- CACHE_AUTH=valkey
|
||||
// highlight-end
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -361,8 +389,7 @@ Use any [Secondary Cache](#secondary-caching-configuration), the config examples
|
||||
{
|
||||
"cache": {
|
||||
"auth": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
"provider": "valkey"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
@@ -371,93 +398,19 @@ Use any [Secondary Cache](#secondary-caching-configuration), the config examples
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="metadata" label="Transform API Calls">
|
||||
</DetailsAdmo>
|
||||
|
||||
API Calls to external (metadata) services used to [Enhance Scrobbles](/configuration/transforms), like calls to [Musicbrainz](/configuration/transforms/musicbrainz), can be cached to avoid duplicate calls and speed up scrobble transformations.
|
||||
### Secondary Caching Store {#secondary-caching}
|
||||
|
||||
By default, these calls are only cached in memory. If you wish for cached calls to be persisted across restarts then setup [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration).
|
||||
Using a secondary store enables:
|
||||
|
||||
##### Configuration
|
||||
* persistence of cached data across restarts
|
||||
* a larger store (more data is saved)
|
||||
* a longer time-to-live in the store (cached data is fetchable for a longer period)
|
||||
|
||||
Use any [Secondary Cache](#secondary-caching-configuration), the config examples below show the default values:
|
||||
These benefits are particularly beneficial when using [transforms](/configuration/transforms) like Musicbrainz and it is **strongly recommended** for these scenarios.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | --------------------- |
|
||||
| `CACHE_METADATA` | No | | The cache type to use |
|
||||
| `CACHE_METADATA_CONN` | No | | |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"metadata": {
|
||||
"provider": "valkey",
|
||||
"connection": "yourConnectionStringHere"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Secondary Caching Configuration
|
||||
|
||||
The type of cache used, and its connection properties, can be configured through **ENV** or **AIO** config.
|
||||
|
||||
<Tabs groupId="cacheType" queryString>
|
||||
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
**File** cache is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-[cacheName].cache`.
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Example
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | --------- | ------------------------------------------------------------ |
|
||||
| `CACHE_SCROBBLE` | No | `file` | The cache type to use |
|
||||
| `CACHE_SCROBBLE_CONN` | No | `/config` | The directory, within the container, to store the cache file |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Example
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="valkey" label="Valkey">
|
||||
|
||||
[**Valkey**](https://valkey.io/) is an open-source fork of Redis.
|
||||
Currently, Multi-scrobbler only supports [**Valkey**](https://valkey.io/), an open-source fork of Redis, as a secondary store.
|
||||
|
||||
<details>
|
||||
|
||||
@@ -465,11 +418,13 @@ Example
|
||||
|
||||
A valkey container can be added to the [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker):
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="named" label="Named Volume">
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
# adding everything below
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
@@ -482,6 +437,25 @@ volumes:
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="bind" label="Bind Mount">
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- ./valkeyData:/data
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Use `redis://valkey:6379` as the connection string in the configurations below.
|
||||
|
||||
</details>
|
||||
@@ -496,12 +470,16 @@ redis://HOST_IP:HOST_PORT
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
Example
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | -------- | ------------------------------------------------------------------- |
|
||||
| `CACHE_METADATA` | Yes | `valkey` | The cache type to use |
|
||||
| `CACHE_METADATA_CONN` | Yes | | The host/IP and port to connect to EX: `redis://192.168.0.120:6379` |
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
// highlight-start
|
||||
- CACHE_VALKEY=redis://192.168.0.120:6379
|
||||
// highlight-end
|
||||
# ...
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -512,10 +490,7 @@ Example
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"metadata": {
|
||||
"provider": "valkey",
|
||||
"connection": "redis://192.168.0.120:6379"
|
||||
}
|
||||
"valkey": "redis://192.168.0.120:6379"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
@@ -524,11 +499,275 @@ Example
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Database
|
||||
|
||||
Multi-scrobbler depends on a SQLite database (`ms.db`) that is created on first run and stored in the [`CONFIG_DIR`](/installation/?dockerSetting=storage#recommended-settings). When upgrading Multi-scrobbler version, if there are any required database changes than this database is [automatically backed up and migrated.](/updating#database)
|
||||
|
||||
The database stores *all* Plays for your Sources/Clients as well as metadata and debugging information to help troubleshoot issues. Each Play is associated with a Source/Config in the database based on your configuration.
|
||||
|
||||
You **must set IDs for each Source/Client** so that the database can identify these even when the configuration is changed.
|
||||
|
||||
### Retention
|
||||
|
||||
<DetailsAdmo status="note" summary="How much storage does the database use?">
|
||||
|
||||
The amount of data stored for each Play can widely vary based on a few factors:
|
||||
|
||||
* how much data the Source service exposes
|
||||
* how many clients each Source is scrobble to (Plays are duplicated for each Client a Source sends a scrobble to)
|
||||
* if you are using any [Transforms](/configuration/transforms) the diff of each step is stored, along with any external request/response data (like [Musicbrainz queries](/configuration/transforms/musicbrainz))
|
||||
|
||||
The [MS repository contains a benchmark](https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/tests/database/drizzle.test.ts#L551) to measure an average database size in a few common scenarios.
|
||||
|
||||
<Tabs groupId="dbSize">
|
||||
|
||||
<TabItem label="Minimal" value="minimal">
|
||||
|
||||
Assuming your Sources send a minimal amount of data or you have compacted all plays:
|
||||
|
||||
| Play Count | DB Size |
|
||||
| ---------- | -------- |
|
||||
| 100 | `160kb` |
|
||||
| 1000 | `1MB` |
|
||||
| 10k | `10.2MB` |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="With Original Input" value="input">
|
||||
|
||||
Assuming your Sources have a non-trivial amount of input data (like Spotify or Listenbrainz) that is not compacted:
|
||||
|
||||
| Play Count | DB Size |
|
||||
| ---------- | -------- |
|
||||
| 100 | `356kb` |
|
||||
| 1000 | `3MB` |
|
||||
| 10k | `29.6MB` |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="All Debug Deta" value="all">
|
||||
|
||||
Assuming your Sources/Clients:
|
||||
|
||||
* have a non-trivial amount of input data (like Spotify or Listenbrainz)
|
||||
* and has many [Transforms](/configuration/transforms) steps that include requests
|
||||
* nothing is compacted
|
||||
|
||||
| Play Count | DB Size |
|
||||
| ---------- | -------- |
|
||||
| 100 | `684kb` |
|
||||
| 1000 | `6.28MB` |
|
||||
| 10k | `61.3MB` |
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Debug Mode
|
||||
</DetailsAdmo>
|
||||
|
||||
A retention policy can be configured to delete Plays, or unused debug data, from the database after a certain amount of time. If no configuration is provided then a default policy is used that should be reasonable for most users.
|
||||
|
||||
<Tabs groupId="policy" queryString>
|
||||
|
||||
<TabItem label="Compaction" value="compaction">
|
||||
|
||||
The **Compaction** Retention Policy is used to delete different types of debug data from your stored Plays.
|
||||
|
||||
This is a useful way to reduce used storage space when you are not having problems with your Plays, or iterating on a configuration, that requires referencing all this extra data.
|
||||
|
||||
There are two types of data that can be compacted (deleted from the Play):
|
||||
|
||||
* `input` - this is the untouched data retrieved by Multi-scrobbler, from a Source, and used to generate a Play/scrobble. This can be used to reconstruct and replay a Play, when used from troubleshooting or reporting an issue
|
||||
* `transform` - this is all of the steps generated by [transforms](/configuration/transforms), the diff of the Play resulting from the step, and any request/responses used to complete the step
|
||||
|
||||
:::note[Defaults]
|
||||
|
||||
When no Compact configuration is provided, Multi-scrobbler uses this policy:
|
||||
|
||||
* Compact (delete) `input` and `transform` data on all Plays after 3 days
|
||||
|
||||
:::
|
||||
|
||||
#### Configuring Compaction Policy
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Details</summary>
|
||||
|
||||
Each value in the configuration properties below can be either
|
||||
|
||||
* a number of seconds EX `3600` = 10 minutes
|
||||
* a unit of a common duration with the pattern `X unit` EX
|
||||
* `30 minutes`
|
||||
* `5 hours`
|
||||
* `2 days`
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
* `COMPACT_PROPERTIES` - which properties to compact
|
||||
* `RETENTION_COMPACT_AFTER` - Default to use for all Plays
|
||||
* `RETENTION_COMPACT_COMPLETED_AFTER` - Compact only completed Plays after...
|
||||
* `RETENTION_COMPACT_FAILED_AFTER` - Compact only failed Plays after...
|
||||
* `RETENTION_COMPACT_DUPED_AFTER` - Compact only duped/discard Plays after...
|
||||
|
||||
Example
|
||||
|
||||
```ini
|
||||
# only delete input when compacting
|
||||
COMPACT_PROPERTIES=input
|
||||
# compact all plays after 3 days
|
||||
RETENTION_COMPACT_AFTER=3 days
|
||||
# specifically compact completed plays after 30 minutes
|
||||
RETENTION_COMPACT_COMPLETED_AFTER=30 minutes
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Compacting all Play types and deleting both input and transform:
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"database": {
|
||||
"retention": {
|
||||
"compactAfter": "3 days",
|
||||
"compact": [
|
||||
"input",
|
||||
"transform"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Delete only input during compacting
|
||||
* Compact all after 3 days except completed which compacts after 30 minutes
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"database": {
|
||||
"retention": {
|
||||
"compactAfter": {
|
||||
"completed": "30 minutes",
|
||||
"duped": "3 days",
|
||||
"failed": "3 days"
|
||||
},
|
||||
"compact": [
|
||||
"input"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Deletion" value="deletion">
|
||||
|
||||
The **Deletion** Retention Policy is used to delete different types of stored Plays from Multi-scrobbler database.
|
||||
|
||||
**This does not delete Plays from your Clients.** It's only deleting the "in-flight" data MS used to create the scrobble that was eventually sent to your clients.
|
||||
|
||||
:::warning[Plays Should be Ephemeral]
|
||||
|
||||
**Multi-Scrobbler is not designed to store Plays/Scrobbles indefinitely.**
|
||||
|
||||
It should scale fine for thousands of scrobbles but it not meant to store 10's of thousands of scrobbles forever. It is not a scrobbler server.
|
||||
|
||||
You **should** set a reasonable deletion policy so that MS stores less than 1000 scrobbles at a time, ideally less.
|
||||
|
||||
:::
|
||||
|
||||
:::note[Defaults]
|
||||
|
||||
When no Deletion policy configuration is provided, Multi-scrobbler uses this policy:
|
||||
|
||||
* Delete all Plays after 7 days
|
||||
|
||||
:::
|
||||
|
||||
#### Configuring Deletion Policy
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Details</summary>
|
||||
|
||||
Each value in the configuration properties below can be either
|
||||
|
||||
* a number of seconds EX `3600` = 10 minutes
|
||||
* a unit of a common duration with the pattern `X unit` EX
|
||||
* `30 minutes`
|
||||
* `5 hours`
|
||||
* `2 days`
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
* `RETENTION_DELETE_AFTER` - Default to use for all Plays
|
||||
* `RETENTION_DELETE_COMPLETED_AFTER` - Delete only completed Plays after...
|
||||
* `RETENTION_DELETE_FAILED_AFTER` - Delete only failed Plays after...
|
||||
* `RETENTION_DELETE_DUPED_AFTER` - Delete only duped/discard Plays after...
|
||||
|
||||
Example
|
||||
|
||||
```ini
|
||||
# delete all plays after 3 days
|
||||
RETENTION_DELETE_AFTER=3 days
|
||||
# specifically, delete completed plays after 30 minutes
|
||||
RETENTIOND_DELETE_COMPLETED_AFTER=30 minutes
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
Deleting all Play types after 3 days:
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"database": {
|
||||
"retention": {
|
||||
"deleteAfter": "3 days"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Delete all after 3 days except completed which are deleted after 30 minutes
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"database": {
|
||||
"retention": {
|
||||
"deleteAfter": {
|
||||
"completed": "30 minutes",
|
||||
"duped": "3 days",
|
||||
"failed": "3 days"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
</TabItem>
|
||||
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Debug Mode
|
||||
|
||||
Turning on Debug Mode will
|
||||
|
||||
@@ -549,13 +788,13 @@ To set debug mode either add it to [AIO `config.json`](./?configType=aio#configu
|
||||
|
||||
or set the [ENV](./?configType=env#configuration-types) `DEBUG_MODE=true`
|
||||
|
||||
### Disable Web
|
||||
## Disable Web
|
||||
|
||||
If you do not need the dashboard and/or ingress sources, or have security concerns about ingress and cannot control your hosting environment, the web server and API can be disabled.
|
||||
|
||||
:::warning
|
||||
|
||||
Any **[ingress-based sources](/configuration/sources#ingress) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
Any **[ingress-based sources](/configuration/sources?sourceComm=ingress#by-communication-method) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
|
||||
:::
|
||||
|
||||
@@ -566,7 +805,19 @@ Disable using either:
|
||||
|
||||
## Monitoring
|
||||
|
||||
Multi-scrobbler supports some common webhooks and a healthcheck endpoint in order to monitor Sources and Clients for errors.
|
||||
### Upstream Services Status
|
||||
|
||||
A status page for monitoring the reachability of public services used by some Sources/Clients is available at
|
||||
|
||||
**https://status.multi-scrobbler.app**
|
||||
|
||||
This monitor runs on a VPS and checks the uptime of *actual* APIs for these services, not just landing pages. On a ~minute interval it checks:
|
||||
|
||||
* Listenbrainz API
|
||||
* Musicbrainz API
|
||||
* CoverArtArchive API
|
||||
* Last.fm API
|
||||
* A few MS-hosted support services
|
||||
|
||||
### Webhook Configurations
|
||||
|
||||
@@ -617,7 +868,7 @@ Webhooks are configured in the AIO [config.json](#configuration-types) file unde
|
||||
|
||||
#### [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)
|
||||
Refer to the [config schema for GotifyConfig](/playground)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
@@ -643,7 +894,7 @@ multi-scrobbler optionally supports setting message notification priority via `i
|
||||
|
||||
#### [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)
|
||||
Refer to the [config schema for NtfyConfig](/playground)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
@@ -671,7 +922,7 @@ multi-scrobbler optionally supports setting message notification priority via `i
|
||||
|
||||
#### [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)
|
||||
Refer to the [config schema for AppriseConfig](/playground)
|
||||
|
||||
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)/
|
||||
|
||||
@@ -713,5 +964,37 @@ It includes metrics for:
|
||||
* Count of Queued/Scrobbled/Deadletter scrobbles per [Client](/configuration/clients)
|
||||
* Number of issues per Source/Client
|
||||
* If any of these metrics is > 0 it means your Source/Client is not operating normally
|
||||
* Caching metrics
|
||||
|
||||
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
|
||||
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example Usage</summary>
|
||||
|
||||
Update your [Prometheus Configuration file](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file) (usually at `/etc/prometheus.yml`) to add a job to the `scrape_configs`[section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) like this:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
#
|
||||
# ...your other jobs are here
|
||||
#
|
||||
- job_name: multi-scrobbler
|
||||
scrape_interval: 30s
|
||||
metrics_path: '/api/metrics'
|
||||
static_configs:
|
||||
- targets: ['myMultiscrobblerIp:9078']
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo status="tip" summary="Grafana Dashboard">
|
||||
|
||||
Github user [4rft5](https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295) created a Grafana dashboard that uses the Prometheus metrics from above.
|
||||
|
||||
<img src={require('/img/grafana.png').default} height="800"/>
|
||||
|
||||
Download the json dashboard here: [`multiscrobbler-dashboard.json`](/multiscrobbler-dashboard.json)
|
||||
|
||||
</DetailsAdmo>
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: Duplicate Scrobble Guidance
|
||||
description: Common Scenarios and Solutions for Duplicate Scrobbles
|
||||
---
|
||||
|
||||
Multi-scrobbler's flexibility in configuration allows you to build some complex pipelines for scrobbling but it can also be a footgun for duplicating scrobbles.
|
||||
|
||||
## How Duplicates Are Detected
|
||||
|
||||
Multi-scrobbler uses multiple techniques for evaluating the **similarity** of text in scrobbles against existing data.
|
||||
|
||||
These techniques are used on all text from a scrobble (title, artist, album...):
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Character and string normalization</summary>
|
||||
|
||||
Symbols and multiple whitespaces are removed. All characters are converted to lowercase. Non ascii characters are converted to their ascii counterparts.
|
||||
|
||||
```
|
||||
this string! is the. same => this string is the same
|
||||
this string is the same => this string is the same
|
||||
ThIs STRING iS THe SAMe => this string is the same
|
||||
|
||||
Dina Ögon => Dina Ogon
|
||||
Nanã => Nana
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Word-order invariant text similarity</summary>
|
||||
|
||||
Strings with multiple words (tokens) are re-arranged so that tokens are ordered to be as close to other, similar tokens are possible:
|
||||
|
||||
Example 1
|
||||
|
||||
```
|
||||
String A: there change is => there change is
|
||||
String B: there is change => there change is
|
||||
```
|
||||
|
||||
Example 2
|
||||
|
||||
```
|
||||
String A: you, doin hwat are => you doin hwat are
|
||||
String B: what are you doing => you doing what are
|
||||
```
|
||||
|
||||
Then, the strings are independently compared using [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [Dice's Coefficient](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient). The algorithm that gives the highest score for similarity is used so that matching is aggressive.
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
For **Artists**, Multi-scrobbler attempts to extract unique Artists from common patterns in **single-string** artist values and track **titles**.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Matching Common Artist Patterns</summary>
|
||||
|
||||
Common joiners and patterns are detected in artist/track strings and the artists are extracted. Example of common joiners are: `ft.` `feat` `vs.` etc...
|
||||
|
||||
Example 1
|
||||
|
||||
```
|
||||
Melendi, Ryan Lewis, The Righteous Brothers (featuring Joan Jett & The Blackhearts, Robin Schulz)
|
||||
|
||||
becomes
|
||||
|
||||
Melendi, Ryan Lewis, The Righteous Brothers, Joan Jett, The Blackhearts, Robin Schulz
|
||||
```
|
||||
|
||||
Example 2
|
||||
|
||||
```
|
||||
Childish Gambino - 12.38 (feat. 21 Savage, Ink & Kadhja Bonet)
|
||||
|
||||
becomes
|
||||
|
||||
Childish Gambino, 21 Savage, Ink, Kadhja Bonet - 12.38
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
* MS uses "smart" heuristics to prevent artists with actual punctuation (`Tyler, The Creator`) from accidentally being separated
|
||||
* The extraction examples above are done **only for matching** other scrobble data (which is also parsed like the above), the resulting scrobble data is never transformed unless the user explicitly configures it via a [native transform](/configuration/transforms/native)
|
||||
|
||||
:::
|
||||
|
||||
</details>
|
||||
|
||||
The **timestamps** for Scrobbles are compared at different levels of granularity based on where they originate from.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Temporally Comparing Scrobbles</summary>
|
||||
|
||||
Timestamp closeness for two scrobbles can be classified in one of these states:
|
||||
|
||||
**Exact**
|
||||
|
||||
Timestamps differ by 1 seconds or less.
|
||||
|
||||
**Close**
|
||||
|
||||
Timestamps differ by less than the known granularity of the Source they are from. For most Sources (and all Clients) this is 10 seconds or less. For some sources (subsonic) the granularity is 60 seconds because they only report "now playing" changes every 60 seconds.
|
||||
|
||||
**During**
|
||||
|
||||
If the Play was monitoring in real-time by Multi-scrobbler then it knows the time range during which the Play was listened to. If an existing scrobble with similar artist/track/album has a timestamp that occurred *during* the known listening time then it is classified as during.
|
||||
|
||||
**Fuzzy**
|
||||
|
||||
If the timestamp occurs within a few seconds +/- of (existing scrobble + track duration) then, potentially, the Source timestamped the existing scrobble at the *end* of when the user listened to it, rather than when the user *started* listening to it. This is common for Spotify plays.
|
||||
|
||||
___
|
||||
|
||||
Each of the above classification results in a different score that is *roughly* descending from the order given above. Exact score is based on the Souce of the scrobble and if it was monitored in real-time by Multi-Scrobbler.
|
||||
|
||||
</details>
|
||||
|
||||
Finally, all of the above comparison/normalization techniques are combined when comparing a "new" Play/Scrobble against existing data. Each comparison is given a weighted score. If the score is above the match threshold then it is marked as a duplicated.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Match Scoring Example</summary>
|
||||
|
||||
```
|
||||
Frédéric Chopin - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:51-04:00 <-- New scrobble
|
||||
Frédéric Chopin / Krystian Zimerman - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:48-04:00 <-- Existing scrobble from client
|
||||
```
|
||||
|
||||
After comparing against all existing scrobbles from the client, in a timerange around the new scrobble, MS finds a match and logs the match along with the score breakdown of the consistuent parts:
|
||||
|
||||
```
|
||||
Source:
|
||||
Frédéric Chopin - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:51-04:00 (S)
|
||||
|
||||
Closest Scrobble:
|
||||
Frédéric Chopin / Krystian Zimerman - Ballade No. 4 in F Minor, Op. 52 @ 2026-05-04T13:01:48-04:00 (S)
|
||||
|
||||
Artist: (0.30 + Whole Match Bonus 0.52) * (0.3 + Whole Match Bonus 0.05) = 0.29
|
||||
Title: 1.00 * 0.4 = 0.40
|
||||
Time: (Close) 1 * 0.5 = 0.50
|
||||
Time Detail => Existing: 13:01:48-04:00 - Candidate: 13:01:51-04:00 | Temporal Sameness: Close | Play Diff: 3s (Needed <10s) | Range Comparison N/A
|
||||
Score 1.19 => Matched!
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
All of the above techniques and scoring gives Multi-scrobbler a *very good chance* of detecting duplicate scrobbles, regardless of where they were originally from. Of Multi-Scrobbler's [400+ unit tests](https://github.com/FoxxMD/multi-scrobbler/tree/master/src/backend/tests), 70+ are related to testing scoring, artist extraction, and [fuzzing](https://en.wikipedia.org/wiki/Fuzzing) string normalization/simiarlity.
|
||||
|
||||
The dupe detection is so good that it is virtually gauranteed for [most configurations](#valid-configs) and is used for *backlogging* missed scrobbles for Sources that support fetching history.
|
||||
|
||||
**However**, it's not possible to gaurantee this for *all* possible Multi-scrobbler configurations. See the below sections to learn more.
|
||||
|
||||
## Configurations Where Duplicates DO NOT Occur {#valid-configs}
|
||||
|
||||
* Any Source/Client configuration where there is only one Source and one Client and they are not the same service/account
|
||||
* Any Source/Client configuration where scrobbles only flow in **one direction**
|
||||
* IE you *do not* have a Last.fm **Client** Account A that is *also* a Last.fm **Source** Account A
|
||||
|
||||
:::tip
|
||||
|
||||
**The majority of simple Multi-scrobbler configurations fall into these categories. If your workflow is straightforward you do not need to worry about duplicates at all.**
|
||||
|
||||
:::
|
||||
|
||||
Multi-scrobbler can handle recieving the same data from a Source multiple times. This is fine because the data is *always the same* and *always unique* to each Source. Since MS scrobbled the data to your clients you can be pretty confident that the *same* cleanup/matching process for that data will result in an indentical/similar match to the existing scrobble.
|
||||
|
||||
IE Same inputs in -> same inputs out.
|
||||
|
||||
**Even this scenario with multiple, mixed Sources is OK:**
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/unidirectional-flow.png').default}/>
|
||||
|
||||
Because the scrobbles coming from each **Source** are **independent** of each other. And they end up in Clients that are not *also* used as Sources.
|
||||
|
||||
:::tip
|
||||
|
||||
This is a common scenario when scrobbles are *backlogged* from a Source IE on startup MS fetches the last 50 Plays from Spotify/Lastfm/Listenbrainz.
|
||||
|
||||
:::
|
||||
|
||||
## Configurations Where Duplicates CAN Occur {#invalid-configs}
|
||||
|
||||
### Upstream Scrobbles Sent to Multiple Sources
|
||||
|
||||
In this scenario your device using Spotify is configured in MS as a Spotify **Source** but it **also** is scrobbling to Last.fm (via Pano Scrobbler, as an example).
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/dupe sources.png').default}/>
|
||||
|
||||
|
||||
❌ **Time Race Condition**: Potentially, the scrobbles arrive in Multi-scrobbler at exactly the same time. There are no existing scrobbles in Koito to check against, yet. So Multi-scrobbler scrobbles both to Koito, resulting in duplicate scrobbles.
|
||||
|
||||
❌ **Different Scrobble Data**: Pano Scrobbler, or Last.fm, [may modify the scrobble data.](#lastfm) When MS recieves the scrobble from Last.fm it may be *just different enough* that it does not get detected as a duplicate.
|
||||
|
||||
### Sources and Clients are Dependent {#dependent-source-client}
|
||||
|
||||
In this scenario, you have a **Client** that is also a **Source** and that Source is feeding scrobbles to a different Client that receives scrobbles from everywhere (or has some overlap with the original Client).
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/duped client source.png').default}/>
|
||||
|
||||
❌ **Different Scrobble Data**: [Last.fm may modify the scrobble data.](#lastfm) When MS recieves the scrobble from Last.fm it may be *just different enough* that it does not get detected as a duplicate.
|
||||
|
||||
❌ **Consistency Issues**: If the Koito Client has a backed up queue or is not currently scrobbling (imagine it was Last.fm or Listenbrainz, and they had upstream availability issues), then there is no existing scrobble yet and nothing to match against.
|
||||
|
||||
## Configuration Solutions to Avoid Duplicates
|
||||
|
||||
### Independent Sources
|
||||
|
||||
Use a configuration mentioned in [Configurations Where Duplicates DO NOT Occur](#valid-configs): make sure all of your Sources are **independent**. The activity/scrobbles from each Source do not have any upstream duplicates of activity.
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/independent source.png').default}/>
|
||||
|
||||
### Alternative Sources
|
||||
|
||||
Imagine you have a scenario where a device/player can **only** scrobble to Last.fm. But you want to use MS for all other devices/sources and still have all your scrobbles end up in your "main" Last.fm account for social reasons/stats.
|
||||
|
||||
Instead of using the same Last.fm account for all activity and potentially having [dependent client/sources](#dependent-source-client), create an *additional* last.fm account that is used *only for the device that needs it*. This ensures the scrobbles from that device are isolated from the main account and do not prevent duplicates, so you can continue to use the main account as an MS Client:
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/alt source.png').default}/>
|
||||
|
||||
### Mirror-Only Clients or Limiting Scrobble Destination {#isolated-sources}
|
||||
|
||||
In Multi-scrobbler, by default, all Sources scrobble to all Clients. Using [Limit Scrobble Destinations](/configuration/sources/#limiting-scrobble-destination) configuration can ensure that your dependent Source/Clients only scrobble to destinations where the scrobbles won't be duplicated. And/or limit other Sources to *not* scrobble to dependent Sources/Clients.
|
||||
|
||||
Here is an example where all Sources only scrobble to a *dependent* Last.fm Client, and then the *dependent* Last.fm Source only scrobbles to a "mirror-only" (backup) Koito Client:
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/mirror source.png').default}/>
|
||||
|
||||
### Normalize All Scrobbles
|
||||
|
||||
If using one of the above solutions is not possible you can also try to *aggressively normalize all scrobble data* using [the Musicbrainz transform stage](/configuration/transforms/musicbrainz).
|
||||
|
||||
In this scenario you still have [dependent Source/Clients](#dependent-source-client) that are [not isolated](#isolated-sources) but you can *potentially* improve duplicate detection by first using a transform stage to match scrobbles against the Musibrainz database and normalize their track/artist/album data.
|
||||
|
||||
:::warning
|
||||
|
||||
**There is no gaurantee this will work** but it is better than nothing. You should attempt to re-configure MS to use any of the other solutions before using this one.
|
||||
|
||||
:::
|
||||
|
||||
<img className='image-center' src={require('/img/diagrams/normalized dupes.png').default}/>
|
||||
|
||||
## The Last.fm Problem {#lastfm}
|
||||
|
||||
Last.fm will happily accept almost any scrobble data you throw at it.
|
||||
|
||||
It can then decide, opaquely, to match or modify that scrobble to some more "official" data it has server-side. This could be modifying the artists, title, or album sent in the scrobble. It can also mean modifying the [`mbid` (unique identifier)](https://musicbrainz.org/doc/MusicBrainz_Identifier) or adding what it _thinks_ is the correct one.
|
||||
|
||||
There is nothing in the scrobble history API response from Last.fm that identifies it has done any of this. And [it's not super great at being right about these modifications](https://github.com/FoxxMD/multi-scrobbler/issues/333#issuecomment-3145161339).
|
||||
|
||||
What this means for you, the user, is that this is very plausible scenario:
|
||||
|
||||
* You scrobble `Artist Foo, Artist Bar - My Cool Track` to Last.fm via Pano Scrobbler (outside of Multi-scrobbler)
|
||||
* Last.fm modifies the data to `The Artist Foo - My Cool Track ft. Artist Bar (Album Version)`
|
||||
* You _also_ scrobble `Artist Foo, Artist Bar - My Cool Track` to Multi-scrobbler via Pano LFM endpoint **Source**
|
||||
* MS forwards this to Koito Client as-is
|
||||
* Soon after, MS polls your Last.fm **Source** for new scrobbles and finds `The Artist Foo - My Cool Track ft. Artist Bar (Album Version)`
|
||||
* It queries Koito to see if this was scrobbled
|
||||
* It sees that a scrobble did happen at almost the same time BUT the track titles and artist strings are _just different enough_ that it decides it's not a duplicate
|
||||
* MS forwards the scrobble to Koito and you now have a duplicate
|
||||
|
||||
This behavior is unique to Last.fm. It's truly the **worst** out of all the scrobbler services in terms of preserving your data as-is, or providing a unqiue identifier for your data.
|
||||
@@ -1,316 +1,93 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_position: 5
|
||||
title: Kitchen Sink
|
||||
description: Kitchen Sink Examples
|
||||
---
|
||||
|
||||
# Example Complex Scenario
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import MultiUsecase from "@site/src/components/snippets/_scenario-config-multi.mdx"
|
||||
import MultiPersonsServices from "@site/docs/configuration/_kitchensink_configs/_multi-persons-services.mdx"
|
||||
import MultiSourcesClients from "@site/docs/configuration/_kitchensink_configs/_multi-sources-clients.mdx"
|
||||
import Musicbrainz from "@site/docs/configuration/_kitchensink_configs/_musicbrainz.mdx"
|
||||
import SingleUsecase from "@site/src/components/snippets/_scenario-config-single.mdx"
|
||||
|
||||
Scenario:
|
||||
## What's This?
|
||||
|
||||
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
|
||||
* Each person has their own [Maloja](/configuration/clients/maloja) server
|
||||
* Each person has their own Spotify account
|
||||
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
|
||||
* You have your own [Youtube Music](/configuration/soures/youtube-mysic) account you want to scrobble from
|
||||
* Mary has her own [Last.fm](/configuration/clients/lastfm) account she also wants to scrobble to
|
||||
* Fred has his own [Spotify](/configuration/sources/spotify) application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
|
||||
* Fred has a Plex server and wants to scrobble everything he plays
|
||||
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
|
||||
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
|
||||
* Fred also has his own [Jellyfin server](/configurion/sources/jellyfin) and wants to scrobble everything he plays
|
||||
* You have an android music app that can scrobble to a [custom listenbrainz server](/configuration/sources/listenbrainz-endpoint)
|
||||
On this page you find contrived, complex usecases for multi-scrobbler that provide examples of equivalent [configuration types](/configuration#configuration-types) to fulfill that usecase.
|
||||
|
||||
### All-in-one Config
|
||||
These example are useful for:
|
||||
|
||||
```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
|
||||
"maxRequestRetries": 1, // optional, default # of http request retries a source can make before error is thrown. can be overridden by property in individual config
|
||||
"retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config
|
||||
},
|
||||
"clientDefaults": {
|
||||
"maxRequestRetries": 1, // optional, default # of http request retries a client can make before error is thrown. can be overridden by property in individual config
|
||||
"retryMultiplier": 1.5, // optional, default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying). can be overridden by property in individual config
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"type": "spotify",
|
||||
"name": "foxxSpot",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"clientId": "foxxSpotifyAppId",
|
||||
"clientSecret": "foxxSpotifyAppSecret",
|
||||
},
|
||||
"options": {
|
||||
"maxRequestRetries": 2, // override default max retries because spotify can...spotty
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "spotify",
|
||||
"name": "marySpot",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"clientId": "foxxSpotifyAppId", // only need one application, it can be used by all users of this multi-scrobbler instance
|
||||
"clientSecret": "foxxSpotifyAppSecret",
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "spotify",
|
||||
"name": "fredSpot",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"accessToken": "fredsToken",
|
||||
"refreshToken": "fredsRefreshToken",
|
||||
"interval": 120, // he also wants a slower check interval because his application already has heavy api usage
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "plex",
|
||||
"name": "fredPlex",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "plex",
|
||||
"name": "maryPlex",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"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"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "plex",
|
||||
"name": "partyPlex",
|
||||
// omitting clients (or making it empty) will make this Source scrobble to all Clients
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"librariesAllow": ["party"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "jellyfin",
|
||||
"name": "FredJelly",
|
||||
// omitting clients (or making it empty) will make this Source scrobble to all Clients
|
||||
"data": {
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
"usersAllow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "subsonic",
|
||||
"name": "foxxAirsonic",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"user": "foxx",
|
||||
"password": "foxxPassword",
|
||||
"url": "https://airsonic.foxx.example"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "foxxYoutube",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "endpointlz",
|
||||
"name": "listenbrainzfoxx",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"type": "maloja",
|
||||
"name": "foxxMaloja",
|
||||
"data": {
|
||||
"url": "https://maloja.foxx.example",
|
||||
"apiKey": "foxxApiKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "maloja",
|
||||
"name": "fredMaloja",
|
||||
"data": {
|
||||
"url": "https://maloja.fred.example",
|
||||
"apiKey": "fredApiKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "maloja",
|
||||
"name": "maryMaloja",
|
||||
"data": {
|
||||
"url": "https://maloja.mary.example",
|
||||
"apiKey": "maryApiKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "lastfm",
|
||||
"name": "maryLFM",
|
||||
"data": {
|
||||
"apiKey": "maryApiKey",
|
||||
"secret": "marySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
* getting an idea of what you can do with Multi-scrobbler
|
||||
* using these examples as a starting point for your own configuration
|
||||
* providing an example of the same configuration across different [configuration types](/configuration#configuration-types)
|
||||
|
||||
```
|
||||
:::tip
|
||||
|
||||
### Separate JSON files
|
||||
More common, straightforward scenarios can be found in the [**quickstart**](/quickstart) guide.
|
||||
|
||||
```json5 title="CONFIG_DIR/spotify.json"
|
||||
[
|
||||
{
|
||||
// may omit 'type' property since app knows this is file is for spotify configs
|
||||
"name": "foxxSpot",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"clientId": "foxxSpotifyAppId",
|
||||
"clientSecret": "foxxSpotifyAppSecret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "marySpot",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"clientId": "foxxSpotifyAppId",
|
||||
"clientSecret": "foxxSpotifyAppSecret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fredSpot",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"accessToken": "fredsToken",
|
||||
"refreshToken": "fredsRefreshToken",
|
||||
"interval": 120
|
||||
}
|
||||
},
|
||||
]
|
||||
```
|
||||
:::
|
||||
|
||||
```json5 title="CONFIG_DIR/plex.json"
|
||||
[
|
||||
{
|
||||
"name": "fredPlex",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maryPlex",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["maryUser"],
|
||||
"librariesAllow": ["podcasts"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "partyPlex",
|
||||
"data": {
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"librariesAllow": ["party"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
## Examples
|
||||
|
||||
```json5 title="CONFIG_DIR/jellyfin.json"
|
||||
[
|
||||
{
|
||||
"name": "FredJelly",
|
||||
"data": {
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
"usersAllow": ["fredUser"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
### Multiple Sources to Multiple Clients {#multi-sources-clients}
|
||||
|
||||
```json5 title="CONFIG_DIR/ytmusic.json"
|
||||
[
|
||||
{
|
||||
"name": "foxxYoutube",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
```json5 title="CONFIG_DIR/endpointlz.json"
|
||||
[
|
||||
{
|
||||
"name": "listenbrainzfoxx",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"token": "myToken",
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
You are the only user of Multi-scrobbler. You have many Sources you listen to and want to scrobble them to multiple services.
|
||||
|
||||
```json5 title="CONFIG_DIR/maloja.json"
|
||||
[
|
||||
{
|
||||
"name": "foxxMaloja",
|
||||
"data": {
|
||||
"url": "https://maloja.foxx.example",
|
||||
"apiKey": "foxxApiKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fredMaloja",
|
||||
"data": {
|
||||
"url": "https://maloja.fred.example",
|
||||
"apiKey": "fredApiKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maryMaloja",
|
||||
"data": {
|
||||
"url": "https://maloja.mary.example",
|
||||
"apiKey": "maryApiKey"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
* You want to **scrobble from...**
|
||||
* A [Spotify](/configuration/sources/spotify) account
|
||||
* A Navidrome instance that you want to use with the built-in [listenbrainz](/configuration/sources/listenbrainz-endpoint) scrobble function
|
||||
* A shared [Plex](/configuration/sources/plex) instance that you want to scrobble only from a specific library named `oldies`
|
||||
* You want to **scrobble to...**
|
||||
* Your decades-old [Last.fm](/configuration/clients/lastfm) account
|
||||
* A [Koito](/configuration/clients/koito) server for backup
|
||||
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "maryLFM",
|
||||
"data": {
|
||||
"apiKey": "maryApiKey",
|
||||
"secret": "marySecret",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
<details>
|
||||
|
||||
<summary>Configuration</summary>
|
||||
|
||||
<MultiSourcesClients/>
|
||||
|
||||
</details>
|
||||
|
||||
### Scrobble for Multiple People to Multiple Clients {#multi-persons-services}
|
||||
|
||||
<MultiUsecase/>
|
||||
|
||||
You want to scrobble plays for yourself (Foxx), Fred, and Mary:
|
||||
|
||||
* The three of you want to **scrobble from...**
|
||||
* Fred and Mary each have their own [Jellyfin servers](/configuration/sources/jellyfin)
|
||||
* You have a [Plex](/configuration/sources/plex) server
|
||||
* The three of you want to **scrobble to...**
|
||||
* Fred and Mary each have their own [Last.fm](/configuration/clients/lastfm) accounts
|
||||
* You have a [Koito](/configuration/clients/koito) server
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Configuration</summary>
|
||||
|
||||
<MultiPersonsServices/>
|
||||
|
||||
</details>
|
||||
|
||||
### Use Musicbrainz to Fix Scrobbles from Navidrome
|
||||
|
||||
You have multiple Sources of music but your personal, on-filesystem music collection is not well organized.
|
||||
|
||||
You want to use the [Musicbrainz Transformer](/configuration/transforms/musicbrainz) to fix any scrobbles that use Navidrome, where your personal collection is located, so that the scrobble artists/album/title are all standardized.
|
||||
|
||||
* You want to **scrobble from...**
|
||||
* A [Spotify](/configuration/sources/spotify) account
|
||||
* A Navidrome instance that you want to use with the built-in [listenbrainz](/configuration/sources/listenbrainz-endpoint) scrobble function
|
||||
* Plays from this Source should be fixed by Musicbrainz before going to your scrobble Clients
|
||||
* You want to **scrobble to...**
|
||||
* A [Last.fm](/configuration/clients/lastfm) account
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Configuration</summary>
|
||||
|
||||
<Musicbrainz/>
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,13 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ------------------------------------------ | ------- | ------------------------ | -------------------------------------------------------------------- |
|
||||
| _**`APPLEMUSIC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name EX `My Cool Component` |
|
||||
| `APPLEMUSIC_ENABLE` | boolean | true | Should this component be used? |
|
||||
| `APPLEMUSIC_KEY_ID` | string | | |
|
||||
| `APPLEMUSIC_TEAM_ID` | string | | |
|
||||
| `APPLEMUSIC_KEY_P8` | string | | |
|
||||
| _**`APPLEMUSIC_MEDIA_USER_TOKEN`**_ | string | | |
|
||||
| `APPLEMUSIC_TOKEN` | string | | |
|
||||
| `APPLEMUSIC_ORIGIN_HEADER` | string | | Origin header to include in every Apple Music API request. |
|
||||
| `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | boolean | true | Fixes a quirk where Apple Music's history API hides duplicate plays. |
|
||||
| `APPLEMUSIC_NORMALIZE_ALBUM` | boolean | true | Removes extraneous suffixes from album data |
|
||||
@@ -0,0 +1,10 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | ------------------- | ------------------------------------------------- |
|
||||
| _**`AZURA_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name EX `My Cool Component` |
|
||||
| `AZURA_ENABLE` | boolean | true | Should this component be used? |
|
||||
| _**`AZURA_STATION`**_ | string | | The specific station to monitor |
|
||||
| _**`AZURA_URL`**_ | string | | Base URL of the Azuracast instance |
|
||||
| `AZURA_KEY` | string | | API Key used to access data about private streams |
|
||||
| `AZURA_LISTENERS_NUM` | string | | |
|
||||
| `AZURA_LIVE` | boolean | | |
|
||||
@@ -0,0 +1,9 @@
|
||||
| Environmental Variable | Type | Default | Description |
|
||||
| ---------------------- | ------- | ---------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| _**`CC_ID`**_ | string | | A globally unique ID EX `myComponentId` |
|
||||
| `CC_NAME` | string | Value of `CC_ID` | A vanity name EX `My Cool Component` |
|
||||
| `CC_ENABLE` | boolean | true | Should this component be used? |
|
||||
| `CC_BLACKLIST_DEVICES` | string | | DO NOT scrobble from any cast devices on this comma-delimited list that START WITH these values |
|
||||
| `CC_WHITELIST_DEVICES` | string | | ONLY scrobble from any cast device on this comma-delimited list that START WITH these values |
|
||||
| `CC_BLACKLIST_APPS` | string | | DO NOT scrobble from any application on this comma-delimited list that START WITH these values |
|
||||
| `CC_WHITELIST_APPS` | string | | ONLY scrobble from any application on this comma-delimited list that START WITH these values |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user