mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c418d5618 | ||
|
|
e6e782dec8 | ||
|
|
ab7d20d7d3 | ||
|
|
ec4c2cdd56 | ||
|
|
88d3e13b52 | ||
|
|
7bf70d1973 | ||
|
|
8eae2dc69d | ||
|
|
aa44a08e9e | ||
|
|
6071abca49 | ||
|
|
86a719ba4a | ||
|
|
39b6f6e602 | ||
|
|
bfce5dcc54 | ||
|
|
18537e4e08 | ||
|
|
30db8712fc | ||
|
|
1701a5e5f1 | ||
|
|
406c39b6b2 | ||
|
|
ce7ad36db4 | ||
|
|
fa4bf5ab10 | ||
|
|
83d9af2aea | ||
|
|
433761525c | ||
|
|
f7a7a36d8b | ||
|
|
7e375f1531 | ||
|
|
357397f225 | ||
|
|
78bf507226 | ||
|
|
f892851b60 | ||
|
|
4244ddb8c2 | ||
|
|
3e67e9189f | ||
|
|
de1cbb2aae | ||
|
|
db17455b2e | ||
|
|
a7729fbfa6 | ||
|
|
771fdc0686 | ||
|
|
8cad2d4efd | ||
|
|
a094a1aa65 | ||
|
|
f2d45c5622 | ||
|
|
a429bccf07 | ||
|
|
04d950980d | ||
|
|
7ec90b97d6 | ||
|
|
a4f989c5e4 | ||
|
|
da4fbd2f27 | ||
|
|
b9e6f32500 | ||
|
|
5c5c653c3b | ||
|
|
a9e7a4bf16 | ||
|
|
1b4baafa67 | ||
|
|
794ef08aa2 | ||
|
|
fa6ed323ff | ||
|
|
8110e8e9fa | ||
|
|
12a6eec740 | ||
|
|
472021dd2c | ||
|
|
ebb74d1b5c | ||
|
|
ea23a89101 | ||
|
|
9062619d38 | ||
|
|
3fb64be54f | ||
|
|
3e083b19f7 | ||
|
|
e197c71bbb | ||
|
|
7b36d95dfc | ||
|
|
0427b6e945 | ||
|
|
326d9e58d7 | ||
|
|
63c079f08e | ||
|
|
ff74fd7e4c | ||
|
|
680f862ee8 | ||
|
|
abdd7f0b55 | ||
|
|
c017dd734b | ||
|
|
ad478adefd | ||
|
|
b11bd52963 | ||
|
|
538df6b6d0 | ||
|
|
819afcdb8a | ||
|
|
a5693f7c03 | ||
|
|
22eb5b1168 | ||
|
|
9402caa4ae | ||
|
|
0aff56e891 | ||
|
|
a5f6aaa7c0 | ||
|
|
4ee503655c | ||
|
|
e53a82782c | ||
|
|
020c6815ba | ||
|
|
5281a92e81 | ||
|
|
8577eb44af | ||
|
|
edd3cf25b5 | ||
|
|
b17719c868 | ||
|
|
971ec9d4db | ||
|
|
6607654e8a | ||
|
|
891e591ceb | ||
|
|
2540ed3945 | ||
|
|
7ac3f33cd4 | ||
|
|
9b91f6897e | ||
|
|
bc62b99401 | ||
|
|
a5d1cea910 | ||
|
|
3e3473f6a8 | ||
|
|
a0bdd15d15 | ||
|
|
920eb10c05 | ||
|
|
ae7a436758 | ||
|
|
92d246b45a | ||
|
|
0af480ac1a | ||
|
|
9f52c2ec67 | ||
|
|
cfb278087e | ||
|
|
402b0d2cc7 | ||
|
|
67c79f2eb7 | ||
|
|
d759b00c7e | ||
|
|
cac8163e58 | ||
|
|
11dbdaf6cc | ||
|
|
ac6b04f492 | ||
|
|
0790da2e0d | ||
|
|
dffb1c79ec | ||
|
|
46cc9f0b6e | ||
|
|
140e9f0b20 | ||
|
|
0fe5779c4b | ||
|
|
06b1475b63 | ||
|
|
6bc415120d | ||
|
|
e061540e6b | ||
|
|
38bf31a67f | ||
|
|
c8c3b4888f | ||
|
|
70bb30406a | ||
|
|
d9e71f6770 | ||
|
|
7cee9f04cf | ||
|
|
0a673fd333 | ||
|
|
fd316a8e84 | ||
|
|
c1e1de2255 | ||
|
|
9bea31e1b2 | ||
|
|
351edd8102 | ||
|
|
10dcb7f30d | ||
|
|
ffb41b4e30 | ||
|
|
d78bc48e6f | ||
|
|
b4154bb0d5 | ||
|
|
48467b08d3 | ||
|
|
682d507683 | ||
|
|
814e609efc | ||
|
|
652fa5a14c | ||
|
|
39bc9cd410 | ||
|
|
63b6012488 | ||
|
|
32e2aa6974 | ||
|
|
78f4767dd2 | ||
|
|
05a9ca15a9 | ||
|
|
e2c8158aec | ||
|
|
4eee6d4322 | ||
|
|
56f5431dd9 | ||
|
|
a00f94f9b1 | ||
|
|
df4fe87178 | ||
|
|
89c39d2d25 | ||
|
|
b60422c26c | ||
|
|
59fce8271e | ||
|
|
506faf39dc | ||
|
|
adf5401fff | ||
|
|
dbf5cba7cb | ||
|
|
b3a6beb8f4 | ||
|
|
1291f60e62 | ||
|
|
e86945355c | ||
|
|
8dd9958b30 | ||
|
|
869c7dcfe1 | ||
|
|
7068da5fc2 | ||
|
|
3704fba1f8 | ||
|
|
6d99790f7d | ||
|
|
3e7f1965e7 | ||
|
|
87d05b7c75 | ||
|
|
aca23ed90d | ||
|
|
a01c8e3f17 | ||
|
|
eba9bc1b6b | ||
|
|
34be8add14 | ||
|
|
bd44fa5609 | ||
|
|
c81bdf608f | ||
|
|
e9e5987330 | ||
|
|
ce4b0f7aa7 | ||
|
|
eb6e469dc9 | ||
|
|
4f6fb4ffea | ||
|
|
24df22781f | ||
|
|
33fda80d0a | ||
|
|
a3d8372f48 | ||
|
|
b5dbde66c4 | ||
|
|
abf532c6bb | ||
|
|
976c7c0e82 | ||
|
|
06b23d6287 | ||
|
|
38a15d6426 | ||
|
|
c2c86033cb | ||
|
|
72c94ca1e3 | ||
|
|
bf6bd4be55 | ||
|
|
53f78b02fb | ||
|
|
6f5a8a7bc7 | ||
|
|
0d5dd34597 | ||
|
|
a93ec8dd7e | ||
|
|
1e488b576c | ||
|
|
d3d8eb40b5 | ||
|
|
db54761422 | ||
|
|
aa9f20f544 | ||
|
|
60371f52f8 | ||
|
|
87394fbce7 | ||
|
|
72341ff1de | ||
|
|
63cebf3784 | ||
|
|
60f3f05f67 | ||
|
|
ce39065c80 | ||
|
|
3f39e9efa1 | ||
|
|
f37dbd75c6 | ||
|
|
47ad7a3b74 | ||
|
|
91747c0682 | ||
|
|
80f8495f8e | ||
|
|
c6bb258bf4 | ||
|
|
e70abc593c | ||
|
|
22d73c6a7f | ||
|
|
82afe4195c | ||
|
|
5e9409ab66 | ||
|
|
e64854eb14 | ||
|
|
333f70bd7e | ||
|
|
ace7d5efc9 | ||
|
|
cd14f82753 | ||
|
|
aba718aef1 | ||
|
|
c87497071d | ||
|
|
2a35e7cf5c | ||
|
|
3b98036687 | ||
|
|
3fc935b96d | ||
|
|
7586f226ba | ||
|
|
1a0e600db5 | ||
|
|
1dba5a6c6f | ||
|
|
3ec0652fbb | ||
|
|
e71d5983a4 | ||
|
|
bb9b5fc5cd | ||
|
|
89fde5a6fe | ||
|
|
24ff8f2b0f | ||
|
|
f103563cb7 | ||
|
|
1767268a48 | ||
|
|
8ab2d61742 | ||
|
|
132580af6f | ||
|
|
375c0123c6 | ||
|
|
769002412d | ||
|
|
7fa567c1e4 | ||
|
|
8dfc9a6d55 | ||
|
|
0efc85c516 | ||
|
|
dbb8e3c5e0 | ||
|
|
f4f1eed645 | ||
|
|
0a31f51567 | ||
|
|
c255138a6e | ||
|
|
856a05a357 | ||
|
|
18915167f4 | ||
|
|
34b0995d6b | ||
|
|
4e5f38378e | ||
|
|
5d2c9261ae | ||
|
|
e039d4512c | ||
|
|
51509a3531 | ||
|
|
8f032f2379 | ||
|
|
ff11e26219 | ||
|
|
f004aa4cfd | ||
|
|
2104f2a99d | ||
|
|
389129aadf | ||
|
|
f885726ed1 | ||
|
|
3cc452323d | ||
|
|
780acb32d7 | ||
|
|
bf5fdb95f4 | ||
|
|
7d7ccf53bb | ||
|
|
94837b7e03 | ||
|
|
a9c1c7083d | ||
|
|
8dd6fc15cc | ||
|
|
32ca57fc65 | ||
|
|
9d748f26c0 | ||
|
|
5af1454f36 | ||
|
|
bf7a660633 | ||
|
|
f0723f23e8 | ||
|
|
36357135cb | ||
|
|
8be08a5c29 | ||
|
|
9fe5b9b671 | ||
|
|
720c07b008 | ||
|
|
7fdf93c5d3 | ||
|
|
a0fd466bcf | ||
|
|
35d62c913a | ||
|
|
ede2d9f71a | ||
|
|
acccc32813 | ||
|
|
69f73ff09f | ||
|
|
305f3c42ae | ||
|
|
d6ac4c34c5 | ||
|
|
9c6eeaafdb | ||
|
|
6bd841fea2 | ||
|
|
e05c46e9ce | ||
|
|
e94109dc75 | ||
|
|
a68dea5193 | ||
|
|
b5d56b75d7 | ||
|
|
c8daffb935 | ||
|
|
18a7f26c54 | ||
|
|
e7395e79a5 | ||
|
|
08ee6d379f | ||
|
|
9c692539ce | ||
|
|
5cd5061f20 | ||
|
|
46bcf6d6a6 | ||
|
|
4da931269c | ||
|
|
d19d40d479 | ||
|
|
b3e1c7f660 | ||
|
|
1242a710df | ||
|
|
b724eca9be | ||
|
|
376d4936cc | ||
|
|
a5da0573c5 | ||
|
|
4694ce45a7 | ||
|
|
7c7d710cd3 | ||
|
|
e2cd7e2991 | ||
|
|
92d8eb8d6f | ||
|
|
d14e7d9b2e | ||
|
|
e4befae55a | ||
|
|
0e00b425b4 | ||
|
|
fcf6c8eb4c | ||
|
|
242bacc1be | ||
|
|
cbe28f97a0 | ||
|
|
336972f586 | ||
|
|
81edb31e7b | ||
|
|
dbde66ad77 | ||
|
|
efbf2da816 | ||
|
|
38d1db2f3f | ||
|
|
265ae82663 | ||
|
|
2bd2972af6 | ||
|
|
1bdf083e28 | ||
|
|
4c233fdded | ||
|
|
e0060fe3e5 | ||
|
|
2adb3bd052 | ||
|
|
0dca49ab43 | ||
|
|
6a71d4a495 | ||
|
|
230ddf4a48 | ||
|
|
e4e1797ac5 | ||
|
|
1dd3185715 | ||
|
|
4fee231a7b | ||
|
|
b5ead39792 | ||
|
|
190431b9d4 | ||
|
|
210fa35cb6 | ||
|
|
479af1cd5e | ||
|
|
c72831cd12 | ||
|
|
f68a9493ce | ||
|
|
78a93d0809 | ||
|
|
3680ac8af1 | ||
|
|
e74ba52c47 | ||
|
|
d3cd07c5c3 | ||
|
|
2b890d4704 | ||
|
|
47eead2a18 | ||
|
|
38ea3115f6 | ||
|
|
f11a2b5442 | ||
|
|
818f67d6da | ||
|
|
84f353220d | ||
|
|
47541a449b | ||
|
|
1a62edab37 | ||
|
|
5927450840 | ||
|
|
ec9bea70fd | ||
|
|
2f9a45b1e4 | ||
|
|
3c649df2e9 | ||
|
|
ab1766ca0a | ||
|
|
a42de78ad3 | ||
|
|
81bde3a771 | ||
|
|
982e6655ef | ||
|
|
44ce0cba3c | ||
|
|
c1e1c69b5e | ||
|
|
142defdd20 | ||
|
|
aad9f2b4de | ||
|
|
8e92407182 | ||
|
|
94210e29d1 | ||
|
|
d2c6194314 | ||
|
|
11ca7a724d | ||
|
|
3057908074 | ||
|
|
6a9e538845 | ||
|
|
130538cd9a | ||
|
|
1486613744 | ||
|
|
f2516206c0 | ||
|
|
f8902e05a6 | ||
|
|
c59b47c801 | ||
|
|
0e4845f2e2 | ||
|
|
e2775cd9de | ||
|
|
816bec88c8 | ||
|
|
d724bef4e4 | ||
|
|
cac851f120 | ||
|
|
c1df74ee27 | ||
|
|
0c7b51c622 | ||
|
|
56ecad24d4 | ||
|
|
6bb600648a | ||
|
|
fe2610c35a | ||
|
|
84bcbc9cea | ||
|
|
d741377e5a | ||
|
|
1ea106970d | ||
|
|
a879b7aded | ||
|
|
30ee000676 | ||
|
|
703cf80d5a | ||
|
|
34289f0176 | ||
|
|
8b8f32de02 | ||
|
|
095e5cc0d9 | ||
|
|
0c46c0a079 | ||
|
|
dd4b2ebb8c | ||
|
|
d30ee0c35a | ||
|
|
484fb16bba | ||
|
|
f320e6cef0 | ||
|
|
ea2421bc6d | ||
|
|
f43c31fb9a | ||
|
|
6825ad55e3 | ||
|
|
92841f8bb3 | ||
|
|
38e5e4c9a9 | ||
|
|
1bcfed05af | ||
|
|
e00ec4098a | ||
|
|
5d44953435 | ||
|
|
80ad5a3518 | ||
|
|
b5683ba4b8 | ||
|
|
884b3963d3 | ||
|
|
8de2296aaf | ||
|
|
c1869f469c | ||
|
|
86196b3122 | ||
|
|
fd03e42240 | ||
|
|
3e230b0d68 | ||
|
|
133d7f56e5 | ||
|
|
78d048ba62 | ||
|
|
323004a730 | ||
|
|
626bca9951 | ||
|
|
3ae0d4ff45 | ||
|
|
9d85df54e1 | ||
|
|
0ff1c95fd1 | ||
|
|
b93274fd53 | ||
|
|
0f0d1d64f9 | ||
|
|
d77e6c6f22 | ||
|
|
83ff5348c6 | ||
|
|
65012d40ed | ||
|
|
feb02bfa85 | ||
|
|
276e51b8ea | ||
|
|
c83d77c02a | ||
|
|
0cdb48a71a | ||
|
|
1556d7ac69 | ||
|
|
1e65abd962 | ||
|
|
9f11d799ac | ||
|
|
5c0f419e7e | ||
|
|
c33d42808a | ||
|
|
e8716a45b2 | ||
|
|
570dee30e3 | ||
|
|
97222ed233 | ||
|
|
e2a7fff6f9 | ||
|
|
efc8246a76 | ||
|
|
b4a95e0cf9 | ||
|
|
c66d5d4627 | ||
|
|
4de28e0390 | ||
|
|
f74721b366 | ||
|
|
4fb16e7098 | ||
|
|
a76c351206 | ||
|
|
7faf94cfab | ||
|
|
3a1e29f682 | ||
|
|
88cb287df8 | ||
|
|
eb26ee0cd4 | ||
|
|
844833f348 | ||
|
|
38b02cf771 | ||
|
|
1706d5cab9 | ||
|
|
640d2ff28b | ||
|
|
312b6ce987 | ||
|
|
489f70929e | ||
|
|
904ddfa5c6 | ||
|
|
3ed6840607 | ||
|
|
93125dd7c8 | ||
|
|
34356c6c56 | ||
|
|
05466fed2b | ||
|
|
535b818a22 | ||
|
|
e62e04dd14 | ||
|
|
61cd5fcf7b | ||
|
|
f308f71b5d | ||
|
|
c498bde1fd | ||
|
|
0c9b496137 | ||
|
|
9682d4f8f9 | ||
|
|
da76945f5e | ||
|
|
51074a58f4 | ||
|
|
3cc735b9c8 | ||
|
|
b74f31c26d | ||
|
|
4a96cbfb0e | ||
|
|
77a3c352b1 | ||
|
|
598ae01082 | ||
|
|
54e11899a0 | ||
|
|
dc4962ade1 | ||
|
|
ccf317c06f | ||
|
|
aff5717f19 | ||
|
|
fa39209272 | ||
|
|
d63082a889 | ||
|
|
a28d46ecbe | ||
|
|
3bc72ce881 | ||
|
|
314210117b | ||
|
|
57a8e48d3a | ||
|
|
2d3ab86238 | ||
|
|
b76edbf7de | ||
|
|
b2efc34b80 | ||
|
|
dee8d6bc61 | ||
|
|
3e5576481d | ||
|
|
ffd74aea6f | ||
|
|
145af33239 | ||
|
|
3851b2226a | ||
|
|
bb132101b5 | ||
|
|
1e6251edfb | ||
|
|
e251da9b8e | ||
|
|
335ee0cd60 | ||
|
|
dc9f6c22ad | ||
|
|
5568b54c99 | ||
|
|
e6e35e052a | ||
|
|
9c21db8172 | ||
|
|
77c9141d92 | ||
|
|
68e86f4f90 | ||
|
|
5e95f60461 | ||
|
|
908d7d4177 | ||
|
|
de1a9c35ec | ||
|
|
f7fa9cd330 | ||
|
|
ee8b371850 | ||
|
|
601228a369 | ||
|
|
5dcaefc460 | ||
|
|
026e94fc1b | ||
|
|
fd7c8467b4 | ||
|
|
06e651a844 | ||
|
|
1b0e7a3dfd | ||
|
|
abc5d1861a | ||
|
|
4b8129fd19 | ||
|
|
f610f385bd | ||
|
|
6994635fe4 | ||
|
|
61525e1351 | ||
|
|
a50070e133 | ||
|
|
db632424b1 | ||
|
|
6b92920f50 | ||
|
|
a30d362695 | ||
|
|
d105e62f18 | ||
|
|
d5ad796339 | ||
|
|
c0aabefc72 | ||
|
|
259d321f3d | ||
|
|
d2375d92e6 | ||
|
|
f8a63c5df1 | ||
|
|
5004d99473 | ||
|
|
5b26cc73e8 | ||
|
|
72a4c4542b | ||
|
|
fdaeb8f6b8 | ||
|
|
f2f550dae7 | ||
|
|
6780d72296 | ||
|
|
f1784f3c21 | ||
|
|
22d17ba621 | ||
|
|
4b4cc1c438 | ||
|
|
7aac664916 | ||
|
|
12c4a014f2 | ||
|
|
1eada12714 | ||
|
|
5adb831136 | ||
|
|
5b8a8a7449 | ||
|
|
005dae76ab | ||
|
|
cb2c7276dd | ||
|
|
54fe61c51c | ||
|
|
bdf27ec421 | ||
|
|
3addd39fc5 | ||
|
|
19bf456e47 | ||
|
|
66bf889e9f | ||
|
|
09ea158734 | ||
|
|
88c1b553e3 | ||
|
|
1d1fd1b54d | ||
|
|
9d3267b1fc | ||
|
|
22a04adde0 | ||
|
|
1dba90ceb5 | ||
|
|
4e9794d574 | ||
|
|
23a747d3a9 | ||
|
|
4678c35453 | ||
|
|
c153e53c5b | ||
|
|
e20ee09bbb | ||
|
|
619687472d | ||
|
|
b74f23f6d1 | ||
|
|
d073b4c402 | ||
|
|
258883d88b | ||
|
|
29c8d6236a | ||
|
|
a1443594f7 | ||
|
|
ac2e9570e5 | ||
|
|
983b1c67ac | ||
|
|
764493f5c9 | ||
|
|
d7981e7cc0 | ||
|
|
11fbe97325 | ||
|
|
3f37a63439 | ||
|
|
ee78b95617 | ||
|
|
d7091f8a8e | ||
|
|
4b5cdade77 | ||
|
|
cb67d1ae40 | ||
|
|
676af4a2bf | ||
|
|
0feb02babe | ||
|
|
90c481b12b | ||
|
|
59fed488ba | ||
|
|
8431e7b6b2 | ||
|
|
be30cdd540 | ||
|
|
54927ed77c | ||
|
|
97f974d931 | ||
|
|
0de2cc0922 | ||
|
|
e80ad81f05 | ||
|
|
a3960421d7 | ||
|
|
2f2eeeb13a | ||
|
|
eeb026abf0 | ||
|
|
0fed96f59c | ||
|
|
8369939135 | ||
|
|
08056a650f | ||
|
|
5b53b7490c | ||
|
|
ce95c3ae8c | ||
|
|
5e0c4e3e77 | ||
|
|
10bf48df40 | ||
|
|
ae935798f3 | ||
|
|
bf71f8cf86 | ||
|
|
4206696f0d | ||
|
|
2903662f8b | ||
|
|
469847e4b7 | ||
|
|
d7a86b0401 | ||
|
|
4fdd346c00 | ||
|
|
4f26b63ab1 | ||
|
|
0fed91302c | ||
|
|
831cefd9a7 | ||
|
|
114a3dda8b | ||
|
|
55ddd2c5a0 | ||
|
|
1d276186f8 | ||
|
|
fb3886b981 | ||
|
|
2bed7be654 | ||
|
|
5c460ad747 | ||
|
|
32220d7509 | ||
|
|
b0265001f8 | ||
|
|
9ede4634cb | ||
|
|
0e008ecd5c | ||
|
|
2ae82fdd9e | ||
|
|
3b430b95ed | ||
|
|
e907892c4a | ||
|
|
d9cd3764b9 | ||
|
|
9c2bf05a24 | ||
|
|
91e52c4c5d | ||
|
|
b41d7f539f | ||
|
|
9735b980d7 | ||
|
|
706483ed06 | ||
|
|
7760776a5f | ||
|
|
8b9a7893d3 | ||
|
|
729250a2cc | ||
|
|
f14203f85e | ||
|
|
60e880f7f2 | ||
|
|
16cef0ae65 | ||
|
|
12ff1f421e | ||
|
|
61c33da2b0 | ||
|
|
8b0c7205b7 | ||
|
|
b02c96ada8 | ||
|
|
52b20e4c8e | ||
|
|
a94ac8284f | ||
|
|
ee399136a9 | ||
|
|
ab54422acf | ||
|
|
d56c5720eb | ||
|
|
040de4b000 | ||
|
|
815a774e3d | ||
|
|
b2e7f58e14 | ||
|
|
8d124a20ac | ||
|
|
d1a4a54d2a | ||
|
|
049bb06762 | ||
|
|
e5dd473981 | ||
|
|
ca5cc3bb30 | ||
|
|
444d75c803 | ||
|
|
ccda3a6098 | ||
|
|
ce1c70a4e1 | ||
|
|
5fcfa979c5 | ||
|
|
ec4d609ba1 | ||
|
|
b42278a372 | ||
|
|
1f96035ba9 | ||
|
|
332edb5dca | ||
|
|
c5681d31eb | ||
|
|
685d70c639 | ||
|
|
848878bc5e | ||
|
|
088d063d38 | ||
|
|
4e493db987 | ||
|
|
6d017d4b27 | ||
|
|
a34066d650 | ||
|
|
0e34d63c84 | ||
|
|
9dca8ebfa0 | ||
|
|
c908aea1ae | ||
|
|
c7a58bbce6 | ||
|
|
b26f303479 | ||
|
|
13d64d2ce2 | ||
|
|
ddb7fb3d6e | ||
|
|
457967bfb2 | ||
|
|
734742ecbe | ||
|
|
26dab3870a | ||
|
|
ca9acbb089 | ||
|
|
f78d6fec1d | ||
|
|
bafb852545 | ||
|
|
a755db45b9 | ||
|
|
b9715f2159 | ||
|
|
e90dbaf691 | ||
|
|
08ac55f718 | ||
|
|
5bf831e236 | ||
|
|
1905804f2a | ||
|
|
afc931ac8c | ||
|
|
9fb032e030 | ||
|
|
09b0c96263 | ||
|
|
7c66816023 | ||
|
|
3e7e5df5de | ||
|
|
1c84a74034 | ||
|
|
09969dd9d7 | ||
|
|
63b57e1113 | ||
|
|
585677e515 | ||
|
|
57f2b5f7e5 | ||
|
|
3597735556 | ||
|
|
2442cf2286 | ||
|
|
97438e13d2 | ||
|
|
57dff9d122 | ||
|
|
963fc792a0 | ||
|
|
35a6418f38 | ||
|
|
3c0b951132 | ||
|
|
3dc9e7d8b4 | ||
|
|
ab372b8357 | ||
|
|
c79f04581f |
@@ -0,0 +1,25 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||
app:
|
||||
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||
# array). The sample below assumes your primary file is in the root of your project.
|
||||
#
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
- ..:/workspaces:cached
|
||||
command: sleep infinity
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
@@ -0,0 +1,46 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||
{
|
||||
"name": "Node.js",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"dockerComposeFile": [
|
||||
"compose.yml"
|
||||
],
|
||||
|
||||
"service": "app",
|
||||
|
||||
"workspaceFolder": "/workspaces",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [9078,3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"hbenl.vscode-mocha-test-adapter",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"TakumiI.markdowntable"
|
||||
]
|
||||
}
|
||||
},
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Docusaurus"
|
||||
},
|
||||
"9078": {
|
||||
"label": "App"
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
+7
-1
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
*.Dockerfile
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.github
|
||||
@@ -14,7 +15,7 @@ config/*.p8
|
||||
/docs
|
||||
/logs
|
||||
.flatpak-builder
|
||||
flatpak/generated-sources.json
|
||||
**/generated-sources.*
|
||||
flatpak/.flatpak-builder
|
||||
docsite/build
|
||||
docsite/node_modules
|
||||
@@ -22,3 +23,8 @@ docsite/.docusaurus
|
||||
docsite/.cache-loader
|
||||
build
|
||||
*.secrets
|
||||
.devcontainer
|
||||
.vscode
|
||||
**/act/*
|
||||
tmp-*
|
||||
.env
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible problem
|
||||
title: "bug: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- label: I have [checked the FAQ](https://foxxmd.github.io/multi-scrobbler/docs/FAQ/) and [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
|
||||
required: true
|
||||
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If this a how-do-I question, troublehooting, or generally **not** a bug/error with multi-scrobbler please consider [**starting a discussion**](https://github.com/FoxxMD/multi-scrobbler/discussions) instead of opening an issue.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: Describe what you expected to happen and what happened instead. Please provide as much detail as possible.
|
||||
placeholder: The app should have scrobbled from Spotify but instead it crashed with an error about undefined properties, etc...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: What platform are you running multi-scrobbler on?
|
||||
options:
|
||||
- Docker
|
||||
- Local (NodeJS)
|
||||
- Flatpak
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Versions
|
||||
description: What version of multi-scrobbler and issue-related sources/clients are running?
|
||||
placeholder: |
|
||||
- multi-scrobbler: 0.8.3 on docker
|
||||
- maloja 3.1.4
|
||||
- jellyfin 10.8.9
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
Please copy and paste any/all relevant from the DEBUG level log output.
|
||||
Even if your issue does not seem to be shown in the logs you should still add as much detail as possible, a _lack_ of issues in logs may be a good hint during debugging.
|
||||
Your logs will be automatically formatted into code, do not need use backticks.
|
||||
If possible reproduce the issue with [**Debug Mode** enabled](https://foxxmd.github.io/multi-scrobbler/docs/configuration#debug-mode).
|
||||
render: shell
|
||||
placeholder: |
|
||||
[2025-02-14 12:47:57.170 -0500] VERBOSE: [Init] Config Dir ENV: undefined -> Resolved: CWD/config
|
||||
[2025-02-14 12:47:57.236 -0500] INFO : [Init] Debug Mode: NO
|
||||
[2025-02-14 12:47:57.258 -0500] INFO : [Init] Version: master
|
||||
[2025-02-14 12:47:57.259 -0500] INFO : [Init] Generating schema definitions...
|
||||
[2025-02-14 12:47:59.148 -0500] DEBUG : [App] [Scrobblers] [Maloja - default] Found 100 recent scrobbles
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or functionality
|
||||
title: "feat: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
|
||||
required: true
|
||||
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the new functionality
|
||||
description: Please describe the feature or functionality you want multi-scrobbler to have. Use as much detail as possible about the exact behavior you wish to see.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Configuration Example
|
||||
description: |
|
||||
If the functionality involves changes to configuration files please provide an example here
|
||||
render: json5
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Will you help implement it?
|
||||
options:
|
||||
- label: I am willing to contribute code towards completing this feature
|
||||
- label: I am willing to help test preview builds for this feature
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Request a new Source or Client
|
||||
description: Request a new Source or Client to be implemented
|
||||
title: "feat: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check existing knowledge before opening an issue
|
||||
options:
|
||||
- label: I have checked the [documentation](https://foxxmd.github.io/multi-scrobbler/docs/configuration/)
|
||||
required: true
|
||||
- label: I have [searched through existing issues](https://github.com/FoxxMD/multi-scrobbler/issues?q=sort%3Aupdated-desc) and [discussions.](https://github.com/FoxxMD/multi-scrobbler/discussions)
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What Type?
|
||||
description: Is this a Source or a Client?
|
||||
options:
|
||||
- Source
|
||||
- Client
|
||||
default: 1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Name
|
||||
description: "What is the name of the new service/software?"
|
||||
placeholder: "My Cool Player"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Website
|
||||
description: "Website for the service/software. Preferably a github repo or other source-code homepage"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: API Available?
|
||||
description: Does the service have an API or other protocol that multi-scrobbler can use to communicate? **Without this the likelyhood of implementation is very low.**
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
default: 1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Free To Use?
|
||||
description: Is this service/software freely available or does it require purchasing/subscription?
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
default: 1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Willing to finance development?
|
||||
description: If the software is **non-free** are you will to provide a paid account/subscription to multi-scrobbler developer's in order to facilitate development/testing?
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Will you help implement it?
|
||||
options:
|
||||
- label: I am willing to contribute code towards completing this feature
|
||||
- label: I am willing to help test preview builds for this feature
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the new Source/Client here
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
|
||||
|
||||
```
|
||||
Copy and paste as much log data as possible related to this issue here.
|
||||
```
|
||||
|
||||
**Versions (please complete the following information):**
|
||||
Provide version information for any related sources/clients.
|
||||
|
||||
- multi-scrobbler: [e.g. 0.4.0 on docker]
|
||||
- maloja [e.g. 3.1.4]
|
||||
- jellyfin [e.g. 10.8.9]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,5 @@
|
||||
--directory ../../
|
||||
--env-file .github/act/.env
|
||||
--secret-file .github/act/.secrets
|
||||
--artifact-server-path out
|
||||
#--action-offline-mode
|
||||
@@ -0,0 +1,5 @@
|
||||
NO_DOCKER_BUILD=true
|
||||
#https://github.com/nektos/act/issues/329#issuecomment-1905955589
|
||||
ACTIONS_RUNTIME_URL=http://YOUR_HOST_IP:8082/
|
||||
ACTIONS_RUNTIME_TOKEN=foo
|
||||
ACTIONS_CACHE_URL=http://YOUR_HOST_IP:8082/
|
||||
@@ -0,0 +1,3 @@
|
||||
DOCKER_PASSWORD=
|
||||
DOCKER_USERNAME=
|
||||
GITHUB_TOKEN=
|
||||
@@ -0,0 +1,53 @@
|
||||
Testing GH Actions with ACT
|
||||
|
||||
Need to have credentials in a [`.secrets` file](https://nektosact.com/usage/index.html#secrets), copy and rename [`.secrets.example`](./secrets.example) to `.secrets`, then fill out blank fields. Required for docker/metadata-action to read...something. Fails with `Parameter token or opts.auth is required` if they are not supplied.
|
||||
|
||||
An ENV file can also be made by copy and renaming [`.env.example`](./env.example). Set `NO_DOCKER_BUILD=true` if you only want to test APP_VERSION and docker tags output.
|
||||
|
||||
If running a full docker build for multi-runner workflows you will need to create an [artifact server](https://github.com/nektos/act/issues/329#issuecomment-1905955589) for ACT to work:
|
||||
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/jefuller/artifact-server:latest
|
||||
docker run -d --name artifact-server -p 8082:8080 --add-host host.docker.internal:host-gateway -e AUTH_KEY=foo ghcr.io/jefuller/artifact-server:latest
|
||||
```
|
||||
|
||||
Run the following **from this directory** to make use of `.actrc` and proper working directory.
|
||||
|
||||
### Test Branch Test Suite
|
||||
|
||||
```shell
|
||||
act -W '.github/act/testSuite.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
|
||||
### Test Branch Push
|
||||
|
||||
```shell
|
||||
act -W '.github/act/actTest.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
|
||||
|
||||
### Test Tag (Release) Push
|
||||
|
||||
```shell
|
||||
act -W '.github/act/actTest.yml' -e '.github/act/actTagEvent.json'
|
||||
```
|
||||
|
||||
|
||||
### Test Tag (Pre-Release) Push
|
||||
|
||||
```shell
|
||||
act -W '.github/act/actTest.yml' -e '.github/act/actTagPreEvent.json'
|
||||
```
|
||||
|
||||
### Test Multi-Runner Push (WIP)
|
||||
|
||||
```shell
|
||||
act -W '.github/act/multiRunnerTest.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
|
||||
### Test Multi-Runner Bake Push (WIP)
|
||||
|
||||
```shell
|
||||
act -W '.github/act/multiRunnerBakeTest.yml' -e '.github/act/actBranchEvent.json'
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ref": "refs/heads/master",
|
||||
"sha": "14d5eb3313110f2cfd1aa8fe9c229c21901c1eeb"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"action": "synchronize",
|
||||
"number": 210,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/pulls/210",
|
||||
"id": 2123337348,
|
||||
"node_id": "PR_kwDOEpP-x85-j46E",
|
||||
"number": 210,
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"merge_commit_sha": "349bea1f4580b9e61aa22446eae73f8b1225bbb7",
|
||||
"labels": [
|
||||
{
|
||||
"id": 2494982061,
|
||||
"node_id": "MDU6TGFiZWwyNDk0OTgyMDYx",
|
||||
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/labels/bug",
|
||||
"name": "bug",
|
||||
"color": "d73a4a",
|
||||
"default": true,
|
||||
"description": "Something isn't working"
|
||||
},
|
||||
{
|
||||
"id": 7131135993,
|
||||
"node_id": "LA_kwDOEpP-x88AAAABqQx_-Q",
|
||||
"url": "https://api.github.com/repos/FoxxMD/multi-scrobbler/labels/safe%20to%20test",
|
||||
"name": "safe to test",
|
||||
"color": "D4C5F9",
|
||||
"default": false,
|
||||
"description": "trusted to build image"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"draft": false,
|
||||
"head": {
|
||||
"label": "FoxxMD:GH-209/jellyfinCleanup",
|
||||
"ref": "GH-209/jellyfinCleanup",
|
||||
"sha": "e251da9b8eecfcb3aba8756393e5075d0092794b",
|
||||
"user": {
|
||||
"login": "FoxxMD",
|
||||
"id": 4663766,
|
||||
"node_id": "MDQ6VXNlcjQ2NjM3NjY=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4663766?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/FoxxMD",
|
||||
"html_url": "https://github.com/FoxxMD",
|
||||
"followers_url": "https://api.github.com/users/FoxxMD/followers",
|
||||
"following_url": "https://api.github.com/users/FoxxMD/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/FoxxMD/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/FoxxMD/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/FoxxMD/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/FoxxMD/orgs",
|
||||
"repos_url": "https://api.github.com/users/FoxxMD/repos",
|
||||
"events_url": "https://api.github.com/users/FoxxMD/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/FoxxMD/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"repo": {
|
||||
"id": 311688903,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkzMTE2ODg5MDM=",
|
||||
"name": "multi-scrobbler",
|
||||
"full_name": "FoxxMD/multi-scrobbler",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "FoxxMD",
|
||||
"id": 4663766,
|
||||
"node_id": "MDQ6VXNlcjQ2NjM3NjY=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4663766?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/FoxxMD",
|
||||
"html_url": "https://github.com/FoxxMD",
|
||||
"followers_url": "https://api.github.com/users/FoxxMD/followers",
|
||||
"following_url": "https://api.github.com/users/FoxxMD/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/FoxxMD/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/FoxxMD/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/FoxxMD/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/FoxxMD/orgs",
|
||||
"repos_url": "https://api.github.com/users/FoxxMD/repos",
|
||||
"events_url": "https://api.github.com/users/FoxxMD/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/FoxxMD/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/FoxxMD/multi-scrobbler",
|
||||
"description": "Scrobble plays from multiple sources to multiple clients",
|
||||
"fork": false,
|
||||
"created_at": "2020-11-10T14:45:38Z",
|
||||
"updated_at": "2024-10-14T09:21:37Z",
|
||||
"pushed_at": "2024-10-14T15:43:00Z",
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"visibility": "public",
|
||||
"forks": 16,
|
||||
"open_issues": 19,
|
||||
"watchers": 369,
|
||||
"default_branch": "master",
|
||||
"allow_squash_merge": true,
|
||||
"allow_merge_commit": true,
|
||||
"allow_rebase_merge": true,
|
||||
"allow_auto_merge": false,
|
||||
"delete_branch_on_merge": false,
|
||||
"allow_update_branch": false,
|
||||
"use_squash_pr_title_as_default": false,
|
||||
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
||||
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
|
||||
"merge_commit_message": "PR_TITLE",
|
||||
"merge_commit_title": "MERGE_MESSAGE"
|
||||
}
|
||||
},
|
||||
"base": {
|
||||
"label": "FoxxMD:master",
|
||||
"ref": "master",
|
||||
"sha": "9c21db8172b6beff9d321e831e9353bf17e92a37",
|
||||
"repo": {
|
||||
"id": 311688903,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkzMTE2ODg5MDM=",
|
||||
"name": "multi-scrobbler",
|
||||
"full_name": "FoxxMD/multi-scrobbler",
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"created_at": "2020-11-10T14:45:38Z",
|
||||
"updated_at": "2024-10-14T09:21:37Z",
|
||||
"pushed_at": "2024-10-14T15:43:00Z",
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"visibility": "public",
|
||||
"forks": 16,
|
||||
"open_issues": 19,
|
||||
"watchers": 369,
|
||||
"default_branch": "master",
|
||||
"allow_squash_merge": true,
|
||||
"allow_merge_commit": true,
|
||||
"allow_rebase_merge": true,
|
||||
"allow_auto_merge": false,
|
||||
"delete_branch_on_merge": false,
|
||||
"allow_update_branch": false,
|
||||
"use_squash_pr_title_as_default": false,
|
||||
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
||||
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
|
||||
"merge_commit_message": "PR_TITLE",
|
||||
"merge_commit_title": "MERGE_MESSAGE"
|
||||
}
|
||||
},
|
||||
"author_association": "OWNER",
|
||||
"auto_merge": null,
|
||||
"active_lock_reason": null,
|
||||
"merged": false,
|
||||
"mergeable": null,
|
||||
"rebaseable": null,
|
||||
"mergeable_state": "unknown",
|
||||
"merged_by": null,
|
||||
"comments": 0,
|
||||
"review_comments": 0,
|
||||
"maintainer_can_modify": false,
|
||||
"commits": 5,
|
||||
"additions": 84,
|
||||
"deletions": 28,
|
||||
"changed_files": 5
|
||||
},
|
||||
"before": "335ee0cd601f5a82e36643c812507b2b53e7daf0",
|
||||
"after": "e251da9b8eecfcb3aba8756393e5075d0092794b"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ref": "refs/tags/0.8.4",
|
||||
"ref_name": "0.8.4",
|
||||
"before": "0000000000000000000000000000000000000000",
|
||||
"after": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"sha": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"base_ref": "refs/heads/master"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ref": "refs/tags/0.8.5-gh-runner",
|
||||
"ref_name": "0.8.5-gh-runner",
|
||||
"before": "0000000000000000000000000000000000000000",
|
||||
"after": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"sha": "005dae76ab51799d3d55112738e301cb1af0dafd",
|
||||
"base_ref": "refs/heads/master"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Publish Docker image to Dockerhub
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
# don't trigger if just updating docs
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
# use release instead of tags once version is correctly parsed
|
||||
# https://github.com/docker/metadata-action/issues/422
|
||||
# https://github.com/docker/metadata-action/issues/240
|
||||
# release:
|
||||
# types: [ published ]
|
||||
|
||||
jobs:
|
||||
|
||||
push_to_registry:
|
||||
name: Build and push container images
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Debug
|
||||
run: echo $JSON
|
||||
env:
|
||||
JSON: ${{ toJSON(github) }}
|
||||
|
||||
- name: Set git state to ENV
|
||||
id: vars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Check App Version
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo $APP_VERSION
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=edge
|
||||
|
||||
# maybe re-enable branch-named tags in the futures
|
||||
#type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
# flavor: |
|
||||
# latest=false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
id: qemu
|
||||
if: ${{ !env.NO_DOCKER_BUILD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
if: ${{ steps.qemu.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: ${{ steps.qemu.outcome == 'success' }}
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
# https://github.com/docker/build-push-action/issues/1026#issue-2041857786
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{env.APP_VERSION}}
|
||||
push: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -0,0 +1,30 @@
|
||||
variable "DEFAULT_TAG" {
|
||||
default = "multi-scrobbler:local"
|
||||
}
|
||||
|
||||
// Special target: https://github.com/docker/metadata-action#bake-definition
|
||||
target "docker-metadata-action" {
|
||||
tags = ["${DEFAULT_TAG}"]
|
||||
}
|
||||
|
||||
// Default target if none specified
|
||||
group "default" {
|
||||
targets = ["image-local"]
|
||||
}
|
||||
|
||||
target "image" {
|
||||
inherits = ["docker-metadata-action"]
|
||||
}
|
||||
|
||||
target "image-local" {
|
||||
inherits = ["image"]
|
||||
output = ["type=docker"]
|
||||
}
|
||||
|
||||
target "image-all" {
|
||||
inherits = ["image"]
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#with-bake (https://github.com/docker/build-push-action/issues/671#issuecomment-1609106171)
|
||||
# with example from here https://github.com/crazy-max/docker-linguist/blob/master/.github/workflows/build.yml that uses caching
|
||||
#
|
||||
# may have to use this to enable pushing to multiple registries? https://github.com/docker/build-push-action/discussions/1067#discussioncomment-8677682
|
||||
|
||||
name: build
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
|
||||
env:
|
||||
DOCKERHUB_SLUG: foxxmd/multi-scrobbler
|
||||
GHCR_SLUG: ghcr.io/foxxmd/multi-scrobbler
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: appvars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
run: |
|
||||
echo "matrix=$(docker buildx bake --file ./docker-bake.hcl image-all --print | jq -cr '.target."image-all".platforms')" >>${GITHUB_OUTPUT}
|
||||
-
|
||||
name: Show matrix
|
||||
run: |
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_SLUG }}
|
||||
${{ env.GHCR_SLUG }}
|
||||
tags: |
|
||||
type=edge
|
||||
|
||||
# maybe re-enable branch-named tags in the futures
|
||||
#type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
-
|
||||
name: Rename meta bake definition file
|
||||
run: |
|
||||
mv "${{ steps.meta.outputs.bake-file }}" "/tmp/bake-meta.json"
|
||||
-
|
||||
name: Upload meta bake definition
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: bake-meta
|
||||
path: /tmp/bake-meta.json
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
||||
steps:
|
||||
-
|
||||
name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Test App Version Transfer
|
||||
run: |
|
||||
echo "App Version in Build ${{ needs.prepare.appversion.outputs.appversion }}"
|
||||
-
|
||||
name: Download meta bake definition
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: bake-meta
|
||||
path: /tmp
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
buildkitd-flags: "--debug"
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build
|
||||
id: bake
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
files: |
|
||||
./.github/act/docker-bake.hcl
|
||||
/tmp/bake-meta.json
|
||||
targets: image
|
||||
set: |
|
||||
*.args.APP_BUILD_VERSION=${{ needs.prepare.appversion.outputs.appversion }}
|
||||
*.tags=
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.cache-from=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
*.cache-to=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
*.output=type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
-
|
||||
name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
-
|
||||
name: Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
-
|
||||
name: Download meta bake definition
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: bake-meta
|
||||
path: /tmp
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.DOCKERHUB_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
|
||||
$(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
|
||||
docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.GHCR_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
|
||||
$(printf '${{ env.GHCR_SLUG }}@sha256:%s ' *)
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
tag=$(jq -r '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
|
||||
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${tag}
|
||||
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${tag}
|
||||
@@ -0,0 +1,198 @@
|
||||
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||
|
||||
name: build
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
|
||||
env:
|
||||
DOCKERHUB_SLUG: foxxmd/multi-scrobbler
|
||||
GHCR_SLUG: ghcr.io/foxxmd/multi-scrobbler
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: appvars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
# - name: Extract metadata (tags, labels) for Docker
|
||||
# id: meta
|
||||
# uses: docker/metadata-action@v5
|
||||
# with:
|
||||
# images: |
|
||||
# ${{ env.DOCKERHUB_SLUG }}
|
||||
# ${{ env.GHCR_SLUG }}
|
||||
# labels: |
|
||||
# org.opencontainers.image.title=Multi-Scrobbler
|
||||
# org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
# org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
|
||||
# for caching
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
#tags: ${{ steps.meta.outputs.tags }}
|
||||
outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_SLUG }}
|
||||
${{ env.GHCR_SLUG }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=edge
|
||||
|
||||
# maybe re-enable branch-named tags in the futures
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
# flavor: |
|
||||
# latest=false
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_SLUG }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${{ steps.meta.outputs.version }}
|
||||
docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }}
|
||||
@@ -0,0 +1,10 @@
|
||||
name: PR Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
@@ -1,6 +1,5 @@
|
||||
## Checklist before requesting a review
|
||||
|
||||
- [ ] **I am opening this PR for the [`develop` branch](https://github.com/FoxxMD/multi-scrobbler/tree/develop) and NOT `master`.**
|
||||
- [ ] I have read the [contributing guidelines.](../CONTRIBUTING.md)
|
||||
|
||||
## Type of change
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Test Dependabot PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- synchronize
|
||||
- reopened
|
||||
- opened
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests / Build / Sanity Run
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
@@ -1,9 +1,9 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [released]
|
||||
# Review gh actions docs if you want to further define triggers, paths, etc
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
|
||||
|
||||
@@ -12,23 +12,38 @@ jobs:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install app dev dependencies
|
||||
run: npm ci
|
||||
- name: Generate schema files
|
||||
run: npm run schema
|
||||
|
||||
- name: Install docusaurus dependencies
|
||||
run: npm ci
|
||||
working-directory: ./docsite
|
||||
- name: Build website
|
||||
env:
|
||||
ANALYTICS: ${{ vars.ANALYTICS }}
|
||||
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
|
||||
RY_ANALYTICS: ${{ vars.RY_ANALYTICS }}
|
||||
RY_ANALYTICS_SITEID: ${{ vars.RY_ANALYTICS_SITEID }}
|
||||
RY_ANALYTICS_REPLAY: ${{ vars.RY_ANALYTICS_REPLAY }}
|
||||
GSITEVERIFICATION: ${{ vars.GSITEVERIFICATION }}
|
||||
DOCS_BASE: '/multi-scrobbler'
|
||||
run: npm run build
|
||||
working-directory: ./docsite
|
||||
|
||||
# Popular action to deploy to GitHub Pages:
|
||||
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build output to publish to the `gh-pages` branch:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Delete Untagged Packages
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * 0'
|
||||
workflow_run:
|
||||
workflows: ["Publish Docker image to Dockerhub"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
delete-untagged:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 #1.0.16
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# maybe set up for PR close later
|
||||
# https://github.com/dataaxiom/ghcr-cleanup-action?tab=readme-ov-file#delete-image-when-pull-request-is-closed
|
||||
@@ -0,0 +1,235 @@
|
||||
name: PR Workflow
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
- reopened
|
||||
- opened
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
build-snapshot:
|
||||
name: Build OCI snapshot
|
||||
needs: test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
platform: linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
# list all registries to push to and join all non-empty with comma
|
||||
# https://unix.stackexchange.com/a/693165/116849
|
||||
# https://stackoverflow.com/a/9429887/1469797
|
||||
strings=("${VARS_DOCKERHUB_SLUG}" "${VARS_GHCR_SLUG}")
|
||||
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
|
||||
joined_string=$(IFS=, ; echo "${strings[*]}")
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
|
||||
# do not use here!
|
||||
# instead we can specify repo#sha in build-push-action so we only deal with foreign SHA for actually building image
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set short git commit SHA
|
||||
run: |
|
||||
short=$(echo "${{ github.event.pull_request.head.sha }}" | head -c7)
|
||||
echo "COMMIT_SHORT_SHA=$short" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
APP_VERSION: ${{ format('pr{0}-{1}', github.event.number, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# metadata extract for docker labels/image names is done in merge job
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
|
||||
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
|
||||
# for caching
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
# specify foreign repo#sha from PR that is used to build image
|
||||
context: ${{github.event.pull_request.head.repo.clone_url}}#${{github.event.pull_request.head.sha}}
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release-snapshot:
|
||||
name: Merge OCI Images and Push
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
needs:
|
||||
- build-snapshot
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
${{ vars.GHCR_SLUG }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
flavor: |
|
||||
latest=false
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
- name: Create manifest list and push dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
|
||||
- name: Create manifest list and push gchr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
|
||||
- name: Inspect image dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Inspect image ghcr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
|
||||
|
||||
combine-and-comment:
|
||||
name: Leave comment
|
||||
runs-on: ubuntu-latest
|
||||
needs: release-snapshot
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 #2.9.4
|
||||
with:
|
||||
recreate: true
|
||||
header: "pr-release"
|
||||
message: |
|
||||
#### :package: A new release has been made for this pull request.
|
||||
|
||||
To play around with this PR, pull an image:
|
||||
* `foxxmd/multi-scrobbler:pr-${{ github.event.number }}`
|
||||
|
||||
Images are available for x86_64 and ARM64.
|
||||
|
||||
> Latest commit: ${{ github.event.pull_request.head.sha }}
|
||||
@@ -1,102 +1,245 @@
|
||||
# Based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||
|
||||
name: Publish Docker image to Dockerhub
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
# don't trigger if just updating docs
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'README.md'
|
||||
- '.github/**'
|
||||
- 'flatpak/**'
|
||||
# use release instead of tags once version is correctly parsed
|
||||
# https://github.com/docker/metadata-action/issues/422
|
||||
# https://github.com/docker/metadata-action/issues/240
|
||||
# release:
|
||||
# types: [ published ]
|
||||
|
||||
# define in GH Repository -> Actions -> Variables (or act .variables) to enable pushing to registries
|
||||
# -- will only push to registries that are defined
|
||||
# EX
|
||||
# DOCKERHUB_SLUG=foxxmd/multi-scrobbler
|
||||
# GHCR_SLUG=ghcr.io/foxxmd/multi-scrobbler
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
- name: Install dev dependencies
|
||||
run: npm ci
|
||||
- name: Build Backend
|
||||
run: 'npm run build:backend'
|
||||
- name: Test Backend
|
||||
run: npm run test
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: ./.github/workflows/testAndSanity.yml
|
||||
|
||||
push_to_registry:
|
||||
name: Build and push container images
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
name: Build OCI Images
|
||||
if: ${{ github.event_name != 'pull_request' && (vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '') }}
|
||||
needs: test
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- dockerfile: ./Dockerfile
|
||||
suffix: ''
|
||||
platforms: 'linux/amd64,linux/arm64'
|
||||
- dockerfile: ./debian.Dockerfile
|
||||
suffix: '-debian'
|
||||
# can't build arm64 due to a TLS issue when running npm install??
|
||||
# https://github.com/FoxxMD/multi-scrobbler/issues/126
|
||||
platforms: 'linux/amd64,linux/arm64'
|
||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
platform: linux/arm64
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
# list all registries to push to and join all non-empty with comma
|
||||
# https://unix.stackexchange.com/a/693165/116849
|
||||
# https://stackoverflow.com/a/9429887/1469797
|
||||
strings=("${VARS_DOCKERHUB_SLUG}" "${VARS_GHCR_SLUG}")
|
||||
for i in ${!strings[@]}; do [[ -z ${strings[i]} ]] && unset strings[i]; done
|
||||
joined_string=$(IFS=, ; echo "${strings[*]}")
|
||||
echo "REGISTRIES_JOINED=$joined_string" >> $GITHUB_ENV
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
VARS_GHCR_SLUG: ${{vars.GHCR_SLUG}}
|
||||
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set short git commit SHA
|
||||
id: appvars
|
||||
# https://dev.to/hectorleiva/github-actions-and-creating-a-short-sha-hash-8b7
|
||||
# short sha available under env.COMMIT_SHORT_SHA
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short HEAD)
|
||||
branchName=$(git rev-parse --abbrev-ref HEAD)
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
echo "COMMIT_BRANCH=$branchName" >> $GITHUB_ENV
|
||||
|
||||
- name: Get App Version
|
||||
id: appversion
|
||||
env:
|
||||
# use release instead of tags once version is correctly parsed
|
||||
#APP_VERSION: ${{ github.event.release.tag_name }}
|
||||
|
||||
# https://github.com/actions/runner/issues/409#issuecomment-752775072
|
||||
# https://stackoverflow.com/a/69919067/1469797
|
||||
APP_VERSION: ${{ contains(github.ref, 'refs/tags/') && github.ref_name || format('{0}-{1}', env.COMMIT_BRANCH, env.COMMIT_SHORT_SHA ) }}
|
||||
run: |
|
||||
echo appversion=$APP_VERSION >>${GITHUB_OUTPUT}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
# metadata extract for docker labels/image names is done in merge job
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
|
||||
# https://github.com/docker/build-push-action/issues/671#issuecomment-1619353328
|
||||
# for caching
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||
with:
|
||||
build-args: |
|
||||
APP_BUILD_VERSION=${{steps.appversion.outputs.appversion}}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.REGISTRIES_JOINED }}",push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${VARS_DIGEST}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
env:
|
||||
VARS_DIGEST: ${{ steps.build.outputs.digest }}
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #4.6.2
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Merge OCI Images and Push
|
||||
if: ${{ github.event_name != 'pull_request' && (vars.DOCKERHUB_SLUG != '' || vars.GHCR_SLUG != '') }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
needs:
|
||||
- build
|
||||
- test
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #4.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && vars.GHCR_SLUG != '' }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 #5.9.0
|
||||
with:
|
||||
images: |
|
||||
foxxmd/multi-scrobbler
|
||||
ghcr.io/foxxmd/multi-scrobbler
|
||||
${{ vars.DOCKERHUB_SLUG }}
|
||||
${{ vars.GHCR_SLUG }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
# https://github.com/docker/metadata-action/issues/247#issuecomment-1511259674 for NOT is default branch, eventually
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}},suffix=${{ matrix.suffix }}
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }},suffix=${{ matrix.suffix }}
|
||||
type=semver,pattern={{version}},suffix=${{ matrix.suffix }}
|
||||
flavor: |
|
||||
latest=false
|
||||
type=edge
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
# push with branch name as tag if not master/main
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'master') }}
|
||||
|
||||
# tag non-prelease as latest -- has a higher priority than regular tag so it shows first in registries
|
||||
type=match,pattern=\d.\d.\d$,priority=901
|
||||
|
||||
# tag all semver (include pre-release)
|
||||
type=semver,pattern={{version}}
|
||||
# flavor: |
|
||||
# latest=false
|
||||
labels: |
|
||||
org.opencontainers.image.title=Multi-Scrobbler
|
||||
org.opencontainers.image.description=Scrobble from many sources to many clients
|
||||
org.opencontainers.image.vendor=FoxxMD
|
||||
|
||||
- name: Create manifest list and push dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_DOCKERHUB_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
|
||||
- name: Create manifest list and push gchr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${VARS_GHCR_SLUG}@sha256:%s " *)
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
|
||||
- name: Inspect image dockerhub
|
||||
if: ${{ vars.DOCKERHUB_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_DOCKERHUB_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_DOCKERHUB_SLUG: ${{ vars.DOCKERHUB_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
- name: Inspect image ghcr
|
||||
if: ${{ vars.GHCR_SLUG != '' }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${VARS_GHCR_SLUG}:${STEPS_META_OUTPUTS_VERSION}
|
||||
env:
|
||||
VARS_GHCR_SLUG: ${{ vars.GHCR_SLUG }}
|
||||
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Publish Docker image to Dockerhub
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
- name: Install dev dependencies
|
||||
run: npm ci
|
||||
- name: Build Backend
|
||||
run: 'npm run build:backend'
|
||||
- name: Test Backend
|
||||
run: npm run test
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Tests and Sanity Run
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node-version:
|
||||
description: "Node version"
|
||||
required: false
|
||||
default: '20.x'
|
||||
type: string
|
||||
ref:
|
||||
description: "Ref to checkout"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests / Build / Sanity Run
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install concurrently
|
||||
run: npm install -g concurrently
|
||||
|
||||
- name: Install dev dependencies
|
||||
run: npm run install:parallel
|
||||
|
||||
- name: Test Backend
|
||||
run: npm run test
|
||||
|
||||
- name: Build
|
||||
run: npm run build:parallel
|
||||
|
||||
# remove modules that might include dev stuff
|
||||
# so that in the next step we are sure that prod-only runs work correctly
|
||||
- name: Install Prod Deps
|
||||
run: |
|
||||
rm -rf node_modules && \
|
||||
rm -rf docsite/node_modules && \
|
||||
NODE_ENV=production npm ci --omit=dev
|
||||
|
||||
# run app for 10 seconds as sanity check to see if it errors for any reason
|
||||
# easy testcase for missing packages and init errors
|
||||
- name: Sanity Run
|
||||
run: |
|
||||
set +e
|
||||
export NODE_ENV=production
|
||||
timeout --preserve-status 10s node node_modules/.bin/tsx src/backend/index.ts
|
||||
exitcode="$?"
|
||||
if [[ "$exitcode" -eq 143 ]] || [[ "$exitcode" -eq 137 ]]; then
|
||||
echo "App stayed up long enough and exited with expected status"
|
||||
exit 0
|
||||
else
|
||||
echo "App exited with unexpected code $exitcode"
|
||||
exit "$exitcode"
|
||||
fi
|
||||
@@ -118,7 +118,11 @@ dist
|
||||
.pnp.*
|
||||
|
||||
config/*.json
|
||||
config/mscache
|
||||
config/yti-*
|
||||
config/*.cache
|
||||
*.txt
|
||||
!robots.txt
|
||||
.idea/
|
||||
|
||||
src/**/**.js
|
||||
@@ -132,3 +136,6 @@ flatpak/generated-sources.json
|
||||
|
||||
build
|
||||
!setupProxy.js
|
||||
**/generated-sources.*
|
||||
.github/act/out
|
||||
tmp-*
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://lnav.org/schemas/format-v1.schema.json",
|
||||
"error_log": {
|
||||
"title": "Multi-Scrobbler Log",
|
||||
"description": "Logs from Multi Scrobbler",
|
||||
"regex": {
|
||||
"std": {
|
||||
"pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?)(?:.*?)\\] (?<level>[A-Z]*) : (?<body>.*)"
|
||||
}
|
||||
},
|
||||
"level-field": "level",
|
||||
"level": {
|
||||
"info": "^(?i)(?:info)$",
|
||||
"debug": "^(?i)(?:debug)$",
|
||||
"error": "^(?i)(error)$",
|
||||
"warning": "^(?i)(warn)$",
|
||||
"trace": "^(?i)verbose$",
|
||||
"critical": "^(?i)(?:crit|Cr)$",
|
||||
"fatal": "^(?i)(?:alert|fatal|panic|Al|Em)$"
|
||||
},
|
||||
"sample": [
|
||||
{
|
||||
"line": "[2024-12-26 01:58:48.261 -0500] DEBUG : [App] [Sources] [Jellyfin - unnamed] Last activity was at 01:58:48-05:00 | Next check in 10.00s | No new tracks discovered",
|
||||
"level": "debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"reporter": "dot",
|
||||
"extension": "ts",
|
||||
"import": "tsx/esm"
|
||||
"import": "tsx/esm",
|
||||
"spec": "./src/backend/tests/**/*.test.ts",
|
||||
"file": [
|
||||
"./src/backend/tests/setup.ts"
|
||||
],
|
||||
"exit": true
|
||||
}
|
||||
|
||||
Vendored
+123
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/index.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "dev (ENV Only)",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/index.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"CONFIG_DIR": "./not-real"
|
||||
},
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "tsx",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug current file in VSCode
|
||||
"program": "${file}",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "schema",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
// Debug app in VSCode
|
||||
"program": "${workspaceFolder}/src/backend/utils/SchemaStaticUtil.ts",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
//"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/src/**/*",
|
||||
"${workspaceFolder}/node_modules/**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"-u",
|
||||
"bdd",
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--reporter",
|
||||
"spec",
|
||||
"--colors",
|
||||
"--config",
|
||||
"${workspaceRoot}/.mocharc.json",
|
||||
"--recursive",
|
||||
"${workspaceFolder}/src/backend/tests/**/*.test.ts"
|
||||
],
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"name": "Mocha Tests",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"-u",
|
||||
"bdd",
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--reporter",
|
||||
"spec",
|
||||
"--colors",
|
||||
"--config", "${workspaceRoot}/.mocharc.json",
|
||||
"${file}"
|
||||
],
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"name": "Mocha Test on File",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
// Ignore all dependencies (optional)
|
||||
"${workspaceFolder}/node_modules/**",
|
||||
],
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mochaExplorer.require": "tsx/esm",
|
||||
"mochaExplorer.timeout": 1200000,
|
||||
"mochaExplorer.exit": true,
|
||||
"search.exclude": {
|
||||
"**/.docusaurus": true,
|
||||
"**/build": true
|
||||
},
|
||||
"tailwindCSS.files.exclude": [
|
||||
"docsite/**"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": "src/client/index.css"
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "src/backend/tsconfig.json",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "tsc: build - src/backend/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "compose-up-dev",
|
||||
"command": "docker-compose -f docker-compose.dev.yml up",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "compose-build-dev",
|
||||
"command": "docker-compose -f docker-compose.dev.yml build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-2
@@ -2,7 +2,6 @@
|
||||
|
||||
Please follow these guidelines when contributing code to this repository:
|
||||
|
||||
* The PR **must be for the [`develop` branch.](https://github.com/FoxxMD/multi-scrobbler/tree/develop)** The `master` branch is for releases only.
|
||||
* Use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format when creating commits.
|
||||
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `develop`.
|
||||
* Preferably, please use a [feature branch](https://stackoverflow.com/a/39586780/1469797) instead of committing directly to `master`.
|
||||
* Ensure that if your code is covered by [an existing test](./src/backend/tests) that you have updated the test accordingly
|
||||
|
||||
+75
-38
@@ -1,78 +1,115 @@
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
ENV NODE_VERSION=20.18.1
|
||||
|
||||
# borrowing openssl header removal trick from offical docker-node
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
|
||||
RUN \
|
||||
ARCH= OPENSSL_ARCH= && dpkgArch="$(dpkg --print-architecture)" \
|
||||
&& case "${dpkgArch##*-}" in \
|
||||
amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \
|
||||
ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \
|
||||
s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \
|
||||
arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \
|
||||
armhf) ARCH='armv7l' OPENSSL_ARCH='linux-armv4';; \
|
||||
i386) ARCH='x86' OPENSSL_ARCH='linux-elf';; \
|
||||
*) echo "unsupported architecture"; exit 1 ;; \
|
||||
esac && \
|
||||
set -ex && \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
nodejs \
|
||||
npm \
|
||||
&& \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
#ca-certificates \
|
||||
xz-utils \
|
||||
avahi-utils \
|
||||
curl && \
|
||||
echo "**** Fetch and install node****" && \
|
||||
# get node/npm directly from nodejs dist \
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L41
|
||||
curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
|
||||
tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr --strip-components=1 --no-same-owner && \
|
||||
rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
|
||||
ln -s /usr/bin/node /usr/bin/nodejs && \
|
||||
npm update -g npm && \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/tmp/*
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L49
|
||||
# Remove unused OpenSSL headers to save ~34MB
|
||||
# (does not affect arm64 issue below)
|
||||
find /usr/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; && \
|
||||
apt-get purge --auto-remove -y perl xz-utils && \
|
||||
apt-get autoclean && \
|
||||
apt-get autoremove && \
|
||||
rm -rf \
|
||||
/config/.cache \
|
||||
/root/cache \
|
||||
/var/lib/apt/lists/* \
|
||||
/var/tmp/* \
|
||||
/tmp/*
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN npm install -g concurrently
|
||||
|
||||
ARG data_dir=/config
|
||||
VOLUME $data_dir
|
||||
ENV CONFIG_DIR=$data_dir
|
||||
|
||||
COPY docker/root/ /
|
||||
COPY docker/root /
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as build
|
||||
FROM base AS build
|
||||
|
||||
# would very much like to use YARN for dependency management but its got show-stopping bad design for prod dependencies
|
||||
# https://github.com/yarnpkg/yarn/issues/6323 -- always downloads devDependencies -- and im not about to migrate to v2 since alpine doesn't support it yet
|
||||
|
||||
# copy dep/TS config and install dev dependencies
|
||||
#COPY --chown=abc:abc package.json tsconfig.json yarn.lock ./
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --chown=abc:abc docsite/package*.json tsconfig.json ./docsite/
|
||||
|
||||
RUN npm ci \
|
||||
&& chown -R root:root node_modules
|
||||
#RUN yarn install
|
||||
# for debugging, so the build fails faster when timing out (arm64)
|
||||
#RUN npm config set fetch-retries 1 && \
|
||||
# npm config set fetch-retry-mintimeout 5000 && \
|
||||
# npm config set fetch-retry-maxtimeout 5000
|
||||
|
||||
# https://www.npmjs.com/package/tls-test
|
||||
# used to test that the OS supports downloading packages over HTTPS with TLS 1.2 enforced
|
||||
# -- this always succeeds but a good sanity check
|
||||
#RUN npm install -g https://tls-test.npmjs.com/tls-test-1.0.0.tgz
|
||||
|
||||
# This FAILED for node < 20 when building arm64 but not amd64 (and alpine-based Dockerfile has no issues building arm64)
|
||||
# see https://github.com/FoxxMD/multi-scrobbler/issues/126
|
||||
RUN npm run install:parallel \
|
||||
&& chown -R root:root node_modules \
|
||||
&& chown -R root:root docsite/node_modules
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
|
||||
# need to set before build so server/client build is optimized and has constants (if needed)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run build && rm -rf node_modules
|
||||
#RUN yarn run build && rm -rf node_modules
|
||||
RUN npm run build:parallel && rm -rf node_modules && rm -rf docsite/node_modules
|
||||
|
||||
FROM base as app
|
||||
FROM base AS app
|
||||
|
||||
#COPY --chown=abc:abc package.json yarn.lock ./
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
COPY --chown=abc:abc *.json *.js *.ts index.html ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
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
|
||||
#
|
||||
#RUN yarn global add patch-package \
|
||||
# && yarn install --production=true \
|
||||
# && yarn global remove patch-package \
|
||||
# && yarn cache clean --mirror \
|
||||
# && chown abc:abc node_modules \
|
||||
# && rm -rf node_modules/ts-node \
|
||||
# && rm -rf node_modules/typescript
|
||||
|
||||
RUN npm ci --omit=dev \
|
||||
# https://stackoverflow.com/a/63640896/1469797
|
||||
ARG APP_BUILD_VERSION
|
||||
ENV APP_VERSION=$APP_BUILD_VERSION
|
||||
|
||||
RUN npm ci --omit=dev --no-audit \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules \
|
||||
&& rm -rf node_modules/@types
|
||||
&& rm -rf /root/.cache
|
||||
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
|
||||
@@ -3,38 +3,61 @@
|
||||
[](https://github.com/FoxxMD/multi-scrobbler/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://hub.docker.com/r/foxxmd/multi-scrobbler)
|
||||
[](https://foxxmd.github.io/multi-scrobbler/)
|
||||
|
||||
|
||||
<img src="/assets/icon.png" align="right"
|
||||
alt="multi-scrobbler logo" width="180" height="180">
|
||||
|
||||
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
|
||||
A javascript app to scrobble music you listen to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), [ListenBrainz](https://listenbrainz.org), and other services.
|
||||
|
||||
* Supports scrobbling from many **Sources**
|
||||
* [Spotify](/docsite/docs/configuration/configuration.md#spotify)
|
||||
* [Plex](/docsite/docs/configuration/configuration.md#plex) or [Tautulli](/docsite/docs/configuration/configuration.md#tautulli)
|
||||
* [Subsonic-compatible APIs](/docsite/docs/configuration/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
|
||||
* [WebScrobbler](/docsite/docs/configuration/configuration.md#webscrobbler)
|
||||
* [Youtube Music](/docsite/docs/configuration/configuration.md#youtube-music)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm-source)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz--source-)
|
||||
* [Deezer](/docsite/docs/configuration/configuration.md#deezer)
|
||||
* [MPRIS (Linux Desktop)](/docsite/docs/configuration/configuration.md#mpris)
|
||||
* [Mopidy](/docsite/docs/configuration/configuration.md#mopidy)
|
||||
* [JRiver](/docsite/docs/configuration/configuration.md#jriver)
|
||||
* [Kodi](/docsite/docs/configuration/configuration.md#kodi)
|
||||
* [Google Cast (Chromecast)](/docsite/docs/configuration/configuration.md#google-cast--chromecast-)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](/docsite/docs/configuration/configuration.md#maloja)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
|
||||
* Supports scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
|
||||
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/azuracast)
|
||||
* [Deezer](https://foxxmd.github.io/multi-scrobbler/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/google-cast)
|
||||
* [Icecast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/icecast)
|
||||
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jellyfin)
|
||||
* [JRiver](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jriver)
|
||||
* [Kodi](https://foxxmd.github.io/multi-scrobbler/configuration/sources/kodi)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/sources/koito-source)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-endpoint)
|
||||
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-endpoint)
|
||||
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/sources/maloja)
|
||||
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mopidy)
|
||||
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpris)
|
||||
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/configuration/sources/musikcube)
|
||||
* [Plex](https://foxxmd.github.io/multi-scrobbler/configuration/sources/plex)
|
||||
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/sources/rocksky-source)
|
||||
* [Spotify](https://foxxmd.github.io/multi-scrobbler/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/configuration/sources/webscrobbler)
|
||||
* [VLC](https://foxxmd.github.io/multi-scrobbler/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/yamaha-musiccast)
|
||||
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/clients/koito)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/lastfm)
|
||||
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/clients/listenbrainz)
|
||||
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/clients/maloja)
|
||||
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/clients/rocksky)
|
||||
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/tealfm)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/configuration#webhook-configurations) or [healthcheck endpoint](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint)
|
||||
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/configuration/clients#now-playing) for scrobble Clients
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Graceful network and client failure handling (queued scrobbles that auto-retry)
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through ENVs or JSON
|
||||
* Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
|
||||
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
|
||||
|
||||
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
|
||||
|
||||
<img src="/assets/status-ui.png" width="800">
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
@@ -45,17 +68,15 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
<img src="/assets/status-ui.png" width="800">
|
||||
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
|
||||
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
|
||||
### Source
|
||||
|
||||
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
|
||||
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, like a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
@@ -65,20 +86,24 @@ Source configurations consist of:
|
||||
|
||||
### Client
|
||||
|
||||
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
* A friendly name.
|
||||
* Any data needed to communicate or authenticate with the Client.
|
||||
|
||||
## Quick Start
|
||||
|
||||
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
|
||||
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
|
||||
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -93,7 +118,11 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
|
||||
Having issues with connections or configuration? Check the [FAQ](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
|
||||
|
||||
## Development
|
||||
|
||||
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
|
||||
RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
avahi \
|
||||
avahi-tools \
|
||||
nodejs \
|
||||
npm \
|
||||
&& \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/tmp/*
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ARG data_dir=/config
|
||||
VOLUME $data_dir
|
||||
ENV CONFIG_DIR=$data_dir
|
||||
|
||||
COPY docker/root/ /
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as build
|
||||
|
||||
# copy dep/TS config and install dev dependencies
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
|
||||
RUN npm ci \
|
||||
&& chown -R root:root node_modules
|
||||
#RUN yarn install
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
|
||||
# need to set before build so server/client build is optimized and has constants (if needed)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run docs:install && npm run build && rm -rf node_modules && rm -rf docsite/node_modules
|
||||
|
||||
FROM base as app
|
||||
|
||||
#COPY --chown=abc:abc package.json yarn.lock ./
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
COPY --from=build --chown=abc:abc /app/docsite /app/docsite
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV IS_DOCKER=true
|
||||
ENV COLORED_STD=true
|
||||
|
||||
# https://stackoverflow.com/a/63640896/1469797
|
||||
ARG APP_BUILD_VERSION
|
||||
ENV APP_VERSION=$APP_BUILD_VERSION
|
||||
|
||||
RUN npm ci --omit=dev \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules \
|
||||
&& rm -rf node_modules/@types
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
EXPOSE $PORT
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 527 KiB After Width: | Height: | Size: 699 KiB |
@@ -1,217 +0,0 @@
|
||||
import {API, Collection, FileInfo, Options} from 'jscodeshift';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
export type TransformFrom = 'none' | 'any' | string;
|
||||
export type TransformTo = 'none' | 'any' | string;
|
||||
export type ImportTypes = 'any' | 'relative' | 'alias';
|
||||
|
||||
const base = process.cwd();
|
||||
export default function transformer(
|
||||
file: FileInfo,
|
||||
{ jscodeshift: j }: API,
|
||||
options: Options,
|
||||
) {
|
||||
|
||||
const tf = options.transformFrom as string | undefined;
|
||||
if(tf === undefined) {
|
||||
throw new Error(`arg '--transformFrom' must be defined.`);
|
||||
}
|
||||
const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[];
|
||||
const tfIsAny = transformFrom.includes('any');
|
||||
|
||||
const tt = options.transformTo as string | undefined;
|
||||
if(tt === undefined) {
|
||||
throw new Error(`arg '--transformTo' must be defined.`);
|
||||
}
|
||||
const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[];
|
||||
|
||||
if(transformTo.length > 1 && transformFrom.length !== transformTo.length) {
|
||||
throw new Error('When more than one Transform To is specified then number of Transform From arguments must match');
|
||||
}
|
||||
|
||||
const itypes = options.importTypes as string | undefined;
|
||||
if(itypes === undefined) {
|
||||
throw new Error(`arg '--importTypes' must be defined.`);
|
||||
}
|
||||
const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[];
|
||||
const importsIsAny = importTypes.includes('any');
|
||||
|
||||
const fileDir = path.dirname(path.join(base, file.path));
|
||||
const processingFilePath = file.path;
|
||||
console.log(`Processing => ${processingFilePath}`);
|
||||
let source: Collection<any>;
|
||||
try {
|
||||
source = j(file.source);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse ${processingFilePath}, will skip`);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const imports = source.find(j.ImportDeclaration)
|
||||
imports.forEach((x) => {
|
||||
const importPath = x.value.source.value as string;
|
||||
const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`;
|
||||
|
||||
const pathInfo = path.parse(importPath);
|
||||
const hasNoExt = pathInfo.ext === '';
|
||||
let filenameFromDir: string;
|
||||
if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) {
|
||||
const pathFull = path.join(fileDir, importPath);
|
||||
const dir = path.dirname(pathFull);
|
||||
const normalExt = pathInfo.ext.replace('.', '');
|
||||
let isRelative: boolean | undefined;
|
||||
let dirExists: boolean | undefined;
|
||||
let fileExists: boolean | undefined;
|
||||
try {
|
||||
// is dir path real?
|
||||
fs.realpathSync(dir);
|
||||
dirExists = true;
|
||||
} catch (e) {
|
||||
isRelative = false;
|
||||
dirExists = false;
|
||||
}
|
||||
|
||||
if(isRelative === undefined) {
|
||||
// if extension is none we need to check for any file in dir with this name
|
||||
if(hasNoExt) {
|
||||
const dirFiles = fs.readdirSync(dir)
|
||||
filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name);
|
||||
if(filenameFromDir === undefined) {
|
||||
fileExists = false;
|
||||
isRelative = false;
|
||||
} else {
|
||||
fileExists = true;
|
||||
isRelative = true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// does a file exist?
|
||||
fs.realpathSync(pathFull);
|
||||
isRelative = true;
|
||||
} catch (e) {
|
||||
isRelative = false;
|
||||
fileExists = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!importsIsAny) {
|
||||
if(!isRelative && !importTypes.includes('alias')) {
|
||||
console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`);
|
||||
return;
|
||||
} else if(isRelative && !importTypes.includes('relative')) {
|
||||
console.log(`${pathFull} => Import is relative but import types does not specify relative`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// determine transformTo
|
||||
|
||||
// if there is only one TO then use it
|
||||
let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined;
|
||||
if(derivedTT === undefined) {
|
||||
// otherwise we find the TO by using the same index as the matching FROM extension type
|
||||
const tfIndex = transformFrom.findIndex((x) => {
|
||||
if(x === 'any') {
|
||||
return true;
|
||||
}
|
||||
if(x === 'none' && hasNoExt) {
|
||||
return true;
|
||||
}
|
||||
if(x === normalExt) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(tfIndex === -1) {
|
||||
console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`);
|
||||
return;
|
||||
}
|
||||
derivedTT = transformTo[tfIndex];
|
||||
}
|
||||
|
||||
let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`;
|
||||
|
||||
let transformedImport: string;
|
||||
|
||||
switch(derivedTT) {
|
||||
case 'none':
|
||||
if(hasNoExt) {
|
||||
console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`);
|
||||
return;
|
||||
}
|
||||
transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name);
|
||||
break;
|
||||
case 'any':
|
||||
if(hasNoExt && filenameFromDir) {
|
||||
transformedImport = filenameFromDir;
|
||||
} else {
|
||||
const otherFilename = fs.readdirSync(dir).find(x => {
|
||||
const pinfo = path.parse(x);
|
||||
return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext;
|
||||
});
|
||||
if(otherFilename === undefined) {
|
||||
console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`);
|
||||
return;
|
||||
}
|
||||
transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename);
|
||||
transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})`
|
||||
}
|
||||
break;
|
||||
default:
|
||||
transformedImport = combineDirFile(pathInfo.dir, `${path.parse(filenameFromDir).name}.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`);
|
||||
j(x).replaceWith(
|
||||
j.importDeclaration(
|
||||
x.node.specifiers,
|
||||
j.stringLiteral(transformedImport)
|
||||
)
|
||||
);
|
||||
|
||||
} else {
|
||||
console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Early exit condition
|
||||
* -----
|
||||
* It is often good practice to exit early and return the original source file
|
||||
* if it does not contain code relevant to the codemod.
|
||||
* See this page for more information:
|
||||
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output
|
||||
*/
|
||||
// if (/* Some condition here */ true) {
|
||||
// return file.source;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Codemod logic goes here 👇
|
||||
* -----
|
||||
* This is where the core logic for your codemod will go,
|
||||
* consider grouping specific actions into 'motions' and running them in sequence
|
||||
*
|
||||
* See this page for more information:
|
||||
* https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions
|
||||
*/
|
||||
//source.findVariableDeclarators('foo').renameTo('bar');
|
||||
|
||||
/**
|
||||
* Return your modified AST here 👇
|
||||
* -----
|
||||
* This is where your modified AST will be transformed back into a string
|
||||
* and written back to the file.
|
||||
*/
|
||||
return source.toSource(options.printOptions);
|
||||
}
|
||||
|
||||
const combineDirFile = (dir: string, file: string) => {
|
||||
if(dir === '') {
|
||||
return file;
|
||||
}
|
||||
return `${dir}${path.sep}${file}`;
|
||||
}
|
||||
+1
-1
@@ -8,5 +8,5 @@ These are **NOT** exhaustive examples. You should consult the **configuration**
|
||||
|
||||
Documentation at
|
||||
|
||||
* [internal docs](../docsite/docs/configuration/configuration.md)
|
||||
* [internal docs](../docsite/docs/configuration/configuration.mdx)
|
||||
* External Link: https://foxxmd.github.io/multi-scrobbler/docs/configuration
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"type": "azuracast",
|
||||
"enable": true,
|
||||
"name": "azura",
|
||||
"data": {
|
||||
"url": "ws://192.168.0.101",
|
||||
"station": "my-station-name",
|
||||
"monitorWhenLive": true,
|
||||
"monitorWhenListeners": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2,18 +2,16 @@
|
||||
"debugMode": false,
|
||||
"disableWeb": false,
|
||||
"sourceDefaults": {
|
||||
"maxPollRetries": 1,
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5,
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn",
|
||||
"logPlayerState": false,
|
||||
"scrobbleThresholds": {
|
||||
"duration": 30,
|
||||
"percent": 50
|
||||
},
|
||||
"options": {
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn",
|
||||
"logPlayerState": false
|
||||
}
|
||||
"maxPollRetries": 1,
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5
|
||||
},
|
||||
"clientDefaults": {
|
||||
"maxRequestRetries": 1,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"name": "DeezerOfficalDeprecated",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/deezer/callback",
|
||||
"interval": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,13 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "FoxxMDeezer",
|
||||
"name": "DeezerARL",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/deezer/callback",
|
||||
"interval": 60
|
||||
"arl": "UOsRPjT3U5Dhaaup3xQ30DWFoSgqugdSYzL3QE743waaVy2eur2ckyJcZ4RV123Q3PsbKXkLwA6ahc9XmEfev6BtfSP8LujE3JswWRQogZCDUOwVzWO5sAig",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"name": "myLfm",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"slug": "mySlug"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"name": "myLz",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "myIcy",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://myIcecast.com/radio"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,12 +4,23 @@
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"users": ["FoxxMD"],
|
||||
"servers": ["myServer","anotherServer"]
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
|
||||
// everything below is optional
|
||||
"usersAllow": ["FoxxMD","SomeOtherUser"],
|
||||
"usersBlock": ["AnotherUser"],
|
||||
"devicesAllow": ["firefox"],
|
||||
"devicesBlock": ["google-home"],
|
||||
"librariesAllow": ["GoodMusic"],
|
||||
"librariesBlock": ["BadMusic"],
|
||||
"additionalAllowedLibraryTypes": ["musicvideos"],
|
||||
"allowUnknown": false,
|
||||
},
|
||||
"options": {
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn"
|
||||
"logPayload": true,
|
||||
"logFilterFailure": "debug"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "koito-client",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "koito-source",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "admin",
|
||||
"url": "http://192.168.0.100:4110"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name": "myLastFm",
|
||||
"name": "myLastFmClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
@@ -8,5 +8,15 @@
|
||||
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myLastFmSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"apiKey": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"secret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/lastfm/callback"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "brainz",
|
||||
"name": "brainzClient",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "FoxxMD"
|
||||
"username": "FoxxMD",
|
||||
"url": "https://api.listenbrainz.org"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "brainzSource",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "FoxxMD",
|
||||
"url": "https://api.listenbrainz.org"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
[
|
||||
{
|
||||
"name": "myMaloja",
|
||||
"name": "myMaloja-client",
|
||||
"enable": true,
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myMaloja-source",
|
||||
"enable": true,
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MyMPD",
|
||||
"data": {
|
||||
"url": "192.168.0.100:6600",
|
||||
"password": "MY_PASSWORD"
|
||||
},
|
||||
"options": {
|
||||
"disableDiscovery": false
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"name": "myYamaha",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "192.168.0.101"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"type": "musikcube",
|
||||
"enable": true,
|
||||
"name": "musikcube",
|
||||
"data": {
|
||||
"url": "ws://localhost:7905",
|
||||
"password": "MY_PASSWORD"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,15 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"name": "MyPlexApi",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
"token": "1234",
|
||||
"url": "http://192.168.0.120:32400",
|
||||
"usersAllow": ["FoxxMD","SomeOtherUser"],
|
||||
"usersBlock": ["AnotherUser"],
|
||||
"devicesAllow": ["firefox"],
|
||||
"devicesBlock": ["google-home"],
|
||||
"librariesAllow": ["GoodMusic"],
|
||||
"librariesBlock": ["BadMusic"]
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true,
|
||||
"logFilterFailure": "debug",
|
||||
"ignoreInvalidCert": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
// DEPRECATED, use API Source instead
|
||||
// rename files to plex.json to use
|
||||
{
|
||||
"name": "MyPlex",
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "myRockskyClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"key": "7cdr86vis3mpq5b1pi01md0hkm7ykzzxjy81r",
|
||||
"handle": "alice.bsky.social"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myRockskySource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"key": "e4xwktm7jxm607x8el1fcda30eu14dzb64h3j",
|
||||
"handle": "alice.bsky.social"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -5,7 +5,8 @@
|
||||
"data": {
|
||||
"url": "http://localhost:4040/airsonic",
|
||||
"user": "yourUser",
|
||||
"password": "yourPassword"
|
||||
"password": "yourPassword",
|
||||
"usersAllow": ["yourUser"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "myTeal",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "myTealSource",
|
||||
"configureAs": "source",
|
||||
"data": {
|
||||
"identifier": "alice.bsky.social",
|
||||
"appPassword": "twog-phu7-4dhe-y4j3"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"enable": true,
|
||||
"name": "MyVlc",
|
||||
"data": {
|
||||
"url": "localhost:8080",
|
||||
"password": "changeme"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "MyWebScrobbler",
|
||||
"data": {
|
||||
"slug": null,
|
||||
"slug": "MyOptionalSlug",
|
||||
"whitelist": [],
|
||||
"blacklist": []
|
||||
}
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
"enable": true,
|
||||
"clients": [],
|
||||
"data": {
|
||||
"cookie": "VISITOR_INFO1_LIVE=jMDXz2_L8rY; __Secure-3PAPISID=3AxsXpSXGqOInSDn1jEKn; DEVICE_INFO=ChxOekU0TmTBpjek5EWZ0G; YSC=7gZdl3Zdl3; SID=TwhNsaZRXYTAtXxzGyu6rZdpg2HvGROeW8J4Ym_FhkhoZMUYEQ.; __Secure-1PSID=TwhNOsaZRXYTyRBe4rxAtXRIKsIEtk_Qot2VLBNfHQrQ.; __Secure-3PSID=ZRXYTAtXRIKsIEtk_Qot2yRBerZdpg2HvvZRXYTAtXRIKsIEtk_Qot2yRBerkuZICFQ.; HSID=A1UMmELW79; SSID=AKhomOs; APISID=IlHHmuzkPdQzZZDhHn3; SAPISID=3AxsXpy0u75Qb/n1jEKn; __Secure-1PAPISID=3AxsXpQb/AkSDn1jEKn; LOGIN_INFO=AFmP6vFpyVCZZAIgDwbkhWMBBhluaIWAPP:QUQ314UW5NWMjNmd2ZUJnYnJsakdIMjZoaE5zVVMjNmd2ZZUiHRlb3ZlV3ZIcUVyRVIMjNmdjNmd2ZZUivYlNqX2ZNZUiHdUNFNFdaYmJIW1NkJRX3hqdlU2YnFESkFuSS1uTldnZVRmLXNjWFc5OUJuR3dTd3JsZGZYa2EtZFQ2a0k2Ry1KQQ==; PREF=volume=26; SIDCC=AFvI_94PxXwls-ndqpGfPgFX3FWj80y_94PxXwls-ndqfSh15sP; __Secure-1PSIDCC=AFvIBnUbRr96I96UCIp2U4T8HRVk2B0HfKzhzxwsiP; __Secure-3PSIDCC=AFvIB3bINuUN0ETDR9gO91wpwWIVmpGki3BxT3bINuUN0ETDR9gO91wCH",
|
||||
"authUser": "0"
|
||||
"cookie": "__Secure-1PSIDTS=sidts-CjEB3EgAEvCd-......",
|
||||
// either cookie or id/secret needs to be provided
|
||||
"clientId": "891098404....apps.googleusercontent.com",
|
||||
"clientSecret": "GOCS..."
|
||||
|
||||
// optional
|
||||
//"redirectUri": "http://my.custom.tld/api/ytmusic/callback?name=MyYTMusic"
|
||||
},
|
||||
"options": {
|
||||
"logDiff": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
#FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy as base
|
||||
FROM ghcr.io/linuxserver/baseimage-debian:bookworm as base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
ENV NODE_VERSION 20.11.1
|
||||
|
||||
# borrowing openssl header removal trick from offical docker-node
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
|
||||
RUN \
|
||||
ARCH= OPENSSL_ARCH= && dpkgArch="$(dpkg --print-architecture)" \
|
||||
&& case "${dpkgArch##*-}" in \
|
||||
amd64) ARCH='x64' OPENSSL_ARCH='linux-x86_64';; \
|
||||
ppc64el) ARCH='ppc64le' OPENSSL_ARCH='linux-ppc64le';; \
|
||||
s390x) ARCH='s390x' OPENSSL_ARCH='linux*-s390x';; \
|
||||
arm64) ARCH='arm64' OPENSSL_ARCH='linux-aarch64';; \
|
||||
armhf) ARCH='armv7l' OPENSSL_ARCH='linux-armv4';; \
|
||||
i386) ARCH='x86' OPENSSL_ARCH='linux-elf';; \
|
||||
*) echo "unsupported architecture"; exit 1 ;; \
|
||||
esac && \
|
||||
set -ex && \
|
||||
echo "**** install build packages ****" && \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
#ca-certificates \
|
||||
xz-utils \
|
||||
curl && \
|
||||
echo "**** Fetch and install node****" && \
|
||||
# get node/npm directly from nodejs dist \
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L41
|
||||
curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
|
||||
tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr --strip-components=1 --no-same-owner && \
|
||||
rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" && \
|
||||
ln -s /usr/bin/node /usr/bin/nodejs && \
|
||||
# curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
# apt-get install --no-install-recommends -y nodejs && \
|
||||
npm update -g npm && \
|
||||
echo "**** cleanup ****" && \
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L49
|
||||
# Remove unused OpenSSL headers to save ~34MB
|
||||
# (does not affect arm64 issue below)
|
||||
find /usr/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; && \
|
||||
apt-get purge --auto-remove -y perl xz-utils && \
|
||||
apt-get autoclean && \
|
||||
apt-get autoremove && \
|
||||
rm -rf \
|
||||
/config/.cache \
|
||||
/root/cache \
|
||||
/var/lib/apt/lists/* \
|
||||
/var/tmp/* \
|
||||
/tmp/*
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ARG data_dir=/config
|
||||
VOLUME $data_dir
|
||||
ENV CONFIG_DIR=$data_dir
|
||||
|
||||
COPY docker/root /
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as build
|
||||
|
||||
COPY --chown=abc:abc package*.json tsconfig.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
|
||||
# for debugging, so the build fails faster when timing out (arm64)
|
||||
#RUN npm config set fetch-retries 1 && \
|
||||
# npm config set fetch-retry-mintimeout 5000 && \
|
||||
# npm config set fetch-retry-maxtimeout 5000
|
||||
|
||||
# https://www.npmjs.com/package/tls-test
|
||||
# used to test that the OS supports downloading packages over HTTPS with TLS 1.2 enforced
|
||||
# -- this always succeeds but a good sanity check
|
||||
#RUN npm install -g https://tls-test.npmjs.com/tls-test-1.0.0.tgz
|
||||
|
||||
# This FAILS when building arm64 but not amd64 (and alpine-based Dockerfile has no issues building arm64)
|
||||
# see https://github.com/FoxxMD/multi-scrobbler/issues/126
|
||||
RUN npm ci \
|
||||
--verbose \
|
||||
# --no-audit \
|
||||
&& chown -R root:root node_modules
|
||||
# running with --no-audit does not have any affect even though the error only logs with prefix 'npm verb audit error'
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
|
||||
# need to set before build so server/client build is optimized and has constants (if needed)
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run build && rm -rf node_modules
|
||||
|
||||
FROM base as app
|
||||
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
COPY --chown=abc:abc patches ./patches
|
||||
COPY --from=build --chown=abc:abc /app/dist /app/dist
|
||||
COPY --from=build --chown=abc:abc /app/src /app/src
|
||||
COPY --from=base /usr/bin /usr/bin
|
||||
COPY --from=base /usr/lib /usr/lib
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV IS_DOCKER=true
|
||||
|
||||
RUN npm ci --omit=dev \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules \
|
||||
&& rm -rf node_modules/@types \
|
||||
/root/.cache
|
||||
|
||||
ARG webPort=9078
|
||||
ENV PORT=$webPort
|
||||
EXPOSE $PORT
|
||||
@@ -0,0 +1,29 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
multi-scrobbler:
|
||||
build:
|
||||
context: .
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
#environment:
|
||||
#- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- SPOTIFY_CLIENT_ID=
|
||||
#- SPOTIFY_CLIENT_SECRET=
|
||||
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
|
||||
#- MALOJA_URL=http://maloja:42010
|
||||
#- MALOJA_API_KEY=
|
||||
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
#- PGID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
|
||||
# set if using a source/client with redirect URI that you have not explicitly set and MS is NOT running on the same machine that you will view the dashboard from
|
||||
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
|
||||
#- BASE_URL=http://MyHostIP:9078
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
#networks:
|
||||
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
|
||||
ports:
|
||||
- 9078:9078 # first port is the HOST port multi-scrobbler will serve UI and API on
|
||||
restart: unless-stopped
|
||||
+4
-6
@@ -1,14 +1,12 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- SPOTIFY_CLIENT_ID=
|
||||
#- SPOTIFY_CLIENT_SECRET=
|
||||
#- SPOTIFY_REDIRECT_URI=http://multi-scrobbler-host-IP:9078/callback ## Need to be whitelisted in Spotify dashboard and is used for creating the connection first time
|
||||
#- JELLYFIN_APIKEY=
|
||||
#- JELLYFIN_URL=
|
||||
#- JELLYFIN_USER=
|
||||
#- MALOJA_URL=http://maloja:42010
|
||||
#- MALOJA_API_KEY=
|
||||
#- PUID=1000 # required if running docker on linux host, see main README Docker setup instructions
|
||||
@@ -18,7 +16,7 @@ services:
|
||||
# EX: You will view MS dashboard at 'http://192.168.0.101:9078' -> set BASE_URL=http://192.168.0.101:9078
|
||||
#- BASE_URL=http://MyHostIP:9078
|
||||
volumes:
|
||||
- /path_on_host/multi-scrobbler-config:/config
|
||||
- "./config:/config"
|
||||
#networks:
|
||||
# - (optional to add container to the same bridge network that maloja is inside to be able to use docker internal networking & dns to resolve and connect to maloja URL via http://maloja:port)
|
||||
ports:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
+140
-113
@@ -1,124 +1,53 @@
|
||||
<!-- TOC -->
|
||||
* [Connection Issues](#connection-issues)
|
||||
* [Plex/Tautulli/Jellyfin don't connect](#plextautullijellyfin-dont-connect)
|
||||
* [Jellyfin has warnings about undefined or missing data](#jellyfin-has-warnings-about-undefined-or-missing-data)
|
||||
* [Jellyfin has warnings about missing headers](#jellyfin-has-warnings-about-missing-headers)
|
||||
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
|
||||
* [Sporadic network problems on docker](#sporadic-network-problems-on-docker)
|
||||
* [Configuration Issues](#configuration-issues)
|
||||
* [Config could not be parsed](#config-could-not-be-parsed)
|
||||
* [Scrobbling Issues](#scrobbling-issues)
|
||||
* [Last.fm does not scrobble tracks with multiple artists correctly](#lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
* [Jellyfin does not scrobble tracks with multiple artists correctly](#jellyfin-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
* [Google Cast track information is missing/incorrect or MS player has weird times](#google-cast-track-information-is-missingincorrect-or-ms-player-has-weird-times)
|
||||
* [Google Cast device does not track media](#google-cast-device-does-not-track-media)
|
||||
<!-- TOC -->
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
# Connection Issues
|
||||
## Connection Issues
|
||||
|
||||
## Plex/Tautulli/Jellyfin don't connect
|
||||
### Webscrobbler/Listenbrainz Endpoint doesn't connect {#ingress-connection}
|
||||
|
||||
These three [sources](/#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
|
||||
These sources are **ingress-based** which means that multi-scrobbler waits for the Webscrobbler extension or app using Listenbrainz Endpoint to contact multi-scrobbler, as opposed to multi-scrobbler contacting them.
|
||||
|
||||
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
|
||||
multi-scrobbler will log information about any server that connects to it for these services. In the logs it looks something like this:
|
||||
|
||||
```
|
||||
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] Received request from a new remote address: ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59)
|
||||
2023-02-22T10:55:56-05:00 info : [Ingress - Plex ] ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) Received valid data from server examplePlex for the first time.
|
||||
2023-02-22T10:55:56-05:00 warn : [Plex Request ] Received valid Plex webhook payload but no Plex sources are configured
|
||||
[2025-07-10 12:32:21.417 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] Received request from a new remote address: ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0)
|
||||
[2025-07-10 12:32:21.418 -0400] INFO : [App] [API] [Ingress] [WebScrobbler] [Ingress] [WebScrobbler] ::ffff:127.0.0.1 (192.168.0.100) (UA: Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0) Received data for API URL slug '(none)' for the first time.
|
||||
```
|
||||
It also logs if a server tries to connect to a URL that it does not recognize:
|
||||
```
|
||||
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
|
||||
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.100 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
|
||||
```
|
||||
**So, if you do not see either of these in your logs then Plex/Tautulli/Jellyfin is not able to connect to your multi-scrobbler instance at all.**
|
||||
**So, if you do not see either of these in your logs then the Source is not able to connect to your multi-scrobbler instance at all.**
|
||||
|
||||
This is not something multi-scrobbler can fix and means you have an issue in your network.
|
||||
|
||||
### Troubleshooting
|
||||
#### Troubleshooting
|
||||
|
||||
Check or try all these steps before submitting an issue:
|
||||
|
||||
#### Turn on Debug Logging
|
||||
##### Turn on Debug Logging
|
||||
|
||||
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `LOG_LEVEL=debug`:
|
||||
First, turn on **debug** logging for multi-scrobbler by setting the environmental variable `DEBUG_MODE=true`. Then, check the output for any additional information.
|
||||
|
||||
* using node `LOG_LEVEL=debug ... node src/index.js`
|
||||
* using docker `docker run -e LOG_LEVEL=debug ... foxxmd/multi-scrobbler`
|
||||
##### Check Host name and URL
|
||||
|
||||
Check the output for any additional information.
|
||||
The URLs examples in the [configuration](/configuration#base-url) documentation assume you are running Webscrobbler/LFM Endpoint on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
|
||||
|
||||
#### Check Host name and URL
|
||||
EX `http://localhost:9078/api/webscrobbler` -> `http://192.168.0.140:9078/api/webscrobbler`
|
||||
|
||||
The URLs examples in the [configuration](configuration/configuration.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
|
||||
|
||||
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
|
||||
|
||||
#### Check Firewall and Port Forwarding
|
||||
##### Check Firewall and Port Forwarding
|
||||
|
||||
If the machine multi-scrobbler is running on has a firewall ensure that port **9078** is open. Or if it is in another network entirely make sure your router is forwarding this port and it is open to the correct machine.
|
||||
|
||||
#### Check Source Service Logs
|
||||
##### Check Source Service Logs
|
||||
|
||||
Plex/Tautulli/Jellyfin all have logs that will log if they cannot connect to multi-scrobbler. Check these for further information.
|
||||
###### Webscrobbler
|
||||
|
||||
##### Plex
|
||||
See [Debugging the extension](https://github.com/web-scrobbler/web-scrobbler/wiki/Debug-the-extension) to get logs which should have information about failed requests.
|
||||
|
||||
Settings -> Manage -> Console
|
||||
|
||||
##### Tautulli
|
||||
|
||||
Check the command-line output of the application or docker logs.
|
||||
|
||||
##### Jellyfin
|
||||
|
||||
Administration -> Dashboard -> Advanced -> Logs
|
||||
|
||||
## Jellyfin has warnings about undefined or missing data
|
||||
|
||||
Make sure you have
|
||||
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
|
||||
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
|
||||
|
||||
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
|
||||
|
||||
You can verify the payload sent from the webhook by modifying your jellyfin configuration to include `logPayload: true` which will output the raw payload to DEBUG level logging:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "MyJellyfin",
|
||||
"clients": [],
|
||||
"data": {
|
||||
"users": ["FoxxMD"],
|
||||
"options": {
|
||||
"logPayload": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
If your issue persists and you open an Issue for it please include the raw payload logs in your report.
|
||||
|
||||
## Jellyfin has warnings about missing headers
|
||||
|
||||
If you experience issues trying to scrobble with Jellyfin and find this in your MS logs
|
||||
|
||||
```
|
||||
[API] Jellyfin is not sending a request with valid headers...
|
||||
```
|
||||
|
||||
A workaround that may fix this:
|
||||
|
||||
* In Webhook settings:
|
||||
* [In the webhook you have already configured...](configuration/configuration.md#jellyfin)
|
||||
* Add Request Header...
|
||||
* **Key:** `Content-Type`
|
||||
* **Value:** `application/json`
|
||||
* Then Save
|
||||
|
||||
## Spotify/Deezer/LastFM won't authenticate
|
||||
### Spotify/Deezer/LastFM won't authenticate
|
||||
|
||||
Ensure any **client id** or **secrets** are correct in your configuration.
|
||||
|
||||
@@ -131,19 +60,31 @@ 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`
|
||||
|
||||
## Sporadic network problems on docker
|
||||
### Youtube Music fails after some time
|
||||
|
||||
If you encounter networking issues like:
|
||||
If you see errors in multi-scrobbler for YTM that contain **401** or **403** like
|
||||
|
||||
* sporadic timeouts (`ETIMEDOUT`) without a pattern
|
||||
* DNS errors (`EAI_AGAIN`) that do no occur consistently
|
||||
* Failures to reach a host that was previously fine (`EHOSTUNREACH`)
|
||||
```
|
||||
Error: Could not send the specified request to browse. Status code: 401
|
||||
```
|
||||
|
||||
there may be an issue with the underlying docker image OS (alpine) that may be solved by switching to a different image. Try switching to a `*-debian` variant tag (only available for ARM hosts) to see if this resolves your issue. IE `multi-scrobbler:latest-debian` or `multi-scrobbler:develop-debian`
|
||||
then YTM has invalidated your authentication.
|
||||
|
||||
# Configuration Issues
|
||||
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.**
|
||||
|
||||
## Config could not be parsed
|
||||
#### When using OAuth Client Authentication
|
||||
|
||||
Refresh your authentication by using the **(Re)authenticate** link from MS's web dashboard.
|
||||
|
||||
#### When using Cookies Authentication
|
||||
|
||||
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
|
||||
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
### Config could not be parsed
|
||||
|
||||
If you see something like this in your logs:
|
||||
|
||||
@@ -155,25 +96,21 @@ If you see something like this in your logs:
|
||||
|
||||
It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like [JSONLint](https://jsonlint.com/) to find out where errors you have and fix them.
|
||||
|
||||
# Scrobbling Issues
|
||||
## Scrobbling Issues
|
||||
|
||||
## Last.fm does not scrobble tracks with multiple artists correctly
|
||||
### Last.fm does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
|
||||
|
||||
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
|
||||
|
||||
## Jellyfin does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
|
||||
|
||||
## Google Cast track information is missing/incorrect or MS player has weird times
|
||||
### Google Cast track information is missing/incorrect or MS player has weird times
|
||||
|
||||
The Google Cast integration relies on a few common fields in the data it receives from your casting device. Every platform that can cast (Spotify, Pandora, etc...) *should* use these fields the same but there are slight differences between their implementations that may confuse multi-scrobbler. Specific platforms may also return more information in non-common fields that are undocumented.
|
||||
|
||||
To diagnose these issues you [**must enable payload logging**](configuration/configuration.md#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
|
||||
To diagnose these issues you [**must enable payload logging**](/configuration/sources/google-cast#cast-troubleshooting) for your google cast Source, run MS, and then include logs with this output from that run. Without the raw data logged from your cast device it will be nearly impossible to resolve your issue.
|
||||
|
||||
## Google Cast device does not track media
|
||||
### Google Cast device does not track media
|
||||
|
||||
It is likely the app playing on the cast device is incorrectly reporting the media type as **not music**.
|
||||
|
||||
@@ -183,10 +120,100 @@ MS logs will tell you what type the media is reported as with lines like:
|
||||
My Artist - Example Track has 'unknown' media type and allowUnknownMedia=false, will not track
|
||||
```
|
||||
|
||||
Refer to [Allow Unknown Media Type](configuration/configuration.md#allow-unknown-media-type) section to fix this
|
||||
Refer to [Allow Unknown Media Type](/configuration/sources/google-cast#allow-unknown-media-type) section to fix this
|
||||
|
||||
```
|
||||
My Artist - Example Track has 'movie' media type so will not track
|
||||
```
|
||||
|
||||
Refer to [Force Media Tracking](configuration/configuration.md#forcing-media-tracking) section to fix this
|
||||
Refer to [Force Media Tracking](/configuration/sources/google-cast#forcing-media-tracking) section to fix this
|
||||
|
||||
### VLC is not scrobbling fields correctly
|
||||
|
||||
Before reporting an issue turn on metadata logging in the MS VLC configuration, [see the VLC documentation.](/configuration/sources/vlc#vlc-information-reporting)
|
||||
|
||||
### Youtube Music misses or duplicates scrobbles
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How MS detects YTM history</summary>
|
||||
|
||||
In order for multi-scrobbler to accurately determine if a song has been scrobbled it needs **a source of truth.** For YTM this is a "history" list scraped from the YTM website. Unfortunately, the data in this list can be (often) inconsistent which makes it hard for multi-scrobbler to "trust" that it is correct and determine when/if new scrobbles occur. This inconsistency is not something multi-scrobbler can control -- it is a side-effect of having to use an unofficial method to access YTM (scraping).
|
||||
|
||||
To compensate for this multi-scrobbler resets when it considers this list the "source of truth" based on if the list changes in an inconsistent way between consecutive checks. New scrobbles can only be detected when this list is "OK" as a source of truth for N+1 checks. Therefore, any new tracks that appear when the list is inconsistent will be ignored.
|
||||
|
||||
Duplicate scrobbles can also occur if the change between two checks is technically consistent. For instance, if you listen to a track twice in some period, separated by other music, YTM will sometimes "remove" the track from the earlier time (further down in your history) and "re-add" it at the top of the history.
|
||||
|
||||
</details>
|
||||
|
||||
#### Reporting YTM scrobble issues
|
||||
|
||||
If you experience scrobble detection issues you can help improve MS's YTM heureistic by providing thorough feedback as [an issue.](https://github.com/FoxxMD/multi-scrobbler/issues/new?assignees=&labels=bug&projects=&template=01-bug-report.yml&title=bug%3A+) **Please do the following to provide the most useful report:**
|
||||
|
||||
##### Turn on Change Detection
|
||||
|
||||
In your YTM configuration (`ytmusic.json`) add `logDiff` under `options` like this:
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "MyYTM",
|
||||
"data": { ... },
|
||||
"options": {
|
||||
"logDiff": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or set either ENVs:
|
||||
|
||||
* `YTM_LOG_DIFF=true`
|
||||
* [`DEBUG_MODE=true`](/configuration#debug-mode)
|
||||
|
||||
This will cause MS to log YTM history changes similar to this:
|
||||
|
||||
```
|
||||
[Ytmusic - MyYTM] Changes from last seen list:
|
||||
1. (tuhe1CpHRxY) KNOWER - I’m The President --- undefined => Moved - Originally at 6
|
||||
2. (Mtg8V6Xa2nc) Vulfpeck - Romanian Drinking Song --- Schvitz => Moved - Originally at 1
|
||||
3. (rxbCaiyYSXM) Nightmares On Wax - You Wish --- In A Space Outta Sound => Moved - Originally at 2
|
||||
4. (tMt_YXr90AM) Gorillaz - O Green World --- undefined => Moved - Originally at 3
|
||||
...
|
||||
```
|
||||
|
||||
Which are essential to troubleshooting this behavior.
|
||||
|
||||
|
||||
##### Turn on File Logging
|
||||
|
||||
Optionally, if your logs are busy due to many other sources, you can log **only** YTM logs to a file. Turn this on with the `logToFile` config option:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "MyYTM",
|
||||
"data": { ... },
|
||||
"options": {
|
||||
"logDiff": true,
|
||||
"logToFile": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will cause MS to write YTM logs to a file in your `logs` folder named `ytmusic-MyYTM.log` (based on your source name).
|
||||
|
||||
##### Provide Detail and Context
|
||||
|
||||
Provide a detailed account of how you were using YTM when the issue occurred, including things like:
|
||||
|
||||
* the platform listening on (desktop, mobile, 3rd party client, etc...)
|
||||
* any changes in platform
|
||||
* > I switched from desktop to listening on my phone...
|
||||
* how you were listening to music
|
||||
* > I was playing an album start to finish
|
||||
* > I listened to two songs in a row, then browsed for a new song in library by artist, then went back to a song in the queue...
|
||||
|
||||
Explain the expected behavior (it should have scrobbled songs x, y, then z) and what actually happened (it scrobbled songs x, then y, then x again, then z)
|
||||
|
||||
Provide ALL logs from the time when the issue occurred including logs from BEFORE (ideally 2-3 minutes of logs) and AFTER the issue.
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"position": 2,
|
||||
"position": 5,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Configuring Multi-Scrobbler and examples"
|
||||
"type": "doc",
|
||||
"id": "configuration/configuration"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Scrobble Clients",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "configuration/clients/clients"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
|
||||
|
||||
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
|
||||
## Clients
|
||||
|
||||
| Name | Now Playing |
|
||||
| :-------------------------------------------------- | :---------- |
|
||||
| [Last.fm](/configuration/clients/lastfm) | ✅ |
|
||||
| [Listenbrainz](/configuration/clients/listenbrainz) | ✅ |
|
||||
| [Koito](/configuration/clients/koito) | ❌ |
|
||||
| [Maloja](/configuration/clients/maloja) | ❌ |
|
||||
| [Rocksky](/configuration/clients/rocksky) | ❌ |
|
||||
| [Teal.fm](/configuration/clients/tealfm) | ❌ |
|
||||
|
||||
## Features
|
||||
|
||||
### Dead Scrobbles
|
||||
|
||||
If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up.
|
||||
|
||||
After it stops _automatically_ retrying the scrobble still exists and can be retried from the dashboard. Dead scrobbles persist between restart so you will never lose a scrobble that hasn't been successfully submitted yet.
|
||||
|
||||
### Now Playing
|
||||
|
||||
Multi-scrobbler can report the currently **playing** tracks it is monitoring to some Scrobble Clients via their individual **Now Playing** functionality.
|
||||
|
||||
Now Playing is **default enabled** for all Clients that support it.
|
||||
|
||||
The behavior multi-scrobbler uses for determining Now Playing reporting:
|
||||
|
||||
* Now Playing can be explicitly enabled or disabled globally using ENV `NOW_PLAYING=true` or `NOW_PLAYING=false`
|
||||
* This only affects Clients that don't have behavior set via File/AIO (below)
|
||||
* Now Playing will be only be reported to the same Clients a Source is configured to scrobble to
|
||||
* A Source's Player status must be **Playing**, IE **active**, for reporting to occur
|
||||
* This status can be verified via Player UI on the Dashboard
|
||||
* If multiple Sources having active Players then the scrobble Client will default to reporting the track based on Source **configuration** name, alphabetically
|
||||
* This is the `name` property set in [File or AIO source configs](/configuration?configType=file#configuration-types)
|
||||
|
||||
Clients can customize the Now Playing behavior individually using [File or AIO source configs:](/configuration?configType=file#configuration-types)
|
||||
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "myLastFmClient",
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
// ...
|
||||
},
|
||||
"options": {
|
||||
// disable or enable
|
||||
//"nowPlaying": true
|
||||
//
|
||||
// OR define a list of Source *config* names that should be allowed to report Now Playing
|
||||
// order of the list determines priority for reporting Now Playing
|
||||
//"nowPlaying": ["mySpotify1","myJellyfin2"]
|
||||
}
|
||||
},
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Koito
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/koito.json.example';
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To monitor and re-scrobble activity from a Koito account create a [Koito (Source)](/configuration/sources/koito-source)
|
||||
|
||||
:::
|
||||
|
||||
Setup a [Koito server](https://koito.io/guides/installation/) if you have not already done this.
|
||||
|
||||
* [Create (or get) an API Key](https://koito.io/guides/scrobbler/)
|
||||
* From your Koito server dashboard navigate to **Settings** (bottom-left icon) => **API Keys**
|
||||
* Create a new Key (or use an existing) and Copy the key using the copy icon to the right of the key
|
||||
* EX `pM195xPV98CDpk0QW47FIIOR8AKATAX5DblBF-Jq0t1MbbKL`
|
||||
* Take note of your Koito username (used below as `KOITO_USER`)
|
||||
* Determine your Koito URL
|
||||
* This is the URL you use to access your Koito dashboard OR whatever URL is accessible from your multi-scrobbler instance.
|
||||
* EX dashboard at `http://192.168.0.100:4110` => `KOITO_URL` is set to `http://192.168.0.100:4110`
|
||||
|
||||
:::note[Koito Base URL]
|
||||
|
||||
Using the URL path `/apis/listenbrainz` [Koito docs describe](https://koito.io/guides/scrobbler/) **is not required.** The Koito URL used for multi-scrobbler should only be the **base** URL. Only include a URL path if your Koito **base** url is not at `host:port`. See the table below for examples.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Base URL Examples</summary>
|
||||
| `KOITO_URL` | MS Detected Base URL | Example Scrobble Api URL |
|
||||
| :-------------------------------------------- | :------------------------------ | ---------------------------------------------------- |
|
||||
| `http://192.168.0.100:4110` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
|
||||
| `https://koito.mydomain.com` | `https://koito.mydomain.com` | `https://koito.mydomain.com/apis/listenbrainz/1` |
|
||||
| `http://192.168.0.100:4110/apis/listenbrainz` | `http://192.168.0.100:4110` | `http://192.168.0.100:4110/apis/listenbrainz/1` |
|
||||
| `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito` | `http://192.168.0.100:80/koito/apis/listenbrainz/1` |
|
||||
</details>
|
||||
:::
|
||||
|
||||
|
||||
:::tip
|
||||
|
||||
Ensure that Koito is configured to allow requests from multi-scrobbler! In Koito config set [`KOITO_ALLOWED_HOSTS`](https://koito.io/reference/configuration/#koito_allowed_hosts) to the IP the multi-scrobbler dashboard is accessible from.
|
||||
|
||||
:::
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="KoitoClientConfig" fileContent={JsonConfig} client name="koito">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | --------------------------------- |
|
||||
| KOITO_TOKEN | Yes | | API Key from your Koito Account |
|
||||
| KOITO_USER | Yes | | Your Koito username |
|
||||
| KOITO_URL | No | | The base URL for the Koito server |
|
||||
</Config>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Last.fm
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example';
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
* To monitor and re-scrobble activity from your Last.fm account create a [Last.fm (Source)](/configuration/sources/lastfm-source)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
|
||||
|
||||
:::
|
||||
|
||||
[Register for an API account here.](https://www.last.fm/api/account/create)
|
||||
|
||||
The Callback URL is actually specified by multi-scrobbler but to keep things consistent you should use
|
||||
```
|
||||
http://localhost:9078/lastfm/callback
|
||||
```
|
||||
or replace `localhost:9078` with your own base URL.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="LastfmClientConfig" fileContent={JsonConfig} client name="lastfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|-----------------------------------------|-------------------------------------------------------------------------------|
|
||||
| `LASTFM_API_KEY` | Yes | | Api Key from your API Account |
|
||||
| `LASTFM_SECRET` | Yes | | Shared secret from your API Account |
|
||||
| `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it |
|
||||
| `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. |
|
||||
</Config>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Listenbrainz
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/listenbrainz.json.example';
|
||||
|
||||
:::tip[Other Users]
|
||||
|
||||
* To monitor and re-scrobble activity from a Listenbrainz account create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
|
||||
|
||||
:::
|
||||
|
||||
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)
|
||||
|
||||
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|-------------------------------|---------------------------------|
|
||||
| LZ_TOKEN | Yes | | User token from your LZ profile |
|
||||
| LZ_USER | Yes | | Your LZ username |
|
||||
| LZ_URL | No | https://api.listenbrainz.org/ | The base URL for the LZ server |
|
||||
</Config>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Maloja
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/maloja.json.example';
|
||||
|
||||
Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Maloja Setup Intructions</summary>
|
||||
|
||||
Using Maloja's example `docker-compose.yml`:
|
||||
|
||||
```yaml reference title="~/malojaData/docker-compose.yml"
|
||||
https://github.com/krateng/maloja/blob/master/example-compose.yml
|
||||
```
|
||||
|
||||
Uncomment `environment` and add `MALOJA_FORCE_PASSWORD=CHANGE_ME` to set an admin password
|
||||
|
||||
Start the container:
|
||||
|
||||
```shell title="~/malojaData"
|
||||
docker compose up -d
|
||||
```
|
||||
</details>
|
||||
|
||||
* Navigate to the Admin Panel (Cog in upper-right corner) -> API Keys (or at http://myMalojaServerIP/admin_apikeys)
|
||||
* Create a **New Key** and then copy the generated key value
|
||||
|
||||
Finally, add the Maloja server URL and API Key to the configuration type you choose to use, below.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="MalojaClientConfig" fileContent={JsonConfig} client name="maloja">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|-------------------------------|
|
||||
| `MALOJA_URL` | Yes | | Base URL of your installation |
|
||||
| `MALOJA_API_KEY` | Yes | | Api Key |
|
||||
</Config>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Rocksky
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
|
||||
import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example';
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To monitor and re-scrobble activity **from** Rocksky create a [Rocksky (Source)](/configuration/sources/rocksky-source)
|
||||
|
||||
:::
|
||||
|
||||
[**Rocksky**](https://rocksky.app/) is music tracking and discovery platform built on the AT Protocol (Bluesky).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How does that work?</summary>
|
||||
|
||||
<Bluesky name="Rocksky" lexicon="app.rocksky"/>
|
||||
|
||||
</details>
|
||||
|
||||
:::warning[Public Data]
|
||||
|
||||
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.
|
||||
|
||||
:::
|
||||
|
||||
:::warning[No Scrobbling Guarantee]
|
||||
|
||||
Rocksky **does not guarantee** that every scrobble you send will be persisted. Rocksky's design goal is to store scrobbles with **metdata-complete, 3rd party verifiable track information** so that it can be a metadata-rich discovery platform, rather than an exhaustive scrobble data source-of-truth.
|
||||
|
||||
To do this it *asynchronously* matches your scrobble data against Musicbrainz's and/or Spotify's catalogs, *after the scrobble has been submitted.* **If the scrobble does not end up matching then it is not persisted.**
|
||||
|
||||
If you need your scrobbles to be 100% captured then you should scrobble to an *additional* [Scrobble Client](/configuration/clients).
|
||||
|
||||
:::
|
||||
|
||||
## Setup
|
||||
|
||||
You will need a [Bluesky](https://bsky.app/) account to use Rocksky.
|
||||
|
||||
:::note
|
||||
|
||||
Usage is adapted from [Rocksky docs](https://docs.rocksky.app/migrating-to-rocksky-scrobble-api-957839m0) on [docs.rocksky.app](https://docs.rocksky.app)
|
||||
|
||||
:::
|
||||
|
||||
* Navigate to [rocksky.app](https://rocksky.app/) and sign up/logn using your Bluesky account
|
||||
* Then, navigate to the [RockSky developer dashboard](https://rocksky.app/apikeys) and obtain an API Key
|
||||
|
||||
## Configuration
|
||||
|
||||
### Handle
|
||||
|
||||
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
|
||||
|
||||
* For regular Bluesky account this will be like: `alice.bsky.social`
|
||||
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
|
||||
* For non-Bluesky users, you probably already know your handle
|
||||
|
||||
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| ROCKSKY_HANDLE | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| ROCKSKY_KEY | Yes | | API Key obtained from Rocksky developer dashboard |
|
||||
</Config>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Teal.fm
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/tealfm.json.example';
|
||||
import Bluesky from "@site/src/components/snippets/_bluesky.mdx";
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To monitor and re-scrobble activity from a ATProto Profile create a [teal.fm (Source)](/configuration/sources/tealfm-source)
|
||||
|
||||
:::
|
||||
|
||||
[**teal.fm**](https://teal.fm/) is a social discovery and web viewing service for scrobbles stored/available on the ATProto network/your repository in a [PDS (Personal Data Server)](https://github.com/Bluesky-social/pds).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>How does that work?</summary>
|
||||
|
||||
<Bluesky name="teal.fm" lexicon="fm.teal"/>
|
||||
|
||||
</details>
|
||||
|
||||
:::warning
|
||||
|
||||
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.
|
||||
|
||||
:::
|
||||
|
||||
:::note
|
||||
|
||||
[teal.fm is still in developement](https://notes.teal.fm/3lzlwe6puis2l) and does not have a public website, yet.
|
||||
|
||||
To view your teal.fm scrobbles you can:
|
||||
|
||||
* use [wrapped.baileytownsend.dev](https://wrapped.baileytownsend.dev/) which gives a simple stats overview of your scrobbles
|
||||
* use [atproto.at](https://atproto.at) to view the raw data (convenience URLs are generated in multi-scrobbler for you to do this)
|
||||
|
||||
:::
|
||||
|
||||
## Setup
|
||||
|
||||
#### Bluesky
|
||||
|
||||
* Create a [Bluesky](https://bsky.app) account, if you don't already have one
|
||||
* Navigate to https://bsky.app/settings/app-passwords
|
||||
* Settings -> Privacy and Security -> App passwords
|
||||
* Create a new **App Password** and save it somewhere, it will not be shown again
|
||||
|
||||
## Configuration
|
||||
|
||||
### Handle
|
||||
|
||||
The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD.
|
||||
|
||||
* For regular Bluesky account this will be like: `alice.bsky.social`
|
||||
* For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com`
|
||||
* For non-Bluesky users, you probably already know your handle
|
||||
|
||||
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| TEALFM_IDENTIFIER | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
| TEALFM_APP_PW | Yes | | Bluesky/ATProto network App Password |
|
||||
</Config>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,464 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import SchemaLink from "../../src/components/SchemaLink";
|
||||
import AIOExample from "../../src/components/AIOExample";
|
||||
import FileExample from "../../src/components/FileExample";
|
||||
import ScrobbleThreshold from "@site/src/components/snippets/_scrobble-threshold.mdx"
|
||||
|
||||
import AIOConfig from '!!raw-loader!../../../config/config.json.example';
|
||||
import SpotifyConfig from '!!raw-loader!../../../config/spotify.json.example';
|
||||
|
||||
:::tip
|
||||
|
||||
Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
|
||||
:::
|
||||
|
||||
## Configuration Types
|
||||
|
||||
[**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) are configured using:
|
||||
|
||||
* environmental (ENV) variables
|
||||
* client/source specific json config files
|
||||
* an all-in-one json config file
|
||||
|
||||
**MS will parse configuration from all configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
MS will parse environmental variables present in the OS/container when it is run. **This method means MS does not require files to run.**
|
||||
|
||||
<details>
|
||||
<summary>Use ENV-based configuration if...</summary>
|
||||
|
||||
* You are the only person for whom MS is scrobbling for
|
||||
* You have a very simple setup for MS such as one scrobble [Client](/configuration/clients) and one [Source](/configuration/sources) IE Plex -> Maloja
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
For Docker container...
|
||||
|
||||
```shell
|
||||
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" ...
|
||||
```
|
||||
|
||||
For Docker Compose
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
environment:
|
||||
- SPOTIFY_CLIENT_ID=yourId
|
||||
- SPOTIFY_CLIENT_SECRET=yourSecret
|
||||
- MALOJA_URL=http://domain.tld:42010
|
||||
- MALOJA_API_KEY=1234
|
||||
# ...
|
||||
# ...
|
||||
```
|
||||
|
||||
For a local/node installation export variables before running...
|
||||
|
||||
```shell
|
||||
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret npm run start
|
||||
```
|
||||
</details>
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
|
||||
|
||||
* Local installation -> `PROJECT_DIR/config`
|
||||
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Use File-based configuration if...</summary>
|
||||
|
||||
* You have many [Sources](/configuration/sources)
|
||||
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
|
||||
Funkwhale...
|
||||
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
|
||||
* You want only to scrobble to specific **Clients**
|
||||
* You need to setup more advanced configuration for a Source/Client
|
||||
* Most Source/Clients only support basic configuration through ENV, all configuration is possible using File/AIO
|
||||
|
||||
</details>
|
||||
|
||||
:::tip
|
||||
* There are **example configurations** for all Source/Client types and AIO config located in the [`/config`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
|
||||
* For docker installations these examples are copied to your configuration directory on first-time use.
|
||||
* There is also a [**kitchensink example**](/kitchensink) that provides examples of using all sources/clients in a complex configuration.
|
||||
:::
|
||||
|
||||
Each file is named by the **type** of the Client/Source found in below sections. Each file as an **array** of that type of Client/Source.
|
||||
|
||||
Example directory structure:
|
||||
|
||||
```
|
||||
/CONFIG_DIR
|
||||
plex.json
|
||||
spotify.json
|
||||
maloja.json
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/spotify.json" language="json5">{SpotifyConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="File AIO">
|
||||
MS will parse an **all-in-one** configuration file located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
|
||||
|
||||
* Local installation -> `PROJECT_DIR/config/config.json`
|
||||
* Docker -> `/config/config.json` (in the container) -- see the [install docs](../installation/installation.mdx#docker) for how to configure this correctly
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Use AIO-based configuration if...</summary>
|
||||
|
||||
* You have many [Sources](/configuration/sources)
|
||||
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
|
||||
Funkwhale...
|
||||
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
|
||||
* You want only to scrobble to specific **Clients**
|
||||
* You need to setup [monitoring/webhooks](#monitoring)
|
||||
* You want to setup defaults for all Sources/Clients
|
||||
|
||||
</details>
|
||||
|
||||
**The AIO config also enables setting default options for sources/clients as well as global options for MS itself.**
|
||||
|
||||
:::tip
|
||||
* An example AIO config files can be found in the project directory at [`/config/config.json.example`](https://github.com/FoxxMD/multi-scrobbler/tree/master/config/config.json.example)
|
||||
* For docker installations this example is copied to your configuration directory on first-time use.
|
||||
* There is also a [**kitchensink example**](/kitchensink) that provides examples of using all sources/clients in a complex configuration.
|
||||
:::
|
||||
|
||||
[**Explore the schema for this configuration, along with an example generator and validator, here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Config Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/config.json" language="json5">{AIOConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Setup Sources and Clients
|
||||
|
||||
See the [Configuration Types](#configuration-types) above for your options for creating Source and Client configurations.
|
||||
|
||||
* Reference [**Scrobble Sources**](/configuration/sources) for what Sources are available
|
||||
* Reference [**Scrobble Clients**](/configuration/clients) for what Clients are available
|
||||
|
||||
Each entry for a Source/Client includes a **Configuration** section that describes how to configure it using a [configuration type](#configuration-types).
|
||||
|
||||
## Application Options
|
||||
|
||||
These options affect multi-scrobbler's behavior and are not specific to any source/client.
|
||||
|
||||
### Base URL
|
||||
|
||||
Defines the URL that is used to generate default redirect URLs for authentication on [spotify](/configuration/sources/spotify) and [lastfm](/configuration/clients/lastfm) -- as well as some logging hints.
|
||||
|
||||
* Default => `http://localhost:9078`
|
||||
* Set with [ENV](./?configType=env#configuration-types) `BASE_URL` or `baseUrl` [all-in-one configuration](./?configType=aio#configuration-types)
|
||||
* If protocol is `http` or no protocol is specified MS will try to use port `9078` -- to override this explicitly set the port or use `https`
|
||||
|
||||
Useful when running with [docker](../installation/installation.mdx#docker) so that you do not need to specify redirect URLs for each configuration.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
EX Lastfm Redirect Url is `BASE_URL:PORT/lastfm/callback` (when no other redirectUri is specified for [lastfm configuration](/configuration/clients/lastfm))
|
||||
|
||||
| `BASE_URL` | Redirect URL |
|
||||
|-------------------------------------|----------------------------------------------------------|
|
||||
| `192.168.0.101` | `http://192.168.0.101:9078/lastfm/callback` |
|
||||
| `http://my.domain.local` | `http://my.domain.local:9078/lastfm/callback` |
|
||||
| `http://192.168.0.101/my/subfolder` | `http://192.168.0.101:9078/my/subfolder/lastfm/callback` |
|
||||
|
||||
| `BASE_URL` | Redirect URL |
|
||||
|-----------------------------------|----------------------------------------------------------|
|
||||
| `my.domain.local:80` | `http://192.168.0.101:80/lastfm/callback` |
|
||||
| `my.domain.local:9000` | `http://my.domain.local:9000/lastfm/callback` |
|
||||
| `192.168.0.101:4000/my/subfolder` | `http://192.168.0.101:4000/my/subfolder/lastfm/callback` |
|
||||
| `https://192.168.0.101` | `https://192.168.0.101:443/lastfm/callback` |
|
||||
|
||||
</details>
|
||||
|
||||
### Caching
|
||||
|
||||
Multi-scrobbler can cache some of its data for the purpose of **persisting queued and dead scrobbles** after restarting.
|
||||
|
||||
It supports caching using either:
|
||||
|
||||
* **File** cache stored in the `CONFIG_DIR` directory (next to your File/[AIO](./?configType=aio#configuration-types) file)
|
||||
* [**Valkey**](https://valkey.io/), an open-source fork of Redis.
|
||||
|
||||
**File** caching is **enabled by default** when no other configuration is present.
|
||||
|
||||
The type of cache used, and it's connection properties, can be configured through ENV or **AIO** config.
|
||||
|
||||
##### Caching Configuration
|
||||
|
||||
<Tabs groupId="cache" queryString>
|
||||
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
**File** cache is stored in the `CONFIG_DIR` directory using the pre-defined file name `ms-scrobble.cache`.
|
||||
|
||||
It is **enabled by default** if `CACHE_SCROBBLE`, or the respective AIO configuration, is not defined.
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | -------------------- | ------------------------------------------------------------ |
|
||||
| `CACHE_SCROBBLE` | No | `file` | The cache type to use |
|
||||
| `CACHE_SCROBBLE_CONN` | No | The config directory | The directory, within the container, to store the cache file |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "file",
|
||||
"connection": "/config"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="valkey" label="Valkey">
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
|
||||
<TabItem value="env" label="ENV">
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | -------------------- | ------------------------------------------------------------ |
|
||||
| `CACHE_SCROBBLE` | Yes | `valkey` | The cache type to use |
|
||||
| `CACHE_SCROBBLE_CONN` | Yes | | The host/IP and port to connect to, prefixed with `redis://` -- EX: `redis://192.168.0.120:6379` |
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"cache": {
|
||||
"scrobble": {
|
||||
"provider": "valkey",
|
||||
"connection": "redis://192.168.0.120:6379"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Turning on Debug Mode will
|
||||
|
||||
* override and enable all optional "debugging" options found in configuration
|
||||
* set log output to DEBUG
|
||||
|
||||
Use this as a shortcut for enabling output that can be used for troubleshooting and issue reporting. Be aware that logs will likely be VERY noisy while Debug Mode is on. You should only have this mode on while gathering logs for troubleshooting and then turn it off afterwards.
|
||||
|
||||
To set debug mode either add it to [AIO `config.json`](./?configType=aio#configuration-types)
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"debugMode": true,
|
||||
"sources": [...],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
or set the [ENV](./?configType=env#configuration-types) `DEBUG_MODE=true`
|
||||
|
||||
### Disable Web
|
||||
|
||||
If you do not need the dashboard and/or ingress sources, or have security concerns about ingress and cannot control your hosting environment, the web server and API can be disabled.
|
||||
|
||||
:::warning
|
||||
|
||||
Any **[ingress-based sources](/configuration/sources#ingress) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
|
||||
:::
|
||||
|
||||
Disable using either:
|
||||
|
||||
* ENV `DISABLE_WEB=true`
|
||||
* In [All-in-One File](./?configType=aio#configuration-types) use the top-level property `"disableWeb": true`
|
||||
|
||||
## Monitoring
|
||||
|
||||
Multi-scrobbler supports some common webhooks and a healthcheck endpoint in order to monitor Sources and Clients for errors.
|
||||
|
||||
### Webhook Configurations
|
||||
|
||||
Webhooks will **push** a notification to your configured servers on these events:
|
||||
|
||||
* Source polling started
|
||||
* Source polling retry
|
||||
* Source polling stopped on error
|
||||
* Scrobble client scrobble failure
|
||||
|
||||
Webhooks are configured in the AIO [config.json](#configuration-types) file under the `webhook` top-level property. Multiple webhooks may be configured for each webhook type.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"sources": [
|
||||
//...
|
||||
],
|
||||
"clients": [
|
||||
//...
|
||||
],
|
||||
"webhooks": [
|
||||
{
|
||||
"name": "FirstGotifyServer",
|
||||
"type": "gotify",
|
||||
"url": "http://192.168.0.100:8070",
|
||||
"token": "abcd"
|
||||
},
|
||||
{
|
||||
"name": "SecondGotifyServer",
|
||||
"type": "gotify",
|
||||
//...
|
||||
},
|
||||
{
|
||||
"name": "NtfyServerOne",
|
||||
"type": "ntfy",
|
||||
//...
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### [Gotify](https://gotify.net/)
|
||||
|
||||
Refer to the [config schema for GotifyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FGotifyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"type": "gotify",
|
||||
"name": "MyGotifyFriendlyNameForLogs",
|
||||
"url": "http://192.168.0.100:8070",
|
||||
"token": "AQZI58fA.rfSZbm",
|
||||
"priorities": {
|
||||
"info": 5,
|
||||
"warn": 7,
|
||||
"error": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### [Ntfy](https://ntfy.sh/)
|
||||
|
||||
Refer to the [config schema for NtfyConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FNtfyConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler optionally supports setting message notification priority via `info` `warn` and `error` mappings.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json title="config.json"
|
||||
{
|
||||
"type": "ntfy",
|
||||
"name": "MyNtfyFriendlyNameForLogs",
|
||||
"url": "http://192.168.0.100:9991",
|
||||
"topic": "RvOwKJ1XtIVMXGLR",
|
||||
"username": "Optional",
|
||||
"password": "Optional",
|
||||
"priorities": {
|
||||
"info": 3,
|
||||
"warn": 4,
|
||||
"error": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### [Apprise](https://github.com/caronc/apprise-api)
|
||||
|
||||
Refer to the [config schema for AppriseConfig](https://json-schema.app/view/%23/%23%2Fdefinitions%2FAppriseConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fmaster%2Fsrc%2Fbackend%2Fcommon%2Fschema%2Faio.json)
|
||||
|
||||
multi-scrobbler supports [stateless](https://github.com/caronc/apprise-api?tab=readme-ov-file#stateless-solution) and [persistent storage](https://github.com/caronc/apprise-api?tab=readme-ov-file#persistent-storage-solution) endpoints as well as [tags](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging)/
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
"type": "apprise",
|
||||
"name": "MyAppriseFriendlyNameForLogs",
|
||||
"host": "http://192.168.0.100:8080",
|
||||
"urls": ["gotify://192.168.0.101:8070/MyToken"], // stateless endpoints
|
||||
"keys": ["e90b20526808373353afad7fb98a201198c0c3e0555bea19f182df3388af7b17"], //persistent storage endpoints
|
||||
"tags": ["my","optional","tags"]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Health Endpoint
|
||||
|
||||
An endpoint for monitoring the health of sources/clients is available at GET `http://YourMultiScrobblerDomain/health`
|
||||
|
||||
* Returns `200 OK` when **everything** is working or `500 Internal Server Error` if **anything** is not
|
||||
* The plain url (`/health`) aggregates status of **all clients/sources** -- so any failing client/source will make status return 500
|
||||
* Use query params `type` or `name` to restrict client/sources aggregated IE `/health?type=spotify` or `/health?name=MyMaloja`
|
||||
* On 500 the response returns a JSON payload with `messages` array that describes any issues
|
||||
* For any clients/sources that require authentication `/health` will return 500 if they are **not authenticated**
|
||||
* For sources that poll (spotify, yt music, subsonic) `/health` will 500 if they are **not polling**
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 226 KiB |
+65
-42
@@ -3,27 +3,26 @@ sidebar_position: 3
|
||||
title: Kitchen Sink
|
||||
---
|
||||
|
||||
# Example Config using all Possible Features
|
||||
# Example Complex Scenario
|
||||
|
||||
Scenario:
|
||||
|
||||
* You want to scrobble plays for yourself (Foxx), Fred, and Mary
|
||||
* Each person has their own Maloja server
|
||||
* Each person has their own [Maloja](/configuration/clients/maloja) server
|
||||
* Each person has their own Spotify account
|
||||
* You have your own Airsonic (subsonic) server you to scrobble from
|
||||
* You have your own Youtube Music account you want to scrobble from
|
||||
* Mary has her own Last.fm account she also wants to scrobble to
|
||||
* Fred has his own Spotify application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
|
||||
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
|
||||
* You have your own [Youtube Music](/configuration/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 called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
|
||||
* Fred also has his own Jellyfin server and wants to scrobble everything he plays
|
||||
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
|
||||
* Fred also has his own [Jellyfin server](/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)
|
||||
|
||||
### All-in-one Config
|
||||
|
||||
Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/config.json"
|
||||
{
|
||||
"sourceDefaults": {
|
||||
"maxPollRetries": 0, // optional, default # of automatic polling restarts on error. can be overridden by property in individual config
|
||||
@@ -42,6 +41,8 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"data": {
|
||||
"clientId": "foxxSpotifyAppId",
|
||||
"clientSecret": "foxxSpotifyAppSecret",
|
||||
},
|
||||
"options": {
|
||||
"maxRequestRetries": 2, // override default max retries because spotify can...spotty
|
||||
}
|
||||
},
|
||||
@@ -69,7 +70,9 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "fredPlex",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,8 +80,10 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "maryPlex",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"user": ["mary@email.com"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
|
||||
"libraries": ["podcasts"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["maryUser"], // still need to specify mary as user so not all users who play from 'podcasts' get scrobbled
|
||||
"librariesAllow": ["podcasts"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,7 +91,9 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "partyPlex",
|
||||
// omitting clients (or making it empty) will make this Source scrobble to all Clients
|
||||
"data": {
|
||||
"libraries": ["party"],
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"librariesAllow": ["party"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -94,7 +101,10 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"name": "FredJelly",
|
||||
// omitting clients (or making it empty) will make this Source scrobble to all Clients
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
"usersAllow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -113,9 +123,16 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
|
||||
"authUser": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "endpointlz",
|
||||
"name": "listenbrainzfoxx",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"token": "myToken"
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
@@ -158,9 +175,7 @@ Using just one config file located at `CONFIG_DIR/config.json`:
|
||||
|
||||
### Separate JSON files
|
||||
|
||||
In `CONFIG_DIR/spotify.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/spotify.json"
|
||||
[
|
||||
{
|
||||
// may omit 'type' property since app knows this is file is for spotify configs
|
||||
@@ -191,66 +206,76 @@ In `CONFIG_DIR/spotify.json`:
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/plex.json`
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/plex.json"
|
||||
[
|
||||
{
|
||||
"name": "fredPlex",
|
||||
"clients": ["fredMaloja"],
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["fredUser"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maryPlex",
|
||||
"clients": ["maryMaloja"],
|
||||
"data": {
|
||||
"user": ["mary@email.com"],
|
||||
"libraries": ["podcasts"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"usersallow": ["maryUser"],
|
||||
"librariesAllow": ["podcasts"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "partyPlex",
|
||||
"data": {
|
||||
"libraries": ["party"]
|
||||
"token": "JtRnwQWD__XMJF8sT3jc",
|
||||
"url": "192.168.0.233:32400",
|
||||
"librariesAllow": ["party"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/jellyfin.json`
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/jellyfin.json"
|
||||
[
|
||||
{
|
||||
"name": "FredJelly",
|
||||
"data": {
|
||||
"user": ["fred@email.com"]
|
||||
"url": "http://localhost:8096",
|
||||
"user": "FoxxMD",
|
||||
"apiKey": "c9fae8756fbf481ebd9c5bb56bd6540c",
|
||||
"usersAllow": ["fredUser"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/ytmusic.json`
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/ytmusic.json"
|
||||
[
|
||||
{
|
||||
"type": "ytmusic",
|
||||
"name": "foxxYoutube",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"cookie": "__Secure-3PAPISID=3AxsXpy0MKGu75Qb/AkISXGqOnSDn1jEKn; DEVICE_INFO=ChxOekU0Tmpjek5EWTBPRGd3TlRBMk16QXpNdz09EJbS8Z0GGJbS8Z0G; ...",
|
||||
"authUser": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
```json5 title="CONFIG_DIR/endpointlz.json"
|
||||
[
|
||||
{
|
||||
"name": "listenbrainzfoxx",
|
||||
"clients": ["foxxMaloja"],
|
||||
"data": {
|
||||
"token": "myToken",
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/maloja.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/maloja.json"
|
||||
[
|
||||
{
|
||||
"name": "foxxMaloja",
|
||||
@@ -276,9 +301,7 @@ In `CONFIG_DIR/maloja.json`:
|
||||
]
|
||||
```
|
||||
|
||||
In `CONFIG_DIR/lastfm.json`:
|
||||
|
||||
```json5
|
||||
```json5 title="CONFIG_DIR/lastfm.json"
|
||||
[
|
||||
{
|
||||
"name": "maryLFM",
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -1,73 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Plex/Tautulli
|
||||
---
|
||||
|
||||
Tracks played on [Plex](https://plex.tv/) can be scrobbled either by:
|
||||
* A [Tautulli](https://tautulli.com/) notification agent with a webhook.
|
||||
* Using Plex [Webhooks](https://support.plex.tv/articles/115002267687-webhooks) (restricted to Plex Pass users)
|
||||
|
||||
# Using Tautulli
|
||||
|
||||
## Create a new Notification Agent
|
||||
|
||||
* Navigate to the **Notification Agents** page in **Settings**
|
||||
* Click **Add a new notification agent**
|
||||
* Select **Webhook**
|
||||
|
||||
## Configure the Agent
|
||||
|
||||
The below sections correspond with the tabs available in the notification agent configuration popup.
|
||||
|
||||
### Configuration
|
||||
* Webhook URL -- `http://localhost:9078/tautulli` (substitute your domain if different than the default)
|
||||
* Webhook Method -- POST
|
||||
|
||||
### Triggers
|
||||
|
||||
Select **Watched**
|
||||
|
||||
### Conditions
|
||||
|
||||
Refer to [Tautulli's documentation](https://github.com/Tautulli/Tautulli-Wiki/wiki/Custom-Notification-Conditions) if you need help here. It may be a good idea to restrict notifications to only one library (if you have a Music library, for instance)
|
||||
|
||||
**This app will only scrobble an item if `media_type` is a "track", which is the default for all music.**
|
||||
|
||||
### Data
|
||||
|
||||
Expand the **Watched** dropdown and add the following code block to the **JSON Data** text field:
|
||||
|
||||
```
|
||||
{
|
||||
"artist_name": "{artist_name}",
|
||||
"track_name": "{track_name}",
|
||||
"track_artist": "{track_artist}",
|
||||
"album_name": "{album_name}",
|
||||
"media_type": "{media_type}",
|
||||
"title": "{title}",
|
||||
"duration": "{duration_sec}",
|
||||
"username": "{username}",
|
||||
"server": "{server_name}",
|
||||
"version": "{server_version}",
|
||||
"library": "{library_name}",
|
||||
"player": "{player}",
|
||||
"device": "{device}",
|
||||
"platform": "{platform}",
|
||||
"action": "{action}",
|
||||
"machine_id": "{machine_id}",
|
||||
"session_key": "{session_key}"
|
||||
}
|
||||
```
|
||||
|
||||
**Click the Save button to finish.**
|
||||
|
||||
Your agent is now configured and ready to scrobble.
|
||||
|
||||
# Using Plex Webhooks
|
||||
|
||||
* Navigate to your **Account/Settings** and find the **Webhooks** page
|
||||
* Click **Add Webhook**
|
||||
* URL -- `http://localhost:9078/plex` (substitute your domain if different than the default)
|
||||
* **Save Changes**
|
||||
|
||||
Plex is now configured to scrobble.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Scrobble Sources",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "configuration/sources/sources"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Azuracast
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example';
|
||||
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
|
||||
|
||||
:::tip[Scrobbling Threshold]
|
||||
|
||||
Azuracast may not correctly report track length or position. If this is the case you should reduce the "listened to" **duration** using [Scrobble Thresholds](/configuration/sources#scrobble-thresholds) so that multi-scrobbler scrobbles correctly.
|
||||
|
||||
:::
|
||||
|
||||
The Azuracast server should have **Use High-Performance Now Playing Updates** enabled in _Administration -> System Settings_
|
||||
|
||||
#### URL
|
||||
|
||||
The URL used by MS to connect to Azuracast has the syntax:
|
||||
|
||||
```
|
||||
[ws|wss]://HOST:[PORT]
|
||||
```
|
||||
|
||||
MS will automatically add the path required for websockets, [`/api/live/nowplaying/websocket`](https://www.azuracast.com/docs/developers/now-playing-data/#websockets), to your URL if none is provided. If you use a reverse proxy with a path-based URL or otherwise need a custom path to access the websockets path correctly then explicitly provide it. Examples:
|
||||
|
||||
```
|
||||
URL From Config => MS Uses
|
||||
|
||||
'ws://192.168.0.101' => ws://192.168.0.101/api/live/nowplaying/websocket
|
||||
'ws://azura.mydomain.com' => ws://azura.mydomain.com.com/api/live/nowplaying/websocket
|
||||
'wss://mydomain.com/custom/azura/ws' => wss://mydomain.com/custom/azura/ws
|
||||
```
|
||||
|
||||
#### Scrobble Behavior
|
||||
|
||||
:::info
|
||||
|
||||
<ShouldScrobble filename="azuracast"/>
|
||||
|
||||
:::
|
||||
|
||||
Multi-scrobbler will set the default **scrobbling behavior** based on if either of these settings is configured:
|
||||
|
||||
* live status (`AZURA_LIVE` or `monitorWhenLive`) or
|
||||
* listener count (`AZURA_LISTENERS_NUM` or `monitorWhenListeners`)
|
||||
|
||||
If either is configured then MS will automatically scrobble based on when these conditions are met.
|
||||
|
||||
If neither is configured automatic scrobble defaults to **off** and you should either manually initiate it or explicitly set the default behavior.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `AZ_URL` | Yes | | Azuracast URL *without station name* |
|
||||
| `AZ_STATION` | Yes | | The station name shown on the public page |
|
||||
| `AZURA_LIVE` | No | Yes | Only scrobble when station status is ONLINE |
|
||||
| `AZURA_LISTENERS_NUM` | No | `true` | Only scrobble if station has any listeners (`true`) or listeners are equal-to/greater-than `X` |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/azuracast.json" language="json5">{AzuracastConfig}</CodeBlock>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={AzuracastConfig} name="azuracast"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: Deezer
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import DeezerConfig from '!!raw-loader!@site/../config/deezer.json.example';
|
||||
import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecated.json.example';
|
||||
|
||||
<Tabs groupId="deezerSource" queryString>
|
||||
<TabItem value="arl" label="Unofficial ARL">
|
||||
:::warning
|
||||
|
||||
This Source uses unofficial methods to access Deezer data that are likely in violation of Deezer's TOS. Deezer may change or remove these methods at any time, breaking functionality. Use this Source at your own risk.
|
||||
|
||||
:::
|
||||
|
||||
:::info
|
||||
|
||||
This Source will work only if you have a **Premium** Deezer account.
|
||||
|
||||
:::
|
||||
|
||||
### Retrieve ARL
|
||||
|
||||
The `arl` is a browser cookie that Deezer uses to store your login information. Multi-scrobbler uses the `arl` value to make authorized requests to Deezer's API where it can retrieve listening history.
|
||||
|
||||
**After logging into Deezer**, use one of the methods below to retrieve the `arl` cookie value:
|
||||
|
||||
<Tabs groupId="arlRetrival">
|
||||
<TabItem value="chrome" label="Chrome">
|
||||
1. On deezer.com, press F12 to open the Developer Tools window.
|
||||
2. Go to the Applications tab. Press the '>>' More tabs icon if it's hidden.
|
||||
3. Under the Cookies Filter, click deezer.com, then the text bar name `arl`.
|
||||
4. Copy all the contents shown in the **Cookie Value** pane.
|
||||
</TabItem>
|
||||
<TabItem value="firefox" label="Firefox">
|
||||
1. On deezer.com, hit Ctrl+Shift+I to open Developer Tools
|
||||
2. Go to the Storage Tab, then expand Cookies in the sidebar and select deezer.com
|
||||
3. Find the row with `arl` as the name, then double click the Value column and right click -> copy the value
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={DeezerConfig} name="deezer"/>
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Duplicate detection
|
||||
|
||||
Third party integrations with Deezer (Sonos) may cause Deezer to [report the same track many times in listening history.](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2922374738) If you experience this issue modify your Deezer Source config (file-based only) to include the option `"fuzzyDiscoveryIgnore": "aggressive"`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>deezer.json example</summary>
|
||||
|
||||
```json title="deezer.json"
|
||||
[
|
||||
{
|
||||
"name": "MyDeezer",
|
||||
"data": {
|
||||
"arl": "UOsRPjT3U5Dhaaup3x...",
|
||||
},
|
||||
"options": {
|
||||
"fuzzyDiscoveryIgnore": "aggressive"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
</details>
|
||||
|
||||
This option comes with some trade-offs: MS will aggressively detect repeated tracks within a window of time that should eliminate all duplicates. However, this will also prevent *intentionally* repeated tracks from being scrobbled. See [this thread](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2970417070) for more information on how this works.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="official" label="Official API">
|
||||
|
||||
:::warning
|
||||
|
||||
**This Source is DEPRECATED because Deezer has dropped official API support.** This Source will **not** be removed but no further support or fixes will be given. [See this issue for more discussion.](https://github.com/FoxxMD/multi-scrobbler/issues/175#issuecomment-2296776625)
|
||||
|
||||
Users cannot create new applications on Deezer Developers and there is no guarantee existing applications will continue to work.
|
||||
|
||||
As a workaround consider:
|
||||
|
||||
* Using the alternative [Deezer Source that uses unofficial, internal API via ARL](./?deezerSource=arl)
|
||||
* Connect Deezer with Last.fm and then use [Last.fm as a Source](/configuration/sources/lastfm-source)
|
||||
|
||||
:::
|
||||
|
||||
Create a new application at [Deezer Developers](https://developers.deezer.com/myapps)
|
||||
|
||||
* Application Domain must be the same as your multi-scrobbler domain. Default is `localhost:9078`
|
||||
* Redirect URL must end in `deezer/callback`
|
||||
* Default would be `http://localhost:9078/deezer/callback`
|
||||
|
||||
After application creation you should have credentials displayed in the "My Apps" dashboard. You will need:
|
||||
|
||||
* **Application ID**
|
||||
* **Secret Key**
|
||||
* **Redirect URL** (if not the default)
|
||||
|
||||
**If no access token is provided...**
|
||||
|
||||
After starting multi-scrobbler with credentials in-place open the dashboard (`http://localhost:9078`) and find your Deezer source. Click **(Re)authenticate and (re)start polling** to start the login process. After login is complete polling will begin automatically.
|
||||
|
||||
###### Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|-----------------------------------------|-----------------------------------|
|
||||
| `DEEZER_CLIENT_ID` | Yes | | Your **Application ID** |
|
||||
| `DEEZER_CLIENT_SECRET` | Yes | | Your **Secret Key** |
|
||||
| `DEEZER_REDIRECT_URI` | No | `http://localhost:9078/deezer/callback` | URI must end in `deezer/callback` |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerDeprecatedConfig}</CodeBlock>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={DeezerDeprecatedConfig} name="deezer"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
title: Google Cast (Chromecast)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ChromecastConfig from '!!raw-loader!@site/../config/chromecast.json.example';
|
||||
|
||||
If your media device can be **Cast** to using this button  on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play.
|
||||
|
||||
|
||||
:::note
|
||||
|
||||
Google Cast support is **experimental**. You may experience crashes and errors while using this Source. Please open an issue if you experience problems and include all information detailed in the issue template to help debug your issue.
|
||||
|
||||
:::
|
||||
|
||||
:::note
|
||||
|
||||
This source relies on common, **basic** music data provided by the cast device which will always be less exhaustive than data parsed from full source integrations. If there is an existing [Source](/configuration/sources) it is recommended to configure for it and blacklist the app on Google Cast, rather than relying solely on Google Cast for scrobbling.
|
||||
|
||||
:::
|
||||
|
||||
#### Connecting Devices
|
||||
|
||||
Cast devices can be manually configured using file-based configuration OR automatically discovered using **mDNS.**
|
||||
|
||||
###### mDNS Discovery
|
||||
|
||||
The host machine running multi-scrobbler must be configured to allow [mDNS traffic on port 5353/UDP](https://book.hacktricks.xyz/network-services-pentesting/5353-udp-multicast-dns-mdns).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>OS Specific Instructions</summary>
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="linux" label="Linux">
|
||||
**Docker**
|
||||
|
||||
The host machine must have [avahi-daemon](https://avahi.org/) running to circumvent limitations with DNS resolution due to musl in Alpine. Most major linux distributions package avahi and many have it built-in. Once avahi is running you must pass D-Bus and the avahi daemon socket to your container like so:
|
||||
|
||||
```
|
||||
docker run ... -v /var/run/dbus:/var/run/dbus -v /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket ... foxxmd/multi-scrobbler
|
||||
```
|
||||
|
||||
**Flatpak/Nodejs**
|
||||
|
||||
No additional steps are required.
|
||||
</TabItem>
|
||||
<TabItem value="windows" label="Windows">
|
||||
**Docker**
|
||||
|
||||
Unsupported at this time.
|
||||
|
||||
**Nodejs**
|
||||
|
||||
No additional steps are required.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
#### What Media Does MS Scrobble?
|
||||
|
||||
Cast devices report what type of media the current activity is [(see `metadata` property here)](https://developers.google.com/cast/docs/media/messages#MediaInformation). The reported type is dependent on the application playing the media to correctly report it, the cast device does not magically know what the media is. If an application does not report a type it is always classified as `unknown`.
|
||||
|
||||
**By default, MS will only track media that is reported as `MusicTrack`.**
|
||||
|
||||
##### Allow Unknown Media Type
|
||||
|
||||
Media with an Unknown (`Generic`) media type can be explicitly allowed by setting `"allowUnknownMedia": true` in the file-based configuration. This can also be configured to only allow unknown media types for specific applications by using a list of application names.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="chromecast.json"
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"type": "chromecast",
|
||||
"data": {
|
||||
// only allow unknown if app name contains any of these phrases
|
||||
"allowUnknownMedia": ["smarttube", "default media receiver"]
|
||||
},
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
##### Forcing Media Tracking
|
||||
|
||||
MS can be forced to track media from an application regardless of media type. This is useful if an application incorrectly reports a media type you are sure should be music. Set `"forceMediaRecognitionOn"` in the file-based configuration to a list of application names that should always be tracked.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="chromecast.json"
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"type": "chromecast",
|
||||
"data": {
|
||||
// media from applications that contains these phrases will always be tracked, regardless of media type reported
|
||||
"forceMediaRecognitionOn": ["smarttube", "default media receiver"]
|
||||
},
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Cast Troubleshooting
|
||||
|
||||
Please include any/all logs with raw output if there are any errors encountered as this is critical to diagnosing issues.
|
||||
|
||||
To diagnose bad/incomplete track information or strange MS player behavior please turn on **payload logging** and include log output of the source running to help diagnose this issue. Either enable in config using the below example OR enable [Debug Mode.](/configuration#debug-mode)
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="chromecast.json"
|
||||
[
|
||||
{
|
||||
"name": "MyCast",
|
||||
"type": "chromecast",
|
||||
"data": {
|
||||
//...
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
:::note
|
||||
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
|
||||
:::
|
||||
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------------------------------------------------------|
|
||||
| CC_ENABLE | No | | Set to 'true' to enable Cast monitoring without needing to define other ENVs |
|
||||
| CC_WHITELIST_DEVICES | No | | Only scrobble from these Cast devices. Comma-delimited list. EX mini-home, family-tv |
|
||||
| CC_BLACKLIST_DEVICES | No | | Do not scrobble from these Cast devices. Comma-delimited list |
|
||||
| CC_WHITELIST_APPS | No | | Only scrobble from these casted Apps. Comma-delimited list. EX spotify, pandora |
|
||||
| CC_BLACKLIST_APPS | No | | Do not scrobble from these casted Apps. Comma-delimited list |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/chromecast.json" language="json5">{ChromecastConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<AIOExample data={ChromecastConfig} name="chromecast"/>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Icecast
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import IcecastConfig from '!!raw-loader!@site/../config/icecast.json.example';
|
||||
import ShouldScrobble from "@site/src/components/snippets/_should-scrobble.mdx"
|
||||
|
||||
Parse metadata from any Icecast Station (streams) to use for scrobbling.
|
||||
|
||||
:::note
|
||||
|
||||
There is no standard format for presenting track information in an Icecast stream and the majority of streams that do include metadata only use a single string (`StreamTitle`) from which metadata can be parsed.
|
||||
|
||||
Multi-scrobbler will do its best to parse Artist/Album/Track information from this data but the quality and consistency for this is solely based on how the Icecast broadcaster chooses to format their tracks.
|
||||
|
||||
:::
|
||||
|
||||
:::note[Scrobbling Behavior]
|
||||
|
||||
Multi-scrobbler cannot automatically determine if you are listening to a Station. Therefore, **automatic scrobbling for Icecast Sources is off by default.**
|
||||
|
||||
Use [Should Scrobble Behavior](/configuration/sources#should-scrobble-behavior) to initiate scrobbling or configure automatic scrobble.
|
||||
|
||||
:::
|
||||
|
||||
:::tip[Scrobbling Threshold]
|
||||
|
||||
Icecast does not report track lengths so multi-scrobbler cannot determine if it should scrobble based on percent played. You should instead set a shorter "listened to" **duration** using [Scrobble Thresholds.](/configuration/sources#scrobble-thresholds)
|
||||
|
||||
:::
|
||||
|
||||
|
||||
#### URL and Sources
|
||||
|
||||
The URL should be the URL you would use to stream audio.
|
||||
|
||||
Additional sources are supported based on those found in [`icecast-metadata-stats`](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#supported-status-apis). To use these with automatic parsing [based on the regular URL](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options) include a `sources` array in your config the same way as shown in `icecast-metadata-stats`:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
```json5 title="config.json"
|
||||
[
|
||||
{
|
||||
"name": "myIcy",
|
||||
"enable": true,
|
||||
"data": {
|
||||
"url": "http://myIcecast.com/radio",
|
||||
"sources": ["icy", "ogg", "sevenhtml"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
To manually set these sources include the same [`*Endpoint` options](https://github.com/eshaz/icecast-metadata-js/tree/main/src/icecast-metadata-stats#options) found in `icecast-metadata-stats`.
|
||||
|
||||
If no `sources` are configured for multi-scrobbler it will automatically try to use `icy` and `ogg`.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | :------ | ----------- |
|
||||
| `ICECAST_URL` | Yes | | Icecast URL |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/icecast.json" language="json5">{IcecastConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="IcecastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<AIOExample data={IcecastConfig} name="icecast"/>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="IcecastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Jellyfin
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JellyfinConfig from '!!raw-loader!@site/../config/jellyfin.json.example';
|
||||
|
||||
Must be using Jellyfin 10.7 or greater
|
||||
|
||||
* Create an **API Key** for multi-scrobbler
|
||||
* In the Jellyfin desktop web UI Navigate to -> Administration -> Dashboard -> API Keys (`http://YOUR_JELLYIN_URL/web/index.html#!/apikeys.html`)
|
||||
* Click `+` button and create a new key with **App name** `multi-scrobbler`
|
||||
* Copy the created API Key value for use in configuration below
|
||||
|
||||
|
||||
It is **recommended** to use API Key + username but if you are not an admin for your Jellyfin instance you can also authenticate with your Jellyfin username and **password.**
|
||||
|
||||
:::tip[Important Defaults]
|
||||
|
||||
By default...
|
||||
|
||||
* multi-scrobbler will **only** scrobble for the user authenticated with the API.
|
||||
* Allowed Users (`usersAllow` or `JELLYFIN_USERS_ALLOW`) are only necessary if you want to scrobble for additional users.
|
||||
* multi-scrobbler will **only** scrobble media found in Jellyfin libraries that were labelled as **Music.**
|
||||
* `librariesAllow` or `JELLYFIN_LIBRARIES_ALLOW` will override this
|
||||
* OR use `additionalAllowedLibraryTypes` to allow more types (like `mixed` or `book` for audiobooks)
|
||||
* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio.**
|
||||
* To force multi-scrobbler to scrobble when media is detected as **Unknown** use `"allowUnknown": true` in file/aio configuration.
|
||||
|
||||
:::
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| -------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ |
|
||||
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
|
||||
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
|
||||
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
|
||||
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
|
||||
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
|
||||
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/jellyfin.json" language="json5">{JellyfinConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={JellyfinConfig} name="jellyfin"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: JRiver
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JriverConfig from '!!raw-loader!@site/../config/jriver.json.example';
|
||||
|
||||
In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:
|
||||
|
||||
* Tools -> Options -> Media Network
|
||||
* Check `Use Media Network to share this library...`
|
||||
* If you have `Authentication` checked you will need to provide the **Username** and **Password** in the ENV/File configuration below.
|
||||
|
||||
### URL
|
||||
|
||||
The URL used to connect to JRiver, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
|
||||
|
||||
```
|
||||
Syntax => [protocol]://[hostname]:[port]/[path]
|
||||
Default => http://localhost:52199/MCWS/v1/
|
||||
```
|
||||
|
||||
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
|
||||
|
||||
:::tip[Jriver Settings]
|
||||
|
||||
* Make sure the port number matches what is found in `Advanced` section in the Media Network options.
|
||||
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
|
||||
|
||||
:::
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to JRiver:
|
||||
|
||||
| `url` | Transformed URL |
|
||||
|------------------------------------------|---------------------------------------------|
|
||||
| (none set) | `http://localhost:52199/MCWS/v1/` |
|
||||
| `jriver.mydomain.com` | `http://jriver.mydomain.com:52199/MCWS/v1/` |
|
||||
| `192.168.0.101:3456` | `http://192.168.0.101:3456/MCWS/v1/` |
|
||||
| `mydomain.com:80/jriverReverse/MCWS/v1/` | `mydomain.com:80/jriverReverse/MCWS/v1/` |
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<EnvType>
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|---------------------------------|------------------------------------------------|
|
||||
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
|
||||
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
|
||||
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
|
||||
</EnvType>
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<FileType>
|
||||
<CodeBlock title="CONFIG_DIR/jriver.json" language="json5">{JriverConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="JRiverSourceConfig"/>
|
||||
</FileType>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<AIOType>
|
||||
<AIOExample data={JriverConfig} name="jriver"/>
|
||||
|
||||
or <SchemaLink lower objectName="JRiverSourceConfig"/>
|
||||
</AIOType>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Kodi
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import KodiConfig from '!!raw-loader!@site/../config/kodi.json.example';
|
||||
import Config from '@site/src/components/GenericConfiguration.mdx';
|
||||
|
||||
In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can be done in the Kodi GUI:
|
||||
|
||||
* Settings -> Services -> Control
|
||||
* Check `Allow remote control via HTTP`
|
||||
* Ensure you have a **Username** and **Password** set, you will need to provide them in the ENV/File configuration below.
|
||||
|
||||
### URL
|
||||
|
||||
The URL used to connect to Kodi, set in your [file config as `url`.](#configuration) The URL used to connect ultimately must be formed like this:
|
||||
|
||||
```
|
||||
Syntax => [protocol]://[hostname]:[port]/[path]
|
||||
Default => http://localhost:8080/jsonrpc
|
||||
```
|
||||
|
||||
If any part of this URL is missing multi-scrobbler will use a default value. This also means that if any part of your URL is **not** standard you must explicitly define it.
|
||||
|
||||
:::tip[Kodi Settings]
|
||||
|
||||
* Make sure the port number matches what is found in **Port** in the Control section mentioned above.
|
||||
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.
|
||||
|
||||
:::
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
Examples of a given `url` in your file config and the final URL multi-scrobbler will use to connect to Kodi:
|
||||
|
||||
| `url` | Transformed URL |
|
||||
|---------------------------------------|----------------------------------------------|
|
||||
| (none set) | `http://localhost:8080/jsonrpc` |
|
||||
| `kodi.mydomain.com` | `http://kodi.mydomain.com:8080/jsonrpc` |
|
||||
| `192.168.0.101:3456` | `http://192.168.0.101:3456/jsonprc` |
|
||||
| `mydomain.com:80/kodiReverse/jsonrpc` | `http://mydomain.com:80/kodiReverse/jsonrpc` |
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="KodiSourceConfig" fileContent={KodiConfig} name="kodi">
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|-------------------------------|----------------------------|
|
||||
| KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
|
||||
| KODI_USERNAME | No | | The username set |
|
||||
| KODI_PASSWORD | No | | The password set |
|
||||
</Config>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Koito
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import KoitoConfig from '!!raw-loader!@site/../config/koito.json.example';
|
||||
|
||||
This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured [Clients.](/configuration/clients)
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
To _scrobble to_ a Koito server, create a [Koito (Client)](/configuration/clients/koito)
|
||||
|
||||
:::
|
||||
|
||||
See the [Koito (Client)](/configuration/clients/koito) configuration for general setup. The only difference for **Source** configuration:
|
||||
|
||||
* Cannot be setup with ENV config
|
||||
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Config config="KoitoSourceConfig" fileContent={KoitoConfig} name="koito">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Koito Client config](/configuration/clients/koito) -- multi-scrobbler assumes Koito ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Koito as a Source.
|
||||
:::
|
||||
</Config>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Lastfm (Endpoint)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import LastfmEndpointConfig from '!!raw-loader!@site/../config/endpointlfm.json.example';
|
||||
|
||||
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server.
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
|
||||
* To monitor and re-scrobble activity from your Last.fm account, create a [Last.fm (Source)](/configuration/sources/lastfm-source)
|
||||
|
||||
:::
|
||||
|
||||
### URL
|
||||
|
||||
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Last.fm scrobbles at
|
||||
|
||||
```
|
||||
http://localhost:9078/2.0/
|
||||
```
|
||||
|
||||
which is the "standard" Last.fm server path for scrobbling
|
||||
|
||||
Use a slug only if you need to setup multiple Last.fm Endpoint sources and cannot use different tokens.
|
||||
|
||||
If a slug is used then the URL will be:
|
||||
|
||||
```
|
||||
http://localhost:9078/api/lastfm/mySlug
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="LastFMEndpointSourceConfig" fileContent={LastfmEndpointConfig} name="endpointlfm">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `LFMENDPOINT_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
|
||||
| `LFM_SLUG` | No | | (Optional) The URL suffix to use for accepting LFM scrobbles |
|
||||
</Config>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Lastfm
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import LastfmConfig from '!!raw-loader!@site/../config/lastfm.json.example';
|
||||
|
||||
This Source **monitors your Last.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients)
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
* To **scrobble to** Last.fm, create a [Last.fm (Client)](/configuration/clients/lastfm)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was Last.fm, create a [Last.fm (Endpoint)](/configuration/sources/lastfm-endpoint)
|
||||
|
||||
:::
|
||||
|
||||
To configure this Source use the same registration instructions and configuration data shown in [Last.fm (Client)](/configuration/clients/lastfm) with the difference being `"configureAs": "source"` defined in the File/AIO configuration below.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="LastfmSourceConfig" fileContent={LastfmConfig} name="lastfm">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Last.fm Client config](/configuration/clients/lastfm) -- multi-scrobbler assumes Last.fm ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Last.fm as a Source.
|
||||
:::
|
||||
</Config>
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Listenbrainz (Endpoint)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ListenbrainzEndpointConfig from '!!raw-loader!@site/../config/endpointlz.json.example';
|
||||
|
||||
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
* To **scrobble to** Listenbrainz, create a [Listenbrainz (Client)](/configuration/clients/listenbrainz)
|
||||
* To monitor and re-scrobble activity from your Listenbrainz account, create a [Listenbrainz (Source)](/configuration/sources/listenbrainz-source)
|
||||
|
||||
:::
|
||||
|
||||
### URL
|
||||
|
||||
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Listenbrainz scrobbles at
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/1/submit-listens
|
||||
```
|
||||
|
||||
which is the "standard" Listenbrainz server path for scrobbling.
|
||||
|
||||
In general, you should use `http://myMultiScrobblerIP:9078/1/` as the **base path** when configuring your application to scrobble to multi-scrobbler.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>URL Setup for Popular Apps</summary>
|
||||
|
||||
##### Navidrome
|
||||
|
||||
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
</details>
|
||||
|
||||
Use a slug only if you need to setup multiple Listenbrainz Endpoint sources and cannot use different tokens.
|
||||
|
||||
If a slug is used then the URL will be:
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/api/listenbrainz/mySlug
|
||||
```
|
||||
|
||||
:::note
|
||||
Some Listenbrainz applications may require custom Listenbrainz URLs to be a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
|
||||
:::
|
||||
|
||||
### Token
|
||||
|
||||
Most Listenbrainz clients require a token (Authentication Token) to be provided during setup. This value can be anything you want, just make sure to use the same value for `token` in your multi-scrobbler configuration for the endpoint.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenbrainzEndpointSourceConfig" fileContent={ListenbrainzEndpointConfig} name="endpointlz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `LZENDPOINT_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
|
||||
| `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application |
|
||||
| `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles |
|
||||
</Config>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Listenbrainz
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ListenbrainzConfig from '!!raw-loader!@site/../config/listenbrainz.json.example';
|
||||
|
||||
This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients.
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
* To **scrobble to** a Listenbrainz server, create a [Listenbrainz (Client)](/configuration/clients/listenbrainz)
|
||||
* To accept scrobbles from outside applications as if multi-scrobbler was a Listenbrainz server, create a [Listenbrainz (Endpoint)](/configuration/sources/listenbrainz-endpoint)
|
||||
|
||||
:::
|
||||
|
||||
You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/).
|
||||
|
||||
On your [profile page](https://listenbrainz.org/profile/) find your **User Token** to use in the configuration.
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenBrainzSourceConfig" fileContent={ListenbrainzConfig} name="listenbrainz">
|
||||
:::note
|
||||
You cannot use ENV variables shown in the [Listenbrainz Client config](/configuration/clients/listenbrainz) -- multi-scrobbler assumes Listenbrainz ENVs are always used for the **client** configuration. You must use the [File or AIO](/configuration?configType=file#configuration-types) config to setup Listenbrainz as a Source.
|
||||
:::
|
||||
</Config>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user