mirror of
https://github.com/owntone/owntone-server.git
synced 2025-10-30 00:05:05 -04:00
Compare commits
1387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49171dac1a | ||
|
|
09b9b0c7fc | ||
|
|
6f45f8b4a5 | ||
|
|
921d4446d6 | ||
|
|
bf598153f3 | ||
|
|
c1bdac931e | ||
|
|
a1e4982c0b | ||
|
|
352a73044e | ||
|
|
5f526c7a7e | ||
|
|
b7e385ffe0 | ||
|
|
2eba24b4ba | ||
|
|
336200727d | ||
|
|
b523ea4d35 | ||
|
|
bd5746c83e | ||
|
|
b58307cc37 | ||
|
|
387660d96b | ||
|
|
15fd59b2a1 | ||
|
|
82c8374cad | ||
|
|
1bdf4680ff | ||
|
|
b1d7e4c433 | ||
|
|
dc0eb24e7f | ||
|
|
2dd693c0f2 | ||
|
|
bb64df57ff | ||
|
|
3f0041100a | ||
|
|
9a721e49ab | ||
|
|
ec632e478c | ||
|
|
19012bf616 | ||
|
|
d051d787ba | ||
|
|
d8485bf3c2 | ||
|
|
753a027ce1 | ||
|
|
a84f4e09a0 | ||
|
|
25d2af9343 | ||
|
|
f85a800644 | ||
|
|
ba8212b175 | ||
|
|
d16343bddc | ||
|
|
3be8e4f479 | ||
|
|
e607019a1c | ||
|
|
324b6eb61a | ||
|
|
84d1b091ff | ||
|
|
65b9323488 | ||
|
|
2ddaba6e77 | ||
|
|
5b013115ba | ||
|
|
efbf950068 | ||
|
|
b9d821b46a | ||
|
|
411e028f9f | ||
|
|
569e48ba7d | ||
|
|
31ff67797b | ||
|
|
3bf17d8b6d | ||
|
|
02279236f3 | ||
|
|
13f4c087e8 | ||
|
|
ea6388b51e | ||
|
|
58593809f9 | ||
|
|
f7c4659899 | ||
|
|
2d5dd3d7fd | ||
|
|
978a9b6a96 | ||
|
|
b612e12aca | ||
|
|
75c9db5f59 | ||
|
|
7b91d43274 | ||
|
|
6a21cad6fd | ||
|
|
f8a9b92504 | ||
|
|
5e4d40ee03 | ||
|
|
6d604a176a | ||
|
|
34eedf4d1b | ||
|
|
0017c9cace | ||
|
|
d53856ee63 | ||
|
|
5200c8289f | ||
|
|
c09026f7c3 | ||
|
|
6028c39408 | ||
|
|
ce59d36a96 | ||
|
|
0cfd753770 | ||
|
|
939dab6a48 | ||
|
|
482a5bdafc | ||
|
|
227db7d502 | ||
|
|
03e54140d7 | ||
|
|
78a1137510 | ||
|
|
c1ffbca09b | ||
|
|
051498861e | ||
|
|
ef683f5b02 | ||
|
|
5dc748baf5 | ||
|
|
4bd8736346 | ||
|
|
781110659a | ||
|
|
a7d4501632 | ||
|
|
5018cc4544 | ||
|
|
99ef7b8dfc | ||
|
|
e476032776 | ||
|
|
60b688a182 | ||
|
|
4872bfd7fd | ||
|
|
e80e58f0fd | ||
|
|
9c61ee5158 | ||
|
|
4dc6754726 | ||
|
|
fc24c2279f | ||
|
|
5f2785171c | ||
|
|
0c7e94b903 | ||
|
|
4531eaa75f | ||
|
|
a0d2ddcdc2 | ||
|
|
36d8161a37 | ||
|
|
36736e03a2 | ||
|
|
91bac1273b | ||
|
|
4adb623c3f | ||
|
|
eb33a25ce7 | ||
|
|
f62c5c06a8 | ||
|
|
02625ff67a | ||
|
|
b251a4e418 | ||
|
|
78ffba97d8 | ||
|
|
04c119a3fd | ||
|
|
59050c1018 | ||
|
|
4154a20cda | ||
|
|
3ccd117812 | ||
|
|
3c7e5d404b | ||
|
|
af632f4304 | ||
|
|
708370aab9 | ||
|
|
b5fe530f0d | ||
|
|
8bc8a2c9d8 | ||
|
|
3b336657d4 | ||
|
|
c2460d2f5f | ||
|
|
4b1617971e | ||
|
|
b88df4b4e8 | ||
|
|
f91189d93b | ||
|
|
7193d0a243 | ||
|
|
6b5f4ff4d7 | ||
|
|
82fb9a11b6 | ||
|
|
3eb55620db | ||
|
|
f830949af1 | ||
|
|
d010f8f655 | ||
|
|
e2ae67c021 | ||
|
|
136732b024 | ||
|
|
775eac28a5 | ||
|
|
f3284e03ed | ||
|
|
468e9d8ba7 | ||
|
|
5c90442161 | ||
|
|
b4a73ff344 | ||
|
|
8048bb15ff | ||
|
|
c34c5eb585 | ||
|
|
a229474da7 | ||
|
|
ae59d23660 | ||
|
|
dbfc727b21 | ||
|
|
b52fd89474 | ||
|
|
0c7d6d7770 | ||
|
|
19399f3c08 | ||
|
|
a4086ee314 | ||
|
|
2c517ae8a6 | ||
|
|
858e49bdb3 | ||
|
|
7548e4e059 | ||
|
|
30fdcd4427 | ||
|
|
5b29d43e5b | ||
|
|
ae5be5f83e | ||
|
|
b8a8899868 | ||
|
|
216b6268c7 | ||
|
|
4ecb19724a | ||
|
|
ae50fe548f | ||
|
|
ce3db11cfd | ||
|
|
80b9d8d648 | ||
|
|
3677f9d757 | ||
|
|
895f8376fd | ||
|
|
ad143e88c2 | ||
|
|
f037635042 | ||
|
|
4577c7ace3 | ||
|
|
7dd41179db | ||
|
|
eade83d381 | ||
|
|
786d8cbc09 | ||
|
|
95de42e6be | ||
|
|
56c9408ef9 | ||
|
|
c38f35d3f9 | ||
|
|
bbf7c28349 | ||
|
|
1ce771c900 | ||
|
|
2b6a756740 | ||
|
|
6091ae31aa | ||
|
|
40c658cb8b | ||
|
|
23c67a3eb6 | ||
|
|
180f7393a4 | ||
|
|
830b40cc40 | ||
|
|
fc071513f9 | ||
|
|
e35afb6474 | ||
|
|
b923601823 | ||
|
|
5c89fa0882 | ||
|
|
558814f91f | ||
|
|
3b01f0fc64 | ||
|
|
82933f0afb | ||
|
|
8e9e939e49 | ||
|
|
2e4e741e9a | ||
|
|
f5aecdc4a4 | ||
|
|
a4e09b27f4 | ||
|
|
0aedfe1ad4 | ||
|
|
24017f21f9 | ||
|
|
1c5425ba2a | ||
|
|
e7fcf7dd80 | ||
|
|
582074e8ff | ||
|
|
4f5e702f5d | ||
|
|
30323712f9 | ||
|
|
2a0badfea4 | ||
|
|
42564905e0 | ||
|
|
31ccda3f60 | ||
|
|
d8c3631cd7 | ||
|
|
0d00df7b7d | ||
|
|
709b60858d | ||
|
|
ed8e5710f3 | ||
|
|
f108e531bb | ||
|
|
d99342e586 | ||
|
|
be44c0ce9f | ||
|
|
15e8854349 | ||
|
|
ed266e3b30 | ||
|
|
b11865289a | ||
|
|
2b8bbb774f | ||
|
|
357657f586 | ||
|
|
722307653a | ||
|
|
9bea7fef7d | ||
|
|
9b4d04000f | ||
|
|
4b75ab4ae7 | ||
|
|
fa1f10fae9 | ||
|
|
13a8f71c0c | ||
|
|
59b680db9b | ||
|
|
9e2c9fddcb | ||
|
|
2e38df1c40 | ||
|
|
3cb26a8b77 | ||
|
|
a968401d9f | ||
|
|
6481d6f0ee | ||
|
|
7e8672917e | ||
|
|
6c09457e5d | ||
|
|
8d2b4b925d | ||
|
|
7d15faff66 | ||
|
|
444ac4342d | ||
|
|
da30c338fc | ||
|
|
905d0ca88b | ||
|
|
c22372daa6 | ||
|
|
966d563418 | ||
|
|
368bb18aa8 | ||
|
|
23f624aec5 | ||
|
|
eecc915a6a | ||
|
|
296dbe7a78 | ||
|
|
aa9ca1b4ae | ||
|
|
0d981d0ae1 | ||
|
|
1c79273d76 | ||
|
|
f47a636b99 | ||
|
|
9af5e74047 | ||
|
|
4fd02c36ef | ||
|
|
4a616b7e10 | ||
|
|
bdad6d61bf | ||
|
|
ef3e64b9c9 | ||
|
|
0b86cc18c7 | ||
|
|
43f4a23b1e | ||
|
|
ad2ec2252f | ||
|
|
f94763d985 | ||
|
|
d137b8d157 | ||
|
|
173e3fb8a1 | ||
|
|
fef602de2f | ||
|
|
4694d46508 | ||
|
|
173139515f | ||
|
|
aab6f6c718 | ||
|
|
b068b5f745 | ||
|
|
07afe390f7 | ||
|
|
f587a78e22 | ||
|
|
68a5254efb | ||
|
|
069c00ce30 | ||
|
|
51b76d0b73 | ||
|
|
bc2fa02589 | ||
|
|
3682fdb269 | ||
|
|
4980218fc5 | ||
|
|
038c741052 | ||
|
|
2547336576 | ||
|
|
aab49945d4 | ||
|
|
b5977b5633 | ||
|
|
3f9e400dbd | ||
|
|
deeeaad3b4 | ||
|
|
3eb9a72073 | ||
|
|
f8cc5edea4 | ||
|
|
b8bd0ee847 | ||
|
|
09c83768b1 | ||
|
|
c13e6ad672 | ||
|
|
bf2e468350 | ||
|
|
acbc335897 | ||
|
|
675b090c0b | ||
|
|
1db7b53df0 | ||
|
|
d09cdfe582 | ||
|
|
35ac036d92 | ||
|
|
1fbaf96ebd | ||
|
|
b01e644ccf | ||
|
|
17d48a379a | ||
|
|
b578926d77 | ||
|
|
05d9447c3c | ||
|
|
56aee6ad05 | ||
|
|
369e27fbed | ||
|
|
45f7defa09 | ||
|
|
7eac8adb83 | ||
|
|
8cfb3db6dd | ||
|
|
222e6ea2d7 | ||
|
|
9b56727361 | ||
|
|
0c9a803b53 | ||
|
|
d64c30b5ab | ||
|
|
200d9abc43 | ||
|
|
5acf9dd336 | ||
|
|
5fec4bbe34 | ||
|
|
b460be8873 | ||
|
|
b5c7dfaf59 | ||
|
|
7cde752f20 | ||
|
|
d16373d711 | ||
|
|
05b0def840 | ||
|
|
3091290677 | ||
|
|
d26e9fd5c4 | ||
|
|
59559847ac | ||
|
|
c72dc48b1d | ||
|
|
330023c940 | ||
|
|
1abc75362e | ||
|
|
3d27b1c25a | ||
|
|
66de2f4a96 | ||
|
|
aaea7135a0 | ||
|
|
f54b2e5b0b | ||
|
|
55720b8182 | ||
|
|
eca8f40afc | ||
|
|
26a03a1219 | ||
|
|
8526268d70 | ||
|
|
a82c80eb65 | ||
|
|
6b0e57c221 | ||
|
|
ed81d32ab3 | ||
|
|
c5cb67ff07 | ||
|
|
570c663178 | ||
|
|
b477121dda | ||
|
|
24d2204fb0 | ||
|
|
cb74bc17be | ||
|
|
30b9653c69 | ||
|
|
110de022a1 | ||
|
|
cc5ef9bce8 | ||
|
|
555c08cfbe | ||
|
|
317df2454d | ||
|
|
b7af43873b | ||
|
|
a93777aeec | ||
|
|
d6d5912de1 | ||
|
|
97d0e90408 | ||
|
|
aae2904a57 | ||
|
|
792e04b47d | ||
|
|
6f818b917c | ||
|
|
fcb8d67859 | ||
|
|
e3c8d1fab9 | ||
|
|
b9b36855f4 | ||
|
|
ea7efdd869 | ||
|
|
714fc4e1b8 | ||
|
|
bb8b2a72e4 | ||
|
|
a1e68a1aa6 | ||
|
|
8e82fc9f9f | ||
|
|
aa14e0ea4d | ||
|
|
461d5497cf | ||
|
|
76d517ecbb | ||
|
|
612c62dcb8 | ||
|
|
6354f9972a | ||
|
|
4796963781 | ||
|
|
5cbc520712 | ||
|
|
71d48452d8 | ||
|
|
e9ed220853 | ||
|
|
8140e008f0 | ||
|
|
591a0b6b83 | ||
|
|
991ed0e765 | ||
|
|
7760554cb7 | ||
|
|
9283d0c3c2 | ||
|
|
f6ffa321bb | ||
|
|
1d529e436f | ||
|
|
6fbf1f0ac5 | ||
|
|
2e019690cd | ||
|
|
01dbda3ec0 | ||
|
|
e51cb9e71d | ||
|
|
b2fbbd3fa0 | ||
|
|
12321a30da | ||
|
|
b79d0c9f9f | ||
|
|
d6e24f9117 | ||
|
|
8a177ed48d | ||
|
|
614bcaa630 | ||
|
|
6895df17ac | ||
|
|
7ba083bf8b | ||
|
|
8b05411427 | ||
|
|
bb2a778b46 | ||
|
|
a4cc4fde8c | ||
|
|
ca865ecbe0 | ||
|
|
afee011fe8 | ||
|
|
e83ad3e3d2 | ||
|
|
01ca2edc96 | ||
|
|
386ad61bc8 | ||
|
|
9420c52bf7 | ||
|
|
2fe6969f72 | ||
|
|
9fbd07a75d | ||
|
|
ce3c617023 | ||
|
|
a2d56df416 | ||
|
|
c2ad43da93 | ||
|
|
a454f062bb | ||
|
|
91175dc905 | ||
|
|
eca99f120a | ||
|
|
a3ab301cff | ||
|
|
fd322a2941 | ||
|
|
a51da62ca4 | ||
|
|
c7432e6bca | ||
|
|
410d3a0cfa | ||
|
|
76b5da0f0d | ||
|
|
f986eedb25 | ||
|
|
464f87a8db | ||
|
|
3c98ca7928 | ||
|
|
6c7b568e49 | ||
|
|
795c27bdf9 | ||
|
|
e46bf3db0c | ||
|
|
53984fec46 | ||
|
|
2e10ef4266 | ||
|
|
1b2c51bc5e | ||
|
|
d15c91a240 | ||
|
|
9f1a1b3c14 | ||
|
|
8003199fa4 | ||
|
|
bb80ca1c62 | ||
|
|
1810be023d | ||
|
|
40f43840f2 | ||
|
|
d0ff361ae0 | ||
|
|
9a513e41c7 | ||
|
|
66d52d06ab | ||
|
|
e43afc853a | ||
|
|
fba51fb93b | ||
|
|
72ef58d853 | ||
|
|
5571fa628d | ||
|
|
2619f45ab5 | ||
|
|
df86df02dc | ||
|
|
04bca06805 | ||
|
|
7ed66079f2 | ||
|
|
3999449c89 | ||
|
|
0483aad095 | ||
|
|
269aed632f | ||
|
|
6eb5667b4a | ||
|
|
1019b777c3 | ||
|
|
d57b7565da | ||
|
|
08b1b74ddd | ||
|
|
40f2bb5d81 | ||
|
|
ed526f7fe3 | ||
|
|
277e5001a0 | ||
|
|
b46a035a2f | ||
|
|
1e0fffc624 | ||
|
|
3093bfca62 | ||
|
|
f1b4ff8cc7 | ||
|
|
f8e2298b67 | ||
|
|
b50616a065 | ||
|
|
7c6252a108 | ||
|
|
d93f51a6cc | ||
|
|
14766e63cb | ||
|
|
686a453fc5 | ||
|
|
11fc7d3962 | ||
|
|
8aaafe0fc3 | ||
|
|
8c2b44fc6c | ||
|
|
f4ac6f9c1c | ||
|
|
450a333fd6 | ||
|
|
fbdc114288 | ||
|
|
2497fe89df | ||
|
|
d9cd0f4142 | ||
|
|
504c056c87 | ||
|
|
dbaeb7bdca | ||
|
|
158b043f55 | ||
|
|
024aadfe0a | ||
|
|
a93352d993 | ||
|
|
383a58d546 | ||
|
|
ad3be7d2f9 | ||
|
|
4c5888e8a8 | ||
|
|
289e7dcdce | ||
|
|
6ee5911729 | ||
|
|
fde798d5f5 | ||
|
|
b922a6a1a1 | ||
|
|
44896d82f4 | ||
|
|
0e3490e589 | ||
|
|
a415e619af | ||
|
|
2d0747dbe9 | ||
|
|
02307e86cd | ||
|
|
16a76fdc58 | ||
|
|
7c8b787afb | ||
|
|
e891b5d24c | ||
|
|
218d77e070 | ||
|
|
fab5ef505e | ||
|
|
fb067b5cdc | ||
|
|
d6448ada68 | ||
|
|
46dabafbdc | ||
|
|
cdbce17731 | ||
|
|
d1c2f0f9fd | ||
|
|
66b11c96e2 | ||
|
|
ea9df4f8ee | ||
|
|
42069405d6 | ||
|
|
f2e4819565 | ||
|
|
27c9224f69 | ||
|
|
07a5e6858d | ||
|
|
c0192cc1f6 | ||
|
|
ef2740a6dd | ||
|
|
3c85e540b9 | ||
|
|
4b0e3b260c | ||
|
|
d7cbb46264 | ||
|
|
83f95e0381 | ||
|
|
2f8b007d32 | ||
|
|
74ce03deed | ||
|
|
861a18f4e3 | ||
|
|
a79ce01ae1 | ||
|
|
4748e6adb5 | ||
|
|
fd0060b199 | ||
|
|
877efd2aba | ||
|
|
b39dfefd72 | ||
|
|
3b28960675 | ||
|
|
12f728629f | ||
|
|
750f83b7e0 | ||
|
|
5d7e3dc090 | ||
|
|
880f5b2bf6 | ||
|
|
8ae25aaf3e | ||
|
|
09a70ad993 | ||
|
|
7da811e3b3 | ||
|
|
75a1082b0a | ||
|
|
ebec99cc9d | ||
|
|
601f5a7657 | ||
|
|
81cf713a83 | ||
|
|
b9fa790f50 | ||
|
|
73864e82cd | ||
|
|
ba4b2c8ddd | ||
|
|
17ef308489 | ||
|
|
a983302b03 | ||
|
|
dc41f0d84c | ||
|
|
164d6ac9b2 | ||
|
|
66c2873d32 | ||
|
|
75222cafd3 | ||
|
|
94ce56d7b1 | ||
|
|
1e485f7d28 | ||
|
|
6b55ee2890 | ||
|
|
783f918c5e | ||
|
|
4b8ecfe18d | ||
|
|
e1628ff1a9 | ||
|
|
e9485d34ae | ||
|
|
1bee7e0d4b | ||
|
|
3c99e5a35c | ||
|
|
87ec17c243 | ||
|
|
3127da51b1 | ||
|
|
5c19d7d579 | ||
|
|
c8e4862245 | ||
|
|
8f627f1df0 | ||
|
|
c377ae3a64 | ||
|
|
3d9cec4ded | ||
|
|
e0a2ab159e | ||
|
|
e12ab3dd08 | ||
|
|
2bf0505cca | ||
|
|
e82340247d | ||
|
|
40c22e3d2f | ||
|
|
ecab3266ce | ||
|
|
ab63f8f834 | ||
|
|
f08de1fc32 | ||
|
|
f54a435d15 | ||
|
|
028adbaa1c | ||
|
|
f3a656d313 | ||
|
|
f02038f5e9 | ||
|
|
12eaa85c74 | ||
|
|
ee6f81a618 | ||
|
|
8b586728b6 | ||
|
|
ed16cc7928 | ||
|
|
92495a7fac | ||
|
|
282e227c64 | ||
|
|
a0d7c1a34f | ||
|
|
42ac0528a9 | ||
|
|
c0331f527e | ||
|
|
8eae74257d | ||
|
|
8f3c99ec43 | ||
|
|
5e68381fe4 | ||
|
|
b2a957cdec | ||
|
|
d672332750 | ||
|
|
297de1409a | ||
|
|
dab9089f8e | ||
|
|
96cd401852 | ||
|
|
56cea74f38 | ||
|
|
cdd81af979 | ||
|
|
2f21e91610 | ||
|
|
cabd8a3497 | ||
|
|
def9c7c513 | ||
|
|
5924e001df | ||
|
|
1129e65f61 | ||
|
|
74f82c396e | ||
|
|
cab0204d29 | ||
|
|
59bba5e261 | ||
|
|
90a79090ea | ||
|
|
53b06e26e3 | ||
|
|
9fe89a028e | ||
|
|
13131f43ef | ||
|
|
be1bacf278 | ||
|
|
bcdd3b2f65 | ||
|
|
f87b65f086 | ||
|
|
d9818434a0 | ||
|
|
fc0c6cca77 | ||
|
|
5542492d33 | ||
|
|
7ddb4e9bbb | ||
|
|
d6d46de399 | ||
|
|
82a0e77eb6 | ||
|
|
4cbce79a0f | ||
|
|
7dd34792ea | ||
|
|
088c393dd6 | ||
|
|
2d9200fcdf | ||
|
|
ff2d0b4ab1 | ||
|
|
62b42ce354 | ||
|
|
9f719ca155 | ||
|
|
c079df5da7 | ||
|
|
2efad1466f | ||
|
|
4a08644806 | ||
|
|
9dbec4b99e | ||
|
|
725419d4ac | ||
|
|
aed74fbb8a | ||
|
|
c30f44fd01 | ||
|
|
bf73e51262 | ||
|
|
2219e3ce75 | ||
|
|
3af04afa61 | ||
|
|
a5a991e1fa | ||
|
|
325a3609a0 | ||
|
|
2d59762520 | ||
|
|
390c335562 | ||
|
|
e7459e0576 | ||
|
|
9439bcc60c | ||
|
|
9f0fa7c45c | ||
|
|
f8d42a2fef | ||
|
|
f6ee669b80 | ||
|
|
b6ee09925b | ||
|
|
75b8f06e25 | ||
|
|
75fe3f100a | ||
|
|
2bb97c8e0a | ||
|
|
d951957730 | ||
|
|
408057a45d | ||
|
|
84d728d46e | ||
|
|
17d1ceef07 | ||
|
|
9319118190 | ||
|
|
2d7776619f | ||
|
|
2c58351bec | ||
|
|
7edce91474 | ||
|
|
4ae2903b4d | ||
|
|
c255b3a108 | ||
|
|
fc5563fe9d | ||
|
|
7d59abee4f | ||
|
|
4a1b4575fe | ||
|
|
5db55f66c1 | ||
|
|
de847a6711 | ||
|
|
ea947df50a | ||
|
|
7826b36634 | ||
|
|
8a303f340b | ||
|
|
d246c42a99 | ||
|
|
8fe6cba6ef | ||
|
|
0401fb9616 | ||
|
|
73040780b9 | ||
|
|
30fc35097c | ||
|
|
938458b56e | ||
|
|
1e73ba4754 | ||
|
|
b20bdda8e9 | ||
|
|
7d7d38b946 | ||
|
|
4268f41a51 | ||
|
|
bab6146345 | ||
|
|
978e344ce2 | ||
|
|
f156bb357a | ||
|
|
3f3ab829c0 | ||
|
|
195135b1b6 | ||
|
|
4c70105b5e | ||
|
|
73abc84979 | ||
|
|
d4826695e3 | ||
|
|
715e9d32eb | ||
|
|
25e005ff32 | ||
|
|
263a197da4 | ||
|
|
52a915c8a0 | ||
|
|
67e67c8db9 | ||
|
|
0873c6cb65 | ||
|
|
1ef62ac3a6 | ||
|
|
06f658e1c4 | ||
|
|
a2000c0bc7 | ||
|
|
c3d5c6eab9 | ||
|
|
0d11f732e1 | ||
|
|
d6391621a0 | ||
|
|
b8373a4ee0 | ||
|
|
2fda829ac4 | ||
|
|
5115e04664 | ||
|
|
369afe11e3 | ||
|
|
9690bc2447 | ||
|
|
acf8805dac | ||
|
|
58fbcd7e7a | ||
|
|
ae973f312a | ||
|
|
185e09c118 | ||
|
|
595c91d5d6 | ||
|
|
465232f8b9 | ||
|
|
13ff8fdb8e | ||
|
|
5ce78d041d | ||
|
|
6a93172cb9 | ||
|
|
f00aae6c6c | ||
|
|
16b9de01c7 | ||
|
|
1ccc97d824 | ||
|
|
a2dd2251c9 | ||
|
|
72454de4ef | ||
|
|
677aceccb6 | ||
|
|
60872e0a5a | ||
|
|
c1842e383a | ||
|
|
867ab0e80a | ||
|
|
59a734b04c | ||
|
|
183f6f8ed9 | ||
|
|
ff9537514a | ||
|
|
60f14adb47 | ||
|
|
5e39828966 | ||
|
|
0362896bfb | ||
|
|
e5e7702fc5 | ||
|
|
c96c3966f4 | ||
|
|
aaf349bbcc | ||
|
|
cd5937bbb7 | ||
|
|
1c17231b9e | ||
|
|
a8342dc513 | ||
|
|
945bde7c66 | ||
|
|
1c26681a65 | ||
|
|
31661edc03 | ||
|
|
4946c0e43c | ||
|
|
81d9b1723f | ||
|
|
089df85c1d | ||
|
|
839e475c3e | ||
|
|
72b30aabf9 | ||
|
|
40c423ee3c | ||
|
|
d49074eeae | ||
|
|
be931f4173 | ||
|
|
5640c33a67 | ||
|
|
285270f598 | ||
|
|
4b52df676a | ||
|
|
7b41980ace | ||
|
|
cbedb4d38c | ||
|
|
2451ac608f | ||
|
|
7be1989cd4 | ||
|
|
3e7e03b4c1 | ||
|
|
39f5df8ade | ||
|
|
0a0568c2f5 | ||
|
|
6577004536 | ||
|
|
ad2d0e0bba | ||
|
|
eecd276aa3 | ||
|
|
06a23ea29a | ||
|
|
d6f08a2d70 | ||
|
|
218d2ad143 | ||
|
|
99caa615fc | ||
|
|
a449e7aeb3 | ||
|
|
94f331cf09 | ||
|
|
9869448e17 | ||
|
|
57207c1ff4 | ||
|
|
f2c3e8ff50 | ||
|
|
feaa14b76a | ||
|
|
545b1d8dee | ||
|
|
a1960233bf | ||
|
|
24da82f42b | ||
|
|
2b54de424b | ||
|
|
6a0081cf71 | ||
|
|
b78bba94ef | ||
|
|
0086dde94b | ||
|
|
3ceb76b016 | ||
|
|
ed5f2028a1 | ||
|
|
6fd4db14fb | ||
|
|
8b1c4decf5 | ||
|
|
dc3785888b | ||
|
|
36842dfc04 | ||
|
|
17c6454afa | ||
|
|
a6b2f93f41 | ||
|
|
7adde0340e | ||
|
|
c312b2fdfe | ||
|
|
0f3f8d5a36 | ||
|
|
aa5ae7993a | ||
|
|
581544207b | ||
|
|
0657535d04 | ||
|
|
e1b6f9eb2b | ||
|
|
ee48395f1b | ||
|
|
cee1513966 | ||
|
|
fc192c5dfc | ||
|
|
2b57f1124c | ||
|
|
9705c8cd57 | ||
|
|
c89449e8fd | ||
|
|
e244b82082 | ||
|
|
439867b95b | ||
|
|
979f0c2a33 | ||
|
|
1d426f78a6 | ||
|
|
30aee058bf | ||
|
|
33d28b085f | ||
|
|
152891f6cd | ||
|
|
9b12618b93 | ||
|
|
b1a3941226 | ||
|
|
604b1d3fdf | ||
|
|
a34e14f483 | ||
|
|
db4e145080 | ||
|
|
c8f80a4d78 | ||
|
|
5c8639aeef | ||
|
|
91b0c3a643 | ||
|
|
c055952bcf | ||
|
|
ec07729424 | ||
|
|
bf8e433a0e | ||
|
|
7d52c14dea | ||
|
|
7c3fd78329 | ||
|
|
cd0ec160d6 | ||
|
|
fcdcb9162d | ||
|
|
ed564b1861 | ||
|
|
44b2cb0aa5 | ||
|
|
666ffc35ea | ||
|
|
3f8ca8cda3 | ||
|
|
824a37f0a6 | ||
|
|
5c6546f313 | ||
|
|
46a8051e41 | ||
|
|
4ca397a8f2 | ||
|
|
7e78984f2b | ||
|
|
601962c8d8 | ||
|
|
b59a1b9407 | ||
|
|
2273b917c7 | ||
|
|
9513097dd0 | ||
|
|
006093c643 | ||
|
|
d35d52c6af | ||
|
|
37b1c834c9 | ||
|
|
3f4c6b2cf0 | ||
|
|
ea450665da | ||
|
|
dabda617ee | ||
|
|
9ffe5d7df8 | ||
|
|
2492f51022 | ||
|
|
10ce7f30a4 | ||
|
|
815e7b45d2 | ||
|
|
1e6999587a | ||
|
|
6f4de54fa3 | ||
|
|
cd458682d4 | ||
|
|
25b6f455e3 | ||
|
|
c293f72846 | ||
|
|
68b7ccf4d2 | ||
|
|
91ef635ff5 | ||
|
|
2319ca7cb8 | ||
|
|
49de0240b8 | ||
|
|
4352f54c11 | ||
|
|
d04778fd89 | ||
|
|
74608d8a55 | ||
|
|
aa57cd443f | ||
|
|
b845453274 | ||
|
|
b352e964d0 | ||
|
|
2747289831 | ||
|
|
73daaa9cd7 | ||
|
|
e07a02b027 | ||
|
|
1d90938598 | ||
|
|
ec4bb5a5d1 | ||
|
|
f15a3895c5 | ||
|
|
bd9c844284 | ||
|
|
e89d625f15 | ||
|
|
408ac7e8c2 | ||
|
|
c2c758d9f4 | ||
|
|
564e83fbc4 | ||
|
|
9bf58f8966 | ||
|
|
60083f04f5 | ||
|
|
1bdfd68807 | ||
|
|
8aa2b3d5ac | ||
|
|
1936ce6621 | ||
|
|
617599ee0c | ||
|
|
84f209b520 | ||
|
|
e7ae478e9b | ||
|
|
0fc2032e4a | ||
|
|
0bd7f8ed08 | ||
|
|
051a5e8c6a | ||
|
|
127db529ef | ||
|
|
b24e025b43 | ||
|
|
ff8b8a0399 | ||
|
|
c94b905d72 | ||
|
|
062a98b2a8 | ||
|
|
085f7a68b6 | ||
|
|
1fa2380a79 | ||
|
|
9385f20cc0 | ||
|
|
b2cadaa4d4 | ||
|
|
50697200c4 | ||
|
|
5b46df45ba | ||
|
|
8b634ea4ff | ||
|
|
5a6474b10f | ||
|
|
1ad3cc1730 | ||
|
|
7f13d7ea95 | ||
|
|
a5a97fe5d5 | ||
|
|
4d916810b2 | ||
|
|
d0e701e140 | ||
|
|
fa5b467922 | ||
|
|
2501994707 | ||
|
|
49d75de8e3 | ||
|
|
4af4dd74bd | ||
|
|
151af295eb | ||
|
|
b6d9b61764 | ||
|
|
579c636d50 | ||
|
|
5c2845784f | ||
|
|
95521c8a48 | ||
|
|
6329798154 | ||
|
|
3cadee1d48 | ||
|
|
bb43de465f | ||
|
|
9eb391be8b | ||
|
|
4bab3a448b | ||
|
|
4030dfbad7 | ||
|
|
4d475678d3 | ||
|
|
65e95d79e5 | ||
|
|
1b6db6e370 | ||
|
|
2dc448fa30 | ||
|
|
9491a3b980 | ||
|
|
e3832a052a | ||
|
|
fbc556ed67 | ||
|
|
de3a00e950 | ||
|
|
b4e4583e61 | ||
|
|
74c82141d4 | ||
|
|
f5c6a1c770 | ||
|
|
b6ad73e1fa | ||
|
|
2871a03aa8 | ||
|
|
61bae6367d | ||
|
|
182255cac8 | ||
|
|
0b46ac53ed | ||
|
|
ac2adac8ab | ||
|
|
d6f6508638 | ||
|
|
7119d95713 | ||
|
|
1e5af30519 | ||
|
|
864f28de9a | ||
|
|
ec289a8b1d | ||
|
|
69f4af5df6 | ||
|
|
1c7a4b2a4d | ||
|
|
e03120c944 | ||
|
|
c28d108b96 | ||
|
|
3fe4c9f289 | ||
|
|
54ad586941 | ||
|
|
c5628adb55 | ||
|
|
2fa80b2fd9 | ||
|
|
045edf7c55 | ||
|
|
a7f44dc3e8 | ||
|
|
f430b71645 | ||
|
|
5ea49c94de | ||
|
|
1ea90b9445 | ||
|
|
cbfce63f4d | ||
|
|
37c10cbb10 | ||
|
|
9dc3918914 | ||
|
|
04feda45c9 | ||
|
|
f657780a42 | ||
|
|
2d33dea6d3 | ||
|
|
955b9658e9 | ||
|
|
abbd02e925 | ||
|
|
a076a6d47f | ||
|
|
c5f11e1c14 | ||
|
|
67f716ff43 | ||
|
|
1b666fe936 | ||
|
|
1daf625618 | ||
|
|
5b26bc47fa | ||
|
|
ba3f656b3a | ||
|
|
e26055cb76 | ||
|
|
8085d0344a | ||
|
|
db6279bc88 | ||
|
|
775108f088 | ||
|
|
6f6d804e44 | ||
|
|
5e04a9d22a | ||
|
|
7c9df8cc79 | ||
|
|
d4dbd02930 | ||
|
|
012f5d6635 | ||
|
|
45b50086b9 | ||
|
|
d50c94a63c | ||
|
|
bc120316b3 | ||
|
|
ddf45735e0 | ||
|
|
b604f43a00 | ||
|
|
00343cfa91 | ||
|
|
5d3fa4e087 | ||
|
|
777d98ce80 | ||
|
|
76282e2031 | ||
|
|
b98812f64b | ||
|
|
8d501f9ef1 | ||
|
|
8733eb46f1 | ||
|
|
54884c6870 | ||
|
|
b2ee4f3f19 | ||
|
|
7fcc0473a8 | ||
|
|
8df6f7df45 | ||
|
|
e89c3929cc | ||
|
|
6a84498645 | ||
|
|
418856bff1 | ||
|
|
c9971ad760 | ||
|
|
4f73b3ecad | ||
|
|
545f6c36c9 | ||
|
|
d0cd0c4bc7 | ||
|
|
0940950083 | ||
|
|
e8363781af | ||
|
|
49c4984a77 | ||
|
|
7456d958ba | ||
|
|
e96a5702fd | ||
|
|
a335989155 | ||
|
|
0bd2cb4de3 | ||
|
|
3fc349370f | ||
|
|
99e49cdf6a | ||
|
|
39847bab35 | ||
|
|
85e9b06bca | ||
|
|
3ee9204ff8 | ||
|
|
9394d45de1 | ||
|
|
a39229d7be | ||
|
|
ee94161eb4 | ||
|
|
4332a43fb9 | ||
|
|
9f9420f713 | ||
|
|
84d538e37f | ||
|
|
9c1639d7d7 | ||
|
|
72206de234 | ||
|
|
58b06ee5ff | ||
|
|
bd70054ce9 | ||
|
|
08b4dc3a52 | ||
|
|
c5b88e1eaf | ||
|
|
b783c39164 | ||
|
|
a0c02ce022 | ||
|
|
d627033141 | ||
|
|
3577d0e2a8 | ||
|
|
b55121bb4d | ||
|
|
47ad03b775 | ||
|
|
bb6799eeb2 | ||
|
|
ed40b5361e | ||
|
|
4b5ea11ca0 | ||
|
|
31406da911 | ||
|
|
a828356e0e | ||
|
|
26089a05e0 | ||
|
|
b7ad3c8d45 | ||
|
|
553d35ef82 | ||
|
|
66b1e444d1 | ||
|
|
a9092e54c0 | ||
|
|
f0df3f276f | ||
|
|
35a730793f | ||
|
|
026e80ed64 | ||
|
|
f0cc3ded00 | ||
|
|
00e57a7473 | ||
|
|
2e69720bc1 | ||
|
|
f419869dfc | ||
|
|
d146a9e940 | ||
|
|
b39eb0b51d | ||
|
|
095d60af00 | ||
|
|
5c7ec031b5 | ||
|
|
c84695a06e | ||
|
|
91c1e5b174 | ||
|
|
f19e9fb48b | ||
|
|
4c04bf6e15 | ||
|
|
dae6d1eebc | ||
|
|
7d9b5dd948 | ||
|
|
8e80503e81 | ||
|
|
cd42688357 | ||
|
|
f9d6056844 | ||
|
|
6741fcbc49 | ||
|
|
7142e87cf2 | ||
|
|
11616f5d32 | ||
|
|
406c87f765 | ||
|
|
5cd63294a2 | ||
|
|
f035a0ed3f | ||
|
|
63fe5688ea | ||
|
|
2f0e19cf41 | ||
|
|
3fc149339b | ||
|
|
18e6afc4e0 | ||
|
|
d2c4131d01 | ||
|
|
8d56f91117 | ||
|
|
59d7994de7 | ||
|
|
28585bc217 | ||
|
|
981f2dec36 | ||
|
|
ef378088dd | ||
|
|
ed893d8774 | ||
|
|
d524a3e757 | ||
|
|
356fcf8aa7 | ||
|
|
d68ec8c075 | ||
|
|
9cb961ef7f | ||
|
|
1cf7f81b5a | ||
|
|
e01764d849 | ||
|
|
4574400ce2 | ||
|
|
d4cdf70d62 | ||
|
|
47b0eef3bb | ||
|
|
ee1bd1ebda | ||
|
|
812c46c572 | ||
|
|
5e370e479a | ||
|
|
65c72c484b | ||
|
|
83ac327d7f | ||
|
|
369771bda5 | ||
|
|
253a699001 | ||
|
|
9e9dc27a59 | ||
|
|
912e00d48d | ||
|
|
8049760703 | ||
|
|
84042a4514 | ||
|
|
986fc55dbd | ||
|
|
a9e20abf06 | ||
|
|
e4c47c22b3 | ||
|
|
5fb41171d6 | ||
|
|
c7f71b478f | ||
|
|
43e95e8ba7 | ||
|
|
429178e518 | ||
|
|
4365869fb1 | ||
|
|
8796368b01 | ||
|
|
98a844b409 | ||
|
|
9670f6b079 | ||
|
|
cf8b3ecd3a | ||
|
|
d7d3a0767d | ||
|
|
67de2303f9 | ||
|
|
d266c8a56f | ||
|
|
c34acb16c2 | ||
|
|
ab790c2880 | ||
|
|
8528073003 | ||
|
|
4662cd4cce | ||
|
|
85929dcaa8 | ||
|
|
1aec50bcfd | ||
|
|
89c148411e | ||
|
|
e850549aa1 | ||
|
|
d5335317a6 | ||
|
|
a9e21dcbfd | ||
|
|
3f6c7405ed | ||
|
|
6742272221 | ||
|
|
8b64bb4cd8 | ||
|
|
54c2667aea | ||
|
|
9d092c983b | ||
|
|
b9b8ced689 | ||
|
|
0d94681f16 | ||
|
|
174aa86033 | ||
|
|
b9da6bc80d | ||
|
|
447e042953 | ||
|
|
4315c73775 | ||
|
|
214ef12cb5 | ||
|
|
9b190dcc2e | ||
|
|
012bd30552 | ||
|
|
b213c35801 | ||
|
|
07a95dce96 | ||
|
|
7f6387ff59 | ||
|
|
0f33a896de | ||
|
|
d159a8d5aa | ||
|
|
941fab9023 | ||
|
|
7f2e05284b | ||
|
|
469fcf6707 | ||
|
|
2ad680a96b | ||
|
|
dcbdee6598 | ||
|
|
f934aadb67 | ||
|
|
60015e1da2 | ||
|
|
c20c80b757 | ||
|
|
d84b7dc80f | ||
|
|
6b37de78ba | ||
|
|
9964018f0e | ||
|
|
c513e0044e | ||
|
|
6ae8c4c6bd | ||
|
|
a3942aad81 | ||
|
|
7f38c4fa87 | ||
|
|
a484f89e0a | ||
|
|
294e640ac1 | ||
|
|
dd5b4dcb32 | ||
|
|
d733a17871 | ||
|
|
d38236fb37 | ||
|
|
a264efe2bb | ||
|
|
ca30b82e9a | ||
|
|
c9a35c7346 | ||
|
|
9d62c94d86 | ||
|
|
34ea8115a8 | ||
|
|
19cdd895f0 | ||
|
|
d4c3d27688 | ||
|
|
fee215a25c | ||
|
|
b821fdf01f | ||
|
|
6f1f53007d | ||
|
|
845f111c29 | ||
|
|
2bec9e1886 | ||
|
|
19c315a43a | ||
|
|
493621a28b | ||
|
|
59aa462b05 | ||
|
|
d0fbd68523 | ||
|
|
60374c2f47 | ||
|
|
d4a484f20e | ||
|
|
63586db7b6 | ||
|
|
c8488f999b | ||
|
|
4b62e85c95 | ||
|
|
05486ac7a2 | ||
|
|
c6a57a38f9 | ||
|
|
44d488467e | ||
|
|
f09647d754 | ||
|
|
87ccdf7723 | ||
|
|
ed654381b8 | ||
|
|
9f3fee08b2 | ||
|
|
53e132f441 | ||
|
|
72c5172f12 | ||
|
|
5c573f7138 | ||
|
|
0a072c7889 | ||
|
|
332a57040e | ||
|
|
6c3e74d159 | ||
|
|
583b676489 | ||
|
|
4cddfa4dfc | ||
|
|
ae2a789c22 | ||
|
|
1cdee635d7 | ||
|
|
cad9962cec | ||
|
|
c268d093ca | ||
|
|
9f67f2d4fd | ||
|
|
e4f4824023 | ||
|
|
5e73554ad5 | ||
|
|
a7ca88574c | ||
|
|
56842bb469 | ||
|
|
ea3975753d | ||
|
|
adf9e8c969 | ||
|
|
85955ef13c | ||
|
|
8996021d83 | ||
|
|
20f9284ae4 | ||
|
|
8fc97b1f2e | ||
|
|
5f433a2c20 | ||
|
|
bfd406206b | ||
|
|
897d5854af | ||
|
|
940f30dab6 | ||
|
|
af79503ab6 | ||
|
|
1502efdf43 | ||
|
|
83e3d43a21 | ||
|
|
91c5a4fa67 | ||
|
|
60f1c84a21 | ||
|
|
efe5f24049 | ||
|
|
611c989b91 | ||
|
|
cef98f689b | ||
|
|
3d2392567a | ||
|
|
1c0ceec2be | ||
|
|
fb84247982 | ||
|
|
4d3d738fd2 | ||
|
|
a06beacfcf | ||
|
|
c89aaa895f | ||
|
|
523f88cc0b | ||
|
|
9556548fc7 | ||
|
|
09a4b0d6f7 | ||
|
|
0ec390907d | ||
|
|
9180710953 | ||
|
|
b01ebf6503 | ||
|
|
892ce26f44 | ||
|
|
aef1c5aa3b | ||
|
|
0e0eb474eb | ||
|
|
82db8141ea | ||
|
|
979e60630e | ||
|
|
181a0f171d | ||
|
|
395dd0e3d4 | ||
|
|
eeb4d328c8 | ||
|
|
b7a52d1761 | ||
|
|
dcdf4eacdc | ||
|
|
38090bd73d | ||
|
|
f87c9d510f | ||
|
|
7f1febe9e1 | ||
|
|
6bb5578c3a | ||
|
|
36d76d7e3e | ||
|
|
bbe29a7a63 | ||
|
|
6a2f85e04f | ||
|
|
3b81791cd0 | ||
|
|
0bc13e7c63 | ||
|
|
6183fafaea | ||
|
|
9494f49147 | ||
|
|
387e531d64 | ||
|
|
3a1cc63e8f | ||
|
|
931c7477c3 | ||
|
|
a38da31d17 | ||
|
|
65b16c60fb | ||
|
|
f454e9229e | ||
|
|
a1046f3913 | ||
|
|
d6dd790569 | ||
|
|
78b50bc088 | ||
|
|
0fcf99c8e9 | ||
|
|
1b71cf2a15 | ||
|
|
6bf40c139d | ||
|
|
fc5d284588 | ||
|
|
aa3127167b | ||
|
|
025a1c73a8 | ||
|
|
8e4a3fe16a | ||
|
|
9bd68ca306 | ||
|
|
784de0c4a1 | ||
|
|
b0f418abda | ||
|
|
82fdb7f121 | ||
|
|
f998b1f3dd | ||
|
|
6364515fb7 | ||
|
|
dedd4a95c2 | ||
|
|
537012440b | ||
|
|
701bad466e | ||
|
|
cd62070fdb | ||
|
|
0d095b3037 | ||
|
|
4df644eb3a | ||
|
|
2318739a58 | ||
|
|
580ba924a4 | ||
|
|
093708ff97 | ||
|
|
71d37fc9c7 | ||
|
|
e7a73551ab | ||
|
|
cad4f0b89a | ||
|
|
50da5ccb74 | ||
|
|
ba8959ffe1 | ||
|
|
21173d6480 | ||
|
|
41d80c4f1a | ||
|
|
c6b4f565a5 | ||
|
|
88425fc38d | ||
|
|
95f0b9a780 | ||
|
|
79f6b26cf1 | ||
|
|
8013bac725 | ||
|
|
5abb7c9d57 | ||
|
|
9e93e130db | ||
|
|
9df15c1f58 | ||
|
|
0a91ad8cec | ||
|
|
a55c6ed941 | ||
|
|
90e0be07e2 | ||
|
|
57c91118c2 | ||
|
|
c9c6094e5b | ||
|
|
6069fd07e3 | ||
|
|
a4ae747b15 | ||
|
|
c645d570ea | ||
|
|
6930fcbfb8 | ||
|
|
88b89053d4 | ||
|
|
3c72c009ab | ||
|
|
376f41c464 | ||
|
|
98ee49dca5 | ||
|
|
e94838925e | ||
|
|
18a80f15dd | ||
|
|
81922e147e | ||
|
|
a2c63a5bac | ||
|
|
7841e336b3 | ||
|
|
e77cb3f94e | ||
|
|
4d0c297901 | ||
|
|
bd6f38282c | ||
|
|
50a319df2b | ||
|
|
4736e10d11 | ||
|
|
3377faffb8 | ||
|
|
68d66c3229 | ||
|
|
74f1b93b42 | ||
|
|
4ae73fa9b4 | ||
|
|
2778088c52 | ||
|
|
83b8a4eb3f | ||
|
|
2e31a3d4f3 | ||
|
|
316d932d99 | ||
|
|
631996f133 | ||
|
|
23979d223c | ||
|
|
5f342ea60b | ||
|
|
d38b07bbad | ||
|
|
f9c50b80fc | ||
|
|
e02d891130 | ||
|
|
6ade24c935 | ||
|
|
2272f99599 | ||
|
|
070cf6a5fc | ||
|
|
eee011180f | ||
|
|
c8e46aad42 | ||
|
|
cc3cceaa99 | ||
|
|
8b6336b91d | ||
|
|
aa2512837e | ||
|
|
6ac88d677b | ||
|
|
879feab518 | ||
|
|
40ac39b6df | ||
|
|
dd1b39b347 | ||
|
|
4f848948e7 | ||
|
|
0d15e2a34d | ||
|
|
dbbe0a519d | ||
|
|
e18ef0edd9 | ||
|
|
af07f9eeb4 | ||
|
|
0c43de9735 | ||
|
|
46f87720fb | ||
|
|
c23c2fdc45 | ||
|
|
fc83d807e0 | ||
|
|
b9b5f8b113 | ||
|
|
017e09d7c3 | ||
|
|
b70188f4b8 | ||
|
|
5e7bf1826c | ||
|
|
82a5672d99 | ||
|
|
c8455f936a | ||
|
|
ecc2912155 | ||
|
|
9ca6eb906f | ||
|
|
10a51a0af9 | ||
|
|
7d26c2c954 | ||
|
|
1357fdf78e | ||
|
|
cfd63faf75 | ||
|
|
7c0474b743 | ||
|
|
d468fd358e | ||
|
|
49f442bf9e | ||
|
|
1379914857 | ||
|
|
684f55f603 | ||
|
|
4ff850cd3d | ||
|
|
e437e41870 | ||
|
|
62c72d72d6 | ||
|
|
94c28386a6 | ||
|
|
ce5f9c1e7a | ||
|
|
9ac7612d8a | ||
|
|
48526c2ab7 | ||
|
|
c1373394a8 | ||
|
|
3cbb95976a | ||
|
|
31ab97595e | ||
|
|
c849e3114a | ||
|
|
5c6b5938c1 | ||
|
|
1725d5cef6 | ||
|
|
3754871c73 | ||
|
|
680c27eb66 | ||
|
|
7ed432252b | ||
|
|
0bde7acce8 | ||
|
|
b968055144 | ||
|
|
4fbef7f1f6 | ||
|
|
3f05487abb | ||
|
|
2da9bc9b13 | ||
|
|
b19096ded6 | ||
|
|
da24e9f81b | ||
|
|
431ad51b66 | ||
|
|
ae0fb921d5 | ||
|
|
daba58f05f | ||
|
|
f167f3c2fc | ||
|
|
58c6b2891c | ||
|
|
c020905f2e | ||
|
|
911bd59116 | ||
|
|
f11c66c42d | ||
|
|
c828549b85 | ||
|
|
bfd60a4d30 | ||
|
|
afb897aa47 | ||
|
|
bb294710e6 | ||
|
|
6221f40e96 | ||
|
|
ba7cea01e4 | ||
|
|
76a9339605 | ||
|
|
6bdf07307b | ||
|
|
38ad945ec4 | ||
|
|
8f9f311bdf | ||
|
|
5b148fbf44 | ||
|
|
ffcde46140 | ||
|
|
3b283d1690 | ||
|
|
3788994307 | ||
|
|
9b78237f77 | ||
|
|
bb95878828 | ||
|
|
524898538d | ||
|
|
9340443a3b | ||
|
|
efb9ba7767 | ||
|
|
ccdcc0cbf9 | ||
|
|
02078212ac | ||
|
|
b28da1d57b | ||
|
|
8fb4d8f347 | ||
|
|
b788273e4c | ||
|
|
63bede3ee9 | ||
|
|
cfa8db7b3f | ||
|
|
167ba86211 |
245
.clang-format
Normal file
245
.clang-format
Normal file
@ -0,0 +1,245 @@
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -2
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignArrayOfStructures: Left
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
AlignFunctionPointers: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveShortCaseStatements:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCaseColons: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments:
|
||||
Kind: Always
|
||||
OverEmptyLines: 0
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowBreakBeforeNoexceptSpecifier: Never
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortCompoundRequirementOnASingleLine: true
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: All
|
||||
AlwaysBreakAfterReturnType: All
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BitFieldColonSpacing: Both
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: true
|
||||
AfterClass: true
|
||||
AfterControlStatement: Always
|
||||
AfterEnum: false
|
||||
AfterExternBlock: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterObjCDeclaration: true
|
||||
AfterStruct: false
|
||||
AfterUnion: true
|
||||
BeforeCatch: true
|
||||
BeforeElse: true
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: true
|
||||
IndentBraces: true
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakAdjacentStringLiterals: true
|
||||
BreakAfterAttributes: Leave
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakArrays: true
|
||||
BreakBeforeBinaryOperators: All
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInlineASMColon: OnlyMultiline
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: false
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: false
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseBlocks: false
|
||||
IndentCaseLabels: false
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 2
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
InsertNewlineAtEOF: false
|
||||
InsertTrailingCommas: None
|
||||
IntegerLiteralSeparator:
|
||||
Binary: 0
|
||||
BinaryMinDigits: 0
|
||||
Decimal: 0
|
||||
DecimalMinDigits: 0
|
||||
Hex: 0
|
||||
HexMinDigits: 0
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
KeepEmptyLinesAtEOF: false
|
||||
LambdaBodyIndentation: Signature
|
||||
LineEnding: DeriveLF
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PackConstructorInitializers: BinPack
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakScopeResolution: 500
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Right
|
||||
PPIndentWidth: -1
|
||||
QualifierAlignment: Leave
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: true
|
||||
RemoveBracesLLVM: false
|
||||
RemoveParentheses: Leave
|
||||
RemoveSemicolon: false
|
||||
RequiresClausePosition: OwnLine
|
||||
RequiresExpressionIndentation: OuterScope
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SkipMacroDefinitionBody: false
|
||||
SortIncludes: CaseSensitive
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: LexicographicNumeric
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeJsonColon: false
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: false
|
||||
AfterForeachMacros: false
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: false
|
||||
AfterOverloadedOperator: false
|
||||
AfterPlacementOperator: true
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParens: Never
|
||||
SpacesInParensOptions:
|
||||
InCStyleCasts: false
|
||||
InConditionalStatements: false
|
||||
InEmptyParentheses: false
|
||||
Other: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++03
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseTab: ForIndentation
|
||||
VerilogBreakBetweenInstancePorts: true
|
||||
WhitespaceSensitiveMacros:
|
||||
- BOOST_PP_STRINGIZE
|
||||
- CF_SWIFT_NAME
|
||||
- NS_SWIFT_NAME
|
||||
- PP_STRINGIZE
|
||||
- STRINGIZE
|
||||
...
|
||||
|
||||
7
.dev/Makefile
Normal file
7
.dev/Makefile
Normal file
@ -0,0 +1,7 @@
|
||||
.PHONY: vscode
|
||||
|
||||
vscode:
|
||||
mkdir -p ../.vscode
|
||||
cp -rT ./vscode ../.vscode
|
||||
mkdir -p ../.devcontainer
|
||||
cp -rT ./devcontainer ../.devcontainer
|
||||
20
.dev/devcontainer/.scripts/init-devcontainer-cli.sh
Executable file
20
.dev/devcontainer/.scripts/init-devcontainer-cli.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# cd aliases
|
||||
alias ..='cd ..'
|
||||
alias ...='cd ../..'
|
||||
alias -- -='cd -'
|
||||
|
||||
# bat aliases
|
||||
alias bat='batcat'
|
||||
|
||||
if [ "$ENABLE_ESA" = "1" ]; then
|
||||
if [ "$(command -v eza)" ]; then
|
||||
alias l='eza -la --icons=auto --group-directories-first'
|
||||
alias la='eza -la --icons=auto --group-directories-first'
|
||||
alias ll='eza -l --icons=auto --group-directories-first'
|
||||
alias l.='eza -d .*'
|
||||
alias ls='eza'
|
||||
alias l1='eza -1'
|
||||
fi
|
||||
fi
|
||||
30
.dev/devcontainer/.scripts/install-devcontainer-tools.sh
Executable file
30
.dev/devcontainer/.scripts/install-devcontainer-tools.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Install mkdocs with mkdocs-material theme
|
||||
pipx install --include-deps mkdocs-material
|
||||
pipx inject mkdocs-material mkdocs-minify-plugin
|
||||
|
||||
# Starfish (https://starship.rs/) - shell prompt
|
||||
if [ "$ENABLE_STARSHIP" = "1" ]
|
||||
then
|
||||
curl -sS https://starship.rs/install.sh | sh -s -- -y
|
||||
echo 'eval "$(starship init bash)"' >> ~/.bashrc
|
||||
fi
|
||||
|
||||
# Atuin (https://atuin.sh/) - shell history
|
||||
if [ "$ENABLE_ATUIN" = "1" ]
|
||||
then
|
||||
curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh
|
||||
curl https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o ~/.bash-preexec.sh
|
||||
fi
|
||||
|
||||
# zoxide (https://github.com/ajeetdsouza/zoxide) - replacement for cd
|
||||
if [ "$ENABLE_ZOXIDE" = "1" ]
|
||||
then
|
||||
curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
|
||||
echo 'eval "$(zoxide init bash)"' >> ~/.bashrc
|
||||
fi
|
||||
|
||||
pipx install harlequin
|
||||
pipx install toolong
|
||||
pipx install posting
|
||||
479
.dev/devcontainer/data/devcontainer-owntone.conf
Normal file
479
.dev/devcontainer/data/devcontainer-owntone.conf
Normal file
@ -0,0 +1,479 @@
|
||||
# A quick guide to configuring OwnTone:
|
||||
#
|
||||
# For regular use, the most important setting to configure is "directories",
|
||||
# which should be the location of your media. Whatever user you have set as
|
||||
# "uid" must have read access to this location. If the location is a network
|
||||
# mount, please see the README.
|
||||
#
|
||||
# In all likelihood, that's all you need to do!
|
||||
|
||||
general {
|
||||
# Username
|
||||
# Make sure the user has read access to the library directories you set
|
||||
# below, and full access to the databases, log and local audio
|
||||
uid = "vscode"
|
||||
|
||||
# Database location
|
||||
db_path = "/data/cache/songs3.db"
|
||||
|
||||
# Database backup location
|
||||
# Uncomment and specify a full path to enable abilty to use REST endpoint
|
||||
# to initiate backup of songs3.db
|
||||
# db_backup_path = "/usr/local/var/cache/owntone/songs3.bak"
|
||||
|
||||
# Log file and level
|
||||
# Available levels: fatal, log, warning, info, debug, spam
|
||||
logfile = "/data/logs/owntone.log"
|
||||
loglevel = debug
|
||||
|
||||
# Admin password for the web interface
|
||||
# Note that access to the web interface from computers in
|
||||
# "trusted_network" (see below) does not require password
|
||||
# admin_password = ""
|
||||
|
||||
# Websocket port for the web interface.
|
||||
# websocket_port = 0
|
||||
|
||||
# Websocket interface to bind listener to (e.g. "eth0"). Default is
|
||||
# disabled, which means listen on all interfaces.
|
||||
# websocket_interface = ""
|
||||
|
||||
# Sets who is allowed to connect without authorisation. This applies to
|
||||
# client types like Remotes, DAAP clients (iTunes) and to the web
|
||||
# interface. Options are "any", "localhost" or the prefix to one or
|
||||
# more ipv4/6 networks. The default is { "localhost", "192.168", "fd" }
|
||||
# trusted_networks = { "localhost", "192.168", "fd" }
|
||||
|
||||
# Enable/disable IPv6
|
||||
# ipv6 = no
|
||||
|
||||
# Set this if you want the server to bind to a specific IP address. Can
|
||||
# be ipv6 or ipv4. Default (commented out or "::") is to listen on all
|
||||
# IP addresses.
|
||||
# bind_address = "::"
|
||||
|
||||
# Location of cache database
|
||||
cache_path = "/data/cache/cache.db"
|
||||
|
||||
# DAAP requests that take longer than this threshold (in msec) get their
|
||||
# replies cached for next time. Set to 0 to disable caching.
|
||||
# cache_daap_threshold = 1000
|
||||
|
||||
# When starting playback, autoselect speaker (if none of the previously
|
||||
# selected speakers/outputs are available)
|
||||
# speaker_autoselect = no
|
||||
|
||||
# Most modern systems have a high-resolution clock, but if you are on an
|
||||
# unusual platform and experience audio drop-outs, you can try changing
|
||||
# this option
|
||||
# high_resolution_clock = yes
|
||||
}
|
||||
|
||||
# Library configuration
|
||||
library {
|
||||
# Name of the library as displayed by the clients (%h: hostname). If you
|
||||
# change the name after pairing with Remote you may have to re-pair.
|
||||
name = "My Music on %h"
|
||||
|
||||
# TCP port to listen on. Default port is 3689 (daap)
|
||||
port = 3689
|
||||
|
||||
# Password for the library. Optional.
|
||||
# password = ""
|
||||
|
||||
# Directories to index
|
||||
directories = { "/data/music" }
|
||||
|
||||
# Follow symlinks. Default: true.
|
||||
# follow_symlinks = true
|
||||
|
||||
# Directories containing podcasts
|
||||
# For each directory that is indexed the path is matched against these
|
||||
# names. If there is a match all items in the directory are marked as
|
||||
# podcasts. Eg. if you index /srv/music, and your podcasts are in
|
||||
# /srv/music/Podcasts, you can set this to "/Podcasts".
|
||||
# (changing this setting only takes effect after rescan, see the README)
|
||||
podcasts = { "/Podcasts" }
|
||||
|
||||
# Directories containing audiobooks
|
||||
# For each directory that is indexed the path is matched against these
|
||||
# names. If there is a match all items in the directory are marked as
|
||||
# audiobooks.
|
||||
# (changing this setting only takes effect after rescan, see the README)
|
||||
audiobooks = { "/Audiobooks" }
|
||||
|
||||
# Directories containing compilations (eg soundtracks)
|
||||
# For each directory that is indexed the path is matched against these
|
||||
# names. If there is a match all items in the directory are marked as
|
||||
# compilations.
|
||||
# (changing this setting only takes effect after rescan, see the README)
|
||||
compilations = { "/Compilations" }
|
||||
|
||||
# Compilations usually have many artists, and sometimes no album artist.
|
||||
# If you don't want every artist to be listed in artist views, you can
|
||||
# set a single name which will be used for all compilation tracks
|
||||
# without an album artist, and for all tracks in the compilation
|
||||
# directories.
|
||||
# (changing this setting only takes effect after rescan, see the README)
|
||||
compilation_artist = "Various Artists"
|
||||
|
||||
# If your album and artist lists are cluttered, you can choose to hide
|
||||
# albums and artists with only one track. The tracks will still be
|
||||
# visible in other lists, e.g. songs and playlists. This setting
|
||||
# currently only works in some remotes.
|
||||
# hide_singles = false
|
||||
|
||||
# Internet streams in your playlists will by default be shown in the
|
||||
# "Radio" library, like iTunes does. However, some clients (like
|
||||
# TunesRemote+) won't show the "Radio" library. If you would also like
|
||||
# to have them shown like normal playlists, you can enable this option.
|
||||
# radio_playlists = false
|
||||
|
||||
# These are the default playlists. If you want them to have other names,
|
||||
# you can set it here.
|
||||
# name_library = "Library"
|
||||
# name_music = "Music"
|
||||
# name_movies = "Movies"
|
||||
# name_tvshows = "TV Shows"
|
||||
# name_podcasts = "Podcasts"
|
||||
# name_audiobooks = "Audiobooks"
|
||||
# name_radio = "Radio"
|
||||
|
||||
# Artwork file names (without file type extension)
|
||||
# OwnTone will look for jpg and png files with these base names
|
||||
# artwork_basenames = { "artwork", "cover", "Folder" }
|
||||
|
||||
# Enable searching for artwork corresponding to each individual media
|
||||
# file instead of only looking for album artwork. This is disabled by
|
||||
# default to reduce cache size.
|
||||
# artwork_individual = false
|
||||
|
||||
# File types the scanner should ignore
|
||||
# Non-audio files will never be added to the database, but here you
|
||||
# can prevent the scanner from even probing them. This might improve
|
||||
# scan time. By default .db, .ini, .db-journal, .pdf and .metadata are
|
||||
# ignored.
|
||||
# filetypes_ignore = { ".db", ".ini", ".db-journal", ".pdf", ".metadata" }
|
||||
|
||||
# File paths the scanner should ignore
|
||||
# If you want to exclude files on a more advanced basis you can enter
|
||||
# one or more POSIX regular expressions, and any file with a matching
|
||||
# path will be ignored.
|
||||
# filepath_ignore = { "myregex" }
|
||||
|
||||
# Disable startup file scanning
|
||||
# When OwnTone starts it will do an initial file scan of your
|
||||
# library (and then watch it for changes). If you are sure your library
|
||||
# never changes while OwnTone is not running, you can disable the
|
||||
# initial file scan and save some system ressources. Disabling this scan
|
||||
# may lead to OwnTone's database coming out of sync with the
|
||||
# library. If that happens read the instructions in the README on how
|
||||
# to trigger a rescan.
|
||||
# filescan_disable = false
|
||||
|
||||
# Only use the first genre found in metadata
|
||||
# Some tracks have multiple genres semicolon-separated in the same tag,
|
||||
# e.g. 'Pop;Rock'. If you don't want them listed like this, you can
|
||||
# enable this option and only the first genre will be used (i.e. 'Pop').
|
||||
# only_first_genre = false
|
||||
|
||||
# Should metadata from m3u playlists, e.g. artist and title in EXTINF,
|
||||
# override the metadata we get from radio streams?
|
||||
# m3u_overrides = false
|
||||
|
||||
# Should iTunes metadata override ours?
|
||||
# itunes_overrides = false
|
||||
|
||||
# Should we import the content of iTunes smart playlists?
|
||||
# itunes_smartpl = false
|
||||
|
||||
# Decoding options for DAAP and RSP clients
|
||||
# Since iTunes has native support for mpeg, mp4a, mp4v, alac and wav,
|
||||
# such files will be sent as they are. Any other formats will be decoded
|
||||
# to raw wav. If OwnTone detects a non-iTunes DAAP client, it is
|
||||
# assumed to only support mpeg and wav, other formats will be decoded.
|
||||
# Here you can change when to decode. Note that these settings only
|
||||
# affect serving media to DAAP and RSP clients, they have no effect on
|
||||
# direct AirPlay, Chromecast and local audio playback.
|
||||
# Formats: mp4a, mp4v, mpeg, alac, flac, mpc, ogg, wma, wmal, wmav, aif, wav
|
||||
# Formats that should never be decoded
|
||||
# no_decode = { "format", "format" }
|
||||
# Formats that should always be decoded
|
||||
# force_decode = { "format", "format" }
|
||||
|
||||
# Set ffmpeg filters (similar to 'ffmpeg -af xxx') that you want the
|
||||
# server to use when decoding files from your library. Examples:
|
||||
# { 'volume=replaygain=track' } -> use REPLAYGAIN_TRACK_GAIN metadata
|
||||
# { 'loudnorm=I=-16:LRA=11:TP=-1.5' } -> normalize volume
|
||||
# decode_audio_filters = { }
|
||||
|
||||
# Watch named pipes in the library for data and autostart playback when
|
||||
# there is data to be read. To exclude specific pipes from watching,
|
||||
# consider using the above _ignore options.
|
||||
# pipe_autostart = true
|
||||
|
||||
# Enable automatic rating updates
|
||||
# If enabled, rating is automatically updated after a song has either been
|
||||
# played or skipped (only skipping to the next song is taken into account).
|
||||
# The calculation is taken from the beets plugin "mpdstats" (see
|
||||
# https://beets.readthedocs.io/en/latest/plugins/mpdstats.html).
|
||||
# It consist of calculating a stable rating based only on the play- and
|
||||
# skipcount and a rolling rating based on the current rating and the action
|
||||
# (played or skipped). Both results are combined with a mix-factor of 0.75:
|
||||
# new rating = 0.75 * stable rating + 0.25 * rolling rating)
|
||||
# rating_updates = false
|
||||
|
||||
# By default, ratings are only saved in the server's database. Enable
|
||||
# the below to make the server also read ratings from file metadata and
|
||||
# write on update (requires write access). To avoid excessive writing to
|
||||
# the library, automatic rating updates are not written, even with the
|
||||
# write_rating option enabled.
|
||||
# read_rating = false
|
||||
# write_rating = false
|
||||
# The scale used when reading/writing ratings to files
|
||||
# max_rating = 100
|
||||
|
||||
# Allows creating, deleting and modifying m3u playlists in the library directories.
|
||||
# Only supported by the player web interface and some mpd clients
|
||||
# Defaults to being disabled.
|
||||
# allow_modifying_stored_playlists = false
|
||||
|
||||
# A directory in one of the library directories that will be used as the default
|
||||
# playlist directory. OwnTone creates new playlists in this directory if only
|
||||
# a playlist name is provided (requires "allow_modify_stored_playlists" set to true).
|
||||
# default_playlist_directory = ""
|
||||
|
||||
# By default OwnTone will - like iTunes - clear the playqueue if
|
||||
# playback stops. Setting clear_queue_on_stop_disable to true will keep
|
||||
# the playlist like MPD does. Note that some dacp clients do not show
|
||||
# the playqueue if playback is stopped.
|
||||
# clear_queue_on_stop_disable = false
|
||||
}
|
||||
|
||||
# Local audio output
|
||||
audio {
|
||||
# Name - used in the speaker list in Remote
|
||||
nickname = "Computer"
|
||||
|
||||
# Type of the output (alsa, pulseaudio, dummy or disabled)
|
||||
# type = "alsa"
|
||||
|
||||
# For pulseaudio output, an optional server hostname or IP can be
|
||||
# specified (e.g. "localhost"). If not set, connection is made via local
|
||||
# socket.
|
||||
# server = ""
|
||||
|
||||
# Audio PCM device name for local audio output - ALSA only
|
||||
# card = "default"
|
||||
|
||||
# Mixer channel to use for volume control - ALSA only
|
||||
# If not set, PCM will be used if available, otherwise Master.
|
||||
# mixer = ""
|
||||
|
||||
# Mixer device to use for volume control - ALSA only
|
||||
# If not set, the value for "card" will be used.
|
||||
# mixer_device = ""
|
||||
|
||||
# Enable or disable audio resampling to keep local audio in sync with
|
||||
# e.g. Airplay. This feature relies on accurate ALSA measurements of
|
||||
# delay, and some devices don't provide that. If that is the case you
|
||||
# are better off disabling the feature.
|
||||
# sync_disable = false
|
||||
|
||||
# Here you can adjust when local audio is started relative to other
|
||||
# speakers, e.g. Airplay. Negative values correspond to moving local
|
||||
# audio ahead, positive correspond to delaying it. The unit is
|
||||
# milliseconds. The offset must be between -1000 and 1000 (+/- 1 sec).
|
||||
# offset_ms = 0
|
||||
|
||||
# To calculate what and if resampling is required, local audio delay is
|
||||
# measured each second. After a period the collected measurements are
|
||||
# used to estimate drift and latency, which determines if corrections
|
||||
# are required. This setting sets the length of that period in seconds.
|
||||
# adjust_period_seconds = 100
|
||||
}
|
||||
|
||||
# ALSA device settings
|
||||
# If you have multiple ALSA devices you can configure them individually via
|
||||
# sections like the below. Make sure to set the "card name" correctly. See the
|
||||
# README about ALSA for details. Note that these settings will override the ALSA
|
||||
# settings in the "audio" section above.
|
||||
#alsa "card name" {
|
||||
# Name used in the speaker list. If not set, the card name will be used.
|
||||
# nickname = "Computer"
|
||||
|
||||
# Mixer channel to use for volume control
|
||||
# If not set, PCM will be used if available, otherwise Master
|
||||
# mixer = ""
|
||||
|
||||
# Mixer device to use for volume control
|
||||
# If not set, the card name will be used
|
||||
# mixer_device = ""
|
||||
#}
|
||||
|
||||
# Pipe output
|
||||
# Allows OwnTone to output audio data to a named pipe
|
||||
#fifo {
|
||||
# nickname = "fifo"
|
||||
# path = "/path/to/fifo"
|
||||
#}
|
||||
|
||||
# AirPlay settings common to all devices
|
||||
#airplay_shared {
|
||||
# UDP ports used when airplay devices make connections back to
|
||||
# OwnTone (choosing specific ports may be helpful when running
|
||||
# OwnTone behind a firewall)
|
||||
# control_port = 0
|
||||
# timing_port = 0
|
||||
|
||||
# Switch Airplay 1 streams to uncompressed ALAC (as opposed to regular,
|
||||
# compressed ALAC). Reduces CPU use at the cost of network bandwidth.
|
||||
# uncompressed_alac = false
|
||||
#}
|
||||
|
||||
# AirPlay per device settings
|
||||
# (make sure you get the capitalization of the device name right)
|
||||
#airplay "My AirPlay device" {
|
||||
# OwnTone's volume goes to 11! If that's more than you can handle
|
||||
# you can set a lower value here
|
||||
# max_volume = 11
|
||||
|
||||
# Enable this option to exclude a particular AirPlay device from the
|
||||
# speaker list
|
||||
# exclude = false
|
||||
|
||||
# Enable this option to keep a particular AirPlay device in the speaker
|
||||
# list and thus ignore mdns notifications about it no longer being
|
||||
# present. The speaker will remain until restart of OwnTone.
|
||||
# permanent = false
|
||||
|
||||
# Some devices spuriously disconnect during playback, and based on the
|
||||
# device type OwnTone may attempt to reconnect. Setting this option
|
||||
# overrides this so reconnecting is either always enabled or disabled.
|
||||
# reconnect = false
|
||||
|
||||
# AirPlay password
|
||||
# password = "s1kr3t"
|
||||
|
||||
# Disable AirPlay 1 (RAOP)
|
||||
# raop_disable = false
|
||||
|
||||
# Name used in the speaker list, overrides name from the device
|
||||
# nickname = "My speaker name"
|
||||
#}
|
||||
|
||||
# Chromecast settings
|
||||
# (make sure you get the capitalization of the device name right)
|
||||
#chromecast "My Chromecast device" {
|
||||
# OwnTone's volume goes to 11! If that's more than you can handle
|
||||
# you can set a lower value here
|
||||
# max_volume = 11
|
||||
|
||||
# Enable this option to exclude a particular device from the speaker
|
||||
# list
|
||||
# exclude = false
|
||||
|
||||
# Name used in the speaker list, overrides name from the device
|
||||
# nickname = "My speaker name"
|
||||
#}
|
||||
|
||||
# Spotify settings (only have effect if Spotify enabled - see README/INSTALL)
|
||||
spotify {
|
||||
# Set preferred bitrate for music streaming
|
||||
# 0: No preference (default), 1: 96kbps, 2: 160kbps, 3: 320kbps
|
||||
# bitrate = 0
|
||||
|
||||
# Your Spotify playlists will by default be put in a "Spotify" playlist
|
||||
# folder. If you would rather have them together with your other
|
||||
# playlists you can set this option to true.
|
||||
# base_playlist_disable = false
|
||||
|
||||
# Spotify playlists usually have many artist, and if you don't want
|
||||
# every artist to be listed when artist browsing in Remote, you can set
|
||||
# the artist_override flag to true. This will use the compilation_artist
|
||||
# as album artist for Spotify items.
|
||||
# artist_override = false
|
||||
|
||||
# Similar to the different artists in Spotify playlists, the playlist
|
||||
# items belong to different albums, and if you do not want every album
|
||||
# to be listed when browsing in Remote, you can set the album_override
|
||||
# flag to true. This will use the playlist name as album name for
|
||||
# Spotify items. Notice that if an item is in more than one playlist,
|
||||
# it will only appear in one album when browsing (in which album is
|
||||
# random).
|
||||
# album_override = false
|
||||
}
|
||||
|
||||
# RCP/Roku Soundbridge output settings
|
||||
# (make sure you get the capitalization of the device name right)
|
||||
#rcp "My SoundBridge device" {
|
||||
# Enable this option to exclude a particular device from the speaker
|
||||
# list
|
||||
# exclude = false
|
||||
|
||||
# A Roku/SoundBridge can power up in 2 modes: (default) reconnect to the
|
||||
# previously used library (ie OwnTone) or in a 'cleared library' mode.
|
||||
# The Roku power up behaviour is affected by how OwnTone disconnects
|
||||
# from the Roku device.
|
||||
#
|
||||
# Set to false to maintain default Roku power on behaviour
|
||||
# clear_on_close = false
|
||||
#}
|
||||
|
||||
|
||||
# MPD configuration (only have effect if MPD enabled - see README/INSTALL)
|
||||
mpd {
|
||||
# TCP port to listen on for MPD client requests.
|
||||
# Default port is 6600, set to 0 to disable MPD support.
|
||||
# port = 6600
|
||||
|
||||
# HTTP port to listen for artwork requests (only supported by some MPD
|
||||
# clients and will need additional configuration in the MPD client to
|
||||
# work). Set to 0 to disable serving artwork over http.
|
||||
# http_port = 0
|
||||
}
|
||||
|
||||
# SQLite configuration (allows to modify the operation of the SQLite databases)
|
||||
# Make sure to read the SQLite documentation for the corresponding PRAGMA
|
||||
# statements as changing them from the defaults may increase the possibility of
|
||||
# database corruptions! By default the SQLite default values are used.
|
||||
sqlite {
|
||||
# Cache size in number of db pages for the library database
|
||||
# (SQLite default page size is 1024 bytes and cache size is 2000 pages)
|
||||
# pragma_cache_size_library = 2000
|
||||
|
||||
# Cache size in number of db pages for the daap cache database
|
||||
# (SQLite default page size is 1024 bytes and cache size is 2000 pages)
|
||||
# pragma_cache_size_cache = 2000
|
||||
|
||||
# Sets the journal mode for the database
|
||||
# DELETE (default), TRUNCATE, PERSIST, MEMORY, WAL, OFF
|
||||
# pragma_journal_mode = DELETE
|
||||
|
||||
# Change the setting of the "synchronous" flag
|
||||
# 0: OFF, 1: NORMAL, 2: FULL (default)
|
||||
# pragma_synchronous = 2
|
||||
|
||||
# Number of bytes set aside for memory-mapped I/O for the library database
|
||||
# (requires sqlite 3.7.17 or later)
|
||||
# 0: disables mmap (default), any other value > 0: number of bytes for mmap
|
||||
# pragma_mmap_size_library = 0
|
||||
|
||||
# Number of bytes set aside for memory-mapped I/O for the cache database
|
||||
# (requires sqlite 3.7.17 or later)
|
||||
# 0: disables mmap (default), any other value > 0: number of bytes for mmap
|
||||
# pragma_mmap_size_cache = 0
|
||||
|
||||
# Should the database be vacuumed on startup? (increases startup time,
|
||||
# but may reduce database size). Default is yes.
|
||||
# vacuum = yes
|
||||
}
|
||||
|
||||
# Streaming audio settings for remote connections (ie stream.mp3)
|
||||
streaming {
|
||||
# Sample rate, typically 44100 or 48000
|
||||
# sample_rate = 44100
|
||||
|
||||
# Set the MP3 streaming bit rate (in kbps), valid options: 64 / 96 / 128 / 192 / 320
|
||||
# bit_rate = 192
|
||||
}
|
||||
11
.dev/devcontainer/devcontainer.env
Normal file
11
.dev/devcontainer/devcontainer.env
Normal file
@ -0,0 +1,11 @@
|
||||
# Starfish (https://starship.rs/) - shell prompt
|
||||
ENABLE_STARSHIP=1
|
||||
|
||||
# Atuin (https://atuin.sh/) - shell history
|
||||
ENABLE_ATUIN=1
|
||||
|
||||
# zoxide (https://github.com/ajeetdsouza/zoxide) - replacement for cd
|
||||
ENABLE_ZOXIDE=1
|
||||
|
||||
# eza (https://eza.rocks/) - replacement for ls
|
||||
ENABLE_ESA=1
|
||||
73
.dev/devcontainer/ubuntu/Dockerfile
Normal file
73
.dev/devcontainer/ubuntu/Dockerfile
Normal file
@ -0,0 +1,73 @@
|
||||
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
|
||||
|
||||
ARG USERNAME=vscode
|
||||
|
||||
# Workaround for bug: https://github.com/devcontainers/images/issues/1056
|
||||
RUN userdel -r ubuntu; usermod -u 1000 $USERNAME; groupmod -g 1000 $USERNAME
|
||||
|
||||
RUN apt-get -y update \
|
||||
&& apt-get install -y \
|
||||
# Build tools and dependencies for OwnTone
|
||||
autoconf \
|
||||
automake \
|
||||
autotools-dev \
|
||||
bison \
|
||||
build-essential \
|
||||
flex \
|
||||
gawk \
|
||||
gettext \
|
||||
git \
|
||||
gperf \
|
||||
libasound2-dev \
|
||||
libavahi-client-dev \
|
||||
libavcodec-dev \
|
||||
libavfilter-dev \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libconfuse-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libevent-dev \
|
||||
libgcrypt20-dev \
|
||||
libjson-c-dev \
|
||||
libmxml-dev \
|
||||
libplist-dev \
|
||||
libprotobuf-c-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
libswscale-dev \
|
||||
libtool \
|
||||
libunistring-dev \
|
||||
libwebsockets-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
# Build tools for mmkdocs (OwnTone documentation)
|
||||
python3-pip \
|
||||
# Additional runtime dependencies for dev container
|
||||
avahi-daemon \
|
||||
# Additional debug and devtools for dev container
|
||||
clang \
|
||||
clang-format \
|
||||
clang-tools \
|
||||
gdb \
|
||||
valgrind \
|
||||
# Additional terminal utility applications
|
||||
pipx \
|
||||
# bat - replacement for cat
|
||||
bat \
|
||||
# eza (https://eza.rocks/) - replacement for ls
|
||||
eza \
|
||||
# fuzzy search
|
||||
fzf \
|
||||
# Create folders and set ownership for folders that might be mounted as volumes
|
||||
&& mkdir -p /home/$USERNAME/.local/share \
|
||||
&& chown -R $USERNAME /home/$USERNAME/.local \
|
||||
&& mkdir /commandhistory \
|
||||
&& touch /commandhistory/.bash_history \
|
||||
&& chown -R $USERNAME /commandhistory \
|
||||
&& echo "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \
|
||||
&& echo '[[ -f /scripts/init-devcontainer-cli.sh ]] && source /scripts/init-devcontainer-cli.sh' >> "/home/$USERNAME/.bashrc" \
|
||||
# Create folders for owntone-server data
|
||||
&& mkdir -p /data/logs /data/music /data/cache /data/conf \
|
||||
&& chown -R $USERNAME /data \
|
||||
# Clean up
|
||||
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*
|
||||
55
.dev/devcontainer/ubuntu/devcontainer.json
Normal file
55
.dev/devcontainer/ubuntu/devcontainer.json
Normal file
@ -0,0 +1,55 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
|
||||
{
|
||||
"name": "Ubuntu",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
// "image": "mcr.microsoft.com/devcontainers/base:jammy"
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
|
||||
"runArgs": [
|
||||
// Use host network to be able to connect to remote speakers
|
||||
"--network=host",
|
||||
"--env-file", ".devcontainer/devcontainer.env"
|
||||
],
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"Vue.volar",
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"lokalise.i18n-ally",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
|
||||
// Mounts volumes to keep files / state between container rebuilds
|
||||
"mounts": [
|
||||
// Map script folder to install and init additional tools (see "postCreateCommand" and Dockerfile / .bashrc)
|
||||
"source=${localWorkspaceFolder}/.devcontainer/.scripts,target=/scripts,type=bind,consistency=cached",
|
||||
// Persist ~/.bash_history
|
||||
"source=owntone-bashhistory,target=/commandhistory,type=volume",
|
||||
// Persist ~/.local/share to persist state of additionally installed tools (e. g. atuin, zoxide)
|
||||
"source=owntone-localshare,target=/home/vscode/.local/share,type=volume",
|
||||
// Bind mounts for owntone config file and logs, cache, music directories
|
||||
//"source=<path-to-local-logs-dir>,target=/data/logs,type=bind,consistency=cached",
|
||||
//"source=<path-to-local-cache-dir>,target=/data/cache,type=bind,consistency=cached",
|
||||
//"source=<path-to-local-music-dir>,target=/data/music,type=bind,consistency=cached",
|
||||
"source=${localWorkspaceFolder}/.devcontainer/data/devcontainer-owntone.conf,target=/data/conf/owntone.conf,type=bind,consistency=cached"
|
||||
],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "bash /scripts/install-devcontainer-tools.sh",
|
||||
|
||||
// Start dbus and avahi, required when running owntone-server
|
||||
"postStartCommand": "sudo service dbus start ; sudo avahi-daemon -D"
|
||||
}
|
||||
17
.dev/vscode/c_cpp_properties.json
Normal file
17
.dev/vscode/c_cpp_properties.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"/usr/include/json-c"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/usr/bin/clang",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++17",
|
||||
"intelliSenseMode": "linux-clang-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
28
.dev/vscode/launch.json
Normal file
28
.dev/vscode/launch.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "OwnTone",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/owntone",
|
||||
"args": ["-f", "-c", "/data/conf/owntone.conf", "-w", "${workspaceFolder}/htdocs", "-s", "${workspaceFolder}/sqlext/.libs/owntone-sqlext.so"],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Enable pretty-printing for gdb",
|
||||
"text": "-enable-pretty-printing",
|
||||
"ignoreFailures": true
|
||||
},
|
||||
{
|
||||
"description": "Set Disassembly Flavor to Intel",
|
||||
"text": "-gdb-set disassembly-flavor intel",
|
||||
"ignoreFailures": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "2.0.0"
|
||||
}
|
||||
12
.dev/vscode/settings.json
Normal file
12
.dev/vscode/settings.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"C_Cpp.default.forcedInclude": [
|
||||
"${workspaceFolder}/config.h"
|
||||
],
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[c]": {
|
||||
"editor.detectIndentation": false,
|
||||
"editor.tabSize": 8
|
||||
}
|
||||
}
|
||||
106
.dev/vscode/tasks.json
Normal file
106
.dev/vscode/tasks.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "[server] autoreconf",
|
||||
"type": "shell",
|
||||
"command": "autoreconf -i",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[server] configure",
|
||||
"type": "shell",
|
||||
"command": "./configure",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[server] make",
|
||||
"type": "shell",
|
||||
"command": "make",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[server] scan-build",
|
||||
"type": "shell",
|
||||
"command": "make clean && scan-build --status-bugs -disable-checker deadcode.DeadStores --exclude src/parsers make",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[server] clean",
|
||||
"type": "shell",
|
||||
"command": "make clean",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[web] install",
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/web-src"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[web] build",
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/web-src"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[web] serve",
|
||||
"type": "npm",
|
||||
"script": "serve",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/web-src"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[docs] serve",
|
||||
"type": "shell",
|
||||
"command": "mkdocs serve",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[docs] build",
|
||||
"type": "shell",
|
||||
"command": "mkdocs build",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -13,4 +13,4 @@ Please try to provide the following:
|
||||
- version of owntone
|
||||
- platform
|
||||
|
||||
Steps to reproduce will greatly improve the chance of getting it fixed. If it is not possible then set the log level to debug (in /etc/owntone.conf) and try to get some logging of when the error happens. Don’t cut and paste lengthy log outtakes here on github. Instead, attach the log file.
|
||||
Steps to reproduce will greatly improve the chance of getting it fixed. If it is not possible then set the log level to debug (in /etc/owntone.conf) and try to get some logging of when the error happens. Don’t cut and paste lengthy log outtakes here on github. Instead, attach the log file. Remove parts of the log file that aren't relevant and also make sure to delete Spotify username/tokens.
|
||||
|
||||
39
.github/workflows/build_htdocs.yml
vendored
Normal file
39
.github/workflows/build_htdocs.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: Build htdocs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'web-src/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: web-src
|
||||
run: npm install
|
||||
|
||||
# Build for production with minification (will update web interface
|
||||
# in "../htdocs")
|
||||
- name: Build htdocs
|
||||
working-directory: web-src
|
||||
run: npm run build
|
||||
|
||||
- name: Count changed files
|
||||
id: count
|
||||
run: |
|
||||
git add htdocs/
|
||||
git diff --numstat --staged > diffstat
|
||||
test -s diffstat || { echo "Warning: Push to web-src did not change htdocs"; exit 1; }
|
||||
|
||||
# The GH action email is from https://github.com/orgs/community/discussions/26560
|
||||
- name: Commit and push updated assets
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "[web] Rebuild web interface"
|
||||
git push
|
||||
25
.github/workflows/codeql-analysis.yml
vendored
25
.github/workflows/codeql-analysis.yml
vendored
@ -2,10 +2,19 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, ]
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
schedule:
|
||||
- cron: '0 19 * * 6'
|
||||
|
||||
@ -16,11 +25,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
@ -30,7 +39,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
#- name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v1
|
||||
# uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@ -40,10 +49,10 @@ jobs:
|
||||
# uses a compiled language
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
|
||||
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libxml2-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
|
||||
autoreconf -vi
|
||||
./configure --enable-lastfm --enable-chromecast
|
||||
scan-build --status-bugs -disable-checker deadcode.DeadStores --exclude src/parsers make
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
44
.github/workflows/freebsd.yml
vendored
Normal file
44
.github/workflows/freebsd.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: FreeBSD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
prepare: |
|
||||
pkg install -y gmake autoconf automake libtool pkgconf gettext gperf glib ffmpeg libconfuse libevent libxml2 libgcrypt libunistring libiconv curl libplist libinotify avahi sqlite3 alsa-lib libsodium json-c libwebsockets protobuf-c bison flex
|
||||
pw user add owntone -m -d /usr/local/var/cache/owntone
|
||||
|
||||
run: |
|
||||
autoreconf -vi
|
||||
export CFLAGS="${ARCH} -g -I/usr/local/include -I/usr/include"
|
||||
export LDFLAGS="-L/usr/local/lib -L/usr/lib"
|
||||
./configure --disable-install-system
|
||||
gmake
|
||||
gmake install
|
||||
mkdir -p /srv/music
|
||||
service dbus onestart
|
||||
service avahi-daemon onestart
|
||||
/usr/local/sbin/owntone -f -t
|
||||
2
.github/workflows/gh-pages.yml
vendored
2
.github/workflows/gh-pages.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
84
.github/workflows/macos.yml
vendored
84
.github/workflows/macos.yml
vendored
@ -1,32 +1,42 @@
|
||||
name: MacOS
|
||||
name: macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build tools
|
||||
run: brew install automake autoconf libtool pkg-config
|
||||
run: brew install automake autoconf libtool pkg-config gettext
|
||||
|
||||
- name: Install gperf, bison and flex
|
||||
# MacOS comes with an ancient bison, we need a newer version. Homebrew's
|
||||
# bison and flex are keg-only, which means they are not symlinked into
|
||||
# /usr/local because macOS already provides this software. Homebrew tells
|
||||
# you to adjust the $PATH, but I couldn't make that work, and I think
|
||||
# symlinking is a better solution for simple binaries.
|
||||
# macOS has ancient versions of bison and flex, so we need a newer from
|
||||
# Homebrew. The new versions are installed keg-only, so we must tell
|
||||
# configure/make where to look. Adjusting $PATH doesn't work (maybe
|
||||
# because make invokes the two via ylwrap), so instead symlink the two
|
||||
# into /usr/local/bin.
|
||||
run: |
|
||||
brew install gperf bison flex
|
||||
sudo ln -s /usr/local/opt/bison/bin/bison /usr/local/bin/bison
|
||||
sudo ln -s /usr/local/opt/flex/bin/flex /usr/local/bin/flex
|
||||
sudo ln -s "$(brew --prefix)/opt/bison/bin/bison" /usr/local/bin/bison
|
||||
sudo ln -s "$(brew --prefix)/opt/flex/bin/flex" /usr/local/bin/flex
|
||||
|
||||
- name: Install libinotify-kqueue
|
||||
# brew does not have libinotify package
|
||||
@ -39,40 +49,36 @@ jobs:
|
||||
sudo make install
|
||||
cd ..
|
||||
|
||||
- name: Install sqlite
|
||||
# Brew package does not have unlock-notify
|
||||
run: |
|
||||
wget https://www.sqlite.org/2020/sqlite-autoconf-3310100.tar.gz
|
||||
tar xzf sqlite-autoconf-3310100.tar.gz
|
||||
cd sqlite-autoconf-3310100
|
||||
export CFLAGS='-DSQLITE_ENABLE_UNLOCK_NOTIFY=1'
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
cd ..
|
||||
|
||||
- name: Install ffmpeg
|
||||
# The libbluray ffmpeg dependency fails without the chown (source: stackoverflow)
|
||||
run: |
|
||||
sudo chown -R $(whoami) $(brew --prefix)/*
|
||||
brew install ffmpeg
|
||||
|
||||
- name: Install other dependencies
|
||||
run: brew install libunistring libmxml confuse libplist sqlite libwebsockets libevent libgcrypt json-c protobuf-c libsodium gnutls pulseaudio openssl
|
||||
# libxml2 is included with macOS
|
||||
run: |
|
||||
brew install libunistring confuse libplist libwebsockets libevent libgcrypt json-c protobuf-c libsodium gnutls pulseaudio openssl ffmpeg sqlite
|
||||
|
||||
- name: Configure
|
||||
# We configure a non-privileged setup, since how to add a "owntone" system
|
||||
# user in macOS isn't clear to me (useradd etc. isn't available)
|
||||
run: |
|
||||
export ACLOCAL_PATH="$(brew --prefix)/share/gettext/m4:$ACLOCAL_PATH"
|
||||
export CFLAGS="-I$(brew --prefix)/include -I$(brew --prefix sqlite)/include"
|
||||
export LDFLAGS="-L$(brew --prefix)/lib -L$(brew --prefix sqlite)/lib"
|
||||
autoreconf -fi
|
||||
./configure --enable-chromecast --enable-lastfm --with-pulseaudio
|
||||
./configure --prefix=$HOME/owntone_data/usr --sysconfdir=$HOME/owntone_data/etc --localstatedir=$HOME/owntone_data/var --enable-chromecast --with-pulseaudio
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
# Without setting these env vars the build fails with "fatal error: 'openssl/ssl.h' file not found"
|
||||
# (Solution taken from https://github.com/libimobiledevice/libimobiledevice/issues/389#issuecomment-289284190)
|
||||
export LD_LIBRARY_PATH=$(brew --prefix openssl)/lib
|
||||
export CPATH=$(brew --prefix openssl)/include
|
||||
export PKG_CONFIG_PATH=$(brew --prefix openssl)/lib/pkgconfig
|
||||
make
|
||||
|
||||
- name: Install
|
||||
run: sudo make install
|
||||
run: |
|
||||
make install
|
||||
|
||||
- name: Prepare test run
|
||||
run: |
|
||||
mkdir -p $HOME/owntone_data/media
|
||||
sed -i '' 's/uid = "owntone"/uid = ${USER}/g' $HOME/owntone_data/etc/owntone.conf
|
||||
sed -i '' 's/loglevel = log/loglevel = debug/g' $HOME/owntone_data/etc/owntone.conf
|
||||
sed -i '' 's/directories = { "\/srv\/music" }/directories = { "${HOME}\/owntone_data\/media" }/g' $HOME/owntone_data/etc/owntone.conf
|
||||
|
||||
- name: Test run
|
||||
run: |
|
||||
$HOME/owntone_data/usr/sbin/owntone -f -t
|
||||
|
||||
69
.github/workflows/macos_12.yml
vendored
Normal file
69
.github/workflows/macos_12.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
name: macOS 12
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build tools
|
||||
run: brew install automake autoconf libtool pkg-config gettext
|
||||
|
||||
- name: Install gperf, bison and flex
|
||||
# macOS has ancient versions of bison and flex, so we need a newer from
|
||||
# Homebrew. The new versions are installed keg-only, so we must tell
|
||||
# configure/make where to look. Adjusting $PATH doesn't work (maybe
|
||||
# because make invokes the two via ylwrap), so instead symlink the two
|
||||
# into /usr/local/bin.
|
||||
run: |
|
||||
brew install gperf bison flex
|
||||
sudo ln -s "$(brew --prefix)/opt/bison/bin/bison" /usr/local/bin/bison
|
||||
sudo ln -s "$(brew --prefix)/opt/flex/bin/flex" /usr/local/bin/flex
|
||||
|
||||
- name: Install libinotify-kqueue
|
||||
# brew does not have libinotify package
|
||||
run: |
|
||||
git clone https://github.com/libinotify-kqueue/libinotify-kqueue
|
||||
cd libinotify-kqueue
|
||||
autoreconf -fvi
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
cd ..
|
||||
|
||||
- name: Install other dependencies
|
||||
# libxml2 is included with macOS
|
||||
run: |
|
||||
brew install libunistring confuse libplist libwebsockets libevent libgcrypt json-c protobuf-c libsodium gnutls pulseaudio openssl ffmpeg sqlite
|
||||
|
||||
- name: Configure
|
||||
# We configure a non-privileged setup, since how to add a "owntone" system
|
||||
# user in macOS isn't clear to me (useradd etc. isn't available)
|
||||
run: |
|
||||
export CFLAGS="-I$(brew --prefix)/include -I$(brew --prefix sqlite)/include"
|
||||
export LDFLAGS="-L$(brew --prefix)/lib -L$(brew --prefix sqlite)/lib"
|
||||
autoreconf -fi
|
||||
./configure --prefix=$HOME/owntone_data/usr --sysconfdir=$HOME/owntone_data/etc --localstatedir=$HOME/owntone_data/var --enable-chromecast --with-pulseaudio
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
make
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
make install
|
||||
|
||||
- name: Prepare test run
|
||||
run: |
|
||||
mkdir -p $HOME/owntone_data/media
|
||||
sed -i '' 's/uid = "owntone"/uid = ${USER}/g' $HOME/owntone_data/etc/owntone.conf
|
||||
sed -i '' 's/loglevel = log/loglevel = debug/g' $HOME/owntone_data/etc/owntone.conf
|
||||
sed -i '' 's/directories = { "\/srv\/music" }/directories = { "${HOME}\/owntone_data\/media" }/g' $HOME/owntone_data/etc/owntone.conf
|
||||
|
||||
- name: Test run
|
||||
run: |
|
||||
$HOME/owntone_data/usr/sbin/owntone -f -t
|
||||
45
.github/workflows/ubuntu.yml
vendored
45
.github/workflows/ubuntu.yml
vendored
@ -2,30 +2,51 @@ name: Ubuntu
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'htdocs/**'
|
||||
- 'web-src/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install dependencies
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
|
||||
- name: build and check
|
||||
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libxml2-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
|
||||
|
||||
- name: Build and check
|
||||
run: |
|
||||
autoreconf -vi
|
||||
./configure
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user --enable-chromecast --with-pulseaudio
|
||||
make
|
||||
make check
|
||||
make distcheck
|
||||
- name: build with lastfm, chromecast, pulse
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
autoreconf -vi
|
||||
./configure --enable-lastfm --enable-chromecast --with-pulseaudio
|
||||
make
|
||||
sudo mkdir -p /srv/music
|
||||
sudo make install
|
||||
sudo sed -i 's/loglevel = log/loglevel = debug/g' /etc/owntone.conf
|
||||
|
||||
- name: Install and run avahi-daemon
|
||||
run: |
|
||||
sudo apt-get install -yq avahi-daemon
|
||||
|
||||
- name: Test run
|
||||
run: |
|
||||
sudo /usr/sbin/owntone -f -t
|
||||
|
||||
27
.github/workflows/webui_lint.yml
vendored
Normal file
27
.github/workflows/webui_lint.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Web UI Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'web-src/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'web-src/**'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: web-src
|
||||
run: npm install
|
||||
|
||||
- name: Run linter
|
||||
working-directory: web-src
|
||||
run: npm run lint --no-fix
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -35,14 +35,15 @@ owntone.conf
|
||||
owntone.service
|
||||
owntone@.service
|
||||
|
||||
# ignore debian packaging for convenience
|
||||
debian/
|
||||
|
||||
/.settings
|
||||
/.cproject
|
||||
/.project
|
||||
/.autotools
|
||||
/.vscode
|
||||
|
||||
# ignore MkDocs generated documentation
|
||||
/site/
|
||||
/test/
|
||||
|
||||
/.vscode/
|
||||
/.devcontainer/
|
||||
!/.dev/Makefile
|
||||
|
||||
35
Makefile.am
35
Makefile.am
@ -23,24 +23,29 @@ dist_man_MANS = owntone.8
|
||||
nobase_dist_doc_DATA = \
|
||||
UPGRADING \
|
||||
README.md \
|
||||
docs/index.md \
|
||||
docs/getting-started.md \
|
||||
docs/installation.md \
|
||||
docs/configuration.md \
|
||||
docs/building.md \
|
||||
docs/library.md \
|
||||
docs/control-clients/mobile.md \
|
||||
docs/control-clients/desktop.md \
|
||||
docs/control-clients/web.md \
|
||||
docs/control-clients/cli-api.md \
|
||||
docs/audio-outputs/airplay.md \
|
||||
docs/audio-outputs/chromecast.md \
|
||||
docs/audio-outputs/local-audio.md \
|
||||
docs/audio-outputs/mobile.md \
|
||||
docs/audio-outputs/web.md \
|
||||
docs/audio-outputs/roku.md \
|
||||
docs/audio-outputs/streaming.md \
|
||||
docs/media-clients.md \
|
||||
docs/artwork.md \
|
||||
docs/playlists.md \
|
||||
docs/smart-playlists.md \
|
||||
docs/integrations/spotify.md \
|
||||
docs/integrations/lastfm.md \
|
||||
docs/index.md \
|
||||
docs/outputs/streaming.md \
|
||||
docs/outputs/chromecast.md \
|
||||
docs/outputs/airplay.md \
|
||||
docs/outputs/local-audio.md \
|
||||
docs/installation.md \
|
||||
docs/clients/web-interface.md \
|
||||
docs/clients/remote.md \
|
||||
docs/clients/cli.md \
|
||||
docs/clients/supported-clients.md \
|
||||
docs/clients/mpd.md \
|
||||
docs/smart-playlists.md \
|
||||
docs/artwork.md \
|
||||
docs/library.md \
|
||||
docs/getting-started.md \
|
||||
docs/advanced/radio-streams.md \
|
||||
docs/advanced/multiple-instances.md \
|
||||
docs/advanced/outputs-alsa.md \
|
||||
|
||||
26
README.md
26
README.md
@ -1,15 +1,18 @@
|
||||
# OwnTone (previously forked-daapd)
|
||||
# OwnTone
|
||||
|
||||
OwnTone is a Linux/FreeBSD DAAP (iTunes), MPD (Music Player Daemon) and
|
||||
RSP (Roku) media server.
|
||||
OwnTone is a media server that lets you play audio sources such as local files,
|
||||
Spotify, pipe input or internet radio to AirPlay 1 and 2 receivers, Chromecast
|
||||
receivers, Roku Soundbridge, a browser or the server’s own sound system. Or you
|
||||
can listen to your music via any client that supports mp3 streaming.
|
||||
|
||||
It supports AirPlay devices/speakers, Apple Remote (and compatibles),
|
||||
MPD clients, Chromecast, network streaming, internet radio, Spotify and LastFM.
|
||||
You control the server via a web interface, Apple Remote, an Android remote
|
||||
(e.g. Retune), an MPD client, json API or DACP.
|
||||
|
||||
It does not support streaming video by AirPlay nor Chromecast.
|
||||
OwnTone also serves local files via the Digital Audio Access Protocol (DAAP) to
|
||||
iTunes (Windows), Apple Music (macOS) and Rhythmbox (Linux), and via the Roku
|
||||
Server Protocol (RSP) to Roku devices.
|
||||
|
||||
DAAP stands for Digital Audio Access Protocol which is the protocol used
|
||||
by iTunes and friends to share/stream media libraries over the network.
|
||||
Runs on Linux, BSD and macOS.
|
||||
|
||||
OwnTone was previously called forked-daapd, which again was a rewrite of
|
||||
mt-daapd (Firefly Media Server).
|
||||
@ -17,10 +20,9 @@ mt-daapd (Firefly Media Server).
|
||||
|
||||
## Looking for help?
|
||||
|
||||
Visit the [OwnTone documentation](https://owntone.github.io/owntone-server/installation/) for
|
||||
Visit the [OwnTone documentation](https://owntone.github.io/owntone-server/) for
|
||||
usage and set up instructions, API documentation, etc.
|
||||
|
||||
If you are looking for help on building OwnTone (not using it), then
|
||||
please see the [Installation](https://owntone.github.io/owntone-server/installation/)
|
||||
If you are looking for information on how to get and install OwnTone, then see
|
||||
the [Installation](https://owntone.github.io/owntone-server/installation/)
|
||||
instructions.
|
||||
|
||||
|
||||
72
configure.ac
72
configure.ac
@ -1,14 +1,14 @@
|
||||
dnl Process this file with autoconf to produce a configure script.
|
||||
|
||||
AC_PREREQ([2.60])
|
||||
AC_INIT([owntone], [28.4])
|
||||
AC_INIT([owntone], [29.0])
|
||||
|
||||
AC_CONFIG_SRCDIR([config.h.in])
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
AC_CONFIG_AUX_DIR([build-aux])
|
||||
AC_CONFIG_HEADERS([config.h])
|
||||
AM_INIT_AUTOMAKE([foreign subdir-objects 1.11])
|
||||
AM_SILENT_RULES([no])
|
||||
AM_SILENT_RULES([yes])
|
||||
|
||||
dnl Requires autoconf 2.60
|
||||
AC_USE_SYSTEM_EXTENSIONS
|
||||
@ -50,7 +50,7 @@ AC_CHECK_HEADERS([sys/wait.h sys/param.h dirent.h getopt.h stdint.h], [],
|
||||
[AC_MSG_ERROR([[Missing header required to build OwnTone]])])
|
||||
AC_CHECK_HEADERS([time.h], [],
|
||||
[AC_MSG_ERROR([[Missing header required to build OwnTone]])])
|
||||
AC_CHECK_FUNCS_ONCE([posix_fadvise pipe2])
|
||||
AC_CHECK_FUNCS_ONCE([posix_fadvise pipe2 gettid])
|
||||
AC_CHECK_FUNCS([strptime strtok_r], [],
|
||||
[AC_MSG_ERROR([[Missing function required to build OwnTone]])])
|
||||
|
||||
@ -77,18 +77,35 @@ AC_SEARCH_LIBS([pthread_exit], [pthread], [],
|
||||
AC_SEARCH_LIBS([pthread_setname_np], [pthread],
|
||||
[dnl Validate pthread_setname_np with 2 args (some have 1)
|
||||
AC_MSG_CHECKING([[for two-parameter pthread_setname_np]])
|
||||
AC_TRY_LINK([@%:@include <pthread.h>],
|
||||
[pthread_setname_np(pthread_self(), "name");],
|
||||
AC_LINK_IFELSE([AC_LANG_PROGRAM([[@%:@include <pthread.h>]],
|
||||
[[pthread_setname_np(pthread_self(), "name");]])],
|
||||
[AC_MSG_RESULT([yes])
|
||||
AC_DEFINE([HAVE_PTHREAD_SETNAME_NP], 1,
|
||||
[Define to 1 if you have pthread_setname_np])],
|
||||
[AC_MSG_RESULT([[no]])])],
|
||||
[AC_SEARCH_LIBS([pthread_set_name_np], [pthread],
|
||||
[AC_CHECK_FUNCS([pthread_set_name_np])])])
|
||||
|
||||
AC_SEARCH_LIBS([pthread_getname_np], [pthread],
|
||||
[AC_DEFINE([HAVE_PTHREAD_GETNAME_NP], 1,
|
||||
[Define to 1 if you have pthread_getname_np])]
|
||||
[AC_SEARCH_LIBS([pthread_get_name_np], [pthread],
|
||||
[AC_DEFINE([HAVE_PTHREAD_GETNAME_NP], 1,
|
||||
[Define to 1 if you have pthread_get_name_np])])])
|
||||
AC_SEARCH_LIBS([pthread_getthreadid_np], [pthread],
|
||||
[AC_DEFINE([HAVE_PTHREAD_GETTHREADID_NP], 1,
|
||||
[Define to 1 if you have pthread_getthreadid_np])])
|
||||
AC_SEARCH_LIBS([uuid_generate_random], [uuid],
|
||||
[AC_DEFINE([HAVE_UUID], 1,
|
||||
[Define to 1 if you have uuid_generate_random function])])
|
||||
[Define to 1 if you have uuid_generate_random])])
|
||||
AC_SEARCH_LIBS([copy_file_range], [c],
|
||||
[AC_DEFINE([HAVE_COPY_FILE_RANGE], 1,
|
||||
[Define to 1 if you have copy_file_range])])
|
||||
AC_SEARCH_LIBS([fcopyfile], [c],
|
||||
[AC_DEFINE([HAVE_FCOPYFILE], 1,
|
||||
[Define to 1 if you have fcopyfile])])
|
||||
AC_SEARCH_LIBS([mnt_new_monitor], [mount],
|
||||
[AC_DEFINE([HAVE_LIBMOUNT], 1,
|
||||
[Define to 1 if you have libmount])])
|
||||
|
||||
AC_SEARCH_LIBS([log10], [m])
|
||||
AC_SEARCH_LIBS([lrint], [m])
|
||||
@ -120,13 +137,7 @@ OWNTONE_MODULES_CHECK([OWNTONE], [ZLIB], [zlib], [deflate], [zlib.h])
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [CONFUSE], [libconfuse >= 3.0], [cfg_init], [confuse.h])
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBCURL], [libcurl], [curl_global_init], [curl/curl.h])
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBSODIUM], [libsodium], [sodium_init], [sodium.h])
|
||||
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [MINIXML], [mxml],
|
||||
[mxmlNewElement], [mxml.h],
|
||||
[
|
||||
dnl See mxml-compat.h
|
||||
AC_CHECK_FUNCS([mxmlGetOpaque] [mxmlGetText] [mxmlGetType] [mxmlGetFirstChild])
|
||||
])
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBXML2], [libxml-2.0], [xmlInitParser], [libxml/parser.h])
|
||||
|
||||
OWNTONE_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0],
|
||||
[sqlite3_initialize], [sqlite3.h],
|
||||
@ -148,13 +159,26 @@ OWNTONE_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0],
|
||||
[AC_MSG_RESULT([[runtime will tell]])])
|
||||
])
|
||||
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2],
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2.1.4],
|
||||
[event_base_new], [event2/event.h],
|
||||
[dnl check for old version
|
||||
PKG_CHECK_EXISTS([libevent >= 2.1.4], [],
|
||||
[AC_DEFINE([HAVE_LIBEVENT2_OLD], 1,
|
||||
[Define to 1 if you have libevent 2 (<2.1.4)])])
|
||||
[dnl check for version 2.2 (with websocket server support)
|
||||
PKG_CHECK_EXISTS([libevent >= 2.2.1],
|
||||
[AC_DEFINE([HAVE_LIBEVENT22], 1,
|
||||
[Define to 1 if you have libevent > 2.2])],
|
||||
[])
|
||||
])
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT_PTHREADS], [libevent_pthreads],
|
||||
[evthread_use_pthreads], [event2/thread.h])
|
||||
|
||||
dnl Check for evhttp_connection_get_peer() signature
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
|
||||
#include <event2/http.h>
|
||||
void evhttp_connection_get_peer(struct evhttp_connection *, const char **, ev_uint16_t *);
|
||||
]], [[ ]]
|
||||
)],
|
||||
[AC_MSG_RESULT([yes]); AC_DEFINE([HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR], 1,
|
||||
[Define to 1 if evhttp_connection_get_peer expects const char**])]
|
||||
)
|
||||
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [JSON_C], [json-c],
|
||||
[json_tokener_parse], [json.h],
|
||||
@ -171,6 +195,9 @@ PKG_CHECK_EXISTS([libplist],
|
||||
[OWNTONE_MODULES_CHECK([OWNTONE], [LIBPLIST], [libplist-2.0],
|
||||
[plist_dict_get_item], [plist/plist.h])])
|
||||
|
||||
dnl AC_SEARCH_LIBS does not find plist_get_unix_date_val() on MacOS
|
||||
OWNTONE_CHECK_DECLS([plist_get_unix_date_val], [plist/plist.h])
|
||||
|
||||
AM_PATH_LIBGCRYPT([1:1.7.0])
|
||||
OWNTONE_FUNC_REQUIRE([OWNTONE], [GNU Crypt Library], [LIBGCRYPT], [gcrypt],
|
||||
[gcry_control], [gcrypt.h])
|
||||
@ -238,6 +265,8 @@ OWNTONE_MODULES_CHECK([OWNTONE], [LIBAV],
|
||||
[libavutil/avutil.h])
|
||||
OWNTONE_CHECK_DECLS([avformat_network_init],
|
||||
[libavformat/avformat.h])
|
||||
OWNTONE_CHECK_DECLS([av_dict_iterate],
|
||||
[libavutil/dict.h])
|
||||
])
|
||||
|
||||
AC_CHECK_SIZEOF([void *])
|
||||
@ -259,11 +288,6 @@ OWNTONE_ARG_WITH_CHECK([OWNTONE_OPTS], [libwebsockets support], [libwebsockets],
|
||||
[libwebsockets >= 2.0.2])
|
||||
AM_CONDITIONAL([COND_LIBWEBSOCKETS], [[test "x$with_libwebsockets" = "xyes"]])
|
||||
|
||||
dnl Build with libevent_pthreads
|
||||
OWNTONE_ARG_WITH_CHECK([OWNTONE_OPTS], [libevent_pthreads support],
|
||||
[libevent_pthreads], [LIBEVENT_PTHREADS], [libevent_pthreads],
|
||||
[evthread_use_pthreads], [event2/thread.h])
|
||||
|
||||
dnl Build with Avahi (or Bonjour if not)
|
||||
OWNTONE_ARG_WITH_CHECK([OWNTONE_OPTS], [Avahi mDNS], [avahi], [AVAHI],
|
||||
[avahi-client >= 0.6.24], [avahi_client_new], [avahi-client/client.h])
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Running Multiple Instances
|
||||
|
||||
To run multiple instances of owntone on a server, you should copy
|
||||
To run multiple instances of OwnTone on a server, you should copy
|
||||
`/etc/owntone.conf` to `/etc/owntone-zone.conf` (for each `zone`) and
|
||||
modify the following to be unique across all instances:
|
||||
|
||||
@ -17,9 +17,8 @@ modify the following to be unique across all instances:
|
||||
Then run `owntone -c /etc/owntone-zone.conf` to run owntone with the new
|
||||
zone configuration.
|
||||
|
||||
Owntone has a `systemd` template which lets you run this automatically
|
||||
OwnTone has a `systemd` template which lets you run this automatically
|
||||
on systems that use systemd. You can start or enable the service for
|
||||
a `zone` by `sudo systemctl start owntone@zone` and check that it is
|
||||
running with `sudo systemctl status owntone@zone`. Use `sudo
|
||||
systemctl enable ownton@zone` to get the service to start on reboot.
|
||||
|
||||
systemctl enable owntone@zone` to get the service to start on reboot.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# OwnTone and ALSA
|
||||
# ALSA
|
||||
|
||||
ALSA is one of the main output configuration options for local audio; when using ALSA you will typically let the system select the soundcard on your machine as the `default` device/sound card - a mixer associated with the ALSA device is used for volume control. However if your machine has multiple sound cards and your system chooses the wrong playback device, you will need to manually select the card and mixer to complete the OwnTone configuration.
|
||||
ALSA is one of the main output configuration options for local audio; when using ALSA you will typically let the system select the sound card on your machine as the `default` device/sound card - a mixer associated with the ALSA device is used for volume control. However if your machine has multiple sound cards and your system chooses the wrong playback device, you will need to manually select the card and mixer to complete the OwnTone configuration.
|
||||
|
||||
## Quick introduction to ALSA devices
|
||||
|
||||
@ -13,13 +13,14 @@ Alternative ALSA names can be used to refer to physical ALSA devices and can be
|
||||
* more descriptive rather than being a card number
|
||||
* consistent for USB numeration - USB ALSA devices may not have the same card number across reboots/reconnects
|
||||
|
||||
The ALSA device information required for configuration the server can be deterined using `aplay`, as described in the rest of this document, but OwnTone can also assist; when configured to log at `INFO` level the following information is provided during startup:
|
||||
The ALSA device information required for configuration the server can be determined using `aplay`, as described in the rest of this document, but OwnTone can also assist; when configured to log at `INFO` level the following information is provided during startup:
|
||||
|
||||
```
|
||||
```shell
|
||||
laudio: Available ALSA playback mixer(s) on hw:0 CARD=Intel (HDA Intel): 'Master' 'Headphone' 'Speaker' 'PCM' 'Mic' 'Beep'
|
||||
laudio: Available ALSA playback mixer(s) on hw:1 CARD=E30 (E30): 'E30 '
|
||||
laudio: Available ALSA playback mixer(s) on hw:2 CARD=Seri (Plantronics Blackwire 3210 Seri): 'Sidetone' 'Headset'
|
||||
```
|
||||
|
||||
The `CARD=` string is the alternate ALSA name for the device and can be used in place of the traditional `hw:x` name.
|
||||
|
||||
On this machine the server reports that it can see the onboard HDA Intel sound card and two additional sound cards: a Topping E30 DAC and a Plantronics Headset which are both USB devices. We can address the first ALSA device as `hw:0` or `hw:CARD=Intel` or `hw:Intel` or `plughw:Intel`, the second ALSA device as `hw:1` or `hw:E30` and so forth. The latter 2 devices being on USB will mean that `hw:1` may not always refer to `hw:E30` and thus in such a case using the alternate name is useful.
|
||||
@ -28,8 +29,8 @@ On this machine the server reports that it can see the onboard HDA Intel sound c
|
||||
|
||||
OwnTone can support a single ALSA device or multiple ALSA devices.
|
||||
|
||||
```
|
||||
# example audio section for server for a single soundcard
|
||||
```conf
|
||||
# example audio section for server for a single sound card
|
||||
audio {
|
||||
nickname = "Computer"
|
||||
type = "alsa"
|
||||
@ -40,9 +41,9 @@ audio {
|
||||
}
|
||||
```
|
||||
|
||||
Multiple devices can be made available to OwnTone using seperate `alsa { .. }` sections.
|
||||
Multiple devices can be made available to OwnTone using separate `alsa { .. }` sections.
|
||||
|
||||
```
|
||||
```conf
|
||||
audio {
|
||||
type = "alsa"
|
||||
}
|
||||
@ -62,11 +63,11 @@ NB: When introducing `alsa { .. }` section(s) the ALSA specific configuration in
|
||||
|
||||
If there is only one sound card, verify if the `default` sound device is correct for playback, we will use the `aplay` utility.
|
||||
|
||||
```
|
||||
```shell
|
||||
# generate some audio if you don't have a wav file to hand
|
||||
$ sox -n -c 2 -r 44100 -b 16 -C 128 /tmp/sine441.wav synth 30 sin 500-100 fade h 0.2 30 0.2
|
||||
sox -n -c 2 -r 44100 -b 16 -C 128 /tmp/sine441.wav synth 30 sin 500-100 fade h 0.2 30 0.2
|
||||
|
||||
$ aplay -Ddefault /tmp/sine441.wav
|
||||
aplay -Ddefault /tmp/sine441.wav
|
||||
```
|
||||
|
||||
If you can hear music played then you are good to use `default` for the server configuration. If you can not hear anything from the `aplay` firstly verify (using `alsamixer`) that the sound card is not muted. If the card is not muted AND there is no sound you can try the options below to determine the card and mixer for configuring the server.
|
||||
@ -75,15 +76,15 @@ If you can hear music played then you are good to use `default` for the server c
|
||||
|
||||
As shown above, OwnTone can help, consider the information that logged:
|
||||
|
||||
```
|
||||
```log
|
||||
laudio: Available ALSA playback mixer(s) on hw:0 CARD=Intel (HDA Intel): 'Master' 'Headphone' 'Speaker' 'PCM' 'Mic' 'Beep'
|
||||
laudio: Available ALSA playback mixer(s) on hw:1 CARD=E30 (E30): 'E30 '
|
||||
laudio: Available ALSA playback mixer(s) on hw:2 CARD=Seri (Plantronics Blackwire 3210 Seri): 'Sidetone' 'Headset'
|
||||
```
|
||||
|
||||
Using the information above, we can see 3 soundcards that we could use with OwnTone with the first soundcard having a number of seperate mixer devices (volume control) for headphone and the interal speakers - we'll configure the server to use both these and also the E30 device. The server configuration for theese multiple outputs would be:
|
||||
Using the information above, we can see 3 sound cards that we could use with OwnTone with the first sound card having a number of separate mixer devices (volume control) for headphone and the internal speakers - we'll configure the server to use both these and also the E30 device. The server configuration for these multiple outputs would be:
|
||||
|
||||
```
|
||||
```conf
|
||||
# using ALSA device alias where possible
|
||||
|
||||
alsa "hw:Intel" {
|
||||
@ -109,12 +110,14 @@ alsa "plughw:E30" {
|
||||
NB: it is troublesome to use `hw` or `plughw` ALSA addressing when running OwnTone on a machine with `pulseaudio` and if you wish to use refer to ALSA devices directly that you stop `pulseaudio`.
|
||||
|
||||
## Manually Determining the sound cards you have / ALSA can see
|
||||
|
||||
The example below is how I determined the correct sound card and mixer values for a Raspberry Pi that has an additional DAC card (hat) mounted. Of course using the log output from the server would have given the same results.
|
||||
|
||||
Use `aplay -l` to list all the sound cards and their order as known to the system - you can have multiple `card X, device Y` entries; some cards can also have multiple playback devices such as the RPI's onboard soundcard which feeds both headphone (card 0, device 0) and HDMI (card 0, device 1).
|
||||
Use `aplay -l` to list all the sound cards and their order as known to the system - you can have multiple `card X, device Y` entries; some cards can also have multiple playback devices such as the RPI's onboard sound card which feeds both headphone (card 0, device 0) and HDMI (card 0, device 1).
|
||||
|
||||
```
|
||||
```shell
|
||||
$ aplay -l
|
||||
|
||||
**** List of PLAYBACK Hardware Devices ****
|
||||
card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
|
||||
Subdevices: 6/7
|
||||
@ -135,12 +138,13 @@ card 1: IQaudIODAC [IQaudIODAC], device 0: IQaudIO DAC HiFi pcm512x-hifi-0 []
|
||||
|
||||
On this machine we see the second sound card installed, an IQaudIODAC dac hat, and identified as `card 1 device 0`. This is the playback device we want to be used by the server.
|
||||
|
||||
`hw:1,0` is the IQaudIODAC that we want to use - we verify audiable playback through that sound card using `aplay -Dhw:1 /tmp/sine441.wav`. If the card has only one device, we can simply refer to the sound card using `hw:X` so in this case where the IQaudIODAC only has one device, we can refer to this card as `hw:1` or `hw:1,0`.
|
||||
`hw:1,0` is the IQaudIODAC that we want to use - we verify audible playback through that sound card using `aplay -Dhw:1 /tmp/sine441.wav`. If the card has only one device, we can simply refer to the sound card using `hw:X` so in this case where the IQaudIODAC only has one device, we can refer to this card as `hw:1` or `hw:1,0`.
|
||||
|
||||
Use `aplay -L` to get more information about the PCM devices defined on the system.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ aplay -L
|
||||
|
||||
null
|
||||
Discard all samples (playback) or generate zero samples (capture)
|
||||
default:CARD=ALSA
|
||||
@ -195,7 +199,7 @@ plughw:CARD=IQaudIODAC,DEV=0
|
||||
|
||||
For the server configuration, we will use:
|
||||
|
||||
```
|
||||
```conf
|
||||
audio {
|
||||
nickname = "Computer"
|
||||
type = "alsa"
|
||||
@ -209,7 +213,7 @@ audio {
|
||||
|
||||
Once you have the card number (determined from `aplay -l`) we can inspect/confirm the name of the mixer that can be used for playback (it may NOT be `PCM` as expected by the server). In this example, the card `1` is of interest and thus we use `-c 1` with the following command:
|
||||
|
||||
```
|
||||
```shell
|
||||
$ amixer -c 1
|
||||
Simple mixer control 'DSP Program',0
|
||||
Capabilities: enum
|
||||
@ -236,7 +240,7 @@ This card has multiple controls but we want to find a mixer control listed with
|
||||
|
||||
For the server configuration, we will use:
|
||||
|
||||
```
|
||||
```conf
|
||||
audio {
|
||||
nickname = "Computer"
|
||||
type = "alsa"
|
||||
@ -250,14 +254,13 @@ audio {
|
||||
|
||||
This is the name of the underlying physical device used for the mixer - it is typically the same value as the value of `card` in which case a value is not required by the server configuration. An example of when you want to change explicitly configure this is if you need to use a `dmix` device (see below).
|
||||
|
||||
## Handling Devices that cannot concurrently play multiple audio streams
|
||||
|
||||
## Handling Devices that cannot concurrently play multiple audio streams
|
||||
|
||||
Some devices such as various RPI DAC boards (IQaudio DAC, Allo Boss DAC...) cannot have multiple streams openned at the same time/cannot play multiple sound files at the same time. This results in `Device or resource busy` errors. You can confirm if your sound card has this problem by using the example below once have determined the names/cards information as above.
|
||||
Some devices such as various RPI DAC boards (IQaudio DAC, Allo Boss DAC...) cannot have multiple streams opened at the same time/cannot play multiple sound files at the same time. This results in `Device or resource busy` errors. You can confirm if your sound card has this problem by using the example below once have determined the names/cards information as above.
|
||||
|
||||
Using our `hw:1` device we try:
|
||||
|
||||
```
|
||||
```shell
|
||||
# generate some audio
|
||||
$ sox -n -c 2 -r 44100 -b 16 -C 128 /tmp/sine441.wav synth 30 sin 500-100 fade h 0.2 30 0.2
|
||||
|
||||
@ -295,13 +298,13 @@ aplay: main:788: audio open error: Device or resource busy
|
||||
|
||||
In this instance this device cannot open multiple streams - OwnTone can handle this situation transparently with some audio being truncated from the end of the current file as the server prepares to play the following track. If this handling is causing you problems you may wish to use [ALSA's `dmix` functionally](https://www.alsa-project.org/main/index.php/Asoundrc#Software_mixing) which provides a software mixing module. We will need to define a `dmix` component and configure the server to use that as it's sound card.
|
||||
|
||||
The downside to the `dmix` approach will be the need to fix a samplerate (48000 being the default) for this software mixing module meaning any files that have a mismatched samplerate will be resampled.
|
||||
The downside to the `dmix` approach will be the need to fix a sample rate (48000 being the default) for this software mixing module meaning any files that have a mismatched sample rate will be resampled.
|
||||
|
||||
## ALSA dmix configuration/setup
|
||||
|
||||
A `dmix` device can be defined in `/etc/asound.conf` or `~/.asoundrc` for the same user running OwnTone. We will need to know the underlying physical soundcard to be used: in our examples above, `hw:1,0` / `card 1, device 0` representing our IQaudIODAC as per output of `aplay -l`. We also take the `buffer_size` and `period_size` from the output of playing a sound file via `aplay -v`.
|
||||
A `dmix` device can be defined in `/etc/asound.conf` or `~/.asoundrc` for the same user running OwnTone. We will need to know the underlying physical sound card to be used: in our examples above, `hw:1,0` / `card 1, device 0` representing our IQaudIODAC as per output of `aplay -l`. We also take the `buffer_size` and `period_size` from the output of playing a sound file via `aplay -v`.
|
||||
|
||||
```
|
||||
```conf
|
||||
# use 'dac' as the name of the device: "aplay -Ddac ...."
|
||||
pcm.!dac {
|
||||
type plug
|
||||
@ -316,11 +319,11 @@ pcm.dmixer {
|
||||
ipc_perm 0666 # multi-user sharing permissions
|
||||
|
||||
slave {
|
||||
pcm "hw:1,0" # points at the underlying device - could also simply be hw:1
|
||||
period_time 0
|
||||
period_size 4096 # from the output of aplay -v
|
||||
buffer_size 22052 # from the output of aplay -v
|
||||
rate 44100 # locked in sample rate for resampling on dmix device
|
||||
pcm "hw:1,0" # points at the underlying device - could also simply be hw:1
|
||||
period_time 0
|
||||
period_size 4096 # from the output of aplay -v
|
||||
buffer_size 22052 # from the output of aplay -v
|
||||
rate 44100 # locked in sample rate for resampling on dmix device
|
||||
}
|
||||
hint.description "IQAudio DAC s/w dmix device"
|
||||
}
|
||||
@ -335,7 +338,7 @@ ctl.dmixer {
|
||||
|
||||
Running `aplay -L` we will see our newly defined devices `dac` and `dmixer`
|
||||
|
||||
```
|
||||
```shell
|
||||
$ aplay -L
|
||||
null
|
||||
Discard all samples (playback) or generate zero samples (capture)
|
||||
@ -359,7 +362,7 @@ We will use the newly defined card named `dac` which uses the underlying `hw:1`
|
||||
|
||||
For the final server configuration, we will use:
|
||||
|
||||
```
|
||||
```conf
|
||||
audio {
|
||||
nickname = "Computer"
|
||||
type = "alsa"
|
||||
@ -377,7 +380,7 @@ Once installed the user must setup a virtual device and use this device in the s
|
||||
|
||||
If you wish to use your `hw:0` device for output:
|
||||
|
||||
```
|
||||
```conf
|
||||
# /etc/asound.conf
|
||||
ctl.equal {
|
||||
type equal;
|
||||
@ -401,7 +404,7 @@ pcm.equal {
|
||||
|
||||
and in `owntone.conf`
|
||||
|
||||
```
|
||||
```conf
|
||||
alsa "equal" {
|
||||
nickname = "Equalised Output"
|
||||
# adjust accordingly for mixer with pvolume capability
|
||||
@ -423,7 +426,7 @@ Note however, the equalizer appears to require a `plughw` device which means you
|
||||
|
||||
`mixer` value is wrong. Verify name of `mixer` value in server config against the names from all devices capable of playback using `amixer -c <card number>`. Assume the device is card 1:
|
||||
|
||||
```
|
||||
```shell
|
||||
(IFS=$'\n'
|
||||
CARD=1
|
||||
for i in $(amixer -c ${CARD} scontrols | awk -F\' '{ print $2 }'); do
|
||||
@ -432,9 +435,9 @@ Note however, the equalizer appears to require a `plughw` device which means you
|
||||
)
|
||||
```
|
||||
|
||||
Look at the names output and choose the one that fits. The outputs can be something like:
|
||||
Look at the names output and choose the one that fits. The outputs can be something like:
|
||||
|
||||
```
|
||||
```shell
|
||||
# laptop
|
||||
Master
|
||||
Headphone
|
||||
@ -453,16 +456,16 @@ Note however, the equalizer appears to require a `plughw` device which means you
|
||||
|
||||
* No sound during playback - valid mixer/verified by aplay
|
||||
|
||||
Check that the mixer is not muted or volume set to 0. Using the value of `mixer` as per server config and unmute or set volume to max. Assume the device is card 1 and `mixer = Analogue`:
|
||||
Check that the mixer is not muted or volume set to 0. Using the value of `mixer` as per server config and unmute or set volume to max. Assume the device is card 1 and `mixer = Analogue`:
|
||||
|
||||
```
|
||||
```shell
|
||||
amixer -c 1 set Analogue unmute ## some mixers can not be muted resulting in "invalid command"
|
||||
amixer -c 1 set Analogue 100%
|
||||
```
|
||||
|
||||
An example of a device with volume turned all the way down - notice the `Playback` values are `0`[0%]`:
|
||||
|
||||
```
|
||||
```shell
|
||||
Simple mixer control 'Analogue',0
|
||||
Capabilities: pvolume
|
||||
Playback channels: Front Left - Front Right
|
||||
@ -475,7 +478,7 @@ Note however, the equalizer appears to require a `plughw` device which means you
|
||||
* Server stops playing after moving to new track in paly queue, Error in log `Could not open playback device`
|
||||
The log contains these log lines:
|
||||
|
||||
```
|
||||
```log
|
||||
[2019-06-19 20:52:51] [ LOG] laudio: open '/dev/snd/pcmC0D0p' failed (-16)[2019-06-19 20:52:51] [ LOG] laudio: Could not open playback device: Device or resource busy
|
||||
[2019-06-19 20:52:51] [ LOG] laudio: Device 'hw' does not support quality (48000/16/2), falling back to default
|
||||
[2019-06-19 20:52:51] [ LOG] laudio: open '/dev/snd/pcmC0D0p' failed (-16)[2019-06-19 20:52:51] [ LOG] laudio: Could not open playback device: Device or resource busy
|
||||
@ -487,3 +490,7 @@ Note however, the equalizer appears to require a `plughw` device which means you
|
||||
This error will occur for output hardware that do not support concurrent device open and the server plays 2 files of different bitrate (44.1khz and 48khz) back to back.
|
||||
|
||||
If you observe the error, you will need to use the `dmix` configuration as mentioned above.
|
||||
|
||||
* Volume control on Raspberry Pi with hdmi output doesn't work
|
||||
|
||||
Prior to Debian 13, in the /boot/firmware.config file the entry "dtoverlay=vc4-kms-v3d" was commented out. Since 13 this line is active (not commented out). This changes, among other things, the way the HDMI and BCM devices are handled. Commenting out the entry again will fix the volume control for the hdmi output. If you use the RPi headless commenting this out shouldn't have undesirable effects. See https://forums.raspberrypi.com/viewtopic.php?t=49928&start=1825#p2344272.
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
# OwnTone and Pulseaudio
|
||||
# PulseAudio
|
||||
|
||||
You have the choice of runnning Pulseaudio either in system mode or user mode.
|
||||
You have the choice of running PulseAudio either in system mode or user mode.
|
||||
For headless servers, i.e. systems without desktop users, system mode is
|
||||
recommended.
|
||||
|
||||
If there is a desktop user logged in most of the time, a setup with network
|
||||
access via localhost only for daemons is a more appropriate solution, since the
|
||||
normal user administration (with, e.g., `pulseaudio -k`) works as advertised.
|
||||
Also, the user specific configuration for pulseaudio is preserved across
|
||||
Also, the user specific configuration for PulseAudio is preserved across
|
||||
sessions as expected.
|
||||
|
||||
- [System mode](#system-mode-with-bluetooth-support)
|
||||
- [User mode](#user-mode-with-network-access)
|
||||
|
||||
|
||||
## System Mode with Bluetooth support
|
||||
|
||||
Credit: [Rob Pope](http://robpope.co.uk/blog/post/setting-up-forked-daapd-with-bluetooth)
|
||||
@ -21,22 +20,21 @@ Credit: [Rob Pope](http://robpope.co.uk/blog/post/setting-up-forked-daapd-with-b
|
||||
This guide was written based on headless Debian Jessie platforms. Most of the
|
||||
instructions will require that you are root.
|
||||
|
||||
|
||||
### Step 1: Setting up Pulseaudio
|
||||
### Step 1: Setting up PulseAudio
|
||||
|
||||
If you see a "Connection refused" error when starting the server, then you
|
||||
will probably need to setup Pulseaudio to run in system mode [1]. This means
|
||||
that the Pulseaudio daemon will be started during boot and be available to all
|
||||
will probably need to setup PulseAudio to run in system mode [1]. This means
|
||||
that the PulseAudio daemon will be started during boot and be available to all
|
||||
users.
|
||||
|
||||
How to start Pulseaudio depends on your distribution, but in many cases you will
|
||||
need to add a pulseaudio.service file to /etc/systemd/system with the following
|
||||
content:
|
||||
How to start PulseAudio depends on your distribution, but in many cases you will
|
||||
need to add a `pulseaudio.service` file to `/etc/systemd/system` with the
|
||||
following content:
|
||||
|
||||
```
|
||||
# systemd service file for Pulseaudio running in system mode
|
||||
```conf
|
||||
# systemd service file for PulseAudio running in system mode
|
||||
[Unit]
|
||||
Description=Pulseaudio sound server
|
||||
Description=PulseAudio sound server
|
||||
Before=sound.target
|
||||
|
||||
[Service]
|
||||
@ -46,44 +44,42 @@ ExecStart=/usr/bin/pulseaudio --system --disallow-exit
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
If you want Bluetooth support, you must also configure Pulseaudio to load the
|
||||
If you want Bluetooth support, you must also configure PulseAudio to load the
|
||||
Bluetooth module. First install it (Debian:
|
||||
`apt install pulseaudio-module-bluetooth`) and then add the following to
|
||||
/etc/pulse/system.pa:
|
||||
|
||||
```
|
||||
```conf
|
||||
#### Enable Bluetooth
|
||||
.ifexists module-bluetooth-discover.so
|
||||
load-module module-bluetooth-discover
|
||||
.endif
|
||||
```
|
||||
|
||||
Now you need to make sure that Pulseaudio can communicate with the Bluetooth
|
||||
Now you need to make sure that PulseAudio can communicate with the Bluetooth
|
||||
daemon through D-Bus. On Raspbian this is already enabled, and you can skip this
|
||||
step. Otherwise do one of the following:
|
||||
|
||||
1. Add the pulse user to the bluetooth group: `adduser pulse bluetooth`
|
||||
2. Edit /etc/dbus-1/system.d/bluetooth.conf and change the policy for `<policy context="default"\>` to "allow"
|
||||
|
||||
Phew, almost done with Pulseaudio! Now you should:
|
||||
Phew, almost done with PulseAudio! Now you should:
|
||||
|
||||
1. enable system mode on boot with `systemctl enable pulseaudio`
|
||||
2. reboot (or at least restart dbus and pulseaudio)
|
||||
3. check that the Bluetooth module is loaded with `pactl list modules short`
|
||||
|
||||
|
||||
### Step 2: Setting up the server
|
||||
|
||||
Add the user the server is running as (typically "owntone") to the
|
||||
"pulse-access" group:
|
||||
|
||||
```
|
||||
```shell
|
||||
adduser owntone pulse-access
|
||||
```
|
||||
|
||||
Now (re)start the server.
|
||||
|
||||
|
||||
### Step 3: Adding a Bluetooth device
|
||||
|
||||
To connect with the device, run `bluetoothctl` and then:
|
||||
@ -99,51 +95,45 @@ trust [MAC address]
|
||||
connect [MAC address]
|
||||
```
|
||||
|
||||
Now the speaker should appear. You can also verify that Pulseaudio has detected
|
||||
Now the speaker should appear. You can also verify that PulseAudio has detected
|
||||
the speaker with `pactl list sinks short`.
|
||||
|
||||
|
||||
|
||||
## User Mode with Network Access
|
||||
|
||||
Credit: wolfmanx and [this blog](http://billauer.co.il/blog/2014/01/pa-multiple-users/)
|
||||
|
||||
|
||||
### Step 1: Copy system pulseaudio configuration to the users home directory
|
||||
|
||||
```
|
||||
```shell
|
||||
mkdir -p ~/.pulse
|
||||
cp /etc/pulse/default.pa ~/.pulse/
|
||||
```
|
||||
|
||||
|
||||
### Step 2: Enable TCP access from localhost only
|
||||
|
||||
Edit the file `~/.pulse/default.pa` , adding the following line at the end:
|
||||
|
||||
```
|
||||
```shell
|
||||
load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1
|
||||
```
|
||||
|
||||
### Step 3: Restart the PulseAudio daemon
|
||||
|
||||
### Step 3: Restart the pulseaudio deamon
|
||||
|
||||
```
|
||||
```shell
|
||||
pulseaudio -k
|
||||
# OR
|
||||
pulseaudio -D
|
||||
```
|
||||
|
||||
|
||||
### Step 4: Adjust configuration file
|
||||
### Step 4: Adjust the Configuration File
|
||||
|
||||
In the `audio` section of `/etc/owntone.conf`, set `server` to `localhost`:
|
||||
|
||||
```
|
||||
```conf
|
||||
server = "localhost"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[1] Note that Pulseaudio will warn against system mode. However, in this use
|
||||
case it is actually the solution recommended by the [Pulseaudio folks themselves](https://lists.freedesktop.org/archives/pulseaudio-discuss/2016-August/026823.html).
|
||||
[1] Note that PulseAudio will warn against system mode. However, in this use
|
||||
case it is actually the solution recommended by the [PulseAudio folks themselves](https://lists.freedesktop.org/archives/pulseaudio-discuss/2016-August/026823.html).
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# OwnTone and Radio Stream tweaking
|
||||
# Radio Streams
|
||||
|
||||
Radio streams have many different ways in how metadata is sent. Many should
|
||||
just work as expected, but a few may require some tweaking. If you are not
|
||||
@ -6,11 +6,11 @@ seeing expected title, track, artist, artwork in clients or web UI, the
|
||||
following may help.
|
||||
|
||||
First, understand what and how the particular stream is sending information.
|
||||
ffprobe is a command that can be used to interegrate most of the stream
|
||||
`ffprobe` is a command that can be used to integrate most of the stream
|
||||
information. `ffprobe <http://stream.url>` should give you some useful output,
|
||||
look at the Metadata section, below is an example.
|
||||
|
||||
```
|
||||
```m3u
|
||||
Metadata:
|
||||
icy-br : 320
|
||||
icy-description : DJ-mixed blend of modern and classic rock, electronica, world music, and more. Always 100% commercial-free
|
||||
@ -26,11 +26,10 @@ In the example above, all tags are populated with correct information, no
|
||||
modifications to the server configuration should be needed. Note that
|
||||
StreamUrl points to the artwork image file.
|
||||
|
||||
|
||||
Below is another example that will require some tweaks to the server, Notice
|
||||
`icy-name` is blank and `StreamUrl` doesn't point to an image.
|
||||
|
||||
```
|
||||
```m3u
|
||||
Metadata:
|
||||
icy-br : 127
|
||||
icy-pub : 0
|
||||
@ -44,10 +43,11 @@ Metadata:
|
||||
|
||||
In the above, first fix is the blank name, second is the image artwork.
|
||||
|
||||
### 1) Set stream name/title via the M3U file
|
||||
## 1) Set stream name/title via the M3U file
|
||||
|
||||
Set the name with an EXTINF tag in the m3u playlist file:
|
||||
|
||||
```
|
||||
```m3u
|
||||
#EXTM3U
|
||||
#EXTINF:-1, - My Radio Stream Name
|
||||
http://radio.stream.domain/stream.url
|
||||
@ -58,7 +58,8 @@ Length is -1 since it's a stream, `<Artist Name>` was left blank since
|
||||
`StreamTitle` is accurate in the Metadata but `<Artist Title>` was set to
|
||||
`My Radio Stream Name` since `icy-name` was blank.
|
||||
|
||||
### 2) StreamUrl is a JSON file with metadata
|
||||
## 2) StreamUrl is a JSON file with metadata
|
||||
|
||||
If `StreamUrl` does not point directly to an artwork file then the link may be
|
||||
to a json file that contains an artwork link. If so, you can make the server
|
||||
download the file automatically and search for an artwork link, and also track
|
||||
@ -67,7 +68,7 @@ duration.
|
||||
Try to download the file, e.g. with `curl "https://radio.stream.domain/api9/eventdata/49790578"`.
|
||||
Let's assume you get something like this:
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"eventId": 49793707,
|
||||
"eventStart": "2020-05-08 16:23:03",
|
||||
@ -85,14 +86,15 @@ Let's assume you get something like this:
|
||||
In this case, you would need to tell the server to look for "eventDuration"
|
||||
and "eventImageUrl" (or just "duration" and "url"). You can do that like this:
|
||||
|
||||
```
|
||||
```shell
|
||||
curl -X PUT "http://localhost:3689/api/settings/misc/streamurl_keywords_length" --data "{\"name\":\"streamurl_keywords_length\",\"value\":\"duration\"}"
|
||||
curl -X PUT "http://localhost:3689/api/settings/misc/streamurl_keywords_artwork_url" --data "{\"name\":\"streamurl_keywords_artwork_url\",\"value\":\"url\"}
|
||||
```
|
||||
|
||||
If you want multiple search phrases then comma separate, e.g. "duration,length".
|
||||
|
||||
### 3) Set metadata with a custom script
|
||||
## 3) Set metadata with a custom script
|
||||
|
||||
If your radio station publishes metadata via another method than the above, e.g.
|
||||
just on their web site, then you will have to write a script that pulls the
|
||||
metadata and then pushes it to the server. To update metadata for the
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
# Remote access
|
||||
# Remote Access
|
||||
|
||||
It is possible to access a shared library over the internet from a DAAP client
|
||||
like iTunes. You must have remote access to the host machine.
|
||||
|
||||
First log in to the host and forward port 3689 to your local machine. You now
|
||||
need to broadcast the daap service to iTunes on your local machine. On macOS the
|
||||
need to broadcast the DAAP service to iTunes on your local machine. On macOS the
|
||||
command is:
|
||||
|
||||
```
|
||||
dns-sd -P iTunesServer _daap._tcp local 3689 localhost.local 127.0.0.1 "ffid=12345"
|
||||
```shell
|
||||
dns-sd -P iTunesServer _daap._tcp local 3689 localhost.local 127.0.0.1 "txtvers=1" "ffid=12345678" "Database ID=0123456789abcdef" "Machine ID=0123456789abcdef" "Machine Name=owntone" "mtd-version=28.10" "iTSh Version=131073" "Version=196610"
|
||||
```
|
||||
|
||||
The `ffid` key is required but its value does not matter.
|
||||
@ -19,3 +19,10 @@ You can also access your library remotely using something like Zerotier. See [th
|
||||
guide](https://github.com/owntone/owntone-server/wiki/Accessing-Owntone-remotely-through-iTunes-Music-with-Zerotier)
|
||||
for details.
|
||||
|
||||
## Accessing from Internet for authenticated users
|
||||
|
||||
If you intend to access OwnTone directly from Internet, it is recommended to
|
||||
protect it against unauthenticated users.
|
||||
|
||||
[This guide](https://blog.cyril.by/en/software/example-sso-with-authelia-and-owntone)
|
||||
has a detailed setup tutorial to achieve this securely.
|
||||
|
||||
@ -15,7 +15,7 @@ artwork (group artwork) by the following procedure:
|
||||
- failing that, if [directory name].{png,jpg} is found in one of the
|
||||
directories containing files that are part of the group, it is used as the
|
||||
artwork. The first file found is used, ordering is not guaranteed;
|
||||
- failing that, individual files are examined and the first file found
|
||||
- failing that, individual files are examined and the first file found
|
||||
with an embedded artwork is used. Here again, ordering is not guaranteed.
|
||||
|
||||
{artwork,cover,Folder} are the default, you can add other base names in the
|
||||
@ -28,7 +28,7 @@ the list, OwnTone will look for /foo/bar.{jpg,png}.
|
||||
|
||||
You can use symlinks for the artwork files.
|
||||
|
||||
OwnTone caches artwork in a separate cache file. The default path is
|
||||
`/var/cache/owntone/cache.db` and can be configured in the configuration
|
||||
file. The cache.db file can be deleted without losing the library and pairing
|
||||
OwnTone caches artwork in a separate cache file. The default path is
|
||||
`/var/cache/owntone/cache.db` and can be configured in the configuration
|
||||
file. The cache.db file can be deleted without losing the library and pairing
|
||||
informations.
|
||||
|
||||
@ -11,8 +11,7 @@ interface: Select the device and then enter the PIN that the Apple TV displays.
|
||||
|
||||
If your speaker is silent when you start playback, and there is no obvious error
|
||||
message in the log, you can try disabling ipv6 in the config. Some speakers
|
||||
announce that they support ipv6, but in fact don't (at least not with forked-
|
||||
daapd).
|
||||
announce that they support ipv6, but for some reason don't work with OwnTone.
|
||||
|
||||
If the speaker becomes unselected when you start playback, and you in the log
|
||||
see "ANNOUNCE request failed in session startup: 400 Bad Request", then try
|
||||
@ -2,3 +2,8 @@
|
||||
|
||||
OwnTone will discover Chromecast devices available on your network, and you
|
||||
can then select the device as a speaker. There is no configuration required.
|
||||
|
||||
Take note that:
|
||||
|
||||
- Chromecast playback can't be precisely sync'ed with other outputs e.g. AirPlay
|
||||
- Playback to Google Nest Hub doesn't work (Nest Mini does work)
|
||||
24
docs/audio-outputs/local-audio.md
Normal file
24
docs/audio-outputs/local-audio.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Local audio
|
||||
|
||||
## Local audio through ALSA
|
||||
|
||||
In the config file, you can select ALSA for local audio. This is the default.
|
||||
|
||||
When using ALSA, the server will try to synchronize playback with AirPlay. You
|
||||
can adjust the synchronization in the config file.
|
||||
|
||||
For most setups the default values in the config file should work. If they
|
||||
don't, there is help [here](../advanced/outputs-alsa.md)
|
||||
|
||||
## Local audio, Bluetooth and more through PulseAudio
|
||||
|
||||
In the config file, you can select PulseAudio for local audio. In addition to
|
||||
local audio, PulseAudio also supports an array of other targets, e.g. Bluetooth
|
||||
or DLNA. However, PulseAudio does require some setup, so here is a separate page
|
||||
with some help on that: [PulseAudio](../advanced/outputs-pulse.md)
|
||||
|
||||
Note that if you select PulseAudio the "card" setting in the config file has
|
||||
no effect. Instead all sound cards detected by PulseAudio will be listed as
|
||||
speakers by OwnTone.
|
||||
|
||||
You can adjust the latency of PulseAudio playback in the config file.
|
||||
19
docs/audio-outputs/mobile.md
Normal file
19
docs/audio-outputs/mobile.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Listening on your Mobile Device
|
||||
|
||||
## iOS
|
||||
|
||||
On iOS, the options are limited because there are no Media Client apps with DAAP
|
||||
support and because Apple doesn't allow AirPlay receiver apps. OwnTone also
|
||||
can't share music via Home Sharing, which is the protocol Apple uses for sharing
|
||||
media between devices.
|
||||
|
||||
That leaves the following options, which all rely on OwnTone's streaming:
|
||||
|
||||
- listen via the [web interface](web.md)
|
||||
- use a [MPD client app](../control-clients/mobile.md#mpd-client-apps) that supports local playback
|
||||
- connect to the [streaming](streaming.md) endpoint with a media player like VLC
|
||||
|
||||
## Android
|
||||
|
||||
On Android, you can use the same streaming methods described for iOS, but you
|
||||
can also find apps that act as AirPlay receivers.
|
||||
9
docs/audio-outputs/roku.md
Normal file
9
docs/audio-outputs/roku.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Roku devices/speakers
|
||||
|
||||
OwnTone can stream audio to classic RSP/RCP-based devices like Roku Soundbridge
|
||||
M1001 and M2000.
|
||||
|
||||
If the source file is in a non-supported format, like flac, OwnTone will
|
||||
transcode to wav. Transmitting wav requires some bandwidth and the legacy
|
||||
network interfaces of these devices may struggle with that. If so, you can
|
||||
change the transcoding format for the speaker to alac via the [JSON API](../json-api.md#change-an-output).
|
||||
33
docs/audio-outputs/streaming.md
Normal file
33
docs/audio-outputs/streaming.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Streaming
|
||||
|
||||
The streaming option is useful when you want to listen to audio played by
|
||||
OwnTone in a browser or a media player of your choice [^1],[^2].
|
||||
|
||||
You can control playback via the web interface or any of the supported control
|
||||
clients.
|
||||
|
||||
## Listening to Audio in a Media Player
|
||||
|
||||
To listen to audio being played by OwnTone in a media player, follow these
|
||||
steps:
|
||||
|
||||
1. Start playing audio in OwnTone.
|
||||
2. In the web interface, activate the stream in the output menu by clicking
|
||||
on the icon :material-broadcast: next to HTTP Stream.
|
||||
After a few seconds, the audio should play in the background.
|
||||
3. Copy the URL behind the :material-open-in-new: icon next to HTTP Stream.
|
||||
4. Open the copied URL with the media player, e.g., VLC.
|
||||
The URL is usually
|
||||
[http://owntone.local:3689/stream.mp3](http://owntone.local:3689/stream.mp3)
|
||||
or http://SERVER_ADDRESS:3689/stream.mp3
|
||||
|
||||
## Notes
|
||||
|
||||
[^1]: On iOS devices, the streaming option is the only way of listening to your
|
||||
audio, since Apple does not allow AirPlay receiver apps, and because
|
||||
Home Sharing cannot be supported by OwnTone.
|
||||
|
||||
[^2]: For the streaming option to work, MP3 encoding must be supported by
|
||||
`libavcodec`. If it is not, a message will appear in the log file.
|
||||
For example, on Debian or Ubuntu, MP3 encoding support is provided by the
|
||||
package `libavcodec-extra`.
|
||||
19
docs/audio-outputs/web.md
Normal file
19
docs/audio-outputs/web.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Listening to Audio in a Browser
|
||||
|
||||
To listen to audio being played by OwnTone in a browser, follow these
|
||||
steps:
|
||||
|
||||
1. Start playing audio in OwnTone.
|
||||
2. In the web interface, activate the stream in the output menu by clicking
|
||||
on the icon :material-broadcast: next to HTTP Stream.
|
||||
After a few seconds, the audio should play in the background [^1].
|
||||
|
||||
{: class="zoom" }
|
||||
|
||||
For the streaming option to work, MP3 encoding must be supported by
|
||||
`libavcodec`. If it is not, a message will appear in the log file. For example,
|
||||
on Debian or Ubuntu, MP3 encoding support is provided by the package
|
||||
`libavcodec-extra`.
|
||||
|
||||
[^1]: On iOS devices, playing audio in the background when the device is locked
|
||||
is not supported in a private browser tab.
|
||||
381
docs/building.md
Normal file
381
docs/building.md
Normal file
@ -0,0 +1,381 @@
|
||||
# Build Instructions
|
||||
|
||||
This document contains instructions for building OwnTone from the git tree. If
|
||||
you just want to build from a release tarball, you don't need the build tools
|
||||
(git, autotools, autoconf, automake, gawk, gperf, gettext, bison and flex), and
|
||||
you can skip the autoreconf step.
|
||||
|
||||
## Quick Version for Debian/Ubuntu
|
||||
|
||||
If you are the lucky kind, this should get you all the required tools and
|
||||
libraries:
|
||||
|
||||
```bash
|
||||
sudo apt-get install \
|
||||
build-essential git autotools-dev autoconf automake libtool gettext gawk \
|
||||
gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev \
|
||||
libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev \
|
||||
libasound2-dev libxml2-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev \
|
||||
libevent-dev libplist-dev libsodium-dev libjson-c-dev libwebsockets-dev \
|
||||
libcurl4-openssl-dev libprotobuf-c-dev
|
||||
```
|
||||
|
||||
Note that OwnTone will also work with other versions and flavours of
|
||||
libgcrypt and libcurl, so the above are just suggestions.
|
||||
|
||||
The following features require extra packages, and that you add a configure
|
||||
argument when you run ./configure:
|
||||
|
||||
Feature | Configure argument | Packages
|
||||
---------------------|--------------------------|-------------------------------------
|
||||
Chromecast | `--enable-chromecast` | libgnutls*-dev
|
||||
PulseAudio | `--with-pulseaudio` | libpulse-dev
|
||||
|
||||
These features can be disabled saving you package dependencies:
|
||||
|
||||
Feature | Configure argument | Packages
|
||||
---------------------|--------------------------|-------------------------------------
|
||||
Spotify (built-in) | `--disable-spotify` | libprotobuf-c-dev
|
||||
Player web UI | `--disable-webinterface` | libwebsockets-dev
|
||||
Live web UI | `--without-libwebsockets`| libwebsockets-dev
|
||||
|
||||
Then run the following (adding configure arguments for optional features):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
autoreconf -i
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Using `--enable-install-user` means that `make install` will also add system
|
||||
user and group for owntone.
|
||||
|
||||
With the above configure arguments, a systemd service file will be installed to
|
||||
`/etc/systemd/system/owntone.service` so that the server will start on boot.
|
||||
Use `--disable-install-systemd` if you don't want that.
|
||||
|
||||
Now edit `/etc/owntone.conf`. Note the guide at the top highlighting which
|
||||
settings that normally require modification.
|
||||
|
||||
Start the server with `sudo systemctl start owntone` and check that it is
|
||||
running with `sudo systemctl status owntone`.
|
||||
|
||||
See the [Documentation](getting-started.md) for usage information.
|
||||
|
||||
## Quick version for Fedora
|
||||
|
||||
If you haven't already enabled the free RPM fusion packages do that, since you
|
||||
will need ffmpeg. You can google how to do that. Then run:
|
||||
|
||||
```bash
|
||||
sudo dnf install \
|
||||
git automake autoconf gettext-devel gperf gawk libtool bison flex \
|
||||
sqlite-devel libconfuse-devel libunistring-devel libxml2-devel libevent-devel \
|
||||
avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel \
|
||||
libplist-devel libsodium-devel json-c-devel libwebsockets-devel \
|
||||
libcurl-devel protobuf-c-devel
|
||||
```
|
||||
|
||||
Clone the OwnTone repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
```
|
||||
|
||||
Then run the following:
|
||||
|
||||
```bash
|
||||
autoreconf -i
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Using `--enable-install-user` means that `make install` will also add system
|
||||
user and group for owntone.
|
||||
|
||||
With the above configure arguments, a systemd service file will be installed to
|
||||
`/etc/systemd/system/owntone.service` so that the server will start on boot.
|
||||
Use `--disable-install-systemd` if you don't want that.
|
||||
|
||||
Now edit `/etc/owntone.conf`. Note the guide at the top highlighting which
|
||||
settings that normally require modification.
|
||||
|
||||
Start the server with `sudo systemctl start owntone` and check that it is
|
||||
running with `sudo systemctl status owntone`.
|
||||
|
||||
See the [Documentation](getting-started.md) for usage information.
|
||||
|
||||
## Quick Version for FreeBSD
|
||||
|
||||
There is a script in the 'scripts' folder that will at least attempt to do all
|
||||
the work for you. And should the script not work for you, you can still look
|
||||
through it and use it as an installation guide.
|
||||
|
||||
## Quick Version for macOS Using Homebrew
|
||||
|
||||
This workflow file used for building OwnTone via Github actions includes
|
||||
all the steps that you need to execute:
|
||||
[.github/workflows/macos.yml](https://github.com/owntone/owntone-server/blob/master/.github/workflows/macos.yml)
|
||||
|
||||
## "Quick" Version for macOS Using MacPorts
|
||||
|
||||
Caution:
|
||||
|
||||
1) this approach may be out of date, consider using the Homebrew method above
|
||||
since it is continuously tested.
|
||||
2) MacPorts requires many downloads and lots of time to install (and sometimes
|
||||
build) ports. You will need a decent network connection and some patience!
|
||||
|
||||
Install MacPorts (which requires Xcode): <https://www.macports.org/install.php>
|
||||
|
||||
```bash
|
||||
sudo port install \
|
||||
autoconf automake libtool pkgconfig git gperf bison flex libgcrypt \
|
||||
libunistring libconfuse ffmpeg libevent json-c libwebsockets curl \
|
||||
libplist libsodium protobuf-c libxml2
|
||||
```
|
||||
|
||||
Download, configure, build and install the [libinotify-kqueue library](https://github.com/libinotify-kqueue/libinotify-kqueue)
|
||||
|
||||
Add the following to `.bashrc`:
|
||||
|
||||
```bash
|
||||
# add /usr/local to pkg-config path
|
||||
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/local/lib/pkgconfig
|
||||
# libunistring doesn't support pkg-config, set overrides
|
||||
export LIBUNISTRING_CFLAGS=-I/opt/local/include
|
||||
export LIBUNISTRING_LIBS="-L/opt/local/lib -lunistring"
|
||||
```
|
||||
|
||||
Optional features require the following additional ports:
|
||||
|
||||
Feature | Configure argument | Ports
|
||||
--------------------|--------------------------|-------------------
|
||||
Chromecast | `--enable-chromecast` | gnutls
|
||||
PulseAudio | `--with-pulseaudio` | pulseaudio
|
||||
|
||||
Clone the OwnTone repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
```
|
||||
|
||||
Finally, configure, build, install, and add configuration arguments for
|
||||
optional features:
|
||||
|
||||
```bash
|
||||
autoreconf -i
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Note: if for some reason you've installed the `avahi` port, you need to
|
||||
add `--without-avahi` to configure above.
|
||||
|
||||
Edit `/usr/local/etc/owntone.conf` and change the `uid` to a proper
|
||||
system daemon (eg: unknown), and run the following commands:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /usr/local/var/run
|
||||
sudo mkdir -p /usr/local/var/log # or change logfile in conf
|
||||
sudo chown unknown /usr/local/var/cache/owntone # or change conf
|
||||
```
|
||||
|
||||
Run OwnTone:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/sbin/owntone
|
||||
```
|
||||
|
||||
Verify it is running (you need to <kbd>Ctrl</kbd>+<kbd>C</kbd> to stop
|
||||
dns-sd):
|
||||
|
||||
```bash
|
||||
dns-sd -B _daap._tcp
|
||||
```
|
||||
|
||||
## Long Version - Requirements
|
||||
|
||||
Required tools:
|
||||
|
||||
- autotools: autoconf 2.63+, automake 1.10+, libtool 2.2. Run `autoreconf -i`
|
||||
at the top of the source tree to generate the build system.
|
||||
- gettext: libunistring requires iconv and gettext provides the autotools
|
||||
macro definitions for iconv.
|
||||
- gperf
|
||||
- bison 3.0+ (yacc is not sufficient)
|
||||
- flex (lex is not sufficient)
|
||||
|
||||
Libraries:
|
||||
|
||||
- [Avahi](https://avahi.org/) client libraries (avahi-client) 0.6.24+
|
||||
- [SQLite](https://sqlite.org/) 3.5.0+ with the unlock notify API enabled.
|
||||
SQLite needs to be built with the support for the unlock notify API; this is not
|
||||
always the case in binary packages, so you may need to rebuild SQLite to
|
||||
enable the unlock notify API. You can check for the presence of the
|
||||
`sqlite3_unlock_notify` symbol in the sqlite library. Refer to the `SQLITE_ENABLE_UNLOCK_NOTIFY` in the SQLlite documentation.
|
||||
- [FFmpeg](https://ffmpeg.org/)
|
||||
- [libconfuse](https://github.com/libconfuse/libconfuse)
|
||||
- [libevent](https://libevent.org/) 2.1.4+
|
||||
- [libxml2](https://gitlab.gnome.org/GNOME/libxml2)
|
||||
- [Libgcrypt](https://gnupg.org/software/libgcrypt/) 1.2.0+
|
||||
- [zlib](https://zlib.net/)
|
||||
- [libunistring](https://www.gnu.org/software/libunistring/) 0.9.3+
|
||||
- [json-c](https://github.com/json-c/json-c/)
|
||||
- [libcurl](https://curl.se/libcurl/)
|
||||
- [libplist](https://github.com/JonathanBeck/libplist/) 0.16+
|
||||
- [libsodium](https://doc.libsodium.org/)
|
||||
- [protobuf-c](https://github.com/protobuf-c/protobuf-c/)
|
||||
- [alsa-lib](https://github.com/alsa-project/alsa-lib/) (optional - ALSA local audio)
|
||||
often already installed as part of your distro
|
||||
- [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) (optional - PulseAudio local audio)
|
||||
- [GnuTLS](https://www.gnutls.org/) (optional - Chromecast support)
|
||||
- [Libwebsockets](https://libwebsockets.org/) 2.0.2+ (optional - websocket support)
|
||||
|
||||
Note: If using binary packages, remember that you need the development packages to
|
||||
build OwnTone (usually suffixed with -dev or -devel).
|
||||
|
||||
## Long Version - Building and Installing
|
||||
|
||||
Start by generating the build system by running `autoreconf -i`. This will
|
||||
generate the configure script and `Makefile.in`.
|
||||
|
||||
To display the configure options `run ./configure --help`.
|
||||
|
||||
Support for Spotify is optional. Use `--disable-spotify` to disable this feature.
|
||||
|
||||
Support for LastFM scrobbling is optional. Use `--enable-lastfm` to enable this
|
||||
feature.
|
||||
|
||||
Support for the MPD protocol is optional. Use `--disable-mpd` to disable this
|
||||
feature.
|
||||
|
||||
Support for Chromecast devices is optional. Use `--enable-chromecast` to enable
|
||||
this feature.
|
||||
|
||||
The player web interface is optional. Use `--disable-webinterface` to disable
|
||||
this feature.
|
||||
If enabled, `sudo make install` will install the prebuild html, js, css files.
|
||||
The prebuild files are:
|
||||
|
||||
- `htdocs/index.html`
|
||||
- `htdocs/player/*`
|
||||
|
||||
The source for the player web interface is located under the `web-src` folder and
|
||||
requires nodejs >= 6.0 to be built. In the `web-src` folder run `npm install` to
|
||||
install all dependencies for the player web interface. After that run `npm run build`.
|
||||
This will build the web interface and update the `htdocs` folder.
|
||||
|
||||
To serve the web interface locally you can run `npm run serve`, which will make
|
||||
it reachable at [localhost:3000](http://localhost:3000). The command expects the
|
||||
server be running at [localhost:3689](http://localhost:3689) and proxies API
|
||||
calls to this location. If the server is running at a different location you
|
||||
can use `export VITE_OWNTONE_URL=http://owntone.local:3689`.
|
||||
|
||||
Building with libwebsockets is required if you want the web interface.
|
||||
It will be enabled if the library is present (with headers).
|
||||
Use `--without-libwebsockets` to disable.
|
||||
|
||||
Building with PulseAudio is optional. It will be enabled if the library is
|
||||
present (with headers). Use `--without-pulseaudio` to disable.
|
||||
|
||||
Recommended build settings:
|
||||
|
||||
```bash
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user
|
||||
```
|
||||
|
||||
After configure run the usual make, and if that went well, `sudo make install`.
|
||||
|
||||
With the above configure arguments, a systemd service file will be installed to
|
||||
`/etc/systemd/system/owntone.service` so that the server will start on boot.
|
||||
Use `--disable-install-systemd` if you don't want that.
|
||||
|
||||
Using `--enable-install-user` means that `make install` will also add a system
|
||||
user and group for owntone.
|
||||
|
||||
After installation:
|
||||
|
||||
- edit the configuration file, `/etc/owntone.conf`
|
||||
- make sure the Avahi daemon is installed and running (Debian:
|
||||
`apt install avahi-daemon`)
|
||||
|
||||
OwnTone will drop privileges to any user you specify in the configuration file
|
||||
if it's started as root.
|
||||
|
||||
This user must have read permission to your library and read/write permissions
|
||||
to the database location (`$localstatedir/cache/owntone` by default).
|
||||
|
||||
## Web source formatting/linting
|
||||
|
||||
The source code follows certain formatting conventions for maintainability and
|
||||
readability. To ensure that the source code follows these conventions,
|
||||
[Prettier](https://prettier.io/) is used.
|
||||
|
||||
The command `npm run format` applies formatting conventions to the source code
|
||||
based on a preset configuration. Note that a additional configuration is made in
|
||||
the file `.prettierrc.json`.
|
||||
|
||||
To flag programming errors, bugs, stylistic errors and suspicious constructs in
|
||||
the source code, [ESLint](https://eslint.org) is used.
|
||||
|
||||
ESLint has been configured following this [guide](https://vueschool.io/articles/vuejs-tutorials/eslint-and-prettier-with-vite-and-vue-js-3/).
|
||||
|
||||
`npm run lint` lints the source code and fixes all automatically fixable errors.
|
||||
|
||||
## Non-Priviliged User Version for Development
|
||||
|
||||
OwnTone is meant to be run as system wide daemon, but for development purposes
|
||||
you may want to run it isolated to your regular user.
|
||||
|
||||
The following description assumes that you want all runtime data stored in
|
||||
`$HOME/owntone_data` and the source in `$HOME/projects/owntone-server`.
|
||||
|
||||
Prepare directories for runtime data:
|
||||
|
||||
```bash
|
||||
mkdir -p $HOME/owntone_data/etc
|
||||
mkdir -p $HOME/owntone_data/media
|
||||
```
|
||||
|
||||
Copy one or more mp3 file to test with to `owntone_data/media`.
|
||||
|
||||
Checkout OwnTone and configure build:
|
||||
|
||||
```bash
|
||||
cd $HOME/projects
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
autoreconf -vi
|
||||
./configure --prefix=$HOME/owntone_data/usr --sysconfdir=$HOME/owntone_data/etc --localstatedir=$HOME/owntone_data/var
|
||||
```
|
||||
|
||||
Build and install runtime:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
Edit `owntone_data/etc/owntone.conf`, find the following configuration settings
|
||||
and set them to these values:
|
||||
|
||||
```conf
|
||||
uid = ${USER}
|
||||
loglevel = "debug"
|
||||
directories = { "${HOME}/owntone_data/media" }
|
||||
```
|
||||
|
||||
Run the server:
|
||||
|
||||
```bash
|
||||
./src/owntone -f
|
||||
```
|
||||
|
||||
Note: You can also use the copy of the binary located in `$HOME/owntone_data/usr/sbin`
|
||||
1
docs/changelog.md
Normal file
1
docs/changelog.md
Normal file
@ -0,0 +1 @@
|
||||
--8<-- "ChangeLog"
|
||||
@ -1,24 +0,0 @@
|
||||
# MPD clients
|
||||
|
||||
You can - to some extent - use clients for MPD to control OwnTone.
|
||||
|
||||
By default OwnTone listens on port 6600 for MPD clients. You can change
|
||||
this in the configuration file.
|
||||
|
||||
Currently only a subset of the commands offered by MPD (see [MPD protocol documentation](http://www.musicpd.org/doc/protocol/))
|
||||
are supported.
|
||||
|
||||
Due to some differences between OwnTone and MPD not all commands will act the
|
||||
same way they would running MPD:
|
||||
|
||||
- crossfade, mixrampdb, mixrampdelay and replaygain will have no effect
|
||||
- single, repeat: unlike MPD, OwnTone does not support setting single and repeat separately
|
||||
on/off, instead repeat off, repeat all and repeat single are supported. Thus setting single on
|
||||
will result in repeat single, repeat on results in repeat all.
|
||||
|
||||
The following table shows what is working for a selection of MPD clients:
|
||||
|
||||
| Client | Type | Status |
|
||||
| --------------------------------------------- | ------ | --------------- |
|
||||
| [mpc](http://www.musicpd.org/clients/mpc/) | CLI | Working commands: mpc, add, crop, current, del (ranges are not yet supported), play, next, prev (behaves like cdprev), pause, toggle, cdprev, seek, clear, outputs, enable, disable, playlist, ls, load, volume, repeat, random, single, search, find, list, update (initiates an init-rescan, the path argument is not supported) |
|
||||
| [ympd](http://www.ympd.org/) | Web | Everything except "add stream" should work |
|
||||
@ -1,66 +0,0 @@
|
||||
# Using Remote
|
||||
|
||||
Remote gets a list of output devices from the server; this list includes any
|
||||
and all devices on the network we know of that advertise AirPlay: AirPort
|
||||
Express, Apple TV, ... It also includes the local audio output, that is, the
|
||||
sound card on the server (even if there is no soundcard).
|
||||
|
||||
OwnTone remembers your selection and the individual volume for each
|
||||
output device; selected devices will be automatically re-selected, except if
|
||||
they return online during playback.
|
||||
|
||||
## Pairing
|
||||
|
||||
1. Open the [web interface](http://owntone.local:3689)
|
||||
2. Start Remote, go to Settings, Add Library
|
||||
3. Enter the pair code in the web interface (update the page with F5 if it does
|
||||
not automatically pick up the pairing request)
|
||||
|
||||
If Remote doesn't connect to OwnTone after you entered the pairing code
|
||||
something went wrong. Check the log file to see the error message. Here are
|
||||
some common reasons:
|
||||
|
||||
- You did not enter the correct pairing code
|
||||
|
||||
You will see an error in the log about pairing failure with a HTTP response code
|
||||
that is *not* 0.
|
||||
|
||||
Solution: Try again.
|
||||
|
||||
- No response from Remote, possibly a network issue
|
||||
|
||||
If you see an error in the log with either:
|
||||
|
||||
- a HTTP response code that is 0
|
||||
- "Empty pairing request callback"
|
||||
|
||||
it means that OwnTone could not establish a connection to Remote. This
|
||||
might be a network issue, your router may not be allowing multicast between the
|
||||
Remote device and the host OwnTone is running on.
|
||||
|
||||
Solution 1: Sometimes it resolves the issue if you force Remote to quit, restart
|
||||
it and do the pairing proces again. Another trick is to establish some other
|
||||
connection (eg SSH) from the iPod/iPhone/iPad to the host.
|
||||
|
||||
Solution 2: Check your router settings if you can whitelist multicast addresses
|
||||
under IGMP settings. For Apple Bonjour, setting a multicast address of
|
||||
224.0.0.251 and a netmask of 255.255.255.255 should work.
|
||||
|
||||
- Otherwise try using avahi-browse for troubleshooting:
|
||||
|
||||
- in a terminal, run `avahi-browse -r -k _touch-remote._tcp`
|
||||
- start Remote, goto Settings, Add Library
|
||||
- after a couple seconds at most, you should get something similar to this:
|
||||
|
||||
```
|
||||
+ ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3 _touch-remote._tcp local
|
||||
= ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3 _touch-remote._tcp local
|
||||
hostname = [Foobar.local]
|
||||
address = [192.168.1.1]
|
||||
port = [49160]
|
||||
txt = ["DvTy=iPod touch" "RemN=Remote" "txtvers=1" "RemV=10000" "Pair=FAEA410630AEC05E" "DvNm=Foobar"]
|
||||
```
|
||||
|
||||
Hit Ctrl-C to terminate avahi-browse.
|
||||
|
||||
- To check for network issues you can try to connect to address and port with telnet.
|
||||
@ -1,43 +0,0 @@
|
||||
# Supported clients
|
||||
|
||||
OwnTone supports these kinds of clients:
|
||||
|
||||
- DAAP clients, like iTunes or Rhythmbox
|
||||
- Remote clients, like Apple Remote or compatibles for Android/Windows Phone
|
||||
- AirPlay devices, like AirPort Express, Shairport and various AirPlay speakers
|
||||
- Chromecast devices
|
||||
- MPD clients, like mpc (see [mpd-clients](#mpd-clients))
|
||||
- MP3 network stream clients, like VLC and almost any other music player
|
||||
- RSP clients, like Roku Soundbridge
|
||||
|
||||
Like iTunes, you can control OwnTone with Remote and stream your music to
|
||||
AirPlay devices.
|
||||
|
||||
A single OwnTone instance can handle several clients concurrently, regardless of
|
||||
the protocol.
|
||||
|
||||
By default all clients on 192.168.* (and the ipv6 equivalent) are allowed to
|
||||
connect without authentication. You can change that in the configuration file.
|
||||
|
||||
Here is a list of working and non-working DAAP and Remote clients. The list is
|
||||
probably obsolete when you read it :-)
|
||||
|
||||
| Client | Developer | Type | Platform | Working (vers.) |
|
||||
| ------------------------ | ----------- | ------ | ------------- | --------------- |
|
||||
| iTunes | Apple | DAAP | Win | Yes (12.10.1) |
|
||||
| Apple Music | Apple | DAAP | MacOS | Yes |
|
||||
| Rhythmbox | Gnome | DAAP | Linux | Yes |
|
||||
| Diapente | diapente | DAAP | Android | Yes |
|
||||
| WinAmp DAAPClient | WardFamily | DAAP | WinAmp | Yes |
|
||||
| Amarok w/DAAP plugin | KDE | DAAP | Linux/Win | Yes (2.8.0) |
|
||||
| Banshee | | DAAP | Linux/Win/OSX | No (2.6.2) |
|
||||
| jtunes4 | | DAAP | Java | No |
|
||||
| Firefly Client | | (DAAP) | Java | No |
|
||||
| Remote | Apple | Remote | iOS | Yes (4.3) |
|
||||
| Retune | SquallyDoc | Remote | Android | Yes (3.5.23) |
|
||||
| TunesRemote+ | Melloware | Remote | Android | Yes (2.5.3) |
|
||||
| Remote for iTunes | Hyperfine | Remote | Android | Yes |
|
||||
| Remote for Windows Phone | Komodex | Remote | Windows Phone | Yes (2.2.1.0) |
|
||||
| TunesRemote SE | | Remote | Java | Yes (r108) |
|
||||
| rtRemote for Windows | bizmodeller | Remote | Windows | Yes (1.2.0.67) |
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
# OwnTone web interface
|
||||
|
||||
Mobile friendly player web interface for [OwnTone](http://owntone.github.io/owntone-server/) build
|
||||
with [Vue.js](https://vuejs.org), [Bulma](http://bulma.io).
|
||||
|
||||
You can find the web interface at [http://owntone.local:3689](http://owntone.local:3689)
|
||||
or alternatively at [http://[your_server_address_here]:3689](http://[your_server_address_here]:3689).
|
||||
|
||||
Use the web interface to control playback, trigger manual library rescans, pair
|
||||
with remotes, select speakers, authenticate with Spotify, etc.
|
||||
|
||||
## Screenshots
|
||||
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
You can find OwnTone's web interface at [http://owntone.local:3689](http://owntone.local:3689)
|
||||
or alternatively at [http://[your_server_address_here]:3689](http://[your_server_address_here]:3689).
|
||||
|
||||
|
||||
## Build Setup
|
||||
|
||||
The source is located in the `web-src` folder.
|
||||
|
||||
```
|
||||
cd web-src
|
||||
```
|
||||
|
||||
The web interface is built with [Vite](https://vitejs.dev/), makes use of Prettier for code formatting
|
||||
and ESLint for code linting (the project was set up following the guide [ESLint and Prettier with Vite and Vue.js 3](https://vueschool.io/articles/vuejs-tutorials/eslint-and-prettier-with-vite-and-vue-js-3/)
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# Serve with hot reload at localhost:3000
|
||||
# (assumes that OwnTone server is running on localhost:3689)
|
||||
npm run serve
|
||||
|
||||
# Serve with hot reload at localhost:3000
|
||||
# (with remote OwnTone server reachable under owntone.local:3689)
|
||||
VITE_OWNTONE_URL=http://owntone.local:3689 npm run serve
|
||||
|
||||
# Build for production with minification (will update web interface
|
||||
# in "../htdocs")
|
||||
npm run build
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Lint code (and fix errors that can be automatically fixed)
|
||||
npm run lint
|
||||
```
|
||||
|
||||
After running `npm run serve` the web interface is reachable at [localhost:3000](http://localhost:3000).
|
||||
By default it expects **owntone** to be running at [localhost:3689](http://localhost:3689) and proxies all
|
||||
JSON API calls to this location.
|
||||
|
||||
If the server is running at a different location you have to set the env variable `VITE_OWNTONE_URL`.
|
||||
49
docs/configuration.md
Normal file
49
docs/configuration.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Configuration
|
||||
|
||||
The configuration of OwnTone is usually located in `/etc/owntone.conf`.
|
||||
|
||||
## Format
|
||||
|
||||
Each setting consists of a name and a value. There are different types of settings: string, integer, boolean, and list.
|
||||
|
||||
Comments are preceded by a hash sign.
|
||||
|
||||
The format is as follow:
|
||||
|
||||
```conf
|
||||
# Section
|
||||
section {
|
||||
# String value
|
||||
setting = "<string-value>"
|
||||
# Integer value
|
||||
setting = <integer-value>
|
||||
# Boolean
|
||||
setting = <true|false>
|
||||
# List
|
||||
setting = { "value a", "value b", "value n"}
|
||||
}
|
||||
```
|
||||
|
||||
Some settings are device specific, in which case you add a section where you specify the device name in the heading. Say you're tired of loud death metal coming from your teenager's room:
|
||||
|
||||
```conf
|
||||
airplay "Jared's Room" {
|
||||
max_volume = 3
|
||||
}
|
||||
```
|
||||
|
||||
## Most important settings
|
||||
|
||||
### general: uid
|
||||
|
||||
Identifier of the user running OwnTone.
|
||||
|
||||
Make sure that this user has read access to your configuration of `directories` in the `library` config section, and has write access to the database (`db_path`), cache directory (`cache_dir`) and log file (`logfile`). If you plan on using local audio then the user must also have access to that.
|
||||
|
||||
### library: directories
|
||||
|
||||
Path to the directory or directories containing the media to index (your library).
|
||||
|
||||
## Other settings
|
||||
|
||||
See the [template configuration file](https://raw.githubusercontent.com/owntone/owntone-server/refs/heads/master/owntone.conf.in) for a description of all the settings.
|
||||
@ -1,10 +1,30 @@
|
||||
# Command line
|
||||
# API and Command Line
|
||||
|
||||
You can choose between:
|
||||
|
||||
- a [MPD command line client](#mpd-clients) (easiest) like `mpc`
|
||||
- curl with OwnTone's JSON API (see [README_JSON_API.md](https://github.com/owntone/owntone-server/blob/master/README_JSON_API.md))
|
||||
- curl with DAAP/DACP commands (hardest)
|
||||
- [The JSON API](#json-api)
|
||||
- [A MPD command line client like mpc](#mpc)
|
||||
- [DAAP/DACP commands](#daapdacp)
|
||||
|
||||
The JSON API is the most versatile and the recommended method, but for simple
|
||||
command line operations, mpc is easier. DAAP/DACP is only for masochists.
|
||||
|
||||
|
||||
## JSON API
|
||||
|
||||
See the [JSON API docs](../json-api.md)
|
||||
|
||||
|
||||
## mpc
|
||||
|
||||
[mpc](https://www.musicpd.org/clients/mpc/) is easy to use for simple operations
|
||||
like enabling speakers, changing volume and getting status.
|
||||
|
||||
Due to differences in implementation between OwnTone and MPD, some mpc commands
|
||||
will work differently or not at all.
|
||||
|
||||
|
||||
## DAAP/DACP
|
||||
|
||||
Here is an example of how to use curl with DAAP/DACP. Say you have a playlist
|
||||
with a radio station, and you want to make a script that starts playback of that
|
||||
@ -18,7 +38,7 @@ station:
|
||||
observe that you must use a session-id < 100, and that you must login and
|
||||
logout):
|
||||
|
||||
```
|
||||
```shell
|
||||
curl "http://localhost:3689/login?pairing-guid=0x1&request-session-id=50"
|
||||
curl "http://localhost:3689/ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x[PLAYLIST-ID]'&container-item-spec='dmap.containeritemid:0x[FILE ID]'&session-id=50"
|
||||
curl "http://localhost:3689/logout?session-id=50"
|
||||
52
docs/control-clients/desktop.md
Normal file
52
docs/control-clients/desktop.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Desktop Remote Control
|
||||
|
||||
To control OwnTone from Linux, Windows or Mac, you can use:
|
||||
|
||||
- [The web interface](#the-web-interface)
|
||||
- [A remote for iTunes/Apple Music](#remotes-for-itunesapple-music)
|
||||
- [A MPD client](#mpd-clients)
|
||||
|
||||
The web interface is the most feature complete and works on all platforms, so
|
||||
on desktop there isn't much reason to use anything else.
|
||||
|
||||
However, instead of a remote control application, you can also connect to
|
||||
OwnTone via a Media Client e.g. iTunes or Apple Music. Media clients will get
|
||||
the media from OwnTone and do the playback themselves (remotes just control
|
||||
OwnTone playback). See [Media Clients](../media-clients.md) for more
|
||||
information.
|
||||
|
||||
|
||||
## The web interface
|
||||
|
||||
See [web interface](web.md).
|
||||
|
||||
|
||||
## Remotes for iTunes/Apple Music
|
||||
|
||||
There are only a few of these, see the below table.
|
||||
|
||||
| Client | Developer | Type | Platform | Working (vers.) |
|
||||
| ------------------------ | ----------- | ------ | --------------- | --------------- |
|
||||
| TunesRemote SE | | Remote | Java | Yes (r108) |
|
||||
| rtRemote for Windows | bizmodeller | Remote | Windows | Yes (1.2.0.67) |
|
||||
|
||||
|
||||
## MPD clients
|
||||
|
||||
There's a range of MPD clients available that also work with OwnTone e.g.
|
||||
Cantata and Plattenalbum.
|
||||
|
||||
The better ones support local playback, speaker control, artwork and automatic
|
||||
discovery of OwnTone's MPD server.
|
||||
|
||||
By default OwnTone listens on port 6600 for MPD clients. You can change
|
||||
this in the configuration file.
|
||||
|
||||
Due to some differences between OwnTone and MPD not all commands will act the
|
||||
same way they would running MPD:
|
||||
|
||||
- crossfade, mixrampdb, mixrampdelay and replaygain will have no effect
|
||||
- single, repeat: unlike MPD, OwnTone does not support setting single and repeat
|
||||
separately on/off, instead repeat off, repeat all and repeat single are
|
||||
supported. Thus setting single on will result in repeat single, repeat on
|
||||
results in repeat all.
|
||||
140
docs/control-clients/mobile.md
Normal file
140
docs/control-clients/mobile.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Mobile Remote Control
|
||||
|
||||
To control OwnTone from your mobile device, you can use:
|
||||
|
||||
- [The web interface](#the-web-interface)
|
||||
- [(iOS) The Remote app from Apple](#apple-remote-app-ios)
|
||||
- [(Android) An iTunes/Apple Music remote app](#remotes-for-itunesapple-music-android)
|
||||
- [A MPD client app](#mpd-client-apps)
|
||||
|
||||
The web interface is the most feature complete, but apps may have UX advantages.
|
||||
The table below shows how some features compare.
|
||||
|
||||
| Feature | Remote | MPD client | Web |
|
||||
| ------------------------------------- | ---------- | ---------- | ---------- |
|
||||
| Browse library | yes | yes | yes |
|
||||
| Control playback and queue | yes | yes | yes |
|
||||
| Artwork | yes | yes | yes |
|
||||
| Individual speaker selection | yes | some | yes |
|
||||
| Individual speaker volume | yes | no | yes |
|
||||
| Volume control using phone buttons | no | ? | no |
|
||||
| Listen on phone | no | some | yes |
|
||||
| Access non-library Spotify tracks | no | no | yes |
|
||||
| Edit and save m3u playlists | no | some | yes |
|
||||
|
||||
While OwnTone supports playing tracks from Spotify, there is no support for
|
||||
Spotify Connect, so you can't control from the Spotify app.
|
||||
|
||||
|
||||
## The web interface
|
||||
|
||||
See [web interface](web.md).
|
||||
|
||||
|
||||
## Apple Remote app (iOS)
|
||||
|
||||
Remote gets a list of output devices from the server; this list includes any
|
||||
and all devices on the network we know of that advertise AirPlay: AirPort
|
||||
Express, Apple TV, … It also includes the local audio output, that is, the
|
||||
sound card on the server (even if there is no sound card).
|
||||
|
||||
OwnTone remembers your selection and the individual volume for each
|
||||
output device; selected devices will be automatically re-selected, except if
|
||||
they return online during playback.
|
||||
|
||||
### Pairing
|
||||
|
||||
1. Open the [web interface](web.md) at either [http://owntone.local:3689](http://owntone.local:3689)
|
||||
or `http://SERVER-IP-ADDRESS:3689`
|
||||
2. Start Remote, go to Settings, Add Library
|
||||
3. Enter the pair code in the web interface (reload the browser page if
|
||||
it does not automatically pick up the pairing request)
|
||||
|
||||
If Remote does not connect to OwnTone after you entered the pairing code
|
||||
something went wrong. Check the log file to see the error message. Here are
|
||||
some common reasons:
|
||||
|
||||
- You did not enter the correct pairing code
|
||||
|
||||
You will see an error in the log about pairing failure with a HTTP response code
|
||||
that is *not* 0.
|
||||
|
||||
Solution: Try again.
|
||||
|
||||
- No response from Remote, possibly a network issue
|
||||
|
||||
If you see an error in the log with either:
|
||||
|
||||
- a HTTP response code that is 0
|
||||
- "Empty pairing request callback"
|
||||
|
||||
it means that OwnTone could not establish a connection to Remote. This
|
||||
might be a network issue, your router may not be allowing multicast between the
|
||||
Remote device and the host OwnTone is running on.
|
||||
|
||||
Solution 1: Sometimes it resolves the issue if you force Remote to quit, restart
|
||||
it and do the pairing process again. Another trick is to establish some other
|
||||
connection (eg SSH) from the iPod/iPhone/iPad to the host.
|
||||
|
||||
Solution 2: Check your router settings if you can whitelist multicast addresses
|
||||
under IGMP settings. For Apple Bonjour, setting a multicast address of
|
||||
224.0.0.251 and a netmask of 255.255.255.255 should work.
|
||||
|
||||
- Otherwise try using `avahi-browse` for troubleshooting:
|
||||
|
||||
- in a terminal, run:
|
||||
|
||||
```shell
|
||||
avahi-browse -r -k _touch-remote._tcp
|
||||
```
|
||||
|
||||
- start Remote, goto Settings, Add Library
|
||||
- after a couple seconds at most, you should get something similar to this:
|
||||
|
||||
```shell
|
||||
+ ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3 _touch-remote._tcp local
|
||||
= ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3 _touch-remote._tcp local
|
||||
hostname = [Foobar.local]
|
||||
address = [192.168.1.1]
|
||||
port = [49160]
|
||||
txt = ["DvTy=iPod touch" "RemN=Remote" "txtvers=1" "RemV=10000" "Pair=FAEA410630AEC05E" "DvNm=Foobar"]
|
||||
```
|
||||
|
||||
Hit Ctrl+C to terminate `avahi-browse`.
|
||||
|
||||
- To check for network issues you can try to connect to the server address and
|
||||
port with [`nc`](https://en.wikipedia.org/wiki/Netcat) or
|
||||
[`telnet`](https://en.wikipedia.org/wiki/Telnet) commands.
|
||||
|
||||
- Some users report that Remote can get in a state where pairing isn't possible
|
||||
and nothing appears in OwnTone's logs. In that case try reinstalling Remote.
|
||||
|
||||
## Remotes for iTunes/Apple Music (Android)
|
||||
|
||||
Google Play doesn't seem to have iTunes/Apple Music remotes any more, so you
|
||||
either need to use the web interface or find an apk for one of the old remotes,
|
||||
like Retune (by SquallyDoc), TunesRemote+ (by Melloware) or Remote for iTunes
|
||||
(by Hyperfine).
|
||||
|
||||
For usage and troubleshooting details, see the instructions for [Apple Remote](#apple-remote-app-ios).
|
||||
|
||||
|
||||
## MPD client apps
|
||||
|
||||
There's a range of MPD clients available from app store that also work with
|
||||
OwnTone e.g. MPD Pilot, MaximumMPD, Rigelian and Stylophone.
|
||||
|
||||
The better ones support local playback, speaker control, artwork and automatic
|
||||
discovery of OwnTone's MPD server.
|
||||
|
||||
By default OwnTone listens on port 6600 for MPD clients. You can change
|
||||
this in the configuration file.
|
||||
|
||||
Due to some differences between OwnTone and MPD not all commands will act the
|
||||
same way they would running MPD:
|
||||
|
||||
- crossfade, mixrampdb, mixrampdelay and replaygain will have no effect
|
||||
- single, repeat: unlike MPD, OwnTone does not support setting single and repeat
|
||||
separately on/off, instead repeat off, repeat all and repeat single are
|
||||
supported. Thus setting single on will result in repeat single, repeat on
|
||||
results in repeat all.
|
||||
44
docs/control-clients/web.md
Normal file
44
docs/control-clients/web.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Web Interface
|
||||
|
||||
The built-in web interface is a mobile-friendly music player and browser for
|
||||
OwnTone.
|
||||
|
||||
You can reach it at [http://owntone.local:3689](http://owntone.local:3689)
|
||||
or depending on the OwnTone installation at `http://<server-address>:<port>`.
|
||||
|
||||
This interface becomes useful when you need to control playback, trigger
|
||||
manual library rescans, pair with remotes, select speakers, grant access to
|
||||
Spotify, and for many other operations.
|
||||
|
||||
Alternatively, you can use a MPD web client like for instance [ympd](http://www.ympd.org/).
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
Below you have a selection of screenshots that shows different part of the
|
||||
interface.
|
||||
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
The web interface is usually reachable at [http://owntone.local:3689](http://owntone.local:3689).
|
||||
But depending on the setup of OwnTone you might need to adjust the server name
|
||||
and port of the server accordingly `http://<server-name>:<port>`.
|
||||
88
docs/development.md
Normal file
88
docs/development.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Development
|
||||
|
||||
## Dev Containers and VSCode
|
||||
|
||||
To set up a development environment for OwnTone, the project includes an example Dev Containers configuration.
|
||||
|
||||
!!! tip "Dev Containers"
|
||||
To learn more about Dev Containers and how to use them check out the documentation at:
|
||||
|
||||
- <https://code.visualstudio.com/docs/devcontainers/containers>
|
||||
- <https://containers.dev/>
|
||||
|
||||
Dev Containers config for OwnTone includes all the necessary and some nice to have tooling:
|
||||
|
||||
- C-tools to build and develop for owntone-server, including autotools, dependencies, etc.
|
||||
- Javascript-tools to build and develop the OwnTone web interface.
|
||||
- Python-tools to build and run the OwnTone documentation with mkdocs.
|
||||
|
||||
### Prerquisites
|
||||
|
||||
1. Install [Docker](https://www.docker.com/get-started).
|
||||
2. Install [Visual Studio Code](https://code.visualstudio.com/).
|
||||
3. Install the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension for Visual Studio Code.
|
||||
|
||||
### Initial setup
|
||||
|
||||
The Dev Container and VSCode example configuration files are located in the project folder `.dev/devcontainer` and `.dev/vscode`.
|
||||
|
||||
To make use of them follow these steps:
|
||||
|
||||
1. Copy the directories or run `make vscode` from inside the `.dev` folder.
|
||||
2. Open your project in Visual Studio Code.
|
||||
3. Open the Command Palette (`Ctrl+Shift+P`) and select `Dev Containers: Reopen in Container`.
|
||||
4. VSCode will build the container and reopen the project inside the container.
|
||||
Be patient, the first run will take several minutes to complete.
|
||||
|
||||
### Usage
|
||||
|
||||
Inside the container you can follow the build instructions (see [Building](building.md)):
|
||||
|
||||
- Build owntone-server
|
||||
|
||||
```bash
|
||||
autoreconf -i
|
||||
./configure
|
||||
make
|
||||
```
|
||||
|
||||
- Build web interface
|
||||
|
||||
```bash
|
||||
cd web-src
|
||||
npm run build
|
||||
```
|
||||
|
||||
Running `owntone-server` from inside the container with the predefined run/debug configuration will
|
||||
use the conf file `.devcontainer/data/devcontainer-owntone.conf` as `owntone.conf`.
|
||||
|
||||
Configure the mount configuration in `.devcontainer/devcontainer.json` to use a different music folder
|
||||
or mount the log folder to a local directory.
|
||||
|
||||
```json
|
||||
// Mounts volumes to keep files / state between container rebuilds
|
||||
"mounts": [
|
||||
//...
|
||||
|
||||
// Bind mounts for owntone config file and logs, cache, music directories
|
||||
//"source=<path-to-local-logs-dir>,target=/data/logs,type=bind,consistency=cached",
|
||||
//"source=<path-to-local-cache-dir>,target=/data/cache,type=bind,consistency=cached",
|
||||
//"source=<path-to-local-music-dir>,target=/data/music,type=bind,consistency=cached",
|
||||
"source=${localWorkspaceFolder}/.devcontainer/data/devcontainer-owntone.conf,target=/data/conf/owntone.conf,type=bind,consistency=cached"
|
||||
],
|
||||
```
|
||||
|
||||
### Dev Container configuration
|
||||
|
||||
The Dev Container example uses an Ubuntu image as base. It contains some additional (opinionated) tools to customize the shell prompt and some terminal niceties:
|
||||
|
||||
- [Starship](https://starship.rs/) to customize the shell prompt.
|
||||
- [eza](https://eza.rocks/) as `ls` replacement.
|
||||
- [Atuin](https://atuin.sh/) for the shell history.
|
||||
|
||||
Take a look at `.devcontainer/devcontainer.env` if you want to disable any of those.
|
||||
|
||||
Additional terminal tools installed are:
|
||||
|
||||
- [zoxide](https://github.com/ajeetdsouza/zoxide) - a smarter `cd`
|
||||
- [bat](https://github.com/sharkdp/bat) - a `cat` clone with syntax highlighting
|
||||
@ -6,7 +6,7 @@ After installation (see [Installation](installation.md)) do the following:
|
||||
needs
|
||||
2. Start or restart the server (usually `/etc/init.d/owntone restart`)
|
||||
3. Go to the web interface [http://owntone.local:3689](http://owntone.local:3689),
|
||||
or, if that won't work, to [http://[your_server_address_here]:3689](http://[your_server_address_here]:3689)
|
||||
or, if that won't work, to http://SERVER_ADDRESS:3689
|
||||
4. Wait for the library scan to complete
|
||||
5. If you will be using a remote, e.g. Apple Remote: Start the remote, go to
|
||||
Settings, Add Library
|
||||
|
||||
@ -7,10 +7,10 @@ hide:
|
||||
# OwnTone
|
||||
|
||||
**OwnTone** is an open source (audio) media server for GNU/Linux, FreeBSD
|
||||
and MacOS.
|
||||
and macOS.
|
||||
|
||||
It allows sharing and streaming your media library to iTunes (DAAP[^1]),
|
||||
Roku (RSP), AirPlay devices (multiroom), Chromecast and also supports local
|
||||
Roku (RSP), AirPlay devices (multi-room), Chromecast and also supports local
|
||||
playback.
|
||||
|
||||
You can control OwnTone via its web interface, Apple Remote (and compatible
|
||||
@ -31,7 +31,7 @@ OwnTone is written in C with a web interface written in Vue.js.
|
||||
## Features
|
||||
|
||||
- Stream to :material-cast-variant: AirPlay (synchronized multiroom) and :material-cast:
|
||||
Chromecast devices
|
||||
Chromecast devices
|
||||
- :material-music-box-multiple-outline: Share local library with iTunes and Roku
|
||||
- :material-volume-high: Local audio playback with ALSA or PulseAudio
|
||||
- Supports multiple different clients:
|
||||
@ -41,11 +41,11 @@ OwnTone is written in C with a web interface written in Vue.js.
|
||||
- :material-console: MPD clients
|
||||
|
||||
- Supports :material-music: music and :material-book-open-variant:
|
||||
audiobook files, :material-microphone: podcast files and :material-rss: RSS
|
||||
and :material-radio: internet radio
|
||||
audiobook files, :material-microphone: podcast files and :material-rss: RSS
|
||||
and :material-radio: internet radio
|
||||
- :material-file-music: Supports audio files in most formats
|
||||
- :material-spotify: Supports playing your Spotify library (requires
|
||||
Spotify premium account)
|
||||
Spotify premium account)
|
||||
- :material-raspberry-pi: Runs on low power devices like the Raspberry Pi
|
||||
|
||||
---
|
||||
@ -54,7 +54,7 @@ OwnTone is written in C with a web interface written in Vue.js.
|
||||
{: class="zoom" }
|
||||
{: class="zoom" }
|
||||
|
||||
_(You can find more screenshots from OwnTone's web interface [here](clients/web-interface.md))_
|
||||
_(You can find more screenshots from OwnTone's web interface [here](control-clients/web.md))_
|
||||
{: class="text-center" }
|
||||
|
||||
---
|
||||
@ -66,7 +66,7 @@ and what features it was built with (e.g. Spotify support).
|
||||
|
||||
How to find out? Go to the [web interface](http://owntone.local:3689) and
|
||||
check. No web interface? Then check the top of OwnTone's log file (usually
|
||||
/var/log/owntone.log).
|
||||
`/var/log/owntone.log`).
|
||||
|
||||
Note that you are viewing a snapshot of the instructions that may or may not
|
||||
match the version of OwnTone that you are using.
|
||||
@ -78,4 +78,4 @@ please see the documentation on [Building from Source](installation.md).
|
||||
|
||||
You can find source and documentation, also for older versions, here:
|
||||
|
||||
- [https://github.com/owntone/owntone-server.git](https://github.com/owntone/owntone-server.git)
|
||||
- [Source Code](https://github.com/owntone/owntone-server.git)
|
||||
|
||||
@ -1,339 +1,19 @@
|
||||
# Installation instructions for OwnTone
|
||||
# How to get and install OwnTone
|
||||
|
||||
This document contains instructions for installing OwnTone from the git tree. If
|
||||
you just want to install from a release tarball, you don't need the build tools
|
||||
(git, autotools, autoconf, automake, gawk, gperf, gettext, bison and flex), and
|
||||
you can skip the autoreconf step.
|
||||
You can compile and run OwnTone on pretty much any Linux- or BSD-platform. The
|
||||
instructions are [here](building.md).
|
||||
|
||||
The source for this version of OwnTone can be found here:
|
||||
[owntone/owntone-server](https://github.com/owntone/owntone-server.git)
|
||||
Apt repositories, images and precompiled binaries are available for some
|
||||
platforms. These can save you some work and make it easier to stay up to date:
|
||||
|
||||
## Quick version for Raspberry Pi OS
|
||||
|
||||
See the instructions here:
|
||||
[OwnTone server (iTunes server) -
|
||||
Raspberry Pi Forums](http://www.raspberrypi.org/phpBB3/viewtopic.php?t=49928)
|
||||
|
||||
## Quick version for Debian/Ubuntu users
|
||||
|
||||
If you are the lucky kind, this should get you all the required tools and
|
||||
libraries:
|
||||
|
||||
```bash
|
||||
sudo apt-get install \
|
||||
build-essential git autotools-dev autoconf automake libtool gettext gawk \
|
||||
gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev \
|
||||
libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev \
|
||||
libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev \
|
||||
libevent-dev libplist-dev libsodium-dev libjson-c-dev libwebsockets-dev \
|
||||
libcurl4-openssl-dev libprotobuf-c-dev
|
||||
```
|
||||
|
||||
Note that OwnTone will also work with other versions and flavours of
|
||||
libgcrypt and libcurl, so the above are just suggestions.
|
||||
|
||||
The following features require extra packages, and that you add a configure
|
||||
argument when you run ./configure:
|
||||
|
||||
Feature | Configure argument | Packages
|
||||
---------------------|--------------------------|-------------------------------------
|
||||
Chromecast | `--enable-chromecast` | libgnutls*-dev
|
||||
Pulseaudio | `--with-pulseaudio` | libpulse-dev
|
||||
|
||||
These features can be disabled saving you package dependencies:
|
||||
|
||||
Feature | Configure argument | Packages
|
||||
---------------------|--------------------------|-------------------------------------
|
||||
Spotify (built-in) | `--disable-spotify` | libprotobuf-c-dev
|
||||
Player web UI | `--disable-webinterface` | libwebsockets-dev
|
||||
Live web UI | `--without-libwebsockets`| libwebsockets-dev
|
||||
|
||||
Then run the following (adding configure arguments for optional features):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
autoreconf -i
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Using `--enable-install-user` means that `make install` will also add system
|
||||
user and group for owntone.
|
||||
|
||||
With the above configure arguments, a systemd service file will be installed to
|
||||
`/etc/systemd/system/owntone.service` so that the server will start on boot.
|
||||
Use `--disable-install-systemd` if you don't want that.
|
||||
|
||||
Now edit `/etc/owntone.conf`. Note the guide at the top highlighting which
|
||||
settings that normally require modification.
|
||||
|
||||
Start the server with `sudo systemctl start owntone` and check that it is
|
||||
running with `sudo systemctl status owntone`.
|
||||
|
||||
See the [Documentation](getting-started.md) for usage information.
|
||||
|
||||
## Quick version for Fedora
|
||||
|
||||
If you haven't already enabled the free RPM fusion packages do that, since you
|
||||
will need ffmpeg. You can google how to do that. Then run:
|
||||
|
||||
```bash
|
||||
sudo dnf install \
|
||||
git automake autoconf gettext-devel gperf gawk libtool bison flex \
|
||||
sqlite-devel libconfuse-devel libunistring-devel mxml-devel libevent-devel \
|
||||
avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel \
|
||||
libplist-devel libsodium-devel json-c-devel libwebsockets-devel \
|
||||
libcurl-devel protobuf-c-devel
|
||||
```
|
||||
|
||||
Clone the OwnTone repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
```
|
||||
|
||||
Then run the following:
|
||||
|
||||
```bash
|
||||
autoreconf -i
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Using `--enable-install-user` means that `make install` will also add system
|
||||
user and group for owntone.
|
||||
|
||||
With the above configure arguments, a systemd service file will be installed to
|
||||
`/etc/systemd/system/owntone.service` so that the server will start on boot.
|
||||
Use `--disable-install-systemd` if you don't want that.
|
||||
|
||||
Now edit `/etc/owntone.conf`. Note the guide at the top highlighting which
|
||||
settings that normally require modification.
|
||||
|
||||
Start the server with `sudo systemctl start owntone` and check that it is
|
||||
running with `sudo systemctl status owntone`.
|
||||
|
||||
See the [Documentation](getting-started.md) for usage information.
|
||||
|
||||
## Quick version for FreeBSD
|
||||
|
||||
There is a script in the 'scripts' folder that will at least attempt to do all
|
||||
the work for you. And should the script not work for you, you can still look
|
||||
through it and use it as an installation guide.
|
||||
|
||||
## Quick version for macOS (using Homebrew)
|
||||
|
||||
This workflow file used for building OwnTone via Github actions includes
|
||||
all the steps that you need to execute:
|
||||
[.github/workflows/macos.yml](https://github.com/owntone/owntone-server/blob/master/.github/workflows/macos.yml)
|
||||
|
||||
## "Quick" version for macOS (using macports)
|
||||
|
||||
Caution:
|
||||
1) this approach may be out of date, consider using the Homebrew method above
|
||||
since it is continuously tested.
|
||||
2) macports requires many downloads and lots of time to install (and sometimes
|
||||
build) ports... you'll want a decent network connection and some patience!
|
||||
|
||||
Install macports (which requires Xcode): <https://www.macports.org/install.php>
|
||||
|
||||
```bash
|
||||
sudo port install \
|
||||
autoconf automake libtool pkgconfig git gperf bison flex libgcrypt \
|
||||
libunistring libconfuse ffmpeg libevent json-c libwebsockets curl \
|
||||
libplist libsodium protobuf-c
|
||||
```
|
||||
|
||||
Download, configure, build and install the Mini-XML library: <http://www.msweet.org/projects.php/Mini-XML>
|
||||
|
||||
Download, configure, build and install the libinotify library: <https://github.com/libinotify-kqueue/libinotify-kqueue>
|
||||
|
||||
Add the following to `.bashrc`:
|
||||
|
||||
```bash
|
||||
# add /usr/local to pkg-config path
|
||||
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/local/lib/pkgconfig
|
||||
# libunistring doesn't support pkg-config, set overrides
|
||||
export LIBUNISTRING_CFLAGS=-I/opt/local/include
|
||||
export LIBUNISTRING_LIBS="-L/opt/local/lib -lunistring"
|
||||
```
|
||||
|
||||
Optional features require the following additional ports:
|
||||
|
||||
Feature | Configure argument | Ports
|
||||
--------------------|--------------------------|-------------------
|
||||
Chromecast | `--enable-chromecast` | gnutls
|
||||
Pulseaudio | `--with-pulseaudio` | pulseaudio
|
||||
|
||||
Clone the OwnTone repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
```
|
||||
|
||||
Finally, configure, build and install, adding configure arguments for
|
||||
optional features:
|
||||
|
||||
```bash
|
||||
autoreconf -i
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Note: if for some reason you've installed the avahi port, you need to
|
||||
add `--without-avahi` to configure above.
|
||||
|
||||
Edit `/usr/local/etc/owntone.conf` and change the `uid` to a nice
|
||||
system daemon (eg: unknown), and run the following:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /usr/local/var/run
|
||||
sudo mkdir -p /usr/local/var/log # or change logfile in conf
|
||||
sudo chown unknown /usr/local/var/cache/owntone # or change conf
|
||||
```
|
||||
|
||||
Run OwnTone:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/sbin/owntone
|
||||
```
|
||||
|
||||
Verify it's running (you need to <kbd>Ctrl</kbd>+<kbd>C</kbd> to stop
|
||||
dns-sd):
|
||||
|
||||
```bash
|
||||
dns-sd -B _daap._tcp
|
||||
```
|
||||
|
||||
## Long version - requirements
|
||||
|
||||
Required tools:
|
||||
|
||||
- autotools: autoconf 2.63+, automake 1.10+, libtool 2.2. Run `autoreconf -i`
|
||||
at the top of the source tree to generate the build system.
|
||||
- gettext: libunistring requires iconv and gettext provides the autotools
|
||||
macro definitions for iconv.
|
||||
- gperf
|
||||
- bison 3.0+ (yacc is not sufficient)
|
||||
- flex (lex is not sufficient)
|
||||
|
||||
Libraries:
|
||||
|
||||
- Avahi client libraries (avahi-client), 0.6.24 minimum
|
||||
from <http://avahi.org/>
|
||||
- sqlite3 3.5.0+ with unlock notify API enabled (read below)
|
||||
from <http://sqlite.org/download.html>
|
||||
- ffmpeg (libav)
|
||||
from <http://ffmpeg.org/>
|
||||
- libconfuse
|
||||
from <http://www.nongnu.org/confuse/>
|
||||
- libevent 2.0+ (best with 2.1.4+)
|
||||
from <http://libevent.org/>
|
||||
- MiniXML (aka mxml or libmxml)
|
||||
from <http://minixml.org/software.php>
|
||||
- gcrypt 1.2.0+
|
||||
from <http://gnupg.org/download/index.en.html#libgcrypt>
|
||||
- zlib
|
||||
from <http://zlib.net/>
|
||||
- libunistring 0.9.3+
|
||||
from <http://www.gnu.org/software/libunistring/#downloading>
|
||||
- libjson-c
|
||||
from <https://github.com/json-c/json-c/wiki>
|
||||
- libcurl
|
||||
from <http://curl.haxx.se/libcurl/>
|
||||
- libplist 0.16+
|
||||
from <http://github.com/JonathanBeck/libplist/downloads>
|
||||
- libsodium
|
||||
from <https://download.libsodium.org/doc/>
|
||||
- libprotobuf-c
|
||||
from <https://github.com/protobuf-c/protobuf-c/wiki>
|
||||
- libasound (optional - ALSA local audio)
|
||||
often already installed as part of your distro
|
||||
- libpulse (optional - Pulseaudio local audio)
|
||||
from <https://www.freedesktop.org/wiki/Software/PulseAudio/Download/>
|
||||
- libgnutls (optional - Chromecast support)
|
||||
from <http://www.gnutls.org/>
|
||||
- libwebsockets 2.0.2+ (optional - websocket support)
|
||||
from <https://libwebsockets.org/>
|
||||
|
||||
If using binary packages, remember that you need the development packages to
|
||||
build OwnTone (usually named -dev or -devel).
|
||||
|
||||
sqlite3 needs to be built with support for the unlock notify API; this isn't
|
||||
always the case in binary packages, so you may need to rebuild sqlite3 to
|
||||
enable the unlock notify API (you can check for the presence of the
|
||||
sqlite3_unlock_notify symbol in the sqlite3 library). Refer to the sqlite3
|
||||
documentation, look for `SQLITE_ENABLE_UNLOCK_NOTIFY`.
|
||||
|
||||
## Long version - building and installing
|
||||
|
||||
Start by generating the build system by running `autoreconf -i`. This will
|
||||
generate the configure script and `Makefile.in`.
|
||||
|
||||
To display the configure options `run ./configure --help`.
|
||||
|
||||
Support for Spotify is optional. Use `--disable-spotify` to disable this feature.
|
||||
|
||||
Support for LastFM scrobbling is optional. Use `--enable-lastfm` to enable this
|
||||
feature.
|
||||
|
||||
Support for the MPD protocol is optional. Use `--disable-mpd` to disable this
|
||||
feature.
|
||||
|
||||
Support for Chromecast devices is optional. Use `--enable-chromecast` to enable
|
||||
this feature.
|
||||
|
||||
The player web interface is optional. Use `--disable-webinterface` to disable
|
||||
this feature.
|
||||
If enabled, `sudo make install` will install the prebuild html, js, css files.
|
||||
The prebuild files are:
|
||||
|
||||
- `htdocs/index.html`
|
||||
- `htdocs/player/*`
|
||||
|
||||
The source for the player web interface is located under the `web-src` folder and
|
||||
requires nodejs >= 6.0 to be built. In the `web-src` folder run `npm install` to
|
||||
install all dependencies for the player web interface. After that run `npm run build`.
|
||||
This will build the web interface and update the `htdocs` folder.
|
||||
(See [Web interface](clients/web-interface.md) for more
|
||||
informations)
|
||||
|
||||
Building with libwebsockets is required if you want the web interface. It will be enabled
|
||||
if the library is present (with headers). Use `--without-libwebsockets` to disable.
|
||||
|
||||
Building with Pulseaudio is optional. It will be enabled if the library is
|
||||
present (with headers). Use `--without-pulseaudio` to disable.
|
||||
|
||||
Recommended build settings:
|
||||
|
||||
```bash
|
||||
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-install-user
|
||||
```
|
||||
|
||||
After configure run the usual make, and if that went well, `sudo make install`.
|
||||
|
||||
With the above configure arguments, a systemd service file will be installed to
|
||||
`/etc/systemd/system/owntone.service` so that the server will start on boot.
|
||||
Use `--disable-install-systemd` if you don't want that.
|
||||
|
||||
Using `--enable-install-user` means that `make install` will also add a system
|
||||
user and group for owntone.
|
||||
|
||||
After installation:
|
||||
|
||||
- edit the configuration file, `/etc/owntone.conf`
|
||||
- make sure the Avahi daemon is installed and running (Debian:
|
||||
`apt install avahi-daemon`)
|
||||
|
||||
OwnTone will drop privileges to any user you specify in the configuration file
|
||||
if it's started as root.
|
||||
|
||||
This user must have read permission to your library and read/write permissions
|
||||
to the database location (`$localstatedir/cache/owntone` by default).
|
||||
|Platform | How to get
|
||||
|----------------------|---------------------------------------------------------
|
||||
|RPi w/Raspberry Pi OS | Add OwnTone repository to apt sources<br>(See: [Raspberry Pi Forums](http://www.raspberrypi.org/phpBB3/viewtopic.php?t=49928))
|
||||
|Debian/Ubuntu amd64 | Download the .deb package as artifact from the [Github workflow](https://github.com/owntone/owntone-apt/actions)<br>(requires that you are logged in)
|
||||
|OpenWrt | Run `opkg install libwebsockets-full owntone`
|
||||
|Docker / Podman | See [official image](https://github.com/owntone/owntone-container)
|
||||
|FreeBSD | Run `pkg install owntone` (See: [FreeBSD ports](https://cgit.freebsd.org/ports/tree/audio/owntone))
|
||||
|
||||
OwnTone is not in the official Debian repositories due to lack of Debian
|
||||
maintainer and Debian policy difficulties concerning the web UI, see
|
||||
[this issue](https://github.com/owntone/owntone-server/issues/552).
|
||||
|
||||
@ -5,4 +5,3 @@ go to the web interface and authorize OwnTone with your LastFM credentials.
|
||||
|
||||
OwnTone will not store your LastFM username/password, only the session key.
|
||||
The session key does not expire.
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ have to trigger updates manually. You can for instance set up a cron job that
|
||||
runs `/usr/bin/curl http://localhost:3689/api/update`
|
||||
|
||||
To logout and remove Spotify tracks + credentials make a request to
|
||||
[http://[your_server_address_here]:3689/api/spotify-logout](http://[your_server_address_here]:3689/api/spotify-logout).
|
||||
[http://owntone.local:3689/api/spotify-logout](http://owntone.local:3689/api/spotify-logout).
|
||||
|
||||
Limitations:
|
||||
You will not be able to do any playlist management through OwnTone - use
|
||||
@ -44,7 +44,7 @@ The easiest way of accomplishing this may be with [Spocon](https://github.com/sp
|
||||
since it requires minimal configuration. After installing, create two pipes
|
||||
(with mkfifo) and set the configuration in the player section:
|
||||
|
||||
```
|
||||
```conf
|
||||
# Audio output device (MIXER, PIPE, STDOUT)
|
||||
output = "PIPE"
|
||||
# Output raw (signed) PCM to this file (`player.output` must be PIPE)
|
||||
|
||||
433
docs/json-api.md
433
docs/json-api.md
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,7 @@ You can do this by using "Update library" from the web interface.
|
||||
Symlinks are supported and dereferenced, but it is best to use them for
|
||||
directories only.
|
||||
|
||||
Files starting with . (dot) and _ (underscore) are ignored.
|
||||
|
||||
## Pipes (for e.g. multiroom with Shairport-sync)
|
||||
|
||||
@ -36,7 +37,7 @@ speakers you have selected (through Remote).
|
||||
|
||||
The format of the audio being written to the pipe must be PCM16.
|
||||
|
||||
You can also start playback of pipes manually. You will find them in remotes
|
||||
You can also start playback of pipes manually. You will find them in remotes
|
||||
listed under "Unknown artist" and "Unknown album". The track title will be the
|
||||
name of the pipe.
|
||||
|
||||
@ -45,7 +46,6 @@ This requires that the metadata pipe has the same filename as the audio pipe
|
||||
plus a ".metadata" suffix. Say Shairport-sync is configured to write audio to
|
||||
"/foo/bar/pipe", then the metadata pipe should be "/foo/bar/pipe.metadata".
|
||||
|
||||
|
||||
## Libraries on network mounts
|
||||
|
||||
Most network filesharing protocols do not offer notifications when the library
|
||||
@ -55,13 +55,13 @@ Instead you can schedule a cron job to update the database.
|
||||
The first step in doing this is to add two entries to the 'directories'
|
||||
configuration item in owntone.conf:
|
||||
|
||||
```
|
||||
```conf
|
||||
directories = { "/some/local/dir", "/your/network/mount/library" }
|
||||
```
|
||||
|
||||
Now you can make a cron job that runs this command:
|
||||
|
||||
```
|
||||
```shell
|
||||
touch /some/local/dir/trigger.init-rescan
|
||||
```
|
||||
|
||||
|
||||
28
docs/media-clients.md
Normal file
28
docs/media-clients.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Media Clients
|
||||
|
||||
Media Clients are applications that download the media from the server and do
|
||||
the playback themselves. OwnTone supports media clients via the DAAP and RSP
|
||||
protocols (so not UPNP).
|
||||
|
||||
Some Media Clients are also able to play video from OwnTone.
|
||||
|
||||
OwnTone can't serve Spotify, internet radio and streams to Media Clients. For
|
||||
that you must let OwnTone do the playback.
|
||||
|
||||
Here is a list of working and non-working DAAP clients. The list is probably
|
||||
obsolete when you read it :-)
|
||||
|
||||
| Client | Developer | Type | Platform | Working (vers.) |
|
||||
| ------------------------ | ----------- | ------ | --------------- | --------------- |
|
||||
| iTunes | Apple | DAAP | Win | Yes (12.10.1) |
|
||||
| Apple Music | Apple | DAAP | macOS | Yes |
|
||||
| Rhythmbox | Gnome | DAAP | Linux | Yes |
|
||||
| Diapente | diapente | DAAP | Android | Yes |
|
||||
| WinAmp DAAPClient | WardFamily | DAAP | WinAmp | Yes |
|
||||
| Amarok w/DAAP plugin | KDE | DAAP | Linux/Win | Yes (2.8.0) |
|
||||
| Banshee | | DAAP | Linux/Win/macOS | No (2.6.2) |
|
||||
| jtunes4 | | DAAP | Java | No |
|
||||
| Firefly Client | | (DAAP) | Java | No |
|
||||
|
||||
Technically, devices like the Roku Soundbridge are both media clients and
|
||||
audio outputs. You can find information about them [here](audio-outputs/roku.md).
|
||||
@ -1,25 +0,0 @@
|
||||
# Local audio
|
||||
|
||||
## Local audio through ALSA
|
||||
|
||||
In the config file, you can select ALSA for local audio. This is the default.
|
||||
|
||||
When using ALSA, the server will try to syncronize playback with AirPlay. You
|
||||
can adjust the syncronization in the config file.
|
||||
|
||||
For most setups the default values in the config file should work. If they
|
||||
don't, there is help [here](../advanced/outputs-alsa.md)
|
||||
|
||||
|
||||
## Local audio, Bluetooth and more through Pulseaudio
|
||||
|
||||
In the config file, you can select Pulseaudio for local audio. In addition to
|
||||
local audio, Pulseaudio also supports an array of other targets, e.g. Bluetooth
|
||||
or DLNA. However, Pulseaudio does require some setup, so here is a separate page
|
||||
with some help on that: [Pulse audio](../advanced/outputs-pulse.md)
|
||||
|
||||
Note that if you select Pulseaudio the "card" setting in the config file has
|
||||
no effect. Instead all soundcards detected by Pulseaudio will be listed as
|
||||
speakers by OwnTone.
|
||||
|
||||
You can adjust the latency of Pulseaudio playback in the config file.
|
||||
@ -1,19 +0,0 @@
|
||||
# MP3 network streaming (streaming to iOS)
|
||||
|
||||
You can listen to audio being played by OwnTone by opening this network
|
||||
stream address in pretty much any music player:
|
||||
|
||||
http://[your hostname/ip address]:3689/stream.mp3
|
||||
|
||||
This is currently the only way of listening to your audio on iOS devices, since
|
||||
Apple does not allow AirPlay receiver apps, and because Apple Home Sharing
|
||||
cannot be supported by OwnTone. So what you can do instead is install a
|
||||
music player app like VLC, connect to the stream and control playback with
|
||||
Remote.
|
||||
|
||||
In the speaker selection list, clicking on the icon should start the stream
|
||||
playing in the background on browsers that support that.
|
||||
|
||||
Note that MP3 encoding must be supported by ffmpeg/libav for this to work. If
|
||||
it is not available you will see a message in the log file. In Debian/Ubuntu you
|
||||
get MP3 encoding support by installing the package "libavcodec-extra".
|
||||
@ -1,4 +1,4 @@
|
||||
# Playlists and internet radio
|
||||
# Playlists and Radio
|
||||
|
||||
OwnTone supports M3U and PLS playlists. Just drop your playlist somewhere
|
||||
in your library with an .m3u or .pls extension and it will pick it up.
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
# OwnTone smart playlists
|
||||
# Smart Playlists
|
||||
|
||||
|
||||
To add a smart playlist to the server, create a new text file with a filename ending with .smartpl;
|
||||
To add a smart playlist to the server, create a new text file with a filename ending with .smartpl;
|
||||
the filename doesn't matter, only the .smartpl ending does. The file must be placed somewhere in your
|
||||
library folder.
|
||||
|
||||
|
||||
## Syntax
|
||||
|
||||
The contents of a smart playlist must follow the syntax:
|
||||
@ -16,7 +14,6 @@ The contents of a smart playlist must follow the syntax:
|
||||
|
||||
There is exactly one smart playlist allowed for a .smartpl file.
|
||||
|
||||
|
||||
An expression consists of:
|
||||
|
||||
```
|
||||
@ -66,6 +63,9 @@ Valid operands include:
|
||||
|
||||
* "string value" (string)
|
||||
* integer (int)
|
||||
* `empty`
|
||||
|
||||
The `empty` operand is only valid with the `is` operator and matches items with no value for the given field-name e.g. `comment`
|
||||
|
||||
Valid operands for the enumeration `data_kind` are:
|
||||
|
||||
@ -82,9 +82,9 @@ Valid operands for the enumeration `media_kind` are:
|
||||
* `audiobook`
|
||||
* `tvshow`
|
||||
|
||||
|
||||
Multiple expressions can be anded or ored together, using the keywords `OR` and `AND`. The unary not operator is also supported using the keyword `NOT`.
|
||||
|
||||
Use parentheses to group e.g. `play_count = 0 and (media_kind is podcast or media_kind is audiobook)`.
|
||||
|
||||
It is possible to define the sort order and limit the number of items by adding an order clause and/or a limit clause after the last expression:
|
||||
|
||||
@ -96,7 +96,6 @@ It is possible to define the sort order and limit the number of items by adding
|
||||
|
||||
There is additionally a special `random` _field-name_ that can be used in conjunction with `limit` to select a random number of items based on current expression.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
@ -144,6 +143,7 @@ This would match any podcast and audiobook file that was never played.
|
||||
limit 10
|
||||
}
|
||||
```
|
||||
|
||||
This would match the last 10 music files added to the library.
|
||||
|
||||
```
|
||||
@ -155,9 +155,21 @@ This would match the last 10 music files added to the library.
|
||||
limit 10
|
||||
}
|
||||
```
|
||||
|
||||
This generates a random set of, maximum of 10, rated Pop music tracks every time the playlist is queried.
|
||||
|
||||
## Date operand syntax
|
||||
```
|
||||
"All Jazz, No Foo" {
|
||||
media_kind is music and
|
||||
genre is "jazz" and
|
||||
(not comment includes "foo" or
|
||||
comment is empty)
|
||||
}
|
||||
```
|
||||
|
||||
This matches both the songs with comments that do not include "foo", but also the songs with no comment.
|
||||
|
||||
## Date Operand Syntax
|
||||
|
||||
One example of a valid date is a date in yyyy-mm-dd format:
|
||||
|
||||
@ -178,7 +190,6 @@ As an example, a valid date might be:
|
||||
|
||||
```3 weeks before today``` or ```3 weeks ago```
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
@ -202,13 +213,11 @@ All dates, except for `YYYY-DD-HH`, are relative to the day of when the server e
|
||||
|
||||
Note that `time_added after 4 weeks ago` and `time_added after last month` are subtly different; the former is exactly 4 weeks ago (from today) whereas the latter is the first day of the previous month.
|
||||
|
||||
## Differences with MT-daapd Smart Playlists
|
||||
|
||||
## Differences to mt-daapd smart playlists
|
||||
The syntax is really close to the mt-daapd smart playlist syntax (see [Multi-Threaded DAAP Daemon Code](https://sourceforge.net/p/mt-daapd/code/HEAD/tree/tags/release-0.2.4.2/contrib/mt-daapd.playlist).
|
||||
|
||||
The syntax is really close to the mt-daapd smart playlist syntax (see
|
||||
http://sourceforge.net/p/mt-daapd/code/HEAD/tree/tags/release-0.2.4.2/contrib/mt-daapd.playlist).
|
||||
|
||||
Even this documentation is based on the file linked above.
|
||||
Even this documentation is based on the document linked above.
|
||||
|
||||
Some differences are:
|
||||
|
||||
@ -216,4 +225,3 @@ Some differences are:
|
||||
* the not operator must be placed before an expression and not before the operator
|
||||
* `||`, `&&`, `!` are not supported (use `or`, `and`, `not`)
|
||||
* comments are not supported
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,26 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png?ver=2.0"
|
||||
href="apple-touch-icon.png?ver=2.0"
|
||||
/>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#00d1b2" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OwnTone</title>
|
||||
<script type="module" crossorigin src="/assets/index.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.css">
|
||||
<script type="module" crossorigin src="./assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="has-navbar-fixed-top has-navbar-fixed-bottom">
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -45,7 +45,13 @@
|
||||
#serial 13
|
||||
|
||||
AC_DEFUN([AX_PROG_FLEX], [
|
||||
AC_REQUIRE([AM_PROG_LEX])
|
||||
dnl --- Start of modified macro ---
|
||||
dnl Original uses 'AC_REQUIRE([AM_PROG_LEX])', but that produces a deprecation
|
||||
dnl warning since autoconf 2.70, because the underlying AC_PROG_LEX now
|
||||
dnl requires an argument. However, we cannot specify it through AM_PROG_LEX
|
||||
dnl until automake 1.17, so users with that will still get the warning.
|
||||
AM_PROG_LEX([noyywrap])
|
||||
dnl --- End of modified macro ---
|
||||
AC_REQUIRE([AC_PROG_EGREP])
|
||||
|
||||
AC_CACHE_CHECK([if flex is the lexer generator],[ax_cv_prog_flex],[
|
||||
|
||||
77
mkdocs.yml
77
mkdocs.yml
@ -46,18 +46,27 @@ theme:
|
||||
# - navigation.indexes
|
||||
- navigation.top
|
||||
palette:
|
||||
- scheme: default
|
||||
# Palette toggle for automatic mode
|
||||
- media: "(prefers-color-scheme)"
|
||||
toggle:
|
||||
icon: material/brightness-auto
|
||||
name: Switch to light mode
|
||||
# Palette toggle for light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: white
|
||||
accent: teal
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
primary: blue grey
|
||||
# Palette toggle for dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: black
|
||||
accent: teal
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to light mode
|
||||
icon: material/brightness-4
|
||||
name: Switch to system preference
|
||||
font:
|
||||
text: Roboto
|
||||
code: Roboto Mono
|
||||
@ -100,8 +109,8 @@ markdown_extensions:
|
||||
- pymdownx.caret
|
||||
- pymdownx.details
|
||||
- pymdownx.emoji:
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
@ -112,6 +121,9 @@ markdown_extensions:
|
||||
repo: mkdocs-material
|
||||
- pymdownx.mark
|
||||
- pymdownx.smartsymbols
|
||||
- pymdownx.snippets:
|
||||
base_path: [!relative $config_dir]
|
||||
check_paths: true
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
@ -127,30 +139,37 @@ markdown_extensions:
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Documentation:
|
||||
- Getting started: getting-started.md
|
||||
- Getting Started: getting-started.md
|
||||
- Installation: installation.md
|
||||
- Configuration: configuration.md
|
||||
- Building: building.md
|
||||
- Library: library.md
|
||||
- Control:
|
||||
- Mobile Device: control-clients/mobile.md
|
||||
- Desktop: control-clients/desktop.md
|
||||
- Browser: control-clients/web.md
|
||||
- API and CLI: control-clients/cli-api.md
|
||||
- Audio Outputs:
|
||||
- AirPlay: audio-outputs/airplay.md
|
||||
- Chromecast: audio-outputs/chromecast.md
|
||||
- Local Audio: audio-outputs/local-audio.md
|
||||
- Mobile Device: audio-outputs/mobile.md
|
||||
- Web: audio-outputs/web.md
|
||||
- Roku: audio-outputs/roku.md
|
||||
- Streaming: audio-outputs/streaming.md
|
||||
- Media Clients: media-clients.md
|
||||
- Artwork: artwork.md
|
||||
- Playlists & radio: playlists.md
|
||||
- Smart playlists: smart-playlists.md
|
||||
- Clients:
|
||||
- Supported clients: clients/supported-clients.md
|
||||
- Remote: clients/remote.md
|
||||
- Web interface: clients/web-interface.md
|
||||
- MPD clients: clients/mpd.md
|
||||
- CLI: clients/cli.md
|
||||
- Outputs:
|
||||
- Local audio: outputs/local-audio.md
|
||||
- Airplay: outputs/airplay.md
|
||||
- Chromecast: outputs/chromecast.md
|
||||
- Streaming: outputs/streaming.md
|
||||
- Services integration:
|
||||
- Playlists and Radio: playlists.md
|
||||
- Smart Playlists: smart-playlists.md
|
||||
- Services Integration:
|
||||
- Spotify: integrations/spotify.md
|
||||
- LastFM: integrations/lastfm.md
|
||||
- Advanced setups:
|
||||
- Alsa: advanced/outputs-alsa.md
|
||||
- Pulse audio: advanced/outputs-pulse.md
|
||||
- Radio streams: advanced/radio-streams.md
|
||||
- Remote access: advanced/remote-access.md
|
||||
- Multiple instances: advanced/multiple-instances.md
|
||||
- Advanced Setup:
|
||||
- ALSA: advanced/outputs-alsa.md
|
||||
- PulseAudio: advanced/outputs-pulse.md
|
||||
- Radio Streams: advanced/radio-streams.md
|
||||
- Remote Access: advanced/remote-access.md
|
||||
- Multiple Instances: advanced/multiple-instances.md
|
||||
- Development: development.md
|
||||
- Changelog: changelog.md
|
||||
- JSON API: json-api.md
|
||||
|
||||
@ -20,7 +20,7 @@ Debug domains; available domains are: \fIconfig\fP, \fIdaap\fP,
|
||||
\fIdb\fP, \fIhttpd\fP, \fImain\fP, \fImdns\fP, \fImisc\fP,
|
||||
\fIrsp\fP, \fIscan\fP, \fIxcode\fP, \fIevent\fP, \fIhttp\fP, \fIremote\fP,
|
||||
\fIdacp\fP, \fIffmpeg\fP, \fIartwork\fP, \fIplayer\fP, \fIraop\fP,
|
||||
\fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIlastfm\fP,
|
||||
\fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIscrobble\fP,
|
||||
\fIcache\fP, \fImpd\fP, \fIstream\fP, \fIcast\fP, \fIfifo\fP, \fIlib\fP,
|
||||
\fIweb\fP, \fIairplay\fP.
|
||||
.TP
|
||||
|
||||
@ -40,20 +40,20 @@ general {
|
||||
|
||||
# Sets who is allowed to connect without authorisation. This applies to
|
||||
# client types like Remotes, DAAP clients (iTunes) and to the web
|
||||
# interface. Options are "any", "localhost" or the prefix to one or
|
||||
# more ipv4/6 networks. The default is { "localhost", "192.168", "fd" }
|
||||
# trusted_networks = { "localhost", "192.168", "fd" }
|
||||
# interface. Options are "any", "lan", "localhost", "none" or the prefix
|
||||
# to one or more ipv4/6 networks. The default is { "lan" }
|
||||
# trusted_networks = { "lan" }
|
||||
|
||||
# Enable/disable IPv6
|
||||
ipv6 = yes
|
||||
# ipv6 = no
|
||||
|
||||
# Set this if you want the server to bind to a specific IP address. Can
|
||||
# be ipv6 or ipv4. Default (commented out or "::") is to listen on all
|
||||
# IP addresses.
|
||||
# bind_address = "::"
|
||||
|
||||
# Location of cache database
|
||||
# cache_path = "@localstatedir@/cache/@PACKAGE@/cache.db"
|
||||
# Directory where the server keeps cached data
|
||||
# cache_dir = "@localstatedir@/cache/@PACKAGE@"
|
||||
|
||||
# DAAP requests that take longer than this threshold (in msec) get their
|
||||
# replies cached for next time. Set to 0 to disable caching.
|
||||
@ -124,9 +124,10 @@ library {
|
||||
# hide_singles = false
|
||||
|
||||
# Internet streams in your playlists will by default be shown in the
|
||||
# "Radio" library, like iTunes does. However, some clients (like
|
||||
# "Radio" library, like iTunes does. However, some DAAP clients (like
|
||||
# TunesRemote+) won't show the "Radio" library. If you would also like
|
||||
# to have them shown like normal playlists, you can enable this option.
|
||||
# Note this option is only for DAAP clients (so not the web interface).
|
||||
# radio_playlists = false
|
||||
|
||||
# These are the default playlists. If you want them to have other names,
|
||||
@ -171,6 +172,12 @@ library {
|
||||
# to trigger a rescan.
|
||||
# filescan_disable = false
|
||||
|
||||
# Only use the first genre found in metadata
|
||||
# Some tracks have multiple genres semicolon-separated in the same tag,
|
||||
# e.g. 'Pop;Rock'. If you don't want them listed like this, you can
|
||||
# enable this option and only the first genre will be used (i.e. 'Pop').
|
||||
# only_first_genre = false
|
||||
|
||||
# Should metadata from m3u playlists, e.g. artist and title in EXTINF,
|
||||
# override the metadata we get from radio streams?
|
||||
# m3u_overrides = false
|
||||
@ -181,18 +188,31 @@ library {
|
||||
# Should we import the content of iTunes smart playlists?
|
||||
# itunes_smartpl = false
|
||||
|
||||
# Decoding options for DAAP clients
|
||||
# Transcoding options for DAAP and RSP clients
|
||||
# Since iTunes has native support for mpeg, mp4a, mp4v, alac and wav,
|
||||
# such files will be sent as they are. Any other formats will be decoded
|
||||
# to raw wav. If OwnTone detects a non-iTunes DAAP client, it is
|
||||
# assumed to only support mpeg and wav, other formats will be decoded.
|
||||
# Here you can change when to decode. Note that these settings have no
|
||||
# effect on AirPlay.
|
||||
# such files will be sent as they are. Any other formats will be
|
||||
# transcoded. Some other clients, including Roku/RSP, announce what
|
||||
# formats they support, and the server will transcode to one of those if
|
||||
# necessary. Clients that don't announce supported formats are assumed
|
||||
# to support mpeg (mp3), wav and alac.
|
||||
# Here you can change when and how to transcode. The settings *only*
|
||||
# affect serving audio to DAAP and RSP clients, they have no effect on
|
||||
# direct AirPlay, Chromecast and local audio playback.
|
||||
# Formats: mp4a, mp4v, mpeg, alac, flac, mpc, ogg, wma, wmal, wmav, aif, wav
|
||||
# Formats that should never be decoded
|
||||
# Formats that should never be transcoded
|
||||
# no_decode = { "format", "format" }
|
||||
# Formats that should always be decoded
|
||||
# Formats that should always be transcoded
|
||||
# force_decode = { "format", "format" }
|
||||
# Prefer transcode to wav (default), alac or mpeg (mp3 with the bit rate
|
||||
# configured below in the streaming section). Note that alac requires
|
||||
# precomputing and caching mp4 headers, which takes both cpu and disk.
|
||||
# prefer_format = "format"
|
||||
|
||||
# Set ffmpeg filters (similar to 'ffmpeg -af xxx') that you want the
|
||||
# server to use when decoding files from your library. Examples:
|
||||
# { 'volume=replaygain=track' } -> use REPLAYGAIN_TRACK_GAIN metadata
|
||||
# { 'loudnorm=I=-16:LRA=11:TP=-1.5' } -> normalize volume
|
||||
# decode_audio_filters = { }
|
||||
|
||||
# Watch named pipes in the library for data and autostart playback when
|
||||
# there is data to be read. To exclude specific pipes from watching,
|
||||
@ -210,6 +230,16 @@ library {
|
||||
# new rating = 0.75 * stable rating + 0.25 * rolling rating)
|
||||
# rating_updates = false
|
||||
|
||||
# By default, ratings are only saved in the server's database. Enable
|
||||
# the below to make the server also read ratings from file metadata and
|
||||
# write on update (requires write access). To avoid excessive writing to
|
||||
# the library, automatic rating updates are not written, even with the
|
||||
# write_rating option enabled.
|
||||
# read_rating = false
|
||||
# write_rating = false
|
||||
# The scale used when reading/writing ratings to files
|
||||
# max_rating = 100
|
||||
|
||||
# Allows creating, deleting and modifying m3u playlists in the library directories.
|
||||
# Only supported by the player web interface and some mpd clients
|
||||
# Defaults to being disabled.
|
||||
@ -302,6 +332,10 @@ audio {
|
||||
# OwnTone behind a firewall)
|
||||
# control_port = 0
|
||||
# timing_port = 0
|
||||
|
||||
# Switch Airplay 1 streams to uncompressed ALAC (as opposed to regular,
|
||||
# compressed ALAC). Reduces CPU use at the cost of network bandwidth.
|
||||
# uncompressed_alac = false
|
||||
#}
|
||||
|
||||
# AirPlay per device settings
|
||||
@ -404,6 +438,10 @@ mpd {
|
||||
# clients and will need additional configuration in the MPD client to
|
||||
# work). Set to 0 to disable serving artwork over http.
|
||||
# http_port = 0
|
||||
|
||||
# Whether to emit an output with plugin type "httpd" to tell clients
|
||||
# that a stream is available for local playback.
|
||||
# enable_httpd_plugin = false
|
||||
}
|
||||
|
||||
# SQLite configuration (allows to modify the operation of the SQLite databases)
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
Description=DAAP/DACP (iTunes), RSP and MPD server, supports AirPlay and Remote
|
||||
Documentation=man:owntone(8)
|
||||
Requires=network.target local-fs.target avahi-daemon.socket
|
||||
After=network.target sound.target remote-fs.target pulseaudio.service
|
||||
After=network-online.target sound.target remote-fs.target pulseaudio.service
|
||||
|
||||
[Service]
|
||||
ExecStart=@sbindir@/owntone -f
|
||||
|
||||
@ -19,7 +19,7 @@ Url: https://github.com/owntone/owntone-server
|
||||
Source0: https://github.com/owntone/%{name}/archive/%{version}/%{name}-%{version}.tar.xz
|
||||
%{?systemd_ordering}
|
||||
BuildRequires: gcc, make, bison, flex, systemd, pkgconfig, libunistring-devel
|
||||
BuildRequires: pkgconfig(zlib), pkgconfig(libconfuse), pkgconfig(mxml)
|
||||
BuildRequires: pkgconfig(zlib), pkgconfig(libconfuse), pkgconfig(libxml-2.0)
|
||||
BuildRequires: pkgconfig(sqlite3) >= 3.5.0, pkgconfig(libevent) >= 2.0.0
|
||||
BuildRequires: pkgconfig(json-c), libgcrypt-devel >= 1.2.0
|
||||
BuildRequires: libgpg-error-devel >= 1.6
|
||||
|
||||
53
scripts/freebsd_install.sh
Normal file → Executable file
53
scripts/freebsd_install.sh
Normal file → Executable file
@ -2,6 +2,16 @@
|
||||
# Credit thorsteneckel who made the how-to that is the basis for this
|
||||
# script, see https://gist.github.com/thorsteneckel/c0610fb415c8d0486bce
|
||||
|
||||
USERNAME=owntone
|
||||
USERGROUP=owntone
|
||||
SYSCONFDIR=/usr/local/etc
|
||||
CONFIG="${SYSCONFDIR}/owntone.conf"
|
||||
|
||||
SUDO=sudo
|
||||
if [ "$(id -u)" == "0" ]; then
|
||||
SUDO=
|
||||
fi
|
||||
|
||||
echo "This script will install OwnTone in FreeBSD. The script is not very polished,"
|
||||
echo "so you might want to look through it before running it."
|
||||
read -p "Continue? [y/N] " yn
|
||||
@ -10,18 +20,17 @@ if [ "$yn" != "y" ]; then
|
||||
fi
|
||||
|
||||
DEPS="gmake autoconf automake libtool gettext gperf glib pkgconf wget git \
|
||||
ffmpeg libconfuse libevent mxml libgcrypt libunistring libiconv curl \
|
||||
ffmpeg libconfuse libevent libxml2 libgcrypt libunistring libiconv curl \
|
||||
libplist libinotify avahi sqlite3 alsa-lib libsodium json-c libwebsockets
|
||||
protobuf-c bison flex"
|
||||
echo "The script can install the following dependency packages for you:"
|
||||
echo $DEPS
|
||||
read -p "Should the script install these packages? [y/N] " yn
|
||||
if [ "$yn" = "y" ]; then
|
||||
sudo pkg install $DEPS;
|
||||
$SUDO pkg install $DEPS;
|
||||
fi
|
||||
|
||||
WORKDIR=~/owntone_build
|
||||
CONFIG=/usr/local/etc/owntone.conf
|
||||
read -p "Should the script create $WORKDIR and use it for building? [Y/n] " yn
|
||||
if [ "$yn" = "n" ]; then
|
||||
exit
|
||||
@ -50,27 +59,39 @@ if [ "$yn" = "y" ]; then
|
||||
#export ZLIB_CFLAGS=-I/usr/include
|
||||
#export ZLIB_LIBS="-L/usr/lib -lz"
|
||||
|
||||
export CFLAGS="-march=native -g -I/usr/local/include -I/usr/include"
|
||||
# some compilers don't support -march=native, so only try it in likely cases
|
||||
ARCH=
|
||||
UNAME_PROCESSOR=`(uname -p) 2>/dev/null` || UNAME_PROCESSOR=unknown
|
||||
case "$UNAME_PROCESSOR" in
|
||||
amd64|x86_64|i686|i386)
|
||||
ARCH="-march=native"
|
||||
esac
|
||||
|
||||
export CFLAGS="${ARCH} -g -I/usr/local/include -I/usr/include"
|
||||
export LDFLAGS="-L/usr/local/lib -L/usr/lib"
|
||||
./configure --disable-install-systemd && gmake
|
||||
./configure --disable-install-systemd --with-user=$USERNAME --with-group=$USERGROUP --sysconfdir=$SYSCONFDIR && gmake
|
||||
|
||||
read -p "Should the script install owntone and add service startup scripts? [y/N] " yn
|
||||
if [ "$yn" = "y" ]; then
|
||||
sudo gmake install
|
||||
$SUDO gmake install
|
||||
|
||||
sudo sed -i -- 's/\/var\/cache/\/usr\/local\/var\/cache/g' $CONFIG
|
||||
$SUDO sed -i -- 's/\/var\/cache/\/usr\/local\/var\/cache/g' $CONFIG
|
||||
# Setup user and startup scripts
|
||||
echo "owntone::::::owntone:/nonexistent:/usr/sbin/nologin:" | sudo adduser -w no -D -f -
|
||||
sudo chown -R owntone:owntone /usr/local/var/cache/owntone
|
||||
if $(id $USERNAME >/dev/null 2>&1); then
|
||||
else
|
||||
echo "${USERNAME}::::::${USERGROUP}:/nonexistent:/usr/sbin/nologin:" | $SUDO adduser -w no -D -f -
|
||||
fi
|
||||
|
||||
$SUDO chown -R ${USERNAME}:${USERGROUP} /usr/local/var/cache/owntone
|
||||
if [ ! -f scripts/freebsd_start.sh ]; then
|
||||
echo "Could not find FreeBSD startup script"
|
||||
exit
|
||||
fi
|
||||
sudo install -m 755 scripts/freebsd_start.sh /usr/local/etc/rc.d/owntone
|
||||
$SUDO install -m 755 scripts/freebsd_start.sh /usr/local/etc/rc.d/owntone
|
||||
|
||||
service owntone enabled
|
||||
if [ $? -ne 0 ]; then
|
||||
sudo sh -c 'echo "owntone_enable=\"YES\"" >> /etc/rc.conf'
|
||||
$SUDO sh -c 'echo "owntone_enable=\"YES\"" >> /etc/rc.conf'
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -81,19 +102,19 @@ read -p "Should the script enable and start dbus and avahi-daemon? [y/N] " yn
|
||||
if [ "$yn" = "y" ]; then
|
||||
service dbus enabled
|
||||
if [ $? -ne 0 ]; then
|
||||
sudo sh -c 'echo "dbus_enable=\"YES\"" >> /etc/rc.conf'
|
||||
$SUDO sh -c 'echo "dbus_enable=\"YES\"" >> /etc/rc.conf'
|
||||
fi
|
||||
sudo service dbus start
|
||||
$SUDO service dbus start
|
||||
|
||||
service avahi-daemon enabled
|
||||
if [ $? -ne 0 ]; then
|
||||
sudo sh -c 'echo "avahi_daemon_enable=\"YES\"" >> /etc/rc.conf'
|
||||
$SUDO sh -c 'echo "avahi_daemon_enable=\"YES\"" >> /etc/rc.conf'
|
||||
fi
|
||||
sudo service avahi-daemon start
|
||||
$SUDO service avahi-daemon start
|
||||
fi
|
||||
|
||||
read -p "Should the script (re)start owntone and display the log output? [y/N] " yn
|
||||
if [ "$yn" = "y" ]; then
|
||||
sudo service owntone restart
|
||||
$SUDO service owntone restart
|
||||
tail -f /usr/local/var/log/owntone.log
|
||||
fi
|
||||
|
||||
@ -53,8 +53,8 @@ GPERF_FILES = \
|
||||
|
||||
GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h)
|
||||
|
||||
LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l
|
||||
PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y
|
||||
LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l parsers/mpd_lexer.l
|
||||
PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y parsers/mpd_parser.y
|
||||
|
||||
# This flag is given to Bison and tells it to produce headers. Note that
|
||||
# automake recognizes this flag too, and has special logic around it, so don't
|
||||
@ -88,24 +88,27 @@ owntone_SOURCES = main.c \
|
||||
library/filescanner.c library/filescanner.h \
|
||||
library/filescanner_ffmpeg.c library/filescanner_playlist.c \
|
||||
library/filescanner_smartpl.c library/filescanner_itunes.c \
|
||||
library/filescanner_mountwatch.c \
|
||||
library/rssscanner.c \
|
||||
library.c library.h \
|
||||
$(MDNS_SRC) mdns.h \
|
||||
remote_pairing.c remote_pairing.h \
|
||||
httpd.c httpd.h \
|
||||
httpd_rsp.c httpd_rsp.h \
|
||||
httpd_libevhttp.c \
|
||||
httpd.c httpd.h httpd_internal.h \
|
||||
httpd_rsp.c \
|
||||
httpd_daap.c httpd_daap.h \
|
||||
httpd_dacp.c httpd_dacp.h \
|
||||
httpd_jsonapi.c httpd_jsonapi.h \
|
||||
httpd_streaming.c httpd_streaming.h \
|
||||
httpd_oauth.c httpd_oauth.h \
|
||||
httpd_artworkapi.c httpd_artworkapi.h \
|
||||
httpd_dacp.c \
|
||||
httpd_jsonapi.c \
|
||||
httpd_streaming.c \
|
||||
httpd_oauth.c \
|
||||
httpd_artworkapi.c \
|
||||
http.c http.h \
|
||||
dmap_common.c dmap_common.h \
|
||||
transcode.c transcode.h \
|
||||
artwork.c artwork.h \
|
||||
misc.c misc.h \
|
||||
misc_json.c misc_json.h \
|
||||
misc_xml.c misc_xml.h \
|
||||
rng.c rng.h \
|
||||
smartpl_query.c smartpl_query.h \
|
||||
player.c player.h \
|
||||
@ -117,15 +120,17 @@ owntone_SOURCES = main.c \
|
||||
outputs/rtp_common.h outputs/rtp_common.c \
|
||||
outputs/raop.c outputs/airplay.c $(PAIR_AP_SRC) \
|
||||
outputs/airplay_events.c outputs/airplay_events.h \
|
||||
outputs/streaming.c outputs/dummy.c outputs/fifo.c outputs/rcp.c \
|
||||
outputs/streaming.c \
|
||||
outputs/dummy.c outputs/fifo.c outputs/rcp.c \
|
||||
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
|
||||
evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \
|
||||
evthr.c evthr.h \
|
||||
$(SPOTIFY_SRC) \
|
||||
$(LASTFM_SRC) \
|
||||
listenbrainz.c listenbrainz.h \
|
||||
$(MPD_SRC) \
|
||||
listener.c listener.h \
|
||||
commands.c commands.h \
|
||||
mxml-compat.h \
|
||||
outputs/plist_wrap.h \
|
||||
$(LIBWEBSOCKETS_SRC) \
|
||||
$(GPERF_SRC) \
|
||||
|
||||
518
src/artwork.c
518
src/artwork.c
@ -71,7 +71,7 @@
|
||||
|
||||
// See online_source_is_failing()
|
||||
#define ONLINE_SEARCH_COOLDOWN_TIME 3600
|
||||
#define ONLINE_SEARCH_FAILURES_MAX 3
|
||||
#define ONLINE_SEARCH_FAILURES_MAX 5
|
||||
|
||||
enum artwork_cache
|
||||
{
|
||||
@ -113,6 +113,8 @@ struct artwork_ctx {
|
||||
uint32_t media_kind;
|
||||
// Input data for group handlers
|
||||
int64_t persistentid;
|
||||
// Input data for queue item handlers
|
||||
struct db_queue_item *queue_item;
|
||||
|
||||
// Not to be used by handler - query for item or group
|
||||
struct query_params qp;
|
||||
@ -149,6 +151,19 @@ enum parse_result {
|
||||
ONLINE_SOURCE_PARSE_NO_PARSER,
|
||||
};
|
||||
|
||||
// Remember previous artwork searches, used to avoid futile requests
|
||||
struct online_search_history {
|
||||
pthread_mutex_t mutex;
|
||||
int last_id;
|
||||
uint32_t last_hash;
|
||||
int last_max_w;
|
||||
int last_max_h;
|
||||
int last_response_code;
|
||||
char *last_artwork_url;
|
||||
time_t last_timestamp;
|
||||
int count_failures;
|
||||
};
|
||||
|
||||
struct online_source {
|
||||
// Name of online source
|
||||
const char *name;
|
||||
@ -156,8 +171,7 @@ struct online_source {
|
||||
|
||||
// How to authorize (using the Authorize http header)
|
||||
const char *auth_header;
|
||||
const char *auth_key;
|
||||
const char *auth_secret;
|
||||
int (*credentials_get)(char **auth_key, char **auth_secret);
|
||||
|
||||
// How to search for artwork
|
||||
const char *search_endpoint;
|
||||
@ -167,18 +181,7 @@ struct online_source {
|
||||
const char *template;
|
||||
} query_parts[8];
|
||||
|
||||
// Remember previous artwork searches, used to avoid futile requests
|
||||
struct {
|
||||
pthread_mutex_t mutex;
|
||||
int last_id;
|
||||
uint32_t last_hash;
|
||||
int last_max_w;
|
||||
int last_max_h;
|
||||
int last_response_code;
|
||||
char *last_artwork_url;
|
||||
time_t last_timestamp;
|
||||
int count_failures;
|
||||
} search_history;
|
||||
struct online_search_history *search_history;
|
||||
|
||||
// Function that can extract the artwork url from the parsed json response
|
||||
enum parse_result (*response_jparse)(char **artwork_url, json_object *response, int max_w, int max_h);
|
||||
@ -191,6 +194,8 @@ static const char *cover_extension[] =
|
||||
"jpg", "png",
|
||||
};
|
||||
|
||||
static pthread_mutex_t artwork_cache_stash_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* ----------------- DECLARE AND CONFIGURE SOURCE HANDLERS ----------------- */
|
||||
|
||||
/* Forward - group handlers */
|
||||
@ -207,6 +212,8 @@ static int source_item_ownpl_get(struct artwork_ctx *ctx);
|
||||
static int source_item_spotifywebapi_search_get(struct artwork_ctx *ctx);
|
||||
static int source_item_discogs_get(struct artwork_ctx *ctx);
|
||||
static int source_item_coverartarchive_get(struct artwork_ctx *ctx);
|
||||
/* Forward - queue item handlers */
|
||||
static int source_queue_item_artwork_url_get(struct artwork_ctx *ctx);
|
||||
|
||||
/* List of sources that can provide artwork for a group (i.e. usually an album
|
||||
* identified by a persistentid). The source handlers will be called in the
|
||||
@ -335,6 +342,13 @@ static struct artwork_source artwork_item_source[] =
|
||||
.media_kinds = MEDIA_KIND_MUSIC,
|
||||
.cache = NEVER,
|
||||
},
|
||||
{
|
||||
.name = "own (pipe)",
|
||||
.handler = source_item_own_get,
|
||||
.data_kinds = (1 << DATA_KIND_PIPE),
|
||||
.media_kinds = MEDIA_KIND_ALL,
|
||||
.cache = ON_SUCCESS | ON_FAILURE,
|
||||
},
|
||||
{
|
||||
.name = NULL,
|
||||
.handler = NULL,
|
||||
@ -343,17 +357,43 @@ static struct artwork_source artwork_item_source[] =
|
||||
}
|
||||
};
|
||||
|
||||
/* List of sources that can provide artwork for a queue item. The source
|
||||
* handlers will be called in the order of this list. Must be terminated by a
|
||||
* NULL struct.
|
||||
*/
|
||||
static struct artwork_source artwork_queue_item_source[] =
|
||||
{
|
||||
{
|
||||
.name = "artwork url",
|
||||
.handler = source_queue_item_artwork_url_get,
|
||||
.cache = NEVER,
|
||||
},
|
||||
{
|
||||
.name = NULL,
|
||||
.handler = NULL,
|
||||
.cache = 0,
|
||||
}
|
||||
};
|
||||
|
||||
/* Forward - parsers of online source responses */
|
||||
static enum parse_result response_jparse_spotify(char **artwork_url, json_object *response, int max_w, int max_h);
|
||||
static enum parse_result response_jparse_discogs(char **artwork_url, json_object *response, int max_w, int max_h);
|
||||
static enum parse_result response_jparse_musicbrainz(char **artwork_url, json_object *response, int max_w, int max_h);
|
||||
|
||||
static int credentials_get_spotify(char **auth_key, char **auth_secret);
|
||||
static int credentials_get_discogs(char **auth_key, char **auth_secret);
|
||||
|
||||
static struct online_search_history search_history_spotify = { .mutex = PTHREAD_MUTEX_INITIALIZER };
|
||||
static struct online_search_history search_history_discogs = { .mutex = PTHREAD_MUTEX_INITIALIZER };
|
||||
static struct online_search_history search_history_musicbrainz = { .mutex = PTHREAD_MUTEX_INITIALIZER };
|
||||
|
||||
/* Definitions of online sources */
|
||||
static struct online_source spotify_source =
|
||||
static const struct online_source spotify_source =
|
||||
{
|
||||
.name = "Spotify",
|
||||
.setting_name = "use_artwork_source_spotify",
|
||||
.auth_header = "Bearer $SECRET$",
|
||||
.credentials_get = credentials_get_spotify,
|
||||
.search_endpoint = "https://api.spotify.com/v1/search",
|
||||
.search_param = "type=track&limit=1&$QUERY$",
|
||||
.query_parts =
|
||||
@ -363,16 +403,15 @@ static struct online_source spotify_source =
|
||||
{ NULL, NULL },
|
||||
},
|
||||
.response_jparse = response_jparse_spotify,
|
||||
.search_history = { .mutex = PTHREAD_MUTEX_INITIALIZER },
|
||||
.search_history = &search_history_spotify,
|
||||
};
|
||||
|
||||
static struct online_source discogs_source =
|
||||
static const struct online_source discogs_source =
|
||||
{
|
||||
.name = "Discogs",
|
||||
.setting_name = "use_artwork_source_discogs",
|
||||
.auth_header = "Discogs key=$KEY$, secret=$SECRET$",
|
||||
.auth_key = "ivzUxlkUiwpptDKpSCHF",
|
||||
.auth_secret = "CYLZyExtlznKCupoIIhTpHVDReLunhUo",
|
||||
.credentials_get = credentials_get_discogs,
|
||||
.search_endpoint = "https://api.discogs.com/database/search",
|
||||
.search_param = "type=release&per_page=1&$QUERY$",
|
||||
.query_parts =
|
||||
@ -383,10 +422,10 @@ static struct online_source discogs_source =
|
||||
{ NULL, NULL },
|
||||
},
|
||||
.response_jparse = response_jparse_discogs,
|
||||
.search_history = { .mutex = PTHREAD_MUTEX_INITIALIZER },
|
||||
.search_history = &search_history_discogs,
|
||||
};
|
||||
|
||||
static struct online_source musicbrainz_source =
|
||||
static const struct online_source musicbrainz_source =
|
||||
{
|
||||
.name = "Musicbrainz",
|
||||
.setting_name = "use_artwork_source_coverartarchive",
|
||||
@ -399,7 +438,7 @@ static struct online_source musicbrainz_source =
|
||||
{ NULL, NULL },
|
||||
},
|
||||
.response_jparse = response_jparse_musicbrainz,
|
||||
.search_history = { .mutex = PTHREAD_MUTEX_INITIALIZER },
|
||||
.search_history = &search_history_musicbrainz,
|
||||
};
|
||||
|
||||
|
||||
@ -442,7 +481,6 @@ artwork_read_byurl(struct evbuffer *evbuf, const char *url)
|
||||
ret = http_client_request(&client, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_ART, "Request to '%s' failed with return value %d\n", url, ret);
|
||||
goto out;
|
||||
}
|
||||
|
||||
@ -574,20 +612,6 @@ size_calculate(int *dst_w, int *dst_h, int src_w, int src_h, int max_w, int max_
|
||||
DPRINTF(E_DBG, L_ART, "Rescale required, destination width %d height %d\n", *dst_w, *dst_h);
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBEVENT2_OLD
|
||||
// This is not how this function is actually defined in libevent 2.1+, but it
|
||||
// works as a less optimal stand-in
|
||||
int
|
||||
evbuffer_add_buffer_reference(struct evbuffer *outbuf, struct evbuffer *inbuf)
|
||||
{
|
||||
uint8_t *buf = evbuffer_pullup(inbuf, -1);
|
||||
if (!buf)
|
||||
return -1;
|
||||
|
||||
return evbuffer_add_reference(outbuf, buf, evbuffer_get_length(inbuf), NULL, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Either gets the artwork file given in "path" (rescaled if needed) or rescales
|
||||
* the artwork given in "inbuf".
|
||||
@ -603,6 +627,8 @@ evbuffer_add_buffer_reference(struct evbuffer *outbuf, struct evbuffer *inbuf)
|
||||
static int
|
||||
artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is_embedded, enum data_kind data_kind, struct artwork_req_params req_params)
|
||||
{
|
||||
struct transcode_decode_setup_args xcode_decode_args = { .profile = XCODE_JPEG }; // Covers XCODE_PNG too
|
||||
struct transcode_encode_setup_args xcode_encode_args = { 0 };
|
||||
struct decode_ctx *xcode_decode = NULL;
|
||||
struct encode_ctx *xcode_encode = NULL;
|
||||
struct transcode_evbuf_io xcode_evbuf_io = { 0 };
|
||||
@ -636,13 +662,16 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is
|
||||
}
|
||||
|
||||
xcode_evbuf_io.evbuf = xcode_buf;
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, NULL, &xcode_evbuf_io, 0); // Covers XCODE_PNG too
|
||||
xcode_decode_args.evbuf_io = &xcode_evbuf_io;
|
||||
xcode_decode_args.is_http = (data_kind == DATA_KIND_HTTP);
|
||||
}
|
||||
else
|
||||
{
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, path, NULL, 0); // Covers XCODE_PNG too
|
||||
xcode_decode_args.path = path;
|
||||
xcode_decode_args.is_http = (data_kind == DATA_KIND_HTTP);
|
||||
}
|
||||
|
||||
xcode_decode = transcode_decode_setup(xcode_decode_args);
|
||||
if (!xcode_decode)
|
||||
{
|
||||
if (path)
|
||||
@ -701,15 +730,19 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is
|
||||
goto out;
|
||||
}
|
||||
|
||||
xcode_encode_args.src_ctx = xcode_decode;
|
||||
xcode_encode_args.width = dst_width;
|
||||
xcode_encode_args.height = dst_height;
|
||||
if (dst_format == ART_FMT_JPEG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_JPEG;
|
||||
else if (dst_format == ART_FMT_PNG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_PNG;
|
||||
else if (dst_format == ART_FMT_VP8)
|
||||
xcode_encode = transcode_encode_setup(XCODE_VP8, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_VP8;
|
||||
else
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_JPEG;
|
||||
|
||||
xcode_encode = transcode_encode_setup(xcode_encode_args);
|
||||
if (!xcode_encode)
|
||||
{
|
||||
if (path)
|
||||
@ -917,7 +950,7 @@ artwork_get_bydir(struct evbuffer *evbuf, char *out_path, size_t len, char *dir,
|
||||
* before making a request. Stashes result in cache, also if negative.
|
||||
*
|
||||
* @out artwork Image data
|
||||
* @in url URL of the artwork
|
||||
* @in url HTTP(S) URL of the artwork
|
||||
* @in req_params Requested max size/format
|
||||
* @return ART_FMT_* on success, ART_E_NONE or ART_E_ERROR
|
||||
*/
|
||||
@ -931,12 +964,18 @@ artwork_get_byurl(struct evbuffer *artwork, const char *url, struct artwork_req_
|
||||
CHECK_NULL(L_ART, raw = evbuffer_new());
|
||||
format = ART_E_ERROR;
|
||||
|
||||
// Accessing the cache is thread safe, the purpose of the lock is to make the
|
||||
// artwork stash more effective if we have parallel requests for the same url.
|
||||
// It will assure that the artwork from the first request is downloaded and
|
||||
// stashed before processing the next request.
|
||||
pthread_mutex_lock(&artwork_cache_stash_mutex);
|
||||
ret = cache_artwork_read(raw, url, &format);
|
||||
if (ret < 0)
|
||||
{
|
||||
format = artwork_read_byurl(raw, url);
|
||||
cache_artwork_stash(raw, url, format);
|
||||
}
|
||||
pthread_mutex_unlock(&artwork_cache_stash_mutex);
|
||||
|
||||
// If we couldn't read, or we have cached a negative result from the last attempt, we stop now
|
||||
if (format <= 0)
|
||||
@ -954,6 +993,43 @@ artwork_get_byurl(struct evbuffer *artwork, const char *url, struct artwork_req_
|
||||
|
||||
/* ------------------------- ONLINE SOURCE HANDLING ----------------------- */
|
||||
|
||||
#ifdef SPOTIFY
|
||||
static int
|
||||
credentials_get_spotify(char **auth_key, char **auth_secret)
|
||||
{
|
||||
struct spotifywebapi_status_info webapi_info;
|
||||
struct spotifywebapi_access_token webapi_token;
|
||||
|
||||
spotifywebapi_status_info_get(&webapi_info);
|
||||
if (!webapi_info.token_valid)
|
||||
return -1; // Not logged in
|
||||
|
||||
spotifywebapi_access_token_get(&webapi_token);
|
||||
if (!webapi_token.token)
|
||||
return -1;
|
||||
|
||||
*auth_key = NULL;
|
||||
*auth_secret = webapi_token.token;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
static int
|
||||
credentials_get_spotify(char **auth_key, char **auth_secret)
|
||||
{
|
||||
*auth_key = NULL;
|
||||
*auth_secret = NULL;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int
|
||||
credentials_get_discogs(char **auth_key, char **auth_secret)
|
||||
{
|
||||
*auth_key = strdup("ivzUxlkUiwpptDKpSCHF");
|
||||
*auth_secret = strdup("CYLZyExtlznKCupoIIhTpHVDReLunhUo");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum parse_result
|
||||
response_jparse_discogs(char **artwork_url, json_object *response, int max_w, int max_h)
|
||||
{
|
||||
@ -1048,7 +1124,7 @@ response_jparse_spotify(char **artwork_url, json_object *response, int max_w, in
|
||||
}
|
||||
|
||||
static enum parse_result
|
||||
online_source_response_parse(char **artwork_url, struct online_source *src, struct evbuffer *response, int max_w, int max_h)
|
||||
online_source_response_parse(char **artwork_url, const struct online_source *src, struct evbuffer *response, int max_w, int max_h)
|
||||
{
|
||||
json_object *jresponse;
|
||||
char *body;
|
||||
@ -1076,7 +1152,7 @@ online_source_response_parse(char **artwork_url, struct online_source *src, stru
|
||||
}
|
||||
|
||||
static int
|
||||
online_source_request_url_make(char *url, size_t url_size, struct online_source *src, struct artwork_ctx *ctx)
|
||||
online_source_search_url_make(char *url, size_t url_size, const struct online_source *src, struct artwork_ctx *ctx)
|
||||
{
|
||||
struct db_queue_item *queue_item;
|
||||
struct keyval query = { 0 };
|
||||
@ -1119,6 +1195,13 @@ online_source_request_url_make(char *url, size_t url_size, struct online_source
|
||||
goto error;
|
||||
}
|
||||
|
||||
if ((artist && strncmp(CFG_NAME_UNKNOWN_ARTIST, artist, strlen(CFG_NAME_UNKNOWN_ARTIST)) == 0) ||
|
||||
(album && strncmp(CFG_NAME_UNKNOWN_ALBUM, album, strlen(CFG_NAME_UNKNOWN_ARTIST)) == 0) )
|
||||
{
|
||||
DPRINTF(E_DBG, L_ART, "Skipping online artwork search for unknown artist/album\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
for (i = 0; src->query_parts[i].key; i++)
|
||||
{
|
||||
if (!album && strstr(src->query_parts[i].template, "$ALBUM$"))
|
||||
@ -1138,14 +1221,17 @@ online_source_request_url_make(char *url, size_t url_size, struct online_source
|
||||
ret = keyval_add(&query, src->query_parts[i].key, param);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_ART, "keyval_add() failed in request_url_make()\n");
|
||||
DPRINTF(E_LOG, L_ART, "keyval_add() failed in online_source_request_url_make()\n");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
encoded_query = http_form_urlencode(&query);
|
||||
if (!encoded_query)
|
||||
goto error;
|
||||
{
|
||||
DPRINTF(E_WARN, L_ART, "URL encoding for online artwork search failed\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
snprintf(url, url_size, "%s?%s", src->search_endpoint, src->search_param);
|
||||
if (safe_snreplace(url, url_size, "$QUERY$", encoded_query) < 0)
|
||||
@ -1168,135 +1254,142 @@ online_source_request_url_make(char *url, size_t url_size, struct online_source
|
||||
}
|
||||
|
||||
static int
|
||||
online_source_search_check_last(char **last_artwork_url, struct online_source *src, uint32_t hash, int max_w, int max_h)
|
||||
online_source_search_check_last(char **last_artwork_url, const struct online_source *src, uint32_t hash, int max_w, int max_h)
|
||||
{
|
||||
struct online_search_history *history = src->search_history;
|
||||
bool is_same;
|
||||
|
||||
pthread_mutex_lock(&src->search_history.mutex);
|
||||
is_same = (hash == history->last_hash) &&
|
||||
(max_w == history->last_max_w) &&
|
||||
(max_h == history->last_max_h);
|
||||
|
||||
is_same = (hash == src->search_history.last_hash) &&
|
||||
(max_w == src->search_history.last_max_w) &&
|
||||
(max_h == src->search_history.last_max_h);
|
||||
|
||||
// Copy this to the caller while we have the lock anyway
|
||||
if (is_same)
|
||||
*last_artwork_url = safe_strdup(src->search_history.last_artwork_url);
|
||||
|
||||
pthread_mutex_unlock(&src->search_history.mutex);
|
||||
*last_artwork_url = safe_strdup(history->last_artwork_url);
|
||||
|
||||
return is_same ? 0 : -1;
|
||||
}
|
||||
|
||||
static bool
|
||||
online_source_is_failing(struct online_source *src, int id)
|
||||
online_source_is_failing(const struct online_source *src, int id)
|
||||
{
|
||||
struct online_search_history *history = src->search_history;
|
||||
bool is_failing;
|
||||
|
||||
pthread_mutex_lock(&src->search_history.mutex);
|
||||
|
||||
// If the last request was more than ONLINE_SEARCH_COOLDOWN_TIME ago we will always try again
|
||||
if (time(NULL) > src->search_history.last_timestamp + ONLINE_SEARCH_COOLDOWN_TIME)
|
||||
if (time(NULL) > history->last_timestamp + ONLINE_SEARCH_COOLDOWN_TIME)
|
||||
is_failing = false;
|
||||
// We won't try again if the source was not replying as expected
|
||||
else if (src->search_history.last_response_code != HTTP_OK)
|
||||
else if (history->last_response_code != HTTP_OK)
|
||||
is_failing = true;
|
||||
// The playback source has changed since the last search, let's give it a chance
|
||||
// (internet streams can feed us with garbage search metadata, but will not change id)
|
||||
else if (id != src->search_history.last_id)
|
||||
else if (id != history->last_id)
|
||||
is_failing = false;
|
||||
// We allow up to ONLINE_SEARCH_FAILURES_MAX for the same track id before declaring failure
|
||||
else if (src->search_history.count_failures < ONLINE_SEARCH_FAILURES_MAX)
|
||||
else if (history->count_failures < ONLINE_SEARCH_FAILURES_MAX)
|
||||
is_failing = false;
|
||||
else
|
||||
is_failing = true;
|
||||
|
||||
pthread_mutex_unlock(&src->search_history.mutex);
|
||||
|
||||
return is_failing;
|
||||
}
|
||||
|
||||
static void
|
||||
online_source_history_update(struct online_source *src, int id, uint32_t request_hash, int response_code, const char *artwork_url)
|
||||
online_source_history_update(const struct online_source *src, int id, uint32_t request_hash, int response_code, const char *artwork_url, int max_w, int max_h)
|
||||
{
|
||||
pthread_mutex_lock(&src->search_history.mutex);
|
||||
struct online_search_history *history = src->search_history;
|
||||
|
||||
src->search_history.last_id = id;
|
||||
src->search_history.last_hash = request_hash;
|
||||
src->search_history.last_response_code = response_code;
|
||||
src->search_history.last_timestamp = time(NULL);
|
||||
history->last_id = id;
|
||||
history->last_hash = request_hash;
|
||||
history->last_response_code = response_code;
|
||||
history->last_timestamp = time(NULL);
|
||||
history->last_max_w = max_w;
|
||||
history->last_max_h = max_h;
|
||||
|
||||
free(src->search_history.last_artwork_url);
|
||||
src->search_history.last_artwork_url = safe_strdup(artwork_url); // FIXME should free this on exit
|
||||
free(history->last_artwork_url);
|
||||
history->last_artwork_url = safe_strdup(artwork_url); // FIXME should free this on exit
|
||||
|
||||
if (artwork_url)
|
||||
src->search_history.count_failures = 0;
|
||||
history->count_failures = 0;
|
||||
else
|
||||
src->search_history.count_failures++;
|
||||
history->count_failures++;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&src->search_history.mutex);
|
||||
static int
|
||||
auth_header_add(struct keyval *headers, const struct online_source *src)
|
||||
{
|
||||
char auth_header[512];
|
||||
char *auth_key;
|
||||
char *auth_secret;
|
||||
int ret;
|
||||
|
||||
if (!src->auth_header)
|
||||
return 0; // Nothing to do
|
||||
|
||||
ret = src->credentials_get(&auth_key, &auth_secret);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
snprintf(auth_header, sizeof(auth_header), "%s", src->auth_header);
|
||||
ret = ((safe_snreplace(auth_header, sizeof(auth_header), "$KEY$", auth_key) < 0) ||
|
||||
(safe_snreplace(auth_header, sizeof(auth_header), "$SECRET$", auth_secret) < 0));
|
||||
|
||||
free(auth_key);
|
||||
free(auth_secret);
|
||||
|
||||
if (ret)
|
||||
{
|
||||
DPRINTF(E_WARN, L_ART, "Cannot make request for online artwork, auth header is too long\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
keyval_add(headers, "Authorization", auth_header);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *
|
||||
online_source_search(struct online_source *src, struct artwork_ctx *ctx)
|
||||
online_source_artwork_url_get(const char *search_url, const struct online_source *src, int id, int max_w, int max_h)
|
||||
{
|
||||
char *artwork_url;
|
||||
struct http_client_ctx client = { 0 };
|
||||
struct keyval output_headers = { 0 };
|
||||
uint32_t hash;
|
||||
char url[2048];
|
||||
char auth_header[256];
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Trying %s for %s\n", src->name, ctx->dbmfi->path);
|
||||
|
||||
ret = online_source_request_url_make(url, sizeof(url), src, ctx);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_ART, "Skipping artwork source %s, could not construct a request URL\n", src->name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Be nice to our peer + improve response times by not repeating search requests
|
||||
hash = djb_hash(url, strlen(url));
|
||||
ret = online_source_search_check_last(&artwork_url, src, hash, ctx->req_params.max_w, ctx->req_params.max_h);
|
||||
hash = djb_hash(search_url, strlen(search_url));
|
||||
ret = online_source_search_check_last(&artwork_url, src, hash, max_w, max_h);
|
||||
if (ret == 0)
|
||||
{
|
||||
return artwork_url; // Will be NULL if we are repeating a search that failed
|
||||
}
|
||||
|
||||
// If our recent searches have been futile we may give the source a break
|
||||
if (online_source_is_failing(src, ctx->id))
|
||||
if (online_source_is_failing(src, id))
|
||||
{
|
||||
DPRINTF(E_DBG, L_ART, "Skipping artwork source %s, too many failed requests\n", src->name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (src->auth_header)
|
||||
ret = auth_header_add(&output_headers, src);
|
||||
if (ret < 0)
|
||||
{
|
||||
snprintf(auth_header, sizeof(auth_header), "%s", src->auth_header);
|
||||
if ((safe_snreplace(auth_header, sizeof(auth_header), "$KEY$", src->auth_key) < 0) ||
|
||||
(safe_snreplace(auth_header, sizeof(auth_header), "$SECRET$", src->auth_secret) < 0))
|
||||
{
|
||||
DPRINTF(E_WARN, L_ART, "Cannot make request for online artwork, auth header is too long\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
keyval_add(&output_headers, "Authorization", auth_header);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CHECK_NULL(L_ART, client.input_body = evbuffer_new());
|
||||
client.url = url;
|
||||
client.url = search_url;
|
||||
client.output_headers = &output_headers;
|
||||
|
||||
ret = http_client_request(&client, NULL);
|
||||
keyval_clear(&output_headers);
|
||||
if (ret < 0 || client.response_code != HTTP_OK)
|
||||
{
|
||||
DPRINTF(E_WARN, L_ART, "Artwork request to '%s' failed, response code %d\n", url, client.response_code);
|
||||
DPRINTF(E_WARN, L_ART, "Artwork request to '%s' failed, response code %d\n", search_url, client.response_code);
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = online_source_response_parse(&artwork_url, src, client.input_body, ctx->req_params.max_w, ctx->req_params.max_h);
|
||||
ret = online_source_response_parse(&artwork_url, src, client.input_body, max_w, max_h);
|
||||
if (ret == ONLINE_SOURCE_PARSE_NOT_FOUND)
|
||||
DPRINTF(E_DBG, L_ART, "No image tag found in response from source '%s'\n", src->name);
|
||||
else if (ret == ONLINE_SOURCE_PARSE_INVALID)
|
||||
@ -1309,18 +1402,43 @@ online_source_search(struct online_source *src, struct artwork_ctx *ctx)
|
||||
if (ret != ONLINE_SOURCE_PARSE_OK)
|
||||
goto error;
|
||||
|
||||
online_source_history_update(src, ctx->id, hash, client.response_code, artwork_url);
|
||||
online_source_history_update(src, id, hash, client.response_code, artwork_url, max_w, max_h);
|
||||
evbuffer_free(client.input_body);
|
||||
return artwork_url;
|
||||
|
||||
error:
|
||||
online_source_history_update(src, ctx->id, hash, client.response_code, NULL);
|
||||
online_source_history_update(src, id, hash, client.response_code, NULL, max_w, max_h);
|
||||
evbuffer_free(client.input_body);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *
|
||||
online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
|
||||
{
|
||||
struct online_search_history *history = src->search_history;
|
||||
char search_url[2048];
|
||||
char *artwork_url;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Trying %s for %s\n", src->name, ctx->dbmfi->path);
|
||||
|
||||
ret = online_source_search_url_make(search_url, sizeof(search_url), src, ctx);
|
||||
if (ret < 0)
|
||||
return NULL;
|
||||
|
||||
// The protection against flooding the online source with requests requires
|
||||
// that online_source_request() isn't called in parallel
|
||||
pthread_mutex_lock(&history->mutex);
|
||||
|
||||
artwork_url = online_source_artwork_url_get(search_url, src, ctx->id, ctx->req_params.max_w, ctx->req_params.max_h);
|
||||
|
||||
pthread_mutex_unlock(&history->mutex);
|
||||
|
||||
return artwork_url;
|
||||
}
|
||||
|
||||
static bool
|
||||
online_source_is_enabled(struct online_source *src)
|
||||
online_source_is_enabled(const struct online_source *src)
|
||||
{
|
||||
struct settings_category *category;
|
||||
bool enabled;
|
||||
@ -1511,29 +1629,23 @@ source_item_own_get(struct artwork_ctx *ctx)
|
||||
return artwork_get(ctx->evbuf, path, NULL, false, ctx->data_kind, ctx->req_params);
|
||||
}
|
||||
|
||||
/*
|
||||
* Downloads the artwork from the location pointed to by queue_item->artwork_url
|
||||
*/
|
||||
static int
|
||||
source_item_artwork_url_get(struct artwork_ctx *ctx)
|
||||
{
|
||||
struct db_queue_item *queue_item;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Trying artwork url for %s\n", ctx->dbmfi->path);
|
||||
|
||||
queue_item = db_queue_fetch_byfileid(ctx->id);
|
||||
if (!queue_item || !queue_item->artwork_url)
|
||||
if (ctx->queue_item)
|
||||
{
|
||||
free_queue_item(queue_item, 0);
|
||||
return ART_E_NONE;
|
||||
ret = source_queue_item_artwork_url_get(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx->queue_item = db_queue_fetch_byfileid(ctx->id);
|
||||
ret = source_queue_item_artwork_url_get(ctx);
|
||||
free_queue_item(ctx->queue_item, 0);
|
||||
}
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, queue_item->artwork_url, ctx->req_params);
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", queue_item->artwork_url);
|
||||
|
||||
free_queue_item(queue_item, 0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@ -1547,17 +1659,22 @@ static int
|
||||
source_item_pipe_get(struct artwork_ctx *ctx)
|
||||
{
|
||||
struct db_queue_item *queue_item;
|
||||
const char *proto = "file:";
|
||||
const char *proto_file = "file:";
|
||||
bool is_file;
|
||||
char *path;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Trying pipe metadata from %s.metadata\n", ctx->dbmfi->path);
|
||||
|
||||
queue_item = db_queue_fetch_byfileid(ctx->id);
|
||||
if (!queue_item || !queue_item->artwork_url || strncmp(queue_item->artwork_url, proto, strlen(proto)) != 0)
|
||||
if (!queue_item || !queue_item->artwork_url)
|
||||
goto notfound;
|
||||
|
||||
path = queue_item->artwork_url + strlen(proto);
|
||||
is_file = (strncmp(queue_item->artwork_url, proto_file, strlen(proto_file)) == 0);
|
||||
if (!is_file)
|
||||
goto notfound;
|
||||
|
||||
path = queue_item->artwork_url + strlen(proto_file);
|
||||
|
||||
// Sometimes the file has been replaced, but queue_item->artwork_url hasn't
|
||||
// been updated yet. In that case just stop now.
|
||||
@ -1642,26 +1759,13 @@ source_item_spotifywebapi_track_get(struct artwork_ctx *ctx)
|
||||
static int
|
||||
source_item_spotifywebapi_search_get(struct artwork_ctx *ctx)
|
||||
{
|
||||
struct spotifywebapi_status_info webapi_info;
|
||||
struct spotifywebapi_access_token webapi_token;
|
||||
char *url;
|
||||
int ret;
|
||||
|
||||
if (!online_source_is_enabled(&spotify_source))
|
||||
return ART_E_NONE;
|
||||
|
||||
spotifywebapi_status_info_get(&webapi_info);
|
||||
if (!webapi_info.token_valid)
|
||||
return ART_E_NONE; // Not logged in
|
||||
|
||||
spotifywebapi_access_token_get(&webapi_token);
|
||||
if (!webapi_token.token)
|
||||
return ART_E_ERROR;
|
||||
|
||||
spotify_source.auth_secret = webapi_token.token;
|
||||
|
||||
url = online_source_search(&spotify_source, ctx);
|
||||
free(webapi_token.token);
|
||||
if (!url)
|
||||
return ART_E_NONE;
|
||||
|
||||
@ -1754,11 +1858,46 @@ source_item_ownpl_get(struct artwork_ctx *ctx)
|
||||
return format;
|
||||
}
|
||||
|
||||
/*
|
||||
* Downloads artwork from ctx->queue_item->artwork_url
|
||||
*/
|
||||
static int
|
||||
source_queue_item_artwork_url_get(struct artwork_ctx *ctx)
|
||||
{
|
||||
const char *proto_http = "http:";
|
||||
const char *proto_https = "https:";
|
||||
const char *artwork_url;
|
||||
bool is_http;
|
||||
bool is_https;
|
||||
int ret;
|
||||
|
||||
if (!ctx->queue_item || !ctx->queue_item->artwork_url)
|
||||
goto notfound;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Trying artwork url for %s\n", ctx->queue_item->path);
|
||||
|
||||
artwork_url = ctx->queue_item->artwork_url;
|
||||
|
||||
is_http = (strncmp(artwork_url, proto_http, strlen(proto_http)) == 0);
|
||||
is_https = (strncmp(artwork_url, proto_https, strlen(proto_https)) == 0);
|
||||
if (!is_http && !is_https)
|
||||
goto notfound;
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, artwork_url, ctx->req_params);
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", artwork_url);
|
||||
|
||||
return ret;
|
||||
|
||||
notfound:
|
||||
return ART_E_NONE;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------- SOURCE PROCESSING --------------------------- */
|
||||
|
||||
static int
|
||||
process_items(struct artwork_ctx *ctx, int item_mode)
|
||||
process_file_items(struct artwork_ctx *ctx, int item_mode)
|
||||
{
|
||||
struct db_media_file_info dbmfi;
|
||||
int i;
|
||||
@ -1902,16 +2041,55 @@ process_group(struct artwork_ctx *ctx)
|
||||
}
|
||||
|
||||
invalid_group:
|
||||
return process_items(ctx, 0);
|
||||
return process_file_items(ctx, 0);
|
||||
}
|
||||
|
||||
static int
|
||||
process_queue_item(struct artwork_ctx *ctx)
|
||||
{
|
||||
const char *source_name;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
for (i = 0; artwork_queue_item_source[i].handler; i++)
|
||||
{
|
||||
// If just one handler says we should not cache a negative result then we obey that
|
||||
if ((artwork_queue_item_source[i].cache & ON_FAILURE) == 0)
|
||||
ctx->cache = NEVER;
|
||||
|
||||
source_name = artwork_queue_item_source[i].name;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Checking queue item source '%s'\n", source_name);
|
||||
|
||||
ret = artwork_queue_item_source[i].handler(ctx);
|
||||
if (ret > 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_ART, "Artwork for queue item '%s' found in source '%s'\n", ctx->queue_item->title, source_name);
|
||||
ctx->cache = artwork_queue_item_source[i].cache;
|
||||
return ret;
|
||||
}
|
||||
else if (ret == ART_E_ABORT)
|
||||
{
|
||||
DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for queue item '%s'\n", source_name, ctx->queue_item->title);
|
||||
ctx->cache = NEVER;
|
||||
}
|
||||
else if (ret == ART_E_ERROR)
|
||||
{
|
||||
DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for queue item '%s'\n", source_name, ctx->queue_item->title);
|
||||
ctx->cache = NEVER;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------ ARTWORK API ------------------------------ */
|
||||
|
||||
int
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
|
||||
artwork_get_by_file_id(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
|
||||
{
|
||||
struct artwork_ctx ctx;
|
||||
struct artwork_ctx ctx = { 0 };
|
||||
char filter[32];
|
||||
int ret;
|
||||
|
||||
@ -1920,8 +2098,6 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int forma
|
||||
if (id == DB_MEDIA_FILE_NON_PERSISTENT_ID)
|
||||
return -1;
|
||||
|
||||
memset(&ctx, 0, sizeof(struct artwork_ctx));
|
||||
|
||||
ctx.qp.type = Q_ITEMS;
|
||||
ctx.qp.filter = filter;
|
||||
ctx.evbuf = evbuf;
|
||||
@ -1940,7 +2116,7 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int forma
|
||||
|
||||
// Note: process_items will set ctx.persistentid for the following process_group()
|
||||
// - and do nothing else if artwork_individual is not configured by user
|
||||
ret = process_items(&ctx, 1);
|
||||
ret = process_file_items(&ctx, 1);
|
||||
if (ret > 0)
|
||||
{
|
||||
if (ctx.cache & ON_SUCCESS)
|
||||
@ -1970,15 +2146,13 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int forma
|
||||
}
|
||||
|
||||
int
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
|
||||
artwork_get_by_group_id(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
|
||||
{
|
||||
struct artwork_ctx ctx;
|
||||
struct artwork_ctx ctx = { 0 };
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_ART, "Artwork request for group %d (max_w=%d, max_h=%d)\n", id, max_w, max_h);
|
||||
|
||||
memset(&ctx, 0, sizeof(struct artwork_ctx));
|
||||
|
||||
/* Get the persistent id for the given group id */
|
||||
ret = db_group_persistentid_byid(id, &ctx.persistentid);
|
||||
if (ret < 0)
|
||||
@ -2013,6 +2187,48 @@ artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h, int form
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
artwork_get_by_queue_item_id(struct evbuffer *evbuf, int item_id, int max_w, int max_h, int format)
|
||||
{
|
||||
struct artwork_ctx ctx = { 0 };
|
||||
struct db_queue_item *queue_item;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_ART, "Artwork request for queue item %d (max_w=%d, max_h=%d)\n", item_id, max_w, max_h);
|
||||
|
||||
queue_item = db_queue_fetch_byitemid(item_id);
|
||||
if (!queue_item)
|
||||
return -1;
|
||||
|
||||
if (queue_item->file_id != DB_MEDIA_FILE_NON_PERSISTENT_ID)
|
||||
{
|
||||
ret = artwork_get_by_file_id(evbuf, queue_item->file_id, max_w, max_h, format);
|
||||
free_queue_item(queue_item, 0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ctx.queue_item = queue_item;
|
||||
ctx.evbuf = evbuf;
|
||||
ctx.req_params.max_w = max_w;
|
||||
ctx.req_params.max_h = max_h;
|
||||
ctx.req_params.format = format;
|
||||
ctx.cache = ON_FAILURE;
|
||||
|
||||
ret = process_queue_item(&ctx);
|
||||
if (ret > 0)
|
||||
{
|
||||
// No caching of queue item artwork implemented as of yet
|
||||
free_queue_item(queue_item, 0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_ART, "No artwork found for queue item %d\n", item_id);
|
||||
|
||||
free_queue_item(queue_item, 0);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Checks if the file is an artwork file */
|
||||
bool
|
||||
artwork_file_is_artwork(const char *filename)
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
#include <stdbool.h>
|
||||
|
||||
/*
|
||||
* Get the artwork image for an individual item (track)
|
||||
* Get the artwork image for an individual library item (track)
|
||||
*
|
||||
* @out evbuf Event buffer that will contain the (scaled) image
|
||||
* @in id The mfi item id
|
||||
@ -23,7 +23,7 @@
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int format);
|
||||
artwork_get_by_file_id(struct evbuffer *evbuf, int id, int max_w, int max_h, int format);
|
||||
|
||||
/*
|
||||
* Get the artwork image for a group (an album or an artist)
|
||||
@ -36,7 +36,21 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int forma
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h, int format);
|
||||
artwork_get_by_group_id(struct evbuffer *evbuf, int id, int max_w, int max_h, int format);
|
||||
|
||||
/*
|
||||
* Get the artwork image for a queue item. If the queue item is in the library,
|
||||
* this will return the same as artwork_get_by_file_id
|
||||
*
|
||||
* @out evbuf Event buffer that will contain the (scaled) image
|
||||
* @in item_id The queue item id
|
||||
* @in max_w Requested maximum image width (may not be obeyed)
|
||||
* @in max_h Requested maximum image height (may not be obeyed)
|
||||
* @in format Requested format (may not be obeyed), 0 for default
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_by_queue_item_id(struct evbuffer *evbuf, int item_id, int max_w, int max_h, int format);
|
||||
|
||||
/*
|
||||
* Checks if the file is an artwork file (based on user config)
|
||||
|
||||
1382
src/cache.c
1382
src/cache.c
File diff suppressed because it is too large
Load Diff
17
src/cache.h
17
src/cache.h
@ -4,7 +4,7 @@
|
||||
|
||||
#include <event2/buffer.h>
|
||||
|
||||
/* ---------------------------- DAAP cache API --------------------------- */
|
||||
/* ----------------------------- DAAP cache API ---------------------------- */
|
||||
|
||||
void
|
||||
cache_daap_suspend(void);
|
||||
@ -19,10 +19,19 @@ void
|
||||
cache_daap_add(const char *query, const char *ua, int is_remote, int msec);
|
||||
|
||||
int
|
||||
cache_daap_threshold(void);
|
||||
cache_daap_threshold_get(void);
|
||||
|
||||
|
||||
/* ---------------------------- Artwork cache API --------------------------- */
|
||||
/* --------------------------- Transcode cache API ------------------------- */
|
||||
|
||||
int
|
||||
cache_xcode_header_get(struct evbuffer *evbuf, int *cached, uint32_t id, const char *format);
|
||||
|
||||
int
|
||||
cache_xcode_toggle(bool enable);
|
||||
|
||||
|
||||
/* ---------------------------- Artwork cache API -------------------------- */
|
||||
|
||||
#define CACHE_ARTWORK_GROUP 0
|
||||
#define CACHE_ARTWORK_INDIVIDUAL 1
|
||||
@ -48,7 +57,7 @@ cache_artwork_stash(struct evbuffer *evbuf, const char *path, int format);
|
||||
int
|
||||
cache_artwork_read(struct evbuffer *evbuf, const char *path, int *format);
|
||||
|
||||
/* ---------------------------- Cache API --------------------------- */
|
||||
/* ------------------------------- Cache API ------------------------------- */
|
||||
|
||||
int
|
||||
cache_init(void);
|
||||
|
||||
@ -63,8 +63,8 @@ command_cb_async(struct commands_base *cmdbase, struct command *cmd)
|
||||
// Command is executed asynchronously
|
||||
cmdstate = cmd->func(cmd->arg, &cmd->ret);
|
||||
|
||||
// Only free arg if there are no pending events (used in worker.c)
|
||||
if (cmdstate != COMMAND_PENDING && cmd->arg)
|
||||
// Only free arg if there are no pending events (used in httpd.c)
|
||||
if (cmdstate != COMMAND_PENDING)
|
||||
free(cmd->arg);
|
||||
|
||||
free(cmd);
|
||||
|
||||
@ -49,13 +49,15 @@ static cfg_opt_t sec_general[] =
|
||||
CFG_STR("db_backup_path", NULL, CFGF_NONE),
|
||||
CFG_STR("logfile", STATEDIR "/log/" PACKAGE ".log", CFGF_NONE),
|
||||
CFG_INT_CB("loglevel", E_LOG, CFGF_NONE, &cb_loglevel),
|
||||
CFG_STR("logformat", "default", CFGF_NONE),
|
||||
CFG_STR("admin_password", NULL, CFGF_NONE),
|
||||
CFG_INT("websocket_port", 3688, CFGF_NONE),
|
||||
CFG_STR("websocket_interface", NULL, CFGF_NONE),
|
||||
CFG_STR_LIST("trusted_networks", "{localhost,192.168,fd}", CFGF_NONE),
|
||||
CFG_BOOL("ipv6", cfg_true, CFGF_NONE),
|
||||
CFG_STR_LIST("trusted_networks", "{lan}", CFGF_NONE),
|
||||
CFG_BOOL("ipv6", cfg_false, CFGF_NONE),
|
||||
CFG_STR("bind_address", NULL, CFGF_NONE),
|
||||
CFG_STR("cache_path", STATEDIR "/cache/" PACKAGE "/cache.db", CFGF_NONE),
|
||||
CFG_STR("cache_dir", STATEDIR "/cache/" PACKAGE, CFGF_NONE),
|
||||
CFG_STR("cache_path", NULL, CFGF_DEPRECATED),
|
||||
CFG_INT("cache_daap_threshold", 1000, CFGF_NONE),
|
||||
CFG_BOOL("speaker_autoselect", cfg_false, CFGF_NONE),
|
||||
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
|
||||
@ -67,6 +69,9 @@ static cfg_opt_t sec_general[] =
|
||||
CFG_INT("db_pragma_cache_size", -1, CFGF_NONE),
|
||||
CFG_STR("db_pragma_journal_mode", NULL, CFGF_NONE),
|
||||
CFG_INT("db_pragma_synchronous", -1, CFGF_NONE),
|
||||
CFG_STR("cache_daap_filename", "daap.db", CFGF_NONE),
|
||||
CFG_STR("cache_artwork_filename", "artwork.db", CFGF_NONE),
|
||||
CFG_STR("cache_xcode_filename", "xcode.db", CFGF_NONE),
|
||||
CFG_STR("allow_origin", "*", CFGF_NONE),
|
||||
CFG_STR("user_agent", PACKAGE_NAME "/" PACKAGE_VERSION, CFGF_NONE),
|
||||
CFG_BOOL("ssl_verifypeer", cfg_true, CFGF_NONE),
|
||||
@ -111,13 +116,20 @@ static cfg_opt_t sec_library[] =
|
||||
CFG_BOOL("itunes_smartpl", cfg_false, CFGF_NONE),
|
||||
CFG_STR_LIST("no_decode", NULL, CFGF_NONE),
|
||||
CFG_STR_LIST("force_decode", NULL, CFGF_NONE),
|
||||
CFG_STR("prefer_format", NULL, CFGF_NONE),
|
||||
CFG_BOOL("pipe_autostart", cfg_true, CFGF_NONE),
|
||||
CFG_INT("pipe_sample_rate", 44100, CFGF_NONE),
|
||||
CFG_INT("pipe_bits_per_sample", 16, CFGF_NONE),
|
||||
CFG_BOOL("rating_updates", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("read_rating", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("write_rating", cfg_false, CFGF_NONE),
|
||||
CFG_INT("max_rating", 100, CFGF_NONE),
|
||||
CFG_BOOL("allow_modifying_stored_playlists", cfg_false, CFGF_NONE),
|
||||
CFG_STR("default_playlist_directory", NULL, CFGF_NONE),
|
||||
CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("only_first_genre", cfg_false, CFGF_NONE),
|
||||
CFG_STR_LIST("decode_audio_filters", NULL, CFGF_NONE),
|
||||
CFG_STR_LIST("decode_video_filters", NULL, CFGF_NONE),
|
||||
CFG_END()
|
||||
};
|
||||
|
||||
@ -153,6 +165,7 @@ static cfg_opt_t sec_airplay_shared[] =
|
||||
{
|
||||
CFG_INT("control_port", 0, CFGF_NONE),
|
||||
CFG_INT("timing_port", 0, CFGF_NONE),
|
||||
CFG_BOOL("uncompressed_alac", cfg_false, CFGF_NONE),
|
||||
CFG_END()
|
||||
};
|
||||
|
||||
@ -205,6 +218,12 @@ static cfg_opt_t sec_spotify[] =
|
||||
CFG_BOOL("base_playlist_disable", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("artist_override", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("album_override", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("disable_legacy_mode", cfg_false, CFGF_NONE),
|
||||
// Issued by Spotify on developer.spotify.com for forked-daapd/OwnTone
|
||||
CFG_STR("webapi_client_id", "0e684a5422384114a8ae7ac020f01789", CFGF_NONE),
|
||||
CFG_STR("webapi_client_secret", "232af95f39014c9ba218285a5c11a239", CFGF_NONE),
|
||||
// Must be in allow-list on developer.spotify.com for forked-daapd/OwnTone
|
||||
CFG_STR("redirect_uri", "https://owntone.github.io/owntone-oauth/spotify/", CFGF_NONE),
|
||||
CFG_END()
|
||||
};
|
||||
|
||||
@ -226,6 +245,7 @@ static cfg_opt_t sec_mpd[] =
|
||||
{
|
||||
CFG_INT("port", 6600, CFGF_NONE),
|
||||
CFG_INT("http_port", 0, CFGF_NONE),
|
||||
CFG_BOOL("enable_httpd_plugin", cfg_false, CFGF_NONE),
|
||||
CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NODEFAULT | CFGF_DEPRECATED),
|
||||
CFG_BOOL("allow_modifying_stored_playlists", cfg_false, CFGF_NODEFAULT | CFGF_DEPRECATED),
|
||||
CFG_STR("default_playlist_directory", NULL, CFGF_NODEFAULT | CFGF_DEPRECATED),
|
||||
@ -304,6 +324,31 @@ cb_loglevel(cfg_t *config, cfg_opt_t *opt, const char *value, void *result)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Makes sure cache_dir ends with a slash
|
||||
static int
|
||||
sanitize_cache_dir(cfg_t *general)
|
||||
{
|
||||
char *dir;
|
||||
const char *s;
|
||||
char *appended;
|
||||
size_t len;
|
||||
|
||||
dir = cfg_getstr(general, "cache_dir");
|
||||
len = strlen(dir);
|
||||
|
||||
s = strrchr(dir, '/');
|
||||
if (s && (s + 1 == dir + len))
|
||||
return 0;
|
||||
|
||||
appended = safe_asprintf("%s/", dir);
|
||||
|
||||
cfg_setstr(general, "cache_dir", appended);
|
||||
|
||||
free(appended);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
conffile_expand_libname(cfg_t *lib)
|
||||
{
|
||||
@ -417,7 +462,6 @@ conffile_expand_libname(cfg_t *lib)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
conffile_load(char *file)
|
||||
{
|
||||
@ -458,6 +502,14 @@ conffile_load(char *file)
|
||||
runas_uid = pw->pw_uid;
|
||||
runas_gid = pw->pw_gid;
|
||||
|
||||
ret = sanitize_cache_dir(cfg_getsec(cfg, "general"));
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_CONF, "Invalid configuration of cache_dir\n");
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
lib = cfg_getsec(cfg, "library");
|
||||
|
||||
if (cfg_size(lib, "directories") == 0)
|
||||
|
||||
312
src/db.c
312
src/db.c
@ -102,6 +102,7 @@ enum fixup_type {
|
||||
};
|
||||
|
||||
struct db_unlock {
|
||||
char thread_name_tid[32];
|
||||
int proceed;
|
||||
pthread_cond_t cond;
|
||||
pthread_mutex_t lck;
|
||||
@ -187,7 +188,7 @@ static const struct col_type_map mfi_cols_map[] =
|
||||
{ "song_length", mfi_offsetof(song_length), DB_TYPE_INT },
|
||||
{ "file_size", mfi_offsetof(file_size), DB_TYPE_INT64 },
|
||||
{ "year", mfi_offsetof(year), DB_TYPE_INT },
|
||||
{ "date_released", mfi_offsetof(date_released), DB_TYPE_INT },
|
||||
{ "date_released", mfi_offsetof(date_released), DB_TYPE_INT64 },
|
||||
{ "track", mfi_offsetof(track), DB_TYPE_INT },
|
||||
{ "total_tracks", mfi_offsetof(total_tracks), DB_TYPE_INT },
|
||||
{ "disc", mfi_offsetof(disc), DB_TYPE_INT },
|
||||
@ -230,6 +231,7 @@ static const struct col_type_map mfi_cols_map[] =
|
||||
{ "channels", mfi_offsetof(channels), DB_TYPE_INT },
|
||||
{ "usermark", mfi_offsetof(usermark), DB_TYPE_INT },
|
||||
{ "scan_kind", mfi_offsetof(scan_kind), DB_TYPE_INT },
|
||||
{ "lyrics", mfi_offsetof(lyrics), DB_TYPE_STRING },
|
||||
};
|
||||
|
||||
/* This list must be kept in sync with
|
||||
@ -371,6 +373,7 @@ static const ssize_t dbmfi_cols_map[] =
|
||||
dbmfi_offsetof(channels),
|
||||
dbmfi_offsetof(usermark),
|
||||
dbmfi_offsetof(scan_kind),
|
||||
dbmfi_offsetof(lyrics),
|
||||
};
|
||||
|
||||
/* This list must be kept in sync with
|
||||
@ -650,6 +653,7 @@ db_pl_type_label(enum pl_type pl_type)
|
||||
struct rng_ctx shuffle_rng;
|
||||
|
||||
static char *db_path;
|
||||
static char *db_sqlite_ext_path;
|
||||
static bool db_rating_updates;
|
||||
|
||||
static __thread sqlite3 *hdl;
|
||||
@ -777,6 +781,7 @@ free_mfi(struct media_file_info *mfi, int content_only)
|
||||
free(mfi->composer_sort);
|
||||
free(mfi->album_artist_sort);
|
||||
free(mfi->virtual_path);
|
||||
free(mfi->lyrics);
|
||||
|
||||
if (!content_only)
|
||||
free(mfi);
|
||||
@ -841,6 +846,7 @@ free_query_params(struct query_params *qp, int content_only)
|
||||
free(qp->filter);
|
||||
free(qp->having);
|
||||
free(qp->order);
|
||||
free(qp->group);
|
||||
|
||||
if (!content_only)
|
||||
free(qp);
|
||||
@ -1135,7 +1141,7 @@ fixup_defaults(char **tag, enum fixup_type fixup, struct fixup_ctx *ctx)
|
||||
|
||||
case DB_FIXUP_SONGALBUMID:
|
||||
if (ctx->mfi && ctx->mfi->songalbumid == 0)
|
||||
ctx->mfi->songalbumid = two_str_hash(ctx->mfi->album_artist, ctx->mfi->album);
|
||||
ctx->mfi->songalbumid = two_str_hash(ctx->mfi->album_artist, ctx->mfi->album) + ctx->mfi->data_kind;
|
||||
break;
|
||||
|
||||
case DB_FIXUP_TITLE:
|
||||
@ -1441,6 +1447,7 @@ unlock_notify_cb(void **args, int nargs)
|
||||
{
|
||||
u = (struct db_unlock *)args[i];
|
||||
|
||||
DPRINTF(E_DBG, L_DB, "Notify DB unlock, thread: %s\n", u->thread_name_tid);
|
||||
CHECK_ERR(L_DB, pthread_mutex_lock(&u->lck));
|
||||
|
||||
u->proceed = 1;
|
||||
@ -1456,6 +1463,8 @@ db_wait_unlock(void)
|
||||
struct db_unlock u;
|
||||
int ret;
|
||||
|
||||
thread_getnametid(u.thread_name_tid, sizeof(u.thread_name_tid));
|
||||
|
||||
u.proceed = 0;
|
||||
CHECK_ERR(L_DB, mutex_init(&u.lck));
|
||||
CHECK_ERR(L_DB, pthread_cond_init(&u.cond, NULL));
|
||||
@ -1472,7 +1481,7 @@ db_wait_unlock(void)
|
||||
}
|
||||
|
||||
CHECK_ERR(L_DB, pthread_mutex_unlock(&u.lck));
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_ERR(L_DB, pthread_cond_destroy(&u.cond));
|
||||
CHECK_ERR(L_DB, pthread_mutex_destroy(&u.lck));
|
||||
@ -1966,6 +1975,8 @@ db_free_query_clause(struct query_clause *qc)
|
||||
free(qc);
|
||||
}
|
||||
|
||||
// Builds the generic parts of the query. Parts that are specific to the query
|
||||
// type are in db_build_query_* implementations.
|
||||
static struct query_clause *
|
||||
db_build_query_clause(struct query_params *qp)
|
||||
{
|
||||
@ -2073,8 +2084,21 @@ db_build_query_items(struct query_params *qp, struct query_clause *qc)
|
||||
char *count;
|
||||
char *query;
|
||||
|
||||
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s;", qc->where);
|
||||
query = sqlite3_mprintf("SELECT f.* FROM files f %s %s %s %s;", qc->where, qc->group, qc->order, qc->index);
|
||||
if (qp->id == 0)
|
||||
{
|
||||
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s;", qc->where);
|
||||
query = sqlite3_mprintf("SELECT f.* FROM files f %s %s %s %s;", qc->where, qc->group, qc->order, qc->index);
|
||||
}
|
||||
else if (qc->where[0] == '\0')
|
||||
{
|
||||
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f WHERE f.id = %d;", qp->id);
|
||||
query = sqlite3_mprintf("SELECT f.* FROM files f WHERE f.id = %d %s %s %s;", qp->id, qc->group, qc->order, qc->index);
|
||||
}
|
||||
else
|
||||
{
|
||||
count = sqlite3_mprintf("SELECT COUNT(*) FROM files f %s AND f.id = %d;", qc->where, qp->id);
|
||||
query = sqlite3_mprintf("SELECT f.* FROM files f %s AND f.id = %d %s %s %s;", qc->where, qp->id, qc->group, qc->order, qc->index);
|
||||
}
|
||||
|
||||
return db_build_query_check(qp, count, query);
|
||||
}
|
||||
@ -2903,7 +2927,9 @@ db_file_inc_playcount_byfilter(const char *filter)
|
||||
return;
|
||||
}
|
||||
|
||||
ret = db_query_run(query, 1, 0);
|
||||
// Perhaps this should in principle emit LISTENER_DATABASE, but that would
|
||||
// cause a lot of useless cache updates
|
||||
ret = db_query_run(query, 1, db_rating_updates ? LISTENER_RATING : 0);
|
||||
if (ret == 0)
|
||||
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
|
||||
#undef Q_TMPL
|
||||
@ -2969,7 +2995,7 @@ db_file_inc_skipcount(int id)
|
||||
return;
|
||||
}
|
||||
|
||||
ret = db_query_run(query, 1, 0);
|
||||
ret = db_query_run(query, 1, db_rating_updates ? LISTENER_RATING : 0);
|
||||
if (ret == 0)
|
||||
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
|
||||
#undef Q_TMPL
|
||||
@ -3137,6 +3163,30 @@ db_file_id_byquery(const char *query)
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool
|
||||
db_file_id_exists(int id)
|
||||
{
|
||||
#define Q_TMPL "SELECT f.id FROM files f WHERE f.id = %d;"
|
||||
char *query;
|
||||
int ret;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, id);
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
ret = db_file_id_byquery(query);
|
||||
|
||||
sqlite3_free(query);
|
||||
|
||||
return (id == ret);
|
||||
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
int
|
||||
db_file_id_bypath(const char *path)
|
||||
{
|
||||
@ -3210,13 +3260,37 @@ db_file_id_byurl(const char *url)
|
||||
}
|
||||
|
||||
int
|
||||
db_file_id_by_virtualpath_match(const char *path)
|
||||
db_file_id_byvirtualpath(const char *virtual_path)
|
||||
{
|
||||
#define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path = %Q;"
|
||||
char *query;
|
||||
int ret;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, virtual_path);
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
ret = db_file_id_byquery(query);
|
||||
|
||||
sqlite3_free(query);
|
||||
|
||||
return ret;
|
||||
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
int
|
||||
db_file_id_byvirtualpath_match(const char *virtual_path)
|
||||
{
|
||||
#define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path LIKE '%%%q%%';"
|
||||
char *query;
|
||||
int ret;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, path);
|
||||
query = sqlite3_mprintf(Q_TMPL, virtual_path);
|
||||
if (!query)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
|
||||
@ -3436,67 +3510,6 @@ db_file_seek_update(int id, uint32_t seek)
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
static int
|
||||
db_file_rating_update(char *query)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = db_query_run(query, 1, 0);
|
||||
|
||||
if (ret == 0)
|
||||
{
|
||||
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
|
||||
listener_notify(LISTENER_RATING);
|
||||
}
|
||||
|
||||
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
|
||||
}
|
||||
|
||||
int
|
||||
db_file_rating_update_byid(uint32_t id, uint32_t rating)
|
||||
{
|
||||
#define Q_TMPL "UPDATE files SET rating = %d WHERE id = %d;"
|
||||
char *query;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, rating, id);
|
||||
|
||||
return db_file_rating_update(query);
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
int
|
||||
db_file_rating_update_byvirtualpath(const char *virtual_path, uint32_t rating)
|
||||
{
|
||||
#define Q_TMPL "UPDATE files SET rating = %d WHERE virtual_path = %Q;"
|
||||
char *query;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, rating, virtual_path);
|
||||
|
||||
return db_file_rating_update(query);
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
int
|
||||
db_file_usermark_update_byid(uint32_t id, uint32_t usermark)
|
||||
{
|
||||
#define Q_TMPL "UPDATE files SET usermark = %d WHERE id = %d;"
|
||||
char *query;
|
||||
int ret;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, usermark, id);
|
||||
|
||||
ret = db_query_run(query, 1, 0);
|
||||
|
||||
if (ret == 0)
|
||||
{
|
||||
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
|
||||
listener_notify(LISTENER_UPDATE);
|
||||
}
|
||||
|
||||
return ((ret < 0) ? -1 : sqlite3_changes(hdl));
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
void
|
||||
db_file_delete_bypath(const char *path)
|
||||
{
|
||||
@ -4780,10 +4793,10 @@ db_admin_delete(const char *key)
|
||||
int
|
||||
db_speaker_save(struct output_device *device)
|
||||
{
|
||||
#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key) VALUES (%" PRIi64 ", %d, %d, %Q, %Q);"
|
||||
#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key, format) VALUES (%" PRIi64 ", %d, %d, %Q, %Q, %d);"
|
||||
char *query;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key);
|
||||
query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key, device->selected_format);
|
||||
|
||||
return db_query_run(query, 1, 0);
|
||||
#undef Q_TMPL
|
||||
@ -4792,7 +4805,7 @@ db_speaker_save(struct output_device *device)
|
||||
int
|
||||
db_speaker_get(struct output_device *device, uint64_t id)
|
||||
{
|
||||
#define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key FROM speakers s WHERE s.id = %" PRIi64 ";"
|
||||
#define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key, s.format FROM speakers s WHERE s.id = %" PRIi64 ";"
|
||||
sqlite3_stmt *stmt;
|
||||
char *query;
|
||||
int ret;
|
||||
@ -4834,6 +4847,8 @@ db_speaker_get(struct output_device *device, uint64_t id)
|
||||
free(device->auth_key);
|
||||
device->auth_key = safe_strdup((char *)sqlite3_column_text(stmt, 3));
|
||||
|
||||
device->selected_format = sqlite3_column_int(stmt, 4);
|
||||
|
||||
#ifdef DB_PROFILE
|
||||
while (db_blocking_step(stmt) == SQLITE_ROW)
|
||||
; /* EMPTY */
|
||||
@ -5271,62 +5286,6 @@ db_queue_add_by_query(struct query_params *qp, char reshuffle, uint32_t item_id,
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds the items of the stored playlist with the given id to the end of the queue
|
||||
*
|
||||
* @param plid Id of the stored playlist
|
||||
* @param reshuffle If 1 queue will be reshuffled after adding new items
|
||||
* @param item_id The base item id, all items after this will be reshuffled
|
||||
* @param position The position in the queue for the new queue item, -1 to add at end of queue
|
||||
* @param count If not NULL returns the number of items added to the queue
|
||||
* @param new_item_id If not NULL return the queue item id of the first new queue item
|
||||
* @return 0 on success, -1 on failure
|
||||
*/
|
||||
int
|
||||
db_queue_add_by_playlistid(int plid, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id)
|
||||
{
|
||||
struct query_params qp;
|
||||
int ret;
|
||||
|
||||
memset(&qp, 0, sizeof(struct query_params));
|
||||
|
||||
qp.id = plid;
|
||||
qp.type = Q_PLITEMS;
|
||||
|
||||
ret = db_queue_add_by_query(&qp, reshuffle, item_id, position, count, new_item_id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds the file with the given id to the queue
|
||||
*
|
||||
* @param id Id of the file
|
||||
* @param reshuffle If 1 queue will be reshuffled after adding new items
|
||||
* @param item_id The base item id, all items after this will be reshuffled
|
||||
* @param position The position in the queue for the new queue item, -1 to add at end of queue
|
||||
* @param count If not NULL returns the number of items added to the queue
|
||||
* @param new_item_id If not NULL return the queue item id of the first new queue item
|
||||
* @return 0 on success, -1 on failure
|
||||
*/
|
||||
int
|
||||
db_queue_add_by_fileid(int id, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id)
|
||||
{
|
||||
struct query_params qp;
|
||||
char buf[124];
|
||||
int ret;
|
||||
|
||||
memset(&qp, 0, sizeof(struct query_params));
|
||||
|
||||
qp.type = Q_ITEMS;
|
||||
snprintf(buf, sizeof(buf), "f.id = %" PRIu32, id);
|
||||
qp.filter = buf;
|
||||
|
||||
ret = db_queue_add_by_query(&qp, reshuffle, item_id, position, count, new_item_id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_enum_start(struct query_params *qp)
|
||||
{
|
||||
@ -5338,7 +5297,9 @@ queue_enum_start(struct query_params *qp)
|
||||
|
||||
qp->stmt = NULL;
|
||||
|
||||
if (qp->sort)
|
||||
if (qp->order)
|
||||
orderby = qp->order;
|
||||
else if (qp->sort)
|
||||
orderby = sort_clause[qp->sort];
|
||||
else
|
||||
orderby = sort_clause[S_POS];
|
||||
@ -6049,6 +6010,48 @@ db_queue_move_bypos(int pos_from, int pos_to)
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
db_queue_move_bypos_range(int range_begin, int range_end, int pos_to)
|
||||
{
|
||||
#define Q_TMPL "UPDATE queue SET pos = CASE WHEN pos < %d THEN pos + %d ELSE pos - %d END, queue_version = %d WHERE pos >= %d AND pos < %d;"
|
||||
int queue_version;
|
||||
char *query;
|
||||
int count;
|
||||
int update_begin;
|
||||
int update_end;
|
||||
int ret;
|
||||
int cut_off;
|
||||
int offset_up;
|
||||
int offset_down;
|
||||
|
||||
queue_version = queue_transaction_begin();
|
||||
|
||||
count = range_end - range_begin;
|
||||
update_begin = MIN(range_begin, pos_to);
|
||||
update_end = MAX(range_begin + count, pos_to + count);
|
||||
|
||||
if (range_begin < pos_to)
|
||||
{
|
||||
cut_off = range_begin + count;
|
||||
offset_up = pos_to - range_begin;
|
||||
offset_down = count;
|
||||
}
|
||||
else
|
||||
{
|
||||
cut_off = range_begin;
|
||||
offset_up = count;
|
||||
offset_down = range_begin - pos_to;
|
||||
}
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, cut_off, offset_up, offset_down, queue_version, update_begin, update_end);
|
||||
ret = db_query_run(query, 1, 0);
|
||||
|
||||
queue_transaction_end(ret, queue_version);
|
||||
|
||||
return ret;
|
||||
#undef Q_TMPL
|
||||
}
|
||||
|
||||
/*
|
||||
* Moves the queue item at the given position to the given target position. The positions
|
||||
* are relavtive to the given base item (item id).
|
||||
@ -6388,8 +6391,6 @@ db_watch_get_byquery(struct watch_info *wi, char *query)
|
||||
ret = db_blocking_step(stmt);
|
||||
if (ret != SQLITE_ROW)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DB, "Watch not found: '%s'\n", query);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_free(query);
|
||||
return -1;
|
||||
@ -6615,7 +6616,7 @@ db_watch_enum_fetchwd(struct watch_enum *we, uint32_t *wd)
|
||||
ret = db_blocking_step(we->stmt);
|
||||
if (ret == SQLITE_DONE)
|
||||
{
|
||||
DPRINTF(E_INFO, L_DB, "End of watch enum results\n");
|
||||
DPRINTF(E_DBG, L_DB, "End of watch enum results\n");
|
||||
return 0;
|
||||
}
|
||||
else if (ret != SQLITE_ROW)
|
||||
@ -6943,9 +6944,6 @@ db_open(void)
|
||||
int synchronous;
|
||||
int mmap_size;
|
||||
|
||||
if (!db_path)
|
||||
return -1;
|
||||
|
||||
ret = sqlite3_open(db_path, &hdl);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
@ -6964,7 +6962,7 @@ db_open(void)
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = sqlite3_load_extension(hdl, PKGLIBDIR "/" PACKAGE_NAME "-sqlext.so", NULL, &errmsg);
|
||||
ret = sqlite3_load_extension(hdl, db_sqlite_ext_path, NULL, &errmsg);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Could not load SQLite extension: %s\n", errmsg);
|
||||
@ -7107,7 +7105,10 @@ db_statements_prepare_ping(const char *table)
|
||||
sqlite3_stmt *stmt;
|
||||
int ret;
|
||||
|
||||
CHECK_NULL(L_DB, query = db_mprintf("UPDATE %s SET db_timestamp = ?, disabled = 0 WHERE path = ? AND db_timestamp >= ?;", table));
|
||||
// The last param will be the file mtime. We must not update if the mtime is
|
||||
// newer or equal than the current db_timestamp, since the file may have been
|
||||
// modified and must be rescanned.
|
||||
CHECK_NULL(L_DB, query = db_mprintf("UPDATE %s SET db_timestamp = ?, disabled = 0 WHERE path = ? AND db_timestamp > ?;", table));
|
||||
|
||||
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
|
||||
if (ret != SQLITE_OK)
|
||||
@ -7144,8 +7145,9 @@ db_statements_prepare(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Returns -2 if backup not enabled in config
|
||||
int
|
||||
db_backup()
|
||||
db_backup(void)
|
||||
{
|
||||
int ret;
|
||||
sqlite3 *backup_hdl;
|
||||
@ -7165,7 +7167,7 @@ db_backup()
|
||||
if (realpath(db_path, resolved_dbp) == NULL || realpath(backup_path, resolved_bp) == NULL)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DB, "Failed to resolve real path of db/backup path: %s\n", strerror(errno));
|
||||
goto error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strcmp(resolved_bp, resolved_dbp) == 0)
|
||||
@ -7180,14 +7182,15 @@ db_backup()
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DB, "Failed to create backup '%s': %s\n", backup_path, sqlite3_errmsg(backup_hdl));
|
||||
goto error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
backup = sqlite3_backup_init(backup_hdl, "main", hdl, "main");
|
||||
if (!backup)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DB, "Failed to initiate backup '%s': %s\n", backup_path, sqlite3_errmsg(backup_hdl));
|
||||
goto error;
|
||||
sqlite3_close(backup_hdl);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = sqlite3_backup_step(backup, -1);
|
||||
@ -7199,10 +7202,7 @@ db_backup()
|
||||
else
|
||||
DPRINTF(E_WARN, L_DB, "Failed to complete backup '%s': %s (%d)\n", backup_path, sqlite3_errstr(ret), ret);
|
||||
|
||||
return ret;
|
||||
|
||||
error:
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
@ -7376,7 +7376,7 @@ db_check_version(void)
|
||||
}
|
||||
|
||||
int
|
||||
db_init(void)
|
||||
db_init(char *sqlite_ext_path)
|
||||
{
|
||||
uint32_t files;
|
||||
uint32_t pls;
|
||||
@ -7393,37 +7393,38 @@ db_init(void)
|
||||
}
|
||||
|
||||
db_path = cfg_getstr(cfg_getsec(cfg, "general"), "db_path");
|
||||
db_sqlite_ext_path = sqlite_ext_path;
|
||||
db_rating_updates = cfg_getbool(cfg_getsec(cfg, "library"), "rating_updates");
|
||||
|
||||
DPRINTF(E_LOG, L_DB, "Configured to use database file '%s'\n", db_path);
|
||||
DPRINTF(E_INFO, L_DB, "Configured to use database file '%s'\n", db_path);
|
||||
|
||||
ret = sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not switch SQLite3 to multithread mode\n");
|
||||
DPRINTF(E_FATAL, L_DB, "Check that SQLite3 has been configured for thread-safe operations\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = sqlite3_enable_shared_cache(1);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not enable SQLite3 shared-cache mode\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = sqlite3_initialize();
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "SQLite3 failed to initialize\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = db_open();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not open database\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = db_check_version();
|
||||
@ -7432,7 +7433,7 @@ db_init(void)
|
||||
DPRINTF(E_FATAL, L_DB, "Database version check errored out, incompatible database\n");
|
||||
|
||||
db_perthread_deinit();
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
else if (ret > 0)
|
||||
{
|
||||
@ -7443,7 +7444,7 @@ db_init(void)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not create tables\n");
|
||||
db_perthread_deinit();
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7461,6 +7462,9 @@ db_init(void)
|
||||
rng_init(&shuffle_rng);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
return -1;
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
35
src/db.h
35
src/db.h
@ -72,6 +72,7 @@ enum query_type {
|
||||
#define DB_ADMIN_START_TIME "start_time"
|
||||
#define DB_ADMIN_LASTFM_SESSION_KEY "lastfm_sk"
|
||||
#define DB_ADMIN_SPOTIFY_REFRESH_TOKEN "spotify_refresh_token"
|
||||
#define DB_ADMIN_LISTENBRAINZ_TOKEN "listenbrainz_token"
|
||||
|
||||
/* Max value for media_file_info->rating (valid range is from 0 to 100) */
|
||||
#define DB_FILES_RATING_MAX 100
|
||||
@ -190,7 +191,7 @@ struct media_file_info {
|
||||
uint32_t song_length;
|
||||
int64_t file_size;
|
||||
uint32_t year; /* TDRC */
|
||||
uint32_t date_released;
|
||||
int64_t date_released; // bumped to (signed) int64 since all 32bits are unsigned
|
||||
|
||||
uint32_t track; /* TRCK */
|
||||
uint32_t total_tracks;
|
||||
@ -248,6 +249,7 @@ struct media_file_info {
|
||||
char *composer_sort;
|
||||
|
||||
uint32_t scan_kind; /* Identifies the library_source that created/updates this item */
|
||||
char *lyrics;
|
||||
};
|
||||
|
||||
#define mfi_offsetof(field) offsetof(struct media_file_info, field)
|
||||
@ -422,6 +424,7 @@ struct db_media_file_info {
|
||||
char *channels;
|
||||
char *usermark;
|
||||
char *scan_kind;
|
||||
char *lyrics;
|
||||
};
|
||||
|
||||
#define dbmfi_offsetof(field) offsetof(struct db_media_file_info, field)
|
||||
@ -683,6 +686,9 @@ db_file_ping_bymatch(const char *path, int isdir);
|
||||
char *
|
||||
db_file_path_byid(int id);
|
||||
|
||||
bool
|
||||
db_file_id_exists(int id);
|
||||
|
||||
int
|
||||
db_file_id_bypath(const char *path);
|
||||
|
||||
@ -693,7 +699,10 @@ int
|
||||
db_file_id_byurl(const char *url);
|
||||
|
||||
int
|
||||
db_file_id_by_virtualpath_match(const char *path);
|
||||
db_file_id_byvirtualpath(const char *virtual_path);
|
||||
|
||||
int
|
||||
db_file_id_byvirtualpath_match(const char *virtual_path);
|
||||
|
||||
struct media_file_info *
|
||||
db_file_fetch_byid(int id);
|
||||
@ -710,15 +719,6 @@ db_file_update(struct media_file_info *mfi);
|
||||
void
|
||||
db_file_seek_update(int id, uint32_t seek);
|
||||
|
||||
int
|
||||
db_file_rating_update_byid(uint32_t id, uint32_t rating);
|
||||
|
||||
int
|
||||
db_file_usermark_update_byid(uint32_t id, uint32_t usermark);
|
||||
|
||||
int
|
||||
db_file_rating_update_byvirtualpath(const char *virtual_path, uint32_t rating);
|
||||
|
||||
void
|
||||
db_file_delete_bypath(const char *path);
|
||||
|
||||
@ -899,12 +899,6 @@ db_queue_add_by_queryafteritemid(struct query_params *qp, uint32_t item_id);
|
||||
int
|
||||
db_queue_add_by_query(struct query_params *qp, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id);
|
||||
|
||||
int
|
||||
db_queue_add_by_playlistid(int plid, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id);
|
||||
|
||||
int
|
||||
db_queue_add_by_fileid(int id, char reshuffle, uint32_t item_id, int position, int *count, int *new_item_id);
|
||||
|
||||
int
|
||||
db_queue_add_start(struct db_queue_add_info *queue_add_info, int pos);
|
||||
|
||||
@ -962,6 +956,9 @@ db_queue_move_byitemid(uint32_t item_id, int pos_to, char shuffle);
|
||||
int
|
||||
db_queue_move_bypos(int pos_from, int pos_to);
|
||||
|
||||
int
|
||||
db_queue_move_bypos_range(int range_begin, int range_end, int pos_to);
|
||||
|
||||
int
|
||||
db_queue_move_byposrelativetoitem(uint32_t from_pos, uint32_t to_offset, uint32_t item_id, char shuffle);
|
||||
|
||||
@ -1024,7 +1021,7 @@ int
|
||||
db_watch_enum_fetchwd(struct watch_enum *we, uint32_t *wd);
|
||||
|
||||
int
|
||||
db_backup();
|
||||
db_backup(void);
|
||||
|
||||
int
|
||||
db_perthread_init(void);
|
||||
@ -1033,7 +1030,7 @@ void
|
||||
db_perthread_deinit(void);
|
||||
|
||||
int
|
||||
db_init(void);
|
||||
db_init(char *sqlite_ext_path);
|
||||
|
||||
void
|
||||
db_deinit(void);
|
||||
|
||||
@ -98,7 +98,8 @@
|
||||
" composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \
|
||||
" channels INTEGER DEFAULT 0," \
|
||||
" usermark INTEGER DEFAULT 0," \
|
||||
" scan_kind INTEGER DEFAULT 0" \
|
||||
" scan_kind INTEGER DEFAULT 0," \
|
||||
" lyrics TEXT DEFAULT NULL COLLATE DAAP" \
|
||||
");"
|
||||
|
||||
#define T_PL \
|
||||
@ -150,8 +151,9 @@
|
||||
" id INTEGER PRIMARY KEY NOT NULL," \
|
||||
" selected INTEGER NOT NULL," \
|
||||
" volume INTEGER NOT NULL," \
|
||||
" name VARCHAR(255) DEFAULT NULL," \
|
||||
" auth_key VARCHAR(2048) DEFAULT NULL" \
|
||||
" name VARCHAR(255) DEFAULT NULL," \
|
||||
" auth_key VARCHAR(2048) DEFAULT NULL," \
|
||||
" format INTEGER DEFAULT 0" \
|
||||
");"
|
||||
|
||||
#define T_INOTIFY \
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
* is a major upgrade. In other words minor version upgrades permit downgrading
|
||||
* the server after the database was upgraded. */
|
||||
#define SCHEMA_VERSION_MAJOR 22
|
||||
#define SCHEMA_VERSION_MINOR 0
|
||||
#define SCHEMA_VERSION_MINOR 2
|
||||
|
||||
int
|
||||
db_init_indices(sqlite3 *hdl);
|
||||
|
||||
@ -1227,6 +1227,44 @@ static const struct db_upgrade_query db_upgrade_v2200_queries[] =
|
||||
{ U_v2200_SCVER_MINOR, "set schema_version_minor to 00" },
|
||||
};
|
||||
|
||||
/* ---------------------------- 22.00 -> 22.01 ------------------------------ */
|
||||
|
||||
#define U_v2201_ALTER_FILES_ADD_LYRICS \
|
||||
"ALTER TABLE files ADD COLUMN lyrics TEXT DEFAULT NULL COLLATE DAAP;"
|
||||
|
||||
#define U_v2201_SCVER_MAJOR \
|
||||
"UPDATE admin SET value = '22' WHERE key = 'schema_version_major';"
|
||||
#define U_v2201_SCVER_MINOR \
|
||||
"UPDATE admin SET value = '01' WHERE key = 'schema_version_minor';"
|
||||
|
||||
static const struct db_upgrade_query db_upgrade_v2201_queries[] =
|
||||
{
|
||||
{ U_v2201_ALTER_FILES_ADD_LYRICS, "alter table files add column lyrics" },
|
||||
|
||||
{ U_v2201_SCVER_MAJOR, "set schema_version_major to 22" },
|
||||
{ U_v2201_SCVER_MINOR, "set schema_version_minor to 01" },
|
||||
};
|
||||
|
||||
|
||||
/* ---------------------------- 22.01 -> 22.02 ------------------------------ */
|
||||
|
||||
#define U_v2202_ALTER_SPEAKERS_ADD_FORMAT \
|
||||
"ALTER TABLE speakers ADD COLUMN format INTEGER DEFAULT 0;"
|
||||
|
||||
#define U_v2202_SCVER_MAJOR \
|
||||
"UPDATE admin SET value = '22' WHERE key = 'schema_version_major';"
|
||||
#define U_v2202_SCVER_MINOR \
|
||||
"UPDATE admin SET value = '02' WHERE key = 'schema_version_minor';"
|
||||
|
||||
static const struct db_upgrade_query db_upgrade_v2202_queries[] =
|
||||
{
|
||||
{ U_v2202_ALTER_SPEAKERS_ADD_FORMAT, "alter table speakers add column format" },
|
||||
|
||||
{ U_v2202_SCVER_MAJOR, "set schema_version_major to 22" },
|
||||
{ U_v2202_SCVER_MINOR, "set schema_version_minor to 02" },
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------- Main upgrade handler -------------------------- */
|
||||
|
||||
int
|
||||
@ -1437,6 +1475,19 @@ db_upgrade(sqlite3 *hdl, int db_ver)
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
/* FALLTHROUGH */
|
||||
|
||||
case 2200:
|
||||
ret = db_generic_upgrade(hdl, db_upgrade_v2201_queries, ARRAY_SIZE(db_upgrade_v2201_queries));
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
/* FALLTHROUGH */
|
||||
|
||||
case 2201:
|
||||
ret = db_generic_upgrade(hdl, db_upgrade_v2202_queries, ARRAY_SIZE(db_upgrade_v2202_queries));
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
/* Last case statement is the only one that ends with a break statement! */
|
||||
break;
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
|
||||
#include "db.h"
|
||||
#include "misc.h"
|
||||
#include "httpd.h"
|
||||
#include "logger.h"
|
||||
#include "dmap_common.h"
|
||||
#include "parsers/daap_parser.h"
|
||||
@ -225,7 +224,6 @@ dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval
|
||||
{
|
||||
switch (df->type)
|
||||
{
|
||||
case DMAP_TYPE_DATE:
|
||||
case DMAP_TYPE_UBYTE:
|
||||
case DMAP_TYPE_USHORT:
|
||||
case DMAP_TYPE_UINT:
|
||||
@ -248,6 +246,7 @@ dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval
|
||||
val.v_u64 = 0;
|
||||
break;
|
||||
|
||||
case DMAP_TYPE_DATE:
|
||||
case DMAP_TYPE_LONG:
|
||||
ret = safe_atoi64(strval, &val.v_i64);
|
||||
if (ret < 0)
|
||||
@ -360,38 +359,12 @@ dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errms
|
||||
dmap_add_string(evbuf, "msts", errmsg);
|
||||
}
|
||||
|
||||
void
|
||||
dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg)
|
||||
{
|
||||
struct evbuffer *evbuf;
|
||||
|
||||
if (!req)
|
||||
return;
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
if (!evbuf)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DMAP, "Could not allocate evbuffer for DMAP error\n");
|
||||
|
||||
httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
|
||||
return;
|
||||
}
|
||||
|
||||
dmap_error_make(evbuf, container, errmsg);
|
||||
|
||||
httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP);
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav)
|
||||
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags)
|
||||
{
|
||||
const struct dmap_field_map *dfm;
|
||||
const struct dmap_field *df;
|
||||
char **strval;
|
||||
char *ptr;
|
||||
int32_t val;
|
||||
int want_mikd;
|
||||
int want_asdk;
|
||||
@ -470,40 +443,7 @@ dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, stru
|
||||
continue;
|
||||
}
|
||||
|
||||
val = 0;
|
||||
|
||||
if (force_wav)
|
||||
{
|
||||
switch (dfm->mfi_offset)
|
||||
{
|
||||
case dbmfi_offsetof(type):
|
||||
ptr = "wav";
|
||||
strval = &ptr;
|
||||
break;
|
||||
|
||||
case dbmfi_offsetof(bitrate):
|
||||
val = 0;
|
||||
ret = safe_atoi32(dbmfi->samplerate, &val);
|
||||
if ((ret < 0) || (val == 0))
|
||||
val = 1411;
|
||||
else
|
||||
val = (val * 8) / 250;
|
||||
|
||||
ptr = NULL;
|
||||
strval = &ptr;
|
||||
break;
|
||||
|
||||
case dbmfi_offsetof(description):
|
||||
ptr = "wav audio file";
|
||||
strval = &ptr;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dmap_add_field(song, df, *strval, val);
|
||||
dmap_add_field(song, df, *strval, 0);
|
||||
|
||||
DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval);
|
||||
}
|
||||
|
||||
@ -78,12 +78,8 @@ dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval
|
||||
void
|
||||
dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errmsg);
|
||||
|
||||
void
|
||||
dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg);
|
||||
|
||||
|
||||
int
|
||||
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav);
|
||||
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags);
|
||||
|
||||
int
|
||||
dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_queue_item *queue_item);
|
||||
|
||||
@ -106,8 +106,8 @@ void evrtsp_connection_fail(struct evrtsp_connection *,
|
||||
|
||||
int evrtsp_hostportfile(char *, char **, u_short *, char **);
|
||||
|
||||
int evrtsp_parse_firstline(struct evrtsp_request *, struct evbuffer*);
|
||||
int evrtsp_parse_headers(struct evrtsp_request *, struct evbuffer*);
|
||||
enum message_read_status evrtsp_parse_firstline(struct evrtsp_request *, struct evbuffer*);
|
||||
enum message_read_status evrtsp_parse_headers(struct evrtsp_request *, struct evbuffer*);
|
||||
|
||||
void evrtsp_start_read(struct evrtsp_connection *);
|
||||
void evrtsp_make_header(struct evrtsp_connection *, struct evrtsp_request *);
|
||||
|
||||
@ -63,17 +63,6 @@
|
||||
#include "log.h"
|
||||
#include "rtsp-internal.h"
|
||||
|
||||
// For compability with libevent 2.0 (HAVE_LIBEVENT2_OLD)
|
||||
#if defined(_EVENT_HAVE_GETNAMEINFO)
|
||||
# define EVENT__HAVE_GETNAMEINFO 1
|
||||
#endif
|
||||
#if defined(_EVENT_HAVE_GETADDRINFO)
|
||||
# define EVENT__HAVE_GETADDRINFO 1
|
||||
#endif
|
||||
#if defined(_EVENT_HAVE_STRSEP)
|
||||
# define EVENT__HAVE_STRSEP 1
|
||||
#endif
|
||||
|
||||
#ifndef EVENT__HAVE_GETNAMEINFO
|
||||
#define NI_MAXSERV 32
|
||||
#define NI_MAXHOST 1025
|
||||
|
||||
467
src/evthr.c
Normal file
467
src/evthr.c
Normal file
@ -0,0 +1,467 @@
|
||||
/*
|
||||
------------------- Thread handling borrowed from libevhtp ---------------------
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2010-2018, Mark Ellzey, Nathan French, Marcus Sundberg
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <sys/queue.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/thread.h>
|
||||
|
||||
#include "evthr.h"
|
||||
|
||||
#ifndef TAILQ_FOREACH_SAFE
|
||||
#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \
|
||||
for ((var) = TAILQ_FIRST((head)); \
|
||||
(var) && ((tvar) = TAILQ_NEXT((var), field), 1); \
|
||||
(var) = (tvar))
|
||||
#endif
|
||||
|
||||
#define _evthr_read(thr, cmd, sock) \
|
||||
(recv(sock, cmd, sizeof(struct evthr_cmd), 0) == sizeof(struct evthr_cmd)) ? 1 : 0
|
||||
|
||||
#define EVTHR_SHARED_PIPE 1
|
||||
|
||||
struct evthr_cmd {
|
||||
uint8_t stop;
|
||||
void *args;
|
||||
evthr_cb cb;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct evthr_pool {
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
int rdr;
|
||||
int wdr;
|
||||
#endif
|
||||
int nthreads;
|
||||
TAILQ_HEAD(evthr_pool_slist, evthr) threads;
|
||||
};
|
||||
|
||||
struct evthr {
|
||||
int rdr;
|
||||
int wdr;
|
||||
char err;
|
||||
struct event *event;
|
||||
struct event_base *evbase;
|
||||
pthread_mutex_t lock;
|
||||
pthread_t *thr;
|
||||
evthr_init_cb init_cb;
|
||||
evthr_exit_cb exit_cb;
|
||||
void *arg;
|
||||
void *aux;
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
int pool_rdr;
|
||||
struct event *shared_pool_ev;
|
||||
#endif
|
||||
TAILQ_ENTRY(evthr) next;
|
||||
};
|
||||
|
||||
|
||||
static void
|
||||
_evthr_read_cmd(evutil_socket_t sock, short which, void *args)
|
||||
{
|
||||
struct evthr *thread;
|
||||
struct evthr_cmd cmd;
|
||||
int stopped;
|
||||
|
||||
if (!(thread = (struct evthr *)args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopped = 0;
|
||||
|
||||
if (_evthr_read(thread, &cmd, sock) == 1) {
|
||||
stopped = cmd.stop;
|
||||
|
||||
if (cmd.cb != NULL) {
|
||||
(cmd.cb)(thread, cmd.args, thread->arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (stopped == 1) {
|
||||
event_base_loopbreak(thread->evbase);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static void *
|
||||
_evthr_loop(void *args)
|
||||
{
|
||||
struct evthr *thread;
|
||||
|
||||
if (!(thread = (struct evthr *)args)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (thread == NULL || thread->thr == NULL) {
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
thread->evbase = event_base_new();
|
||||
thread->event = event_new(thread->evbase, thread->rdr,
|
||||
EV_READ | EV_PERSIST, _evthr_read_cmd, args);
|
||||
|
||||
event_add(thread->event, NULL);
|
||||
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
if (thread->pool_rdr > 0) {
|
||||
thread->shared_pool_ev = event_new(thread->evbase, thread->pool_rdr,
|
||||
EV_READ | EV_PERSIST, _evthr_read_cmd, args);
|
||||
event_add(thread->shared_pool_ev, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
pthread_mutex_lock(&thread->lock);
|
||||
if (thread->init_cb != NULL) {
|
||||
(thread->init_cb)(thread, thread->arg);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&thread->lock);
|
||||
|
||||
event_base_loop(thread->evbase, 0);
|
||||
|
||||
pthread_mutex_lock(&thread->lock);
|
||||
if (thread->exit_cb != NULL) {
|
||||
(thread->exit_cb)(thread, thread->arg);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&thread->lock);
|
||||
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static enum evthr_res
|
||||
evthr_defer(struct evthr *thread, evthr_cb cb, void *arg)
|
||||
{
|
||||
struct evthr_cmd cmd = {
|
||||
.cb = cb,
|
||||
.args = arg,
|
||||
.stop = 0
|
||||
};
|
||||
|
||||
if (send(thread->wdr, &cmd, sizeof(cmd), 0) <= 0) {
|
||||
return EVTHR_RES_RETRY;
|
||||
}
|
||||
|
||||
return EVTHR_RES_OK;
|
||||
}
|
||||
|
||||
static enum evthr_res
|
||||
evthr_stop(struct evthr *thread)
|
||||
{
|
||||
struct evthr_cmd cmd = {
|
||||
.cb = NULL,
|
||||
.args = NULL,
|
||||
.stop = 1
|
||||
};
|
||||
|
||||
if (send(thread->wdr, &cmd, sizeof(struct evthr_cmd), 0) < 0) {
|
||||
return EVTHR_RES_RETRY;
|
||||
}
|
||||
|
||||
pthread_join(*thread->thr, NULL);
|
||||
return EVTHR_RES_OK;
|
||||
}
|
||||
|
||||
struct event_base *
|
||||
evthr_get_base(struct evthr * thr)
|
||||
{
|
||||
return thr ? thr->evbase : NULL;
|
||||
}
|
||||
|
||||
void
|
||||
evthr_set_aux(struct evthr *thr, void *aux)
|
||||
{
|
||||
if (thr) {
|
||||
thr->aux = aux;
|
||||
}
|
||||
}
|
||||
|
||||
void *
|
||||
evthr_get_aux(struct evthr *thr)
|
||||
{
|
||||
return thr ? thr->aux : NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
evthr_free(struct evthr *thread)
|
||||
{
|
||||
if (thread == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (thread->rdr > 0) {
|
||||
close(thread->rdr);
|
||||
}
|
||||
|
||||
if (thread->wdr > 0) {
|
||||
close(thread->wdr);
|
||||
}
|
||||
|
||||
if (thread->thr) {
|
||||
free(thread->thr);
|
||||
}
|
||||
|
||||
if (thread->event) {
|
||||
event_free(thread->event);
|
||||
}
|
||||
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
if (thread->shared_pool_ev) {
|
||||
event_free(thread->shared_pool_ev);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (thread->evbase) {
|
||||
event_base_free(thread->evbase);
|
||||
}
|
||||
|
||||
free(thread);
|
||||
}
|
||||
|
||||
static struct evthr *
|
||||
evthr_wexit_new(evthr_init_cb init_cb, evthr_exit_cb exit_cb, void *args)
|
||||
{
|
||||
struct evthr *thread;
|
||||
int fds[2];
|
||||
|
||||
if (evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evutil_make_socket_nonblocking(fds[0]);
|
||||
evutil_make_socket_nonblocking(fds[1]);
|
||||
|
||||
if (!(thread = calloc(1, sizeof(struct evthr)))) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
thread->thr = malloc(sizeof(pthread_t));
|
||||
thread->arg = args;
|
||||
thread->rdr = fds[0];
|
||||
thread->wdr = fds[1];
|
||||
|
||||
thread->init_cb = init_cb;
|
||||
thread->exit_cb = exit_cb;
|
||||
|
||||
if (pthread_mutex_init(&thread->lock, NULL)) {
|
||||
evthr_free(thread);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
static int
|
||||
evthr_start(struct evthr *thread)
|
||||
{
|
||||
if (thread == NULL || thread->thr == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pthread_create(thread->thr, NULL, _evthr_loop, (void *)thread)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
evthr_pool_free(struct evthr_pool *pool)
|
||||
{
|
||||
struct evthr *thread;
|
||||
struct evthr *save;
|
||||
|
||||
if (pool == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
TAILQ_FOREACH_SAFE(thread, &pool->threads, next, save) {
|
||||
TAILQ_REMOVE(&pool->threads, thread, next);
|
||||
|
||||
evthr_free(thread);
|
||||
}
|
||||
|
||||
free(pool);
|
||||
}
|
||||
|
||||
enum evthr_res
|
||||
evthr_pool_stop(struct evthr_pool *pool)
|
||||
{
|
||||
struct evthr *thr;
|
||||
struct evthr *save;
|
||||
|
||||
if (pool == NULL) {
|
||||
return EVTHR_RES_FATAL;
|
||||
}
|
||||
|
||||
TAILQ_FOREACH_SAFE(thr, &pool->threads, next, save) {
|
||||
evthr_stop(thr);
|
||||
}
|
||||
|
||||
return EVTHR_RES_OK;
|
||||
}
|
||||
|
||||
static inline int
|
||||
get_backlog_(struct evthr *thread)
|
||||
{
|
||||
int backlog = 0;
|
||||
|
||||
ioctl(thread->rdr, FIONREAD, &backlog);
|
||||
|
||||
return (int)(backlog / sizeof(struct evthr_cmd));
|
||||
}
|
||||
|
||||
enum evthr_res
|
||||
evthr_pool_defer(struct evthr_pool *pool, evthr_cb cb, void *arg)
|
||||
{
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
struct evthr_cmd cmd = {
|
||||
.cb = cb,
|
||||
.args = arg,
|
||||
.stop = 0
|
||||
};
|
||||
|
||||
if (send(pool->wdr, &cmd, sizeof(cmd), 0) == -1) {
|
||||
return EVTHR_RES_RETRY;
|
||||
}
|
||||
|
||||
return EVTHR_RES_OK;
|
||||
#endif
|
||||
struct evthr *thread = NULL;
|
||||
struct evthr *min_thread = NULL;
|
||||
int min_backlog = 0;
|
||||
|
||||
if (pool == NULL) {
|
||||
return EVTHR_RES_FATAL;
|
||||
}
|
||||
|
||||
if (cb == NULL) {
|
||||
return EVTHR_RES_NOCB;
|
||||
}
|
||||
|
||||
TAILQ_FOREACH(thread, &pool->threads, next) {
|
||||
int backlog = get_backlog_(thread);
|
||||
|
||||
if (backlog == 0) {
|
||||
min_thread = thread;
|
||||
break;
|
||||
}
|
||||
|
||||
if (min_thread == NULL || backlog < min_backlog) {
|
||||
min_thread = thread;
|
||||
min_backlog = backlog;
|
||||
}
|
||||
}
|
||||
|
||||
return evthr_defer(min_thread, cb, arg);
|
||||
}
|
||||
|
||||
struct evthr_pool *
|
||||
evthr_pool_wexit_new(int nthreads, evthr_init_cb init_cb, evthr_exit_cb exit_cb, void *shared)
|
||||
{
|
||||
struct evthr_pool *pool;
|
||||
int i;
|
||||
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
int fds[2];
|
||||
#endif
|
||||
|
||||
if (nthreads == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(pool = calloc(1, sizeof(struct evthr_pool)))) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pool->nthreads = nthreads;
|
||||
TAILQ_INIT(&pool->threads);
|
||||
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
if (evutil_socketpair(AF_UNIX, SOCK_DGRAM, 0, fds) == -1) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evutil_make_socket_nonblocking(fds[0]);
|
||||
evutil_make_socket_nonblocking(fds[1]);
|
||||
|
||||
pool->rdr = fds[0];
|
||||
pool->wdr = fds[1];
|
||||
#endif
|
||||
|
||||
for (i = 0; i < nthreads; i++) {
|
||||
struct evthr * thread;
|
||||
|
||||
if (!(thread = evthr_wexit_new(init_cb, exit_cb, shared))) {
|
||||
evthr_pool_free(pool);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef EVTHR_SHARED_PIPE
|
||||
thread->pool_rdr = fds[0];
|
||||
#endif
|
||||
|
||||
TAILQ_INSERT_TAIL(&pool->threads, thread, next);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
int
|
||||
evthr_pool_start(struct evthr_pool *pool)
|
||||
{
|
||||
struct evthr *evthr = NULL;
|
||||
|
||||
if (pool == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
TAILQ_FOREACH(evthr, &pool->threads, next) {
|
||||
if (evthr_start(evthr) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
usleep(5000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
43
src/evthr.h
Normal file
43
src/evthr.h
Normal file
@ -0,0 +1,43 @@
|
||||
#ifndef __EVTHR_H__
|
||||
#define __EVTHR_H__
|
||||
|
||||
enum evthr_res {
|
||||
EVTHR_RES_OK = 0,
|
||||
EVTHR_RES_BACKLOG,
|
||||
EVTHR_RES_RETRY,
|
||||
EVTHR_RES_NOCB,
|
||||
EVTHR_RES_FATAL
|
||||
};
|
||||
|
||||
struct evthr_pool;
|
||||
struct evthr;
|
||||
|
||||
typedef void (*evthr_cb)(struct evthr *thr, void *cmd_arg, void *shared);
|
||||
typedef void (*evthr_init_cb)(struct evthr *thr, void *shared);
|
||||
typedef void (*evthr_exit_cb)(struct evthr *thr, void *shared);
|
||||
|
||||
struct event_base *
|
||||
evthr_get_base(struct evthr *thr);
|
||||
|
||||
void
|
||||
evthr_set_aux(struct evthr *thr, void *aux);
|
||||
|
||||
void *
|
||||
evthr_get_aux(struct evthr *thr);
|
||||
|
||||
enum evthr_res
|
||||
evthr_pool_defer(struct evthr_pool *pool, evthr_cb cb, void *arg);
|
||||
|
||||
struct evthr_pool *
|
||||
evthr_pool_wexit_new(int nthreads, evthr_init_cb init_cb, evthr_exit_cb exit_cb, void *shared);
|
||||
|
||||
void
|
||||
evthr_pool_free(struct evthr_pool *pool);
|
||||
|
||||
enum evthr_res
|
||||
evthr_pool_stop(struct evthr_pool *pool);
|
||||
|
||||
int
|
||||
evthr_pool_start(struct evthr_pool *pool);
|
||||
|
||||
#endif /* !__EVTHR_H__ */
|
||||
66
src/http.c
66
src/http.c
@ -36,11 +36,11 @@
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "httpd.h"
|
||||
#include "logger.h"
|
||||
#include "misc.h"
|
||||
#include "conffile.h"
|
||||
@ -172,7 +172,7 @@ http_client_request(struct http_client_ctx *ctx, struct http_client_session *ses
|
||||
res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
|
||||
DPRINTF(E_WARN, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
|
||||
curl_slist_free_all(headers);
|
||||
if (!session)
|
||||
{
|
||||
@ -194,6 +194,41 @@ http_client_request(struct http_client_ctx *ctx, struct http_client_session *ses
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
http_form_urldecode(struct keyval *kv, const char *uri)
|
||||
{
|
||||
struct evhttp_uri *ev_uri = NULL;
|
||||
struct evkeyvalq ev_query = { 0 };
|
||||
struct evkeyval *param;
|
||||
const char *query;
|
||||
int ret;
|
||||
|
||||
ev_uri = evhttp_uri_parse_with_flags(uri, EVHTTP_URI_NONCONFORMANT);
|
||||
if (!ev_uri)
|
||||
return -1;
|
||||
|
||||
query = evhttp_uri_get_query(ev_uri);
|
||||
if (!query)
|
||||
goto error;
|
||||
|
||||
ret = evhttp_parse_query_str(query, &ev_query);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
// musl libc doesn't have sys/queue.h so don't use TAILQ_FOREACH
|
||||
for (param = ev_query.tqh_first; param; param = param->next.tqe_next)
|
||||
keyval_add(kv, param->key, param->value);
|
||||
|
||||
evhttp_uri_free(ev_uri);
|
||||
evhttp_clear_headers(&ev_query);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
evhttp_uri_free(ev_uri);
|
||||
evhttp_clear_headers(&ev_query);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *
|
||||
http_form_urlencode(struct keyval *kv)
|
||||
{
|
||||
@ -240,9 +275,11 @@ http_form_urlencode(struct keyval *kv)
|
||||
int
|
||||
http_stream_setup(char **stream, const char *url)
|
||||
{
|
||||
CURLU *url_handle;
|
||||
CURLUcode rc;
|
||||
struct http_client_ctx ctx;
|
||||
struct httpd_uri_parsed *parsed;
|
||||
struct evbuffer *evbuf;
|
||||
char *path;
|
||||
const char *ext;
|
||||
char *line;
|
||||
char *pos;
|
||||
@ -253,17 +290,28 @@ http_stream_setup(char **stream, const char *url)
|
||||
|
||||
*stream = NULL;
|
||||
|
||||
parsed = httpd_uri_parse(url);
|
||||
if (!parsed)
|
||||
CHECK_NULL(L_HTTP, url_handle = curl_url());
|
||||
|
||||
rc = curl_url_set(url_handle, CURLUPART_URL, url, 0);
|
||||
if (rc != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Couldn't parse internet playlist: '%s'\n", url);
|
||||
curl_url_cleanup(url_handle);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// parsed->path does not include query or fragment, so should work with any url's
|
||||
rc = curl_url_get(url_handle, CURLUPART_PATH, &path, 0);
|
||||
if (rc != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTP, "Couldn't find internet playlist path: '%s'\n", url);
|
||||
curl_url_cleanup(url_handle);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// path does not include query or fragment, so should work with any url's
|
||||
// e.g. http://yp.shoutcast.com/sbin/tunein-station.pls?id=99179772#Air Jazz
|
||||
pl_format = PLAYLIST_UNK;
|
||||
if (parsed->path && (ext = strrchr(parsed->path, '.')))
|
||||
if (path && (ext = strrchr(path, '.')))
|
||||
{
|
||||
if (strcasecmp(ext, ".m3u") == 0)
|
||||
pl_format = PLAYLIST_M3U;
|
||||
@ -271,7 +319,9 @@ http_stream_setup(char **stream, const char *url)
|
||||
pl_format = PLAYLIST_PLS;
|
||||
}
|
||||
|
||||
httpd_uri_free(parsed);
|
||||
curl_free(path);
|
||||
curl_url_cleanup(url_handle);
|
||||
|
||||
|
||||
if (pl_format==PLAYLIST_UNK)
|
||||
{
|
||||
|
||||
14
src/http.h
14
src/http.h
@ -21,7 +21,7 @@ struct http_client_ctx
|
||||
*/
|
||||
const char *url;
|
||||
struct keyval *output_headers;
|
||||
char *output_body;
|
||||
const char *output_body;
|
||||
|
||||
/* A keyval/evbuf to store response headers and body.
|
||||
* Can be set to NULL to ignore that part of the response.
|
||||
@ -37,10 +37,6 @@ struct http_client_ctx
|
||||
|
||||
/* HTTP Response code */
|
||||
int response_code;
|
||||
|
||||
/* Private */
|
||||
int ret;
|
||||
void *evbase;
|
||||
};
|
||||
|
||||
struct http_icy_metadata
|
||||
@ -86,6 +82,14 @@ http_client_request(struct http_client_ctx *ctx, struct http_client_session *ses
|
||||
char *
|
||||
http_form_urlencode(struct keyval *kv);
|
||||
|
||||
/* The reverse of http_form_urlencode, except takes a full url as input.
|
||||
*
|
||||
* @param kv keyval struct allocated by caller where values will be added
|
||||
* @param url with the query to decode
|
||||
* @return 0 if ok, otherwise -1
|
||||
*/
|
||||
int
|
||||
http_form_urldecode(struct keyval *kv, const char *uri);
|
||||
|
||||
/* Returns a newly allocated string with the first stream in the m3u given in
|
||||
* url. If url is not a m3u, the string will be a copy of url.
|
||||
|
||||
1897
src/httpd.c
1897
src/httpd.c
File diff suppressed because it is too large
Load Diff
148
src/httpd.h
148
src/httpd.h
@ -2,112 +2,7 @@
|
||||
#ifndef __HTTPD_H__
|
||||
#define __HTTPD_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <regex.h>
|
||||
#include <time.h>
|
||||
#include <event2/http.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
|
||||
enum httpd_send_flags
|
||||
{
|
||||
HTTPD_SEND_NO_GZIP = (1 << 0),
|
||||
};
|
||||
|
||||
/*
|
||||
* Contains a parsed version of the URI httpd got. The URI may have been
|
||||
* complete:
|
||||
* scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
|
||||
* or relative:
|
||||
* [/path][?query][#fragment]
|
||||
*
|
||||
* We are interested in the path and the query, so they are disassembled to
|
||||
* path_parts and ev_query. If the request is http://x:3689/foo/bar?key1=val1,
|
||||
* then part_parts[0] is "foo", [1] is "bar" and the rest is null.
|
||||
*
|
||||
* Each path_part is an allocated URI decoded string.
|
||||
*/
|
||||
struct httpd_uri_parsed
|
||||
{
|
||||
const char *uri;
|
||||
struct evhttp_uri *ev_uri;
|
||||
struct evkeyvalq ev_query;
|
||||
char *uri_decoded;
|
||||
char *path;
|
||||
char *path_parts[31];
|
||||
};
|
||||
|
||||
/*
|
||||
* A collection of pointers to request data that the reply handlers may need.
|
||||
* Also has the function pointer to the reply handler and a pointer to a reply
|
||||
* evbuffer.
|
||||
*/
|
||||
struct httpd_request {
|
||||
// User-agent (if available)
|
||||
const char *user_agent;
|
||||
// The parsed request URI given to us by httpd_uri_parse
|
||||
struct httpd_uri_parsed *uri_parsed;
|
||||
// Shortcut to &uri_parsed->ev_query
|
||||
struct evkeyvalq *query;
|
||||
// http request struct (if available)
|
||||
struct evhttp_request *req;
|
||||
// Source IP address (ipv4 or ipv6) and port of the request (if available)
|
||||
char *peer_address;
|
||||
unsigned short peer_port;
|
||||
// A pointer to extra data that the module handling the request might need
|
||||
void *extra_data;
|
||||
|
||||
// Reply evbuffer
|
||||
struct evbuffer *reply;
|
||||
|
||||
// A pointer to the handler that will process the request
|
||||
int (*handler)(struct httpd_request *hreq);
|
||||
};
|
||||
|
||||
/*
|
||||
* Maps a regex of the request path to a handler of the request
|
||||
*/
|
||||
struct httpd_uri_map
|
||||
{
|
||||
int method;
|
||||
char *regexp;
|
||||
int (*handler)(struct httpd_request *hreq);
|
||||
regex_t preg;
|
||||
};
|
||||
|
||||
/*
|
||||
* Helper to free the parsed uri struct
|
||||
*/
|
||||
void
|
||||
httpd_uri_free(struct httpd_uri_parsed *parsed);
|
||||
|
||||
/*
|
||||
* Parse an URI into the struct
|
||||
*/
|
||||
struct httpd_uri_parsed *
|
||||
httpd_uri_parse(const char *uri);
|
||||
|
||||
/*
|
||||
* Parse a request into the httpd_request struct. It can later be freed with
|
||||
* free(), unless the module has allocated something to *extra_data. Note that
|
||||
* the pointers in the returned struct are only valid as long as the inputs are
|
||||
* still valid. If req is not null, then we will find the user-agent from the
|
||||
* request headers, except if provided as an argument to this function.
|
||||
*/
|
||||
struct httpd_request *
|
||||
httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed, const char *user_agent, struct httpd_uri_map *uri_map);
|
||||
|
||||
void
|
||||
httpd_stream_file(struct evhttp_request *req, int id);
|
||||
|
||||
bool
|
||||
httpd_request_not_modified_since(struct evhttp_request *req, time_t mtime);
|
||||
|
||||
bool
|
||||
httpd_request_etag_matches(struct evhttp_request *req, const char *etag);
|
||||
|
||||
void
|
||||
httpd_response_not_cachable(struct evhttp_request *req);
|
||||
|
||||
/*
|
||||
* Gzips an evbuffer
|
||||
@ -118,49 +13,6 @@ httpd_response_not_cachable(struct evhttp_request *req);
|
||||
struct evbuffer *
|
||||
httpd_gzip_deflate(struct evbuffer *in);
|
||||
|
||||
/*
|
||||
* This wrapper around evhttp_send_reply should be used whenever a request may
|
||||
* come from a browser. It will automatically gzip if feasible, but the caller
|
||||
* may direct it not to. It will set CORS headers as appropriate. Should be
|
||||
* thread safe.
|
||||
*
|
||||
* @in req The evhttp request struct
|
||||
* @in code HTTP code, e.g. 200
|
||||
* @in reason A brief explanation of the error - if NULL the standard meaning
|
||||
of the error code will be used
|
||||
* @in evbuf Data for the response body
|
||||
* @in flags See flags above
|
||||
*/
|
||||
void
|
||||
httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *evbuf, enum httpd_send_flags flags);
|
||||
|
||||
/*
|
||||
* This is a substitute for evhttp_send_error that should be used whenever an
|
||||
* error may be returned to a browser. It will set CORS headers as appropriate,
|
||||
* which is not possible with evhttp_send_error, because it clears the headers.
|
||||
* Should be thread safe.
|
||||
*
|
||||
* @in req The evhttp request struct
|
||||
* @in error HTTP code, e.g. 200
|
||||
* @in reason A brief explanation of the error - if NULL the standard meaning
|
||||
of the error code will be used
|
||||
*/
|
||||
void
|
||||
httpd_send_error(struct evhttp_request *req, int error, const char *reason);
|
||||
|
||||
/*
|
||||
* Redirects to the given path
|
||||
*/
|
||||
void
|
||||
httpd_redirect_to(struct evhttp_request *req, const char *path);
|
||||
|
||||
|
||||
bool
|
||||
httpd_admin_check_auth(struct evhttp_request *req);
|
||||
|
||||
int
|
||||
httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm);
|
||||
|
||||
int
|
||||
httpd_init(const char *webroot);
|
||||
|
||||
|
||||
@ -20,12 +20,11 @@
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <regex.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "httpd_artworkapi.h"
|
||||
#include "httpd_internal.h"
|
||||
#include "logger.h"
|
||||
#include "misc.h"
|
||||
#include "player.h"
|
||||
@ -40,20 +39,20 @@ request_process(struct httpd_request *hreq, uint32_t *max_w, uint32_t *max_h)
|
||||
*max_w = 0;
|
||||
*max_h = 0;
|
||||
|
||||
param = evhttp_find_header(hreq->query, "maxwidth");
|
||||
param = httpd_query_value_find(hreq->query, "maxwidth");
|
||||
if (param)
|
||||
{
|
||||
ret = safe_atou32(param, max_w);
|
||||
if (ret < 0)
|
||||
DPRINTF(E_LOG, L_WEB, "Invalid width in request: '%s'\n", hreq->uri_parsed->uri);
|
||||
DPRINTF(E_LOG, L_WEB, "Invalid width in request: '%s'\n", hreq->uri);
|
||||
}
|
||||
|
||||
param = evhttp_find_header(hreq->query, "maxheight");
|
||||
param = httpd_query_value_find(hreq->query, "maxheight");
|
||||
if (param)
|
||||
{
|
||||
ret = safe_atou32(param, max_h);
|
||||
if (ret < 0)
|
||||
DPRINTF(E_LOG, L_WEB, "Invalid height in request: '%s'\n", hreq->uri_parsed->uri);
|
||||
DPRINTF(E_LOG, L_WEB, "Invalid height in request: '%s'\n", hreq->uri);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -62,14 +61,10 @@ request_process(struct httpd_request *hreq, uint32_t *max_w, uint32_t *max_h)
|
||||
static int
|
||||
response_process(struct httpd_request *hreq, int format)
|
||||
{
|
||||
struct evkeyvalq *headers;
|
||||
|
||||
headers = evhttp_request_get_output_headers(hreq->req);
|
||||
|
||||
if (format == ART_FMT_PNG)
|
||||
evhttp_add_header(headers, "Content-Type", "image/png");
|
||||
httpd_header_add(hreq->out_headers, "Content-Type", "image/png");
|
||||
else if (format == ART_FMT_JPEG)
|
||||
evhttp_add_header(headers, "Content-Type", "image/jpeg");
|
||||
httpd_header_add(hreq->out_headers, "Content-Type", "image/jpeg");
|
||||
else
|
||||
return HTTP_NOCONTENT;
|
||||
|
||||
@ -79,20 +74,20 @@ response_process(struct httpd_request *hreq, int format)
|
||||
static int
|
||||
artworkapi_reply_nowplaying(struct httpd_request *hreq)
|
||||
{
|
||||
struct player_status status;
|
||||
uint32_t max_w;
|
||||
uint32_t max_h;
|
||||
uint32_t id;
|
||||
int ret;
|
||||
|
||||
ret = request_process(hreq, &max_w, &max_h);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
ret = player_playing_now(&id);
|
||||
if (ret != 0)
|
||||
player_get_status(&status);
|
||||
if (status.status == PLAY_STOPPED)
|
||||
return HTTP_NOTFOUND;
|
||||
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0);
|
||||
ret = artwork_get_by_queue_item_id(hreq->out_body, status.item_id, max_w, max_h, 0);
|
||||
|
||||
return response_process(hreq, ret);
|
||||
}
|
||||
@ -109,11 +104,11 @@ artworkapi_reply_item(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
ret = safe_atou32(hreq->uri_parsed->path_parts[2], &id);
|
||||
ret = safe_atou32(hreq->path_parts[2], &id);
|
||||
if (ret != 0)
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0);
|
||||
ret = artwork_get_by_file_id(hreq->out_body, id, max_w, max_h, 0);
|
||||
|
||||
return response_process(hreq, ret);
|
||||
}
|
||||
@ -130,111 +125,73 @@ artworkapi_reply_group(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
ret = safe_atou32(hreq->uri_parsed->path_parts[2], &id);
|
||||
ret = safe_atou32(hreq->path_parts[2], &id);
|
||||
if (ret != 0)
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
ret = artwork_get_group(hreq->reply, id, max_w, max_h, 0);
|
||||
ret = artwork_get_by_group_id(hreq->out_body, id, max_w, max_h, 0);
|
||||
|
||||
return response_process(hreq, ret);
|
||||
}
|
||||
|
||||
static struct httpd_uri_map artworkapi_handlers[] =
|
||||
{
|
||||
{ EVHTTP_REQ_GET, "^/artwork/nowplaying$", artworkapi_reply_nowplaying },
|
||||
{ EVHTTP_REQ_GET, "^/artwork/item/[[:digit:]]+$", artworkapi_reply_item },
|
||||
{ EVHTTP_REQ_GET, "^/artwork/group/[[:digit:]]+$", artworkapi_reply_group },
|
||||
{ HTTPD_METHOD_GET, "^/artwork/nowplaying$", artworkapi_reply_nowplaying },
|
||||
{ HTTPD_METHOD_GET, "^/artwork/item/[[:digit:]]+$", artworkapi_reply_item },
|
||||
{ HTTPD_METHOD_GET, "^/artwork/group/[[:digit:]]+$", artworkapi_reply_group },
|
||||
{ 0, NULL, NULL }
|
||||
};
|
||||
|
||||
|
||||
/* ------------------------------- API --------------------------------- */
|
||||
void
|
||||
artworkapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed)
|
||||
|
||||
static void
|
||||
artworkapi_request(struct httpd_request *hreq)
|
||||
{
|
||||
struct httpd_request *hreq;
|
||||
int status_code;
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "Artwork api request: '%s'\n", uri_parsed->uri);
|
||||
|
||||
if (!httpd_admin_check_auth(req))
|
||||
if (!httpd_request_is_authorized(hreq))
|
||||
return;
|
||||
|
||||
hreq = httpd_request_parse(req, uri_parsed, NULL, artworkapi_handlers);
|
||||
if (!hreq)
|
||||
if (!hreq->handler)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Unrecognized path '%s' in artwork api request: '%s'\n", uri_parsed->path, uri_parsed->uri);
|
||||
DPRINTF(E_LOG, L_WEB, "Unrecognized path in artwork api request: '%s'\n", hreq->uri);
|
||||
|
||||
httpd_send_error(req, HTTP_BADREQUEST, "Bad Request");
|
||||
httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
CHECK_NULL(L_WEB, hreq->reply = evbuffer_new());
|
||||
|
||||
status_code = hreq->handler(hreq);
|
||||
|
||||
switch (status_code)
|
||||
{
|
||||
case HTTP_OK: /* 200 OK */
|
||||
httpd_send_reply(req, status_code, "OK", hreq->reply, HTTPD_SEND_NO_GZIP);
|
||||
httpd_send_reply(hreq, status_code, "OK", HTTPD_SEND_NO_GZIP);
|
||||
break;
|
||||
case HTTP_NOCONTENT: /* 204 No Content */
|
||||
httpd_send_reply(req, status_code, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP);
|
||||
httpd_send_reply(hreq, status_code, "No Content", HTTPD_SEND_NO_GZIP);
|
||||
break;
|
||||
case HTTP_NOTMODIFIED: /* 304 Not Modified */
|
||||
httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP);
|
||||
httpd_send_reply(hreq, HTTP_NOTMODIFIED, NULL, HTTPD_SEND_NO_GZIP);
|
||||
break;
|
||||
case HTTP_BADREQUEST: /* 400 Bad Request */
|
||||
httpd_send_error(req, status_code, "Bad Request");
|
||||
httpd_send_error(hreq, status_code, "Bad Request");
|
||||
break;
|
||||
case HTTP_NOTFOUND: /* 404 Not Found */
|
||||
httpd_send_error(req, status_code, "Not Found");
|
||||
httpd_send_error(hreq, status_code, "Not Found");
|
||||
break;
|
||||
case HTTP_INTERNAL: /* 500 Internal Server Error */
|
||||
default:
|
||||
httpd_send_error(req, HTTP_INTERNAL, "Internal Server Error");
|
||||
httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error");
|
||||
}
|
||||
|
||||
evbuffer_free(hreq->reply);
|
||||
free(hreq);
|
||||
}
|
||||
|
||||
int
|
||||
artworkapi_is_request(const char *path)
|
||||
struct httpd_module httpd_artworkapi =
|
||||
{
|
||||
if (strncmp(path, "/artwork/", strlen("/artwork/")) == 0)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
artworkapi_init(void)
|
||||
{
|
||||
char buf[64];
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
for (i = 0; artworkapi_handlers[i].handler; i++)
|
||||
{
|
||||
ret = regcomp(&artworkapi_handlers[i].preg, artworkapi_handlers[i].regexp, REG_EXTENDED | REG_NOSUB);
|
||||
if (ret != 0)
|
||||
{
|
||||
regerror(ret, &artworkapi_handlers[i].preg, buf, sizeof(buf));
|
||||
|
||||
DPRINTF(E_FATAL, L_WEB, "artwork api init failed; regexp error: %s\n", buf);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
artworkapi_deinit(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; artworkapi_handlers[i].handler; i++)
|
||||
regfree(&artworkapi_handlers[i].preg);
|
||||
}
|
||||
.name = "Artwork API",
|
||||
.type = MODULE_ARTWORKAPI,
|
||||
.logdomain = L_WEB,
|
||||
.subpaths = { "/artwork/", NULL },
|
||||
.handlers = artworkapi_handlers,
|
||||
.request = artworkapi_request,
|
||||
};
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
#ifndef __HTTPD_ARTWORK_H__
|
||||
#define __HTTPD_ARTWORK_H__
|
||||
|
||||
#include "httpd.h"
|
||||
|
||||
int
|
||||
artworkapi_init(void);
|
||||
|
||||
void
|
||||
artworkapi_deinit(void);
|
||||
|
||||
void
|
||||
artworkapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed);
|
||||
|
||||
int
|
||||
artworkapi_is_request(const char *path);
|
||||
|
||||
#endif
|
||||
591
src/httpd_daap.c
591
src/httpd_daap.c
File diff suppressed because it is too large
Load Diff
@ -2,20 +2,6 @@
|
||||
#ifndef __HTTPD_DAAP_H__
|
||||
#define __HTTPD_DAAP_H__
|
||||
|
||||
#include "httpd.h"
|
||||
|
||||
int
|
||||
daap_init(void);
|
||||
|
||||
void
|
||||
daap_deinit(void);
|
||||
|
||||
void
|
||||
daap_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed);
|
||||
|
||||
int
|
||||
daap_is_request(const char *path);
|
||||
|
||||
int
|
||||
daap_session_is_valid(int id);
|
||||
|
||||
|
||||
895
src/httpd_dacp.c
895
src/httpd_dacp.c
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user