mirror of
https://github.com/owntone/owntone-server.git
synced 2025-10-30 00:05:05 -04:00
Compare commits
1085 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 |
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
|
||||
|
||||
87
.github/workflows/macos.yml
vendored
87
.github/workflows/macos.yml
vendored
@ -1,35 +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
|
||||
|
||||
- name: Update brew
|
||||
run: brew update
|
||||
- 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
|
||||
@ -42,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
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -39,7 +39,11 @@ owntone@.service
|
||||
/.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 \
|
||||
|
||||
19
README.md
19
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).
|
||||
|
||||
50
configure.ac
50
configure.ac
@ -1,7 +1,7 @@
|
||||
dnl Process this file with autoconf to produce a configure script.
|
||||
|
||||
AC_PREREQ([2.60])
|
||||
AC_INIT([owntone], [28.7])
|
||||
AC_INIT([owntone], [29.0])
|
||||
|
||||
AC_CONFIG_SRCDIR([config.h.in])
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
@ -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 syscall])
|
||||
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],
|
||||
@ -149,7 +160,13 @@ OWNTONE_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0],
|
||||
])
|
||||
|
||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2.1.4],
|
||||
[event_base_new], [event2/event.h])
|
||||
[event_base_new], [event2/event.h],
|
||||
[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])
|
||||
|
||||
@ -178,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])
|
||||
@ -245,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 *])
|
||||
|
||||
@ -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 running 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.
|
||||
161
docs/building.md
161
docs/building.md
@ -1,11 +1,11 @@
|
||||
# Build instructions for OwnTone
|
||||
# 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 users
|
||||
## Quick Version for Debian/Ubuntu
|
||||
|
||||
If you are the lucky kind, this should get you all the required tools and
|
||||
libraries:
|
||||
@ -15,7 +15,7 @@ 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 \
|
||||
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
|
||||
```
|
||||
@ -29,7 +29,7 @@ argument when you run ./configure:
|
||||
Feature | Configure argument | Packages
|
||||
---------------------|--------------------------|-------------------------------------
|
||||
Chromecast | `--enable-chromecast` | libgnutls*-dev
|
||||
Pulseaudio | `--with-pulseaudio` | libpulse-dev
|
||||
PulseAudio | `--with-pulseaudio` | libpulse-dev
|
||||
|
||||
These features can be disabled saving you package dependencies:
|
||||
|
||||
@ -73,7 +73,7 @@ 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 \
|
||||
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
|
||||
@ -110,38 +110,37 @@ running with `sudo systemctl status owntone`.
|
||||
|
||||
See the [Documentation](getting-started.md) for usage information.
|
||||
|
||||
## Quick version for FreeBSD
|
||||
## 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)
|
||||
## 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)
|
||||
## "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!
|
||||
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>
|
||||
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
|
||||
libplist libsodium protobuf-c libxml2
|
||||
```
|
||||
|
||||
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>
|
||||
Download, configure, build and install the [libinotify-kqueue library](https://github.com/libinotify-kqueue/libinotify-kqueue)
|
||||
|
||||
Add the following to `.bashrc`:
|
||||
|
||||
@ -158,16 +157,16 @@ Optional features require the following additional ports:
|
||||
Feature | Configure argument | Ports
|
||||
--------------------|--------------------------|-------------------
|
||||
Chromecast | `--enable-chromecast` | gnutls
|
||||
Pulseaudio | `--with-pulseaudio` | pulseaudio
|
||||
PulseAudio | `--with-pulseaudio` | pulseaudio
|
||||
|
||||
Clone the OwnTone repo:
|
||||
Clone the OwnTone repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/owntone/owntone-server.git
|
||||
cd owntone-server
|
||||
```
|
||||
|
||||
Finally, configure, build and install, adding configure arguments for
|
||||
Finally, configure, build, install, and add configuration arguments for
|
||||
optional features:
|
||||
|
||||
```bash
|
||||
@ -177,11 +176,11 @@ make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
Note: if for some reason you've installed the avahi port, you need to
|
||||
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:
|
||||
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
|
||||
@ -195,14 +194,14 @@ Run OwnTone:
|
||||
sudo /usr/local/sbin/owntone
|
||||
```
|
||||
|
||||
Verify it's running (you need to <kbd>Ctrl</kbd>+<kbd>C</kbd> to stop
|
||||
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
|
||||
## Long Version - Requirements
|
||||
|
||||
Required tools:
|
||||
|
||||
@ -216,53 +215,34 @@ Required tools:
|
||||
|
||||
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.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)
|
||||
- [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
|
||||
- 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/>
|
||||
- [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)
|
||||
|
||||
If using binary packages, remember that you need the development packages to
|
||||
build OwnTone (usually named -dev or -devel).
|
||||
Note: If using binary packages, remember that you need the development packages to
|
||||
build OwnTone (usually suffixed with -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
|
||||
## Long Version - Building and Installing
|
||||
|
||||
Start by generating the build system by running `autoreconf -i`. This will
|
||||
generate the configure script and `Makefile.in`.
|
||||
@ -292,13 +272,18 @@ The source for the player web interface is located under the `web-src` folder an
|
||||
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.
|
||||
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 Pulseaudio is optional. It will be enabled if the library is
|
||||
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:
|
||||
@ -328,7 +313,24 @@ 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).
|
||||
|
||||
## Non-priviliged user version (for development)
|
||||
## 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.
|
||||
@ -337,6 +339,7 @@ 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
|
||||
@ -345,6 +348,7 @@ 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
|
||||
@ -362,10 +366,10 @@ make install
|
||||
Edit `owntone_data/etc/owntone.conf`, find the following configuration settings
|
||||
and set them to these values:
|
||||
|
||||
```
|
||||
uid = ${USER}
|
||||
loglevel = "debug"
|
||||
directories = { "${HOME}/owntone_data/media" }
|
||||
```conf
|
||||
uid = ${USER}
|
||||
loglevel = "debug"
|
||||
directories = { "${HOME}/owntone_data/media" }
|
||||
```
|
||||
|
||||
Run the server:
|
||||
@ -373,4 +377,5 @@ Run the server:
|
||||
```bash
|
||||
./src/owntone -f
|
||||
```
|
||||
(you can also use the copy of the binary in `$HOME/owntone_data/usr/sbin`)
|
||||
|
||||
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
|
||||
- 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://SERVER_ADDRESS: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://SERVER_ADDRESS: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.md) (easiest) like `mpc`
|
||||
- curl with OwnTone's JSON API (see [JSON API docs](../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
|
||||
@ -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)
|
||||
|
||||
@ -6,12 +6,13 @@ instructions are [here](building.md).
|
||||
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:
|
||||
|
||||
Platform | How to get
|
||||
----------------------|---------------------------------------------------------
|
||||
RPi w/Raspberry Pi OS | Add OwnTone repository to apt sources, see:<br>[OwnTone server (iTunes server) - Raspberry Pi Forums](http://www.raspberrypi.org/phpBB3/viewtopic.php?t=49928)
|
||||
Debian/Ubuntu amd64 | Download .deb as [artifact from Github workflow](https://github.com/owntone/owntone-apt/actions)<br>(requires that you are logged in)
|
||||
OpenWrt | Run `opkg install libwebsockets-full owntone`
|
||||
Docker | See [linuxserver/docker-daapd](https://github.com/linuxserver/docker-daapd)
|
||||
|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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
408
docs/json-api.md
408
docs/json-api.md
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,6 @@ directories only.
|
||||
|
||||
Files starting with . (dot) and _ (underscore) are ignored.
|
||||
|
||||
|
||||
## Pipes (for e.g. multiroom with Shairport-sync)
|
||||
|
||||
Some programs, like for instance Shairport-sync, can be configured to output
|
||||
@ -38,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.
|
||||
|
||||
@ -47,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
|
||||
@ -57,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,21 +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://owntone.local:3689/stream.mp3](http://owntone.local:3689/stream.mp3)
|
||||
or
|
||||
http://SERVER_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,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -11,16 +11,15 @@
|
||||
<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="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">
|
||||
<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],[
|
||||
|
||||
76
mkdocs.yml
76
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,31 +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,
|
||||
@ -187,19 +188,25 @@ library {
|
||||
# Should we import the content of iTunes smart playlists?
|
||||
# itunes_smartpl = false
|
||||
|
||||
# Decoding options for DAAP and RSP 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 only
|
||||
# affect serving media to DAAP and RSP clients, they have no effect on
|
||||
# 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:
|
||||
@ -223,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.
|
||||
@ -315,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
|
||||
@ -417,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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -20,7 +20,7 @@ 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:"
|
||||
|
||||
@ -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,6 +88,7 @@ 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 \
|
||||
@ -107,6 +108,7 @@ owntone_SOURCES = main.c \
|
||||
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 \
|
||||
@ -125,10 +127,10 @@ owntone_SOURCES = main.c \
|
||||
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) \
|
||||
|
||||
311
src/artwork.c
311
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;
|
||||
@ -192,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 */
|
||||
@ -208,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
|
||||
@ -351,6 +357,24 @@ 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);
|
||||
@ -457,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;
|
||||
}
|
||||
|
||||
@ -604,6 +627,8 @@ size_calculate(int *dst_w, int *dst_h, int src_w, int src_h, int max_w, int max_
|
||||
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 };
|
||||
@ -637,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)
|
||||
@ -702,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)
|
||||
@ -918,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
|
||||
*/
|
||||
@ -932,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)
|
||||
@ -1114,7 +1152,7 @@ online_source_response_parse(char **artwork_url, const struct online_source *src
|
||||
}
|
||||
|
||||
static int
|
||||
online_source_request_url_make(char *url, size_t url_size, const 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 };
|
||||
@ -1157,6 +1195,13 @@ online_source_request_url_make(char *url, size_t url_size, const struct online_s
|
||||
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$"))
|
||||
@ -1176,14 +1221,17 @@ online_source_request_url_make(char *url, size_t url_size, const struct online_s
|
||||
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)
|
||||
@ -1211,18 +1259,13 @@ online_source_search_check_last(char **last_artwork_url, const struct online_sou
|
||||
struct online_search_history *history = src->search_history;
|
||||
bool is_same;
|
||||
|
||||
pthread_mutex_lock(&history->mutex);
|
||||
|
||||
is_same = (hash == history->last_hash) &&
|
||||
(max_w == history->last_max_w) &&
|
||||
(max_h == history->last_max_h);
|
||||
|
||||
// Copy this to the caller while we have the lock anyway
|
||||
if (is_same)
|
||||
*last_artwork_url = safe_strdup(history->last_artwork_url);
|
||||
|
||||
pthread_mutex_unlock(&history->mutex);
|
||||
|
||||
return is_same ? 0 : -1;
|
||||
}
|
||||
|
||||
@ -1232,8 +1275,6 @@ 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(&history->mutex);
|
||||
|
||||
// If the last request was more than ONLINE_SEARCH_COOLDOWN_TIME ago we will always try again
|
||||
if (time(NULL) > history->last_timestamp + ONLINE_SEARCH_COOLDOWN_TIME)
|
||||
is_failing = false;
|
||||
@ -1250,22 +1291,20 @@ online_source_is_failing(const struct online_source *src, int id)
|
||||
else
|
||||
is_failing = true;
|
||||
|
||||
pthread_mutex_unlock(&history->mutex);
|
||||
|
||||
return is_failing;
|
||||
}
|
||||
|
||||
static void
|
||||
online_source_history_update(const 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)
|
||||
{
|
||||
struct online_search_history *history = src->search_history;
|
||||
|
||||
pthread_mutex_lock(&history->mutex);
|
||||
|
||||
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(history->last_artwork_url);
|
||||
history->last_artwork_url = safe_strdup(artwork_url); // FIXME should free this on exit
|
||||
@ -1274,14 +1313,12 @@ online_source_history_update(const struct online_source *src, int id, uint32_t r
|
||||
history->count_failures = 0;
|
||||
else
|
||||
history->count_failures++;
|
||||
|
||||
pthread_mutex_unlock(&history->mutex);
|
||||
}
|
||||
|
||||
static int
|
||||
auth_header_add(struct keyval *headers, const struct online_source *src)
|
||||
{
|
||||
char auth_header[256];
|
||||
char auth_header[512];
|
||||
char *auth_key;
|
||||
char *auth_secret;
|
||||
int ret;
|
||||
@ -1311,34 +1348,24 @@ auth_header_add(struct keyval *headers, const struct online_source *src)
|
||||
}
|
||||
|
||||
static char *
|
||||
online_source_search(const 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];
|
||||
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;
|
||||
@ -1351,18 +1378,18 @@ online_source_search(const struct online_source *src, struct artwork_ctx *ctx)
|
||||
}
|
||||
|
||||
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)
|
||||
@ -1375,16 +1402,41 @@ online_source_search(const 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(const struct online_source *src)
|
||||
{
|
||||
@ -1577,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;
|
||||
}
|
||||
@ -1613,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.
|
||||
@ -1807,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;
|
||||
@ -1955,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;
|
||||
|
||||
@ -1973,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;
|
||||
@ -1993,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)
|
||||
@ -2023,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)
|
||||
@ -2066,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);
|
||||
|
||||
@ -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,10 +116,14 @@ 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),
|
||||
@ -156,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()
|
||||
};
|
||||
|
||||
@ -208,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()
|
||||
};
|
||||
|
||||
@ -229,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),
|
||||
@ -307,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)
|
||||
{
|
||||
@ -420,7 +462,6 @@ conffile_expand_libname(cfg_t *lib)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
conffile_load(char *file)
|
||||
{
|
||||
@ -461,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)
|
||||
|
||||
310
src/db.c
310
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;
|
||||
@ -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
|
||||
|
||||
33
src/db.h
33
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
|
||||
@ -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;
|
||||
|
||||
@ -224,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:
|
||||
@ -247,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,12 +360,11 @@ dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errms
|
||||
}
|
||||
|
||||
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;
|
||||
@ -444,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);
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ void
|
||||
dmap_error_make(struct evbuffer *evbuf, 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);
|
||||
|
||||
38
src/http.c
38
src/http.c
@ -36,6 +36,7 @@
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
@ -171,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)
|
||||
{
|
||||
@ -193,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)
|
||||
{
|
||||
|
||||
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.
|
||||
|
||||
331
src/httpd.c
331
src/httpd.c
@ -34,10 +34,6 @@
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#ifdef HAVE_SYSCALL
|
||||
#include <sys/syscall.h> // get thread ID
|
||||
#endif
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <regex.h>
|
||||
@ -52,9 +48,13 @@
|
||||
#include "httpd.h"
|
||||
#include "httpd_internal.h"
|
||||
#include "transcode.h"
|
||||
#include "cache.h"
|
||||
#include "listener.h"
|
||||
#include "player.h"
|
||||
#ifdef LASTFM
|
||||
# include "lastfm.h"
|
||||
#endif
|
||||
#include "listenbrainz.h"
|
||||
#ifdef HAVE_LIBWEBSOCKETS
|
||||
# include "websocket.h"
|
||||
#endif
|
||||
@ -66,10 +66,6 @@
|
||||
"<h1>%s</h1>\n" \
|
||||
"</body>\n</html>\n"
|
||||
|
||||
#define HTTPD_STREAM_SAMPLE_RATE 44100
|
||||
#define HTTPD_STREAM_BPS 16
|
||||
#define HTTPD_STREAM_CHANNELS 2
|
||||
|
||||
extern struct httpd_module httpd_dacp;
|
||||
extern struct httpd_module httpd_daap;
|
||||
extern struct httpd_module httpd_jsonapi;
|
||||
@ -93,6 +89,7 @@ static struct httpd_module *httpd_modules[] = {
|
||||
|
||||
struct content_type_map {
|
||||
char *ext;
|
||||
enum transcode_profile profile;
|
||||
char *ctype;
|
||||
};
|
||||
|
||||
@ -106,21 +103,25 @@ struct stream_ctx {
|
||||
off_t offset;
|
||||
off_t start_offset;
|
||||
off_t end_offset;
|
||||
int marked;
|
||||
bool no_register_playback;
|
||||
struct transcode_ctx *xcode;
|
||||
};
|
||||
|
||||
static const struct content_type_map ext2ctype[] =
|
||||
{
|
||||
{ ".html", "text/html; charset=utf-8" },
|
||||
{ ".xml", "text/xml; charset=utf-8" },
|
||||
{ ".css", "text/css; charset=utf-8" },
|
||||
{ ".txt", "text/plain; charset=utf-8" },
|
||||
{ ".js", "application/javascript; charset=utf-8" },
|
||||
{ ".gif", "image/gif" },
|
||||
{ ".ico", "image/x-ico" },
|
||||
{ ".png", "image/png" },
|
||||
{ NULL, NULL }
|
||||
{ ".html", XCODE_NONE, "text/html; charset=utf-8" },
|
||||
{ ".xml", XCODE_NONE, "text/xml; charset=utf-8" },
|
||||
{ ".css", XCODE_NONE, "text/css; charset=utf-8" },
|
||||
{ ".txt", XCODE_NONE, "text/plain; charset=utf-8" },
|
||||
{ ".js", XCODE_NONE, "application/javascript; charset=utf-8" },
|
||||
{ ".gif", XCODE_NONE, "image/gif" },
|
||||
{ ".ico", XCODE_NONE, "image/x-ico" },
|
||||
{ ".png", XCODE_PNG, "image/png" },
|
||||
{ ".jpg", XCODE_JPEG, "image/jpeg" },
|
||||
{ ".mp3", XCODE_MP3, "audio/mpeg" },
|
||||
{ ".m4a", XCODE_MP4_ALAC, "audio/mp4" },
|
||||
{ ".wav", XCODE_WAV, "audio/wav" },
|
||||
{ NULL, XCODE_NONE, NULL }
|
||||
};
|
||||
|
||||
static char webroot_directory[PATH_MAX];
|
||||
@ -162,16 +163,85 @@ playcount_inc_cb(void *arg)
|
||||
db_file_inc_playcount(*id);
|
||||
}
|
||||
|
||||
#ifdef LASTFM
|
||||
/* Callback from the worker thread (async operation as it may block) */
|
||||
static void
|
||||
scrobble_cb(void *arg)
|
||||
{
|
||||
int *id = arg;
|
||||
|
||||
#ifdef LASTFM
|
||||
lastfm_scrobble(*id);
|
||||
}
|
||||
#endif
|
||||
listenbrainz_scrobble(*id);
|
||||
}
|
||||
|
||||
static const char *
|
||||
content_type_from_ext(const char *ext)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (!ext)
|
||||
return NULL;
|
||||
|
||||
for (i = 0; ext2ctype[i].ext; i++)
|
||||
{
|
||||
if (strcmp(ext, ext2ctype[i].ext) == 0)
|
||||
return ext2ctype[i].ctype;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *
|
||||
content_type_from_profile(enum transcode_profile profile)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (profile == XCODE_NONE)
|
||||
return NULL;
|
||||
|
||||
for (i = 0; ext2ctype[i].ext; i++)
|
||||
{
|
||||
if (profile == ext2ctype[i].profile)
|
||||
return ext2ctype[i].ctype;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
basic_auth_cred_extract(char **user, char **pwd, const char *auth)
|
||||
{
|
||||
char *decoded = NULL;
|
||||
regex_t preg = { 0 };
|
||||
regmatch_t matchptr[3]; // Room for entire string, username substring and password substring
|
||||
int ret;
|
||||
|
||||
decoded = (char *)b64_decode(NULL, auth);
|
||||
if (!decoded)
|
||||
goto error;
|
||||
|
||||
// Apple Music gives is "(dt:1):password", which we need to support even if it
|
||||
// isn't according to the basic auth RFC that says the username cannot include
|
||||
// a colon
|
||||
ret = regcomp(&preg, "(\\(.*?\\)|[^:]*):(.*)", REG_EXTENDED);
|
||||
if (ret != 0)
|
||||
goto error;
|
||||
|
||||
ret = regexec(&preg, decoded, ARRAY_SIZE(matchptr), matchptr, 0);
|
||||
if (ret != 0 || matchptr[1].rm_so == -1 || matchptr[2].rm_so == -1)
|
||||
goto error;
|
||||
|
||||
*user = strndup(decoded + matchptr[1].rm_so, matchptr[1].rm_eo - matchptr[1].rm_so);
|
||||
*pwd = strndup(decoded + matchptr[2].rm_so, matchptr[2].rm_eo - matchptr[2].rm_so);
|
||||
|
||||
free(decoded);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
free(decoded);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------- MODULES INTERFACE ---------------------------- */
|
||||
@ -424,19 +494,16 @@ httpd_response_not_cachable(struct httpd_request *hreq)
|
||||
static void
|
||||
serve_file(struct httpd_request *hreq)
|
||||
{
|
||||
char *ext;
|
||||
char path[PATH_MAX];
|
||||
char deref[PATH_MAX];
|
||||
char *ctype;
|
||||
const char *ctype;
|
||||
struct stat sb;
|
||||
int fd;
|
||||
int i;
|
||||
uint8_t buf[4096];
|
||||
bool slashed;
|
||||
int ret;
|
||||
|
||||
/* Check authentication */
|
||||
if (!httpd_admin_check_auth(hreq))
|
||||
if (!httpd_request_is_authorized(hreq))
|
||||
return;
|
||||
|
||||
ret = snprintf(path, sizeof(path), "%s%s", webroot_directory, hreq->path);
|
||||
@ -544,19 +611,9 @@ serve_file(struct httpd_request *hreq)
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ctype = "application/octet-stream";
|
||||
ext = strrchr(path, '.');
|
||||
if (ext)
|
||||
{
|
||||
for (i = 0; ext2ctype[i].ext; i++)
|
||||
{
|
||||
if (strcmp(ext, ext2ctype[i].ext) == 0)
|
||||
{
|
||||
ctype = ext2ctype[i].ctype;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ctype = content_type_from_ext(strrchr(path, '.'));
|
||||
if (!ctype)
|
||||
ctype = "application/octet-stream";
|
||||
|
||||
httpd_header_add(hreq->out_headers, "Content-Type", ctype);
|
||||
|
||||
@ -570,7 +627,6 @@ serve_file(struct httpd_request *hreq)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- STREAM HANDLING ----------------------------- */
|
||||
|
||||
// This will triggered in a httpd thread, but since the reading may be in a
|
||||
@ -612,15 +668,13 @@ stream_end(struct stream_ctx *st)
|
||||
static void
|
||||
stream_end_register(struct stream_ctx *st)
|
||||
{
|
||||
if (!st->marked
|
||||
if (!st->no_register_playback
|
||||
&& (st->stream_size > ((st->size * 50) / 100))
|
||||
&& (st->offset > ((st->size * 80) / 100)))
|
||||
{
|
||||
st->marked = 1;
|
||||
st->no_register_playback = true;
|
||||
worker_execute(playcount_inc_cb, &st->id, sizeof(int), 0);
|
||||
#ifdef LASTFM
|
||||
worker_execute(scrobble_cb, &st->id, sizeof(int), 1);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -643,6 +697,9 @@ stream_new(struct media_file_info *mfi, struct httpd_request *hreq, event_callba
|
||||
|
||||
event_active(st->ev, 0, 0);
|
||||
|
||||
if (httpd_query_value_find(hreq->query, "no_register_playback"))
|
||||
st->no_register_playback = true;
|
||||
|
||||
st->id = mfi->id;
|
||||
st->hreq = hreq;
|
||||
return st;
|
||||
@ -653,10 +710,19 @@ stream_new(struct media_file_info *mfi, struct httpd_request *hreq, event_callba
|
||||
}
|
||||
|
||||
static struct stream_ctx *
|
||||
stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, int64_t offset, int64_t end_offset, event_callback_fn stream_cb)
|
||||
stream_new_transcode(struct media_file_info *mfi, enum transcode_profile profile, struct httpd_request *hreq,
|
||||
int64_t offset, int64_t end_offset, event_callback_fn stream_cb)
|
||||
{
|
||||
struct transcode_decode_setup_args decode_args = { 0 };
|
||||
struct transcode_encode_setup_args encode_args = { 0 };
|
||||
struct media_quality quality = { 0 };
|
||||
struct evbuffer *prepared_header = NULL;
|
||||
struct stream_ctx *st;
|
||||
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, 0 };
|
||||
int cached;
|
||||
int ret;
|
||||
|
||||
// We use source sample rate etc, but for MP3 we must set a bit rate
|
||||
quality.bit_rate = 1000 * cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
|
||||
|
||||
st = stream_new(mfi, hreq, stream_cb);
|
||||
if (!st)
|
||||
@ -664,7 +730,27 @@ stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, in
|
||||
goto error;
|
||||
}
|
||||
|
||||
st->xcode = transcode_setup(XCODE_PCM16_HEADER, &quality, mfi->data_kind, mfi->path, mfi->song_length, &st->size);
|
||||
if (profile == XCODE_MP4_ALAC)
|
||||
{
|
||||
CHECK_NULL(L_HTTPD, prepared_header = evbuffer_new());
|
||||
|
||||
ret = cache_xcode_header_get(prepared_header, &cached, mfi->id, "mp4");
|
||||
if (ret < 0 || !cached) // Error or not found
|
||||
{
|
||||
evbuffer_free(prepared_header);
|
||||
prepared_header = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
decode_args.profile = profile;
|
||||
decode_args.is_http = (mfi->data_kind == DATA_KIND_HTTP);
|
||||
decode_args.path = mfi->path;
|
||||
decode_args.len_ms = mfi->song_length;
|
||||
encode_args.profile = profile;
|
||||
encode_args.quality = &quality;
|
||||
encode_args.prepared_header = prepared_header;
|
||||
|
||||
st->xcode = transcode_setup(decode_args, encode_args);
|
||||
if (!st->xcode)
|
||||
{
|
||||
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");
|
||||
@ -673,15 +759,28 @@ stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, in
|
||||
goto error;
|
||||
}
|
||||
|
||||
st->size = transcode_encode_query(st->xcode->encode_ctx, "estimated_size");
|
||||
if (st->size < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, could not determine estimated size\n");
|
||||
|
||||
httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error");
|
||||
goto error;
|
||||
}
|
||||
|
||||
st->stream_size = st->size - offset;
|
||||
if (end_offset > 0)
|
||||
st->stream_size -= (st->size - end_offset);
|
||||
|
||||
st->start_offset = offset;
|
||||
|
||||
if (prepared_header)
|
||||
evbuffer_free(prepared_header);
|
||||
return st;
|
||||
|
||||
error:
|
||||
if (prepared_header)
|
||||
evbuffer_free(prepared_header);
|
||||
stream_free(st);
|
||||
return NULL;
|
||||
}
|
||||
@ -821,7 +920,7 @@ stream_chunk_raw_cb(int fd, short event, void *arg)
|
||||
if (ret == 0)
|
||||
DPRINTF(E_INFO, L_HTTPD, "Done streaming file id %d\n", st->id);
|
||||
else
|
||||
DPRINTF(E_LOG, L_HTTPD, "Streaming error, file id %d\n", st->id);
|
||||
DPRINTF(E_LOG, L_HTTPD, "Read error, file id %d: %s\n", st->id, strerror(errno));
|
||||
|
||||
stream_end(st);
|
||||
return;
|
||||
@ -845,6 +944,39 @@ stream_fail_cb(void *arg)
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- SPEAKER/CACHE HANDLING ------------------------ */
|
||||
|
||||
// Thread: player (must not block)
|
||||
static void
|
||||
speaker_enum_cb(struct player_speaker_info *spk, void *arg)
|
||||
{
|
||||
bool *want_mp4 = arg;
|
||||
|
||||
*want_mp4 = *want_mp4 || (spk->format == MEDIA_FORMAT_ALAC && strcmp(spk->output_type, "RCP/SoundBridge") == 0);
|
||||
}
|
||||
|
||||
// Thread: worker
|
||||
static void
|
||||
speaker_update_handler_cb(void *arg)
|
||||
{
|
||||
const char *prefer_format = cfg_getstr(cfg_getsec(cfg, "library"), "prefer_format");
|
||||
bool want_mp4;
|
||||
|
||||
want_mp4 = (prefer_format && (strcmp(prefer_format, "alac") == 0));
|
||||
if (!want_mp4)
|
||||
player_speaker_enumerate(speaker_enum_cb, &want_mp4);
|
||||
|
||||
cache_xcode_toggle(want_mp4);
|
||||
}
|
||||
|
||||
// Thread: player (must not block)
|
||||
static void
|
||||
httpd_speaker_update_handler(short event_mask, void *ctx)
|
||||
{
|
||||
worker_execute(speaker_update_handler_cb, NULL, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- REQUEST CALLBACKS --------------------------- */
|
||||
|
||||
// Worker thread, invoked by request_cb() below
|
||||
@ -853,9 +985,7 @@ request_async_cb(void *arg)
|
||||
{
|
||||
struct httpd_request *hreq = *(struct httpd_request **)arg;
|
||||
|
||||
#ifdef HAVE_SYSCALL
|
||||
DPRINTF(E_DBG, hreq->module->logdomain, "%s request '%s' in worker thread %ld\n", hreq->module->name, hreq->uri, syscall(SYS_gettid));
|
||||
#endif
|
||||
DPRINTF(E_DBG, hreq->module->logdomain, "%s request '%s'\n", hreq->module->name, hreq->uri);
|
||||
|
||||
// Some handlers require an evbase to schedule events
|
||||
hreq->evbase = worker_evbase_get();
|
||||
@ -913,12 +1043,14 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
||||
{
|
||||
struct media_file_info *mfi = NULL;
|
||||
struct stream_ctx *st = NULL;
|
||||
enum transcode_profile profile;
|
||||
enum transcode_profile spk_profile;
|
||||
const char *param;
|
||||
const char *param_end;
|
||||
const char *ctype;
|
||||
char buf[64];
|
||||
int64_t offset = 0;
|
||||
int64_t end_offset = 0;
|
||||
int transcode;
|
||||
int ret;
|
||||
|
||||
param = httpd_header_find(hreq->in_headers, "Range");
|
||||
@ -973,18 +1105,33 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
||||
}
|
||||
|
||||
param = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
profile = transcode_needed(hreq->user_agent, param, mfi->codectype);
|
||||
if (profile == XCODE_UNKNOWN)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTPD, "Could not serve '%s' to client, unable to determine output format\n", mfi->path);
|
||||
|
||||
transcode = transcode_needed(hreq->user_agent, param, mfi->codectype);
|
||||
if (transcode)
|
||||
httpd_send_error(hreq, HTTP_INTERNAL, "Cannot stream, unable to determine output format");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (profile != XCODE_NONE)
|
||||
{
|
||||
DPRINTF(E_INFO, L_HTTPD, "Preparing to transcode %s\n", mfi->path);
|
||||
|
||||
st = stream_new_transcode(mfi, hreq, offset, end_offset, stream_chunk_xcode_cb);
|
||||
spk_profile = httpd_xcode_profile_get(hreq);
|
||||
if (spk_profile != XCODE_NONE)
|
||||
profile = spk_profile;
|
||||
|
||||
st = stream_new_transcode(mfi, profile, hreq, offset, end_offset, stream_chunk_xcode_cb);
|
||||
if (!st)
|
||||
goto error;
|
||||
|
||||
ctype = content_type_from_profile(profile);
|
||||
if (!ctype)
|
||||
goto error;
|
||||
|
||||
if (!httpd_header_find(hreq->out_headers, "Content-Type"))
|
||||
httpd_header_add(hreq->out_headers, "Content-Type", "audio/wav");
|
||||
httpd_header_add(hreq->out_headers, "Content-Type", ctype);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1027,7 +1174,7 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
||||
// If we are not decoding, send the Content-Length. We don't do that if we
|
||||
// are decoding because we can only guesstimate the size in this case and
|
||||
// the error margin is unknown and variable.
|
||||
if (!transcode)
|
||||
if (profile == XCODE_NONE)
|
||||
{
|
||||
ret = snprintf(buf, sizeof(buf), "%" PRIi64, (int64_t)st->size);
|
||||
if ((ret < 0) || (ret >= sizeof(buf)))
|
||||
@ -1059,7 +1206,7 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
||||
}
|
||||
|
||||
#ifdef HAVE_POSIX_FADVISE
|
||||
if (!transcode)
|
||||
if (profile == XCODE_NONE)
|
||||
{
|
||||
// Hint the OS
|
||||
if ( (ret = posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_WILLNEED)) != 0 ||
|
||||
@ -1081,6 +1228,39 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
||||
free_mfi(mfi, 0);
|
||||
}
|
||||
|
||||
// Returns enum transcode_profile, but is just declared with int so we don't
|
||||
// need to include transcode.h in httpd_internal.h
|
||||
int
|
||||
httpd_xcode_profile_get(struct httpd_request *hreq)
|
||||
{
|
||||
struct player_speaker_info spk;
|
||||
int ret;
|
||||
|
||||
// No peer address if the function is called from httpd_daap.c when the DAAP
|
||||
// cache is being updated
|
||||
if (!hreq->peer_address || !hreq->user_agent)
|
||||
return XCODE_NONE;
|
||||
|
||||
// A Roku Soundbridge may also be RCP device/speaker for which the user may
|
||||
// have set a prefered streaming format, but in all other cases we don't use
|
||||
// speaker configuration (so caller will let transcode_needed decide)
|
||||
if (strncmp(hreq->user_agent, "Roku", strlen("Roku")) != 0)
|
||||
return XCODE_NONE;
|
||||
|
||||
ret = player_speaker_get_byaddress(&spk, hreq->peer_address);
|
||||
if (ret < 0)
|
||||
return XCODE_NONE;
|
||||
|
||||
if (spk.format == MEDIA_FORMAT_WAV)
|
||||
return XCODE_WAV;
|
||||
if (spk.format == MEDIA_FORMAT_MP3)
|
||||
return XCODE_MP3;
|
||||
if (spk.format == MEDIA_FORMAT_ALAC)
|
||||
return XCODE_MP4_ALAC;
|
||||
|
||||
return XCODE_NONE;
|
||||
}
|
||||
|
||||
struct evbuffer *
|
||||
httpd_gzip_deflate(struct evbuffer *in)
|
||||
{
|
||||
@ -1148,7 +1328,6 @@ httpd_gzip_deflate(struct evbuffer *in)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// The httpd_send functions below can be called from a worker thread (with
|
||||
// hreq->is_async) or directly from the httpd thread. In the former case, they
|
||||
// will command sending from the httpd thread, since it is not safe to access
|
||||
@ -1225,12 +1404,18 @@ httpd_send_error(struct httpd_request *hreq, int error, const char *reason)
|
||||
}
|
||||
|
||||
bool
|
||||
httpd_admin_check_auth(struct httpd_request *hreq)
|
||||
httpd_request_is_trusted(struct httpd_request *hreq)
|
||||
{
|
||||
return httpd_backend_peer_is_trusted(hreq->backend);
|
||||
}
|
||||
|
||||
bool
|
||||
httpd_request_is_authorized(struct httpd_request *hreq)
|
||||
{
|
||||
const char *passwd;
|
||||
int ret;
|
||||
|
||||
if (net_peer_address_is_trusted(hreq->peer_address))
|
||||
if (httpd_request_is_trusted(hreq))
|
||||
return true;
|
||||
|
||||
passwd = cfg_getstr(cfg_getsec(cfg, "general"), "admin_password");
|
||||
@ -1284,26 +1469,14 @@ httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passw
|
||||
|
||||
auth += strlen("Basic ");
|
||||
|
||||
authuser = (char *)b64_decode(NULL, auth);
|
||||
if (!authuser)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTPD, "Could not decode Authentication header\n");
|
||||
|
||||
goto need_auth;
|
||||
}
|
||||
|
||||
authpwd = strchr(authuser, ':');
|
||||
if (!authpwd)
|
||||
ret = basic_auth_cred_extract(&authuser, &authpwd, auth);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_HTTPD, "Malformed Authentication header\n");
|
||||
|
||||
free(authuser);
|
||||
goto need_auth;
|
||||
}
|
||||
|
||||
*authpwd = '\0';
|
||||
authpwd++;
|
||||
|
||||
if (user)
|
||||
{
|
||||
if (strcmp(user, authuser) != 0)
|
||||
@ -1311,6 +1484,7 @@ httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passw
|
||||
DPRINTF(E_LOG, L_HTTPD, "Username mismatch\n");
|
||||
|
||||
free(authuser);
|
||||
free(authpwd);
|
||||
goto need_auth;
|
||||
}
|
||||
}
|
||||
@ -1320,11 +1494,12 @@ httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passw
|
||||
DPRINTF(E_LOG, L_HTTPD, "Bad password\n");
|
||||
|
||||
free(authuser);
|
||||
free(authpwd);
|
||||
goto need_auth;
|
||||
}
|
||||
|
||||
free(authuser);
|
||||
|
||||
free(authpwd);
|
||||
return 0;
|
||||
|
||||
need_auth:
|
||||
@ -1457,6 +1632,10 @@ httpd_init(const char *webroot)
|
||||
goto error;
|
||||
}
|
||||
|
||||
// We need to know about speaker format changes so we can ask the cache to
|
||||
// start preparing headers for mp4/alac if selected
|
||||
listener_add(httpd_speaker_update_handler, LISTENER_SPEAKER, NULL);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
@ -1468,6 +1647,8 @@ httpd_init(const char *webroot)
|
||||
void
|
||||
httpd_deinit(void)
|
||||
{
|
||||
listener_remove(httpd_speaker_update_handler);
|
||||
|
||||
// Give modules a chance to hang up connections nicely
|
||||
modules_deinit();
|
||||
|
||||
|
||||
@ -74,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->out_body, 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);
|
||||
}
|
||||
@ -108,7 +108,7 @@ artworkapi_reply_item(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
ret = artwork_get_item(hreq->out_body, 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);
|
||||
}
|
||||
@ -129,7 +129,7 @@ artworkapi_reply_group(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
ret = artwork_get_group(hreq->out_body, 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);
|
||||
}
|
||||
@ -150,7 +150,7 @@ artworkapi_request(struct httpd_request *hreq)
|
||||
{
|
||||
int status_code;
|
||||
|
||||
if (!httpd_admin_check_auth(hreq))
|
||||
if (!httpd_request_is_authorized(hreq))
|
||||
return;
|
||||
|
||||
if (!hreq->handler)
|
||||
|
||||
@ -693,7 +693,7 @@ daap_request_authorize(struct httpd_request *hreq)
|
||||
char *passwd;
|
||||
int ret;
|
||||
|
||||
if (net_peer_address_is_trusted(hreq->peer_address))
|
||||
if (httpd_request_is_trusted(hreq))
|
||||
return 0;
|
||||
|
||||
// Regular DAAP clients like iTunes will login with /login, and we will reply
|
||||
@ -898,7 +898,7 @@ daap_reply_login(struct httpd_request *hreq)
|
||||
CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 32));
|
||||
|
||||
param = httpd_query_value_find(hreq->query, "pairing-guid");
|
||||
if (param && !net_peer_address_is_trusted(hreq->peer_address))
|
||||
if (param && !httpd_request_is_trusted(hreq))
|
||||
{
|
||||
if (strlen(param) < 3)
|
||||
{
|
||||
@ -1146,14 +1146,17 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
|
||||
const struct dmap_field **meta = NULL;
|
||||
struct sort_ctx *sctx;
|
||||
const char *param;
|
||||
const char *client_codecs;
|
||||
const char *accept_codecs;
|
||||
const char *tag;
|
||||
char *last_codectype;
|
||||
size_t len;
|
||||
enum transcode_profile spk_profile;
|
||||
enum transcode_profile profile;
|
||||
struct transcode_metadata_string xcode_metadata;
|
||||
struct media_quality quality = { 0 };
|
||||
uint32_t len_ms;
|
||||
int nmeta = 0;
|
||||
int sort_headers;
|
||||
int nsongs;
|
||||
int transcode;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist);
|
||||
@ -1214,37 +1217,50 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
|
||||
goto error;
|
||||
}
|
||||
|
||||
client_codecs = NULL;
|
||||
accept_codecs = NULL;
|
||||
if (!s->is_remote && hreq->in_headers)
|
||||
{
|
||||
client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
accept_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
}
|
||||
|
||||
spk_profile = httpd_xcode_profile_get(hreq);
|
||||
|
||||
DPRINTF(E_DBG, L_DAAP, "Speaker check of '%s' (codecs '%s') returned %d\n", hreq->user_agent, accept_codecs, spk_profile);
|
||||
|
||||
nsongs = 0;
|
||||
last_codectype = NULL;
|
||||
while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
|
||||
{
|
||||
nsongs++;
|
||||
|
||||
if (!dbmfi.codectype)
|
||||
// Not sure if the is_remote path is really needed. Note that if you
|
||||
// change the below you might need to do the same in rsp_reply_playlist()
|
||||
profile = s->is_remote ? XCODE_WAV : transcode_needed(hreq->user_agent, accept_codecs, dbmfi.codectype);
|
||||
if (profile == XCODE_UNKNOWN)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
|
||||
|
||||
transcode = 0;
|
||||
}
|
||||
else if (s->is_remote)
|
||||
else if (profile != XCODE_NONE)
|
||||
{
|
||||
transcode = 1;
|
||||
}
|
||||
else if (!last_codectype || (strcmp(last_codectype, dbmfi.codectype) != 0))
|
||||
{
|
||||
transcode = transcode_needed(hreq->user_agent, client_codecs, dbmfi.codectype);
|
||||
if (spk_profile != XCODE_NONE)
|
||||
profile = spk_profile;
|
||||
|
||||
free(last_codectype);
|
||||
last_codectype = strdup(dbmfi.codectype);
|
||||
if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
|
||||
len_ms = 3 * 60 * 1000; // just a fallback default
|
||||
|
||||
safe_atoi32(dbmfi.samplerate, &quality.sample_rate);
|
||||
safe_atoi32(dbmfi.bits_per_sample, &quality.bits_per_sample);
|
||||
safe_atoi32(dbmfi.channels, &quality.channels);
|
||||
quality.bit_rate = cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
|
||||
|
||||
transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
|
||||
dbmfi.type = xcode_metadata.type;
|
||||
dbmfi.codectype = xcode_metadata.codectype;
|
||||
dbmfi.description = xcode_metadata.description;
|
||||
dbmfi.file_size = xcode_metadata.file_size;
|
||||
dbmfi.bitrate = xcode_metadata.bitrate;
|
||||
}
|
||||
|
||||
ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers, transcode);
|
||||
ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Failed to encode song metadata\n");
|
||||
@ -1270,7 +1286,6 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
|
||||
|
||||
DPRINTF(E_DBG, L_DAAP, "Done with song list, %d songs\n", nsongs);
|
||||
|
||||
free(last_codectype);
|
||||
db_query_end(&qp);
|
||||
|
||||
if (ret == -100)
|
||||
@ -1971,9 +1986,9 @@ daap_reply_extra_data(struct httpd_request *hreq)
|
||||
}
|
||||
|
||||
if (strcmp(hreq->path_parts[2], "groups") == 0)
|
||||
ret = artwork_get_group(hreq->out_body, id, max_w, max_h, 0);
|
||||
ret = artwork_get_by_group_id(hreq->out_body, id, max_w, max_h, 0);
|
||||
else if (strcmp(hreq->path_parts[2], "items") == 0)
|
||||
ret = artwork_get_item(hreq->out_body, id, max_w, max_h, 0);
|
||||
ret = artwork_get_by_file_id(hreq->out_body, id, max_w, max_h, 0);
|
||||
|
||||
len = evbuffer_get_length(hreq->out_body);
|
||||
|
||||
@ -2264,15 +2279,15 @@ daap_request(struct httpd_request *hreq)
|
||||
|
||||
ret = hreq->handler(hreq);
|
||||
|
||||
daap_reply_send(hreq, ret);
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &end);
|
||||
msec = (end.tv_sec * 1000 + end.tv_nsec / 1000000) - (start.tv_sec * 1000 + start.tv_nsec / 1000000);
|
||||
|
||||
DPRINTF(E_DBG, L_DAAP, "DAAP request handled in %d milliseconds\n", msec);
|
||||
|
||||
if (ret == DAAP_REPLY_OK && msec > cache_daap_threshold() && hreq->user_agent)
|
||||
if (ret == DAAP_REPLY_OK && msec > cache_daap_threshold_get() && hreq->user_agent)
|
||||
cache_daap_add(hreq->uri, hreq->user_agent, ((struct daap_session *)hreq->extra_data)->is_remote, msec);
|
||||
|
||||
daap_reply_send(hreq, ret); // hreq is deallocted
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
111
src/httpd_dacp.c
111
src/httpd_dacp.c
@ -40,6 +40,7 @@
|
||||
#include "conffile.h"
|
||||
#include "artwork.h"
|
||||
#include "dmap_common.h"
|
||||
#include "library.h"
|
||||
#include "db.h"
|
||||
#include "player.h"
|
||||
#include "listener.h"
|
||||
@ -163,7 +164,7 @@ dacp_nowplaying(struct evbuffer *evbuf, struct player_status *status, struct db_
|
||||
* FIXME: Giving the client invalid ids on purpose is hardly ideal, but the
|
||||
* clients don't seem to use these ids for anything other than rating.
|
||||
*/
|
||||
if (queue_item->data_kind == DATA_KIND_HTTP || queue_item->data_kind == DATA_KIND_PIPE)
|
||||
if (queue_item->data_kind == DATA_KIND_HTTP || queue_item->data_kind == DATA_KIND_PIPE || queue_item->file_id == DB_MEDIA_FILE_NON_PERSISTENT_ID)
|
||||
{
|
||||
// Could also use queue_item->queue_version, but it changes a bit too much
|
||||
// leading to Remote reloading too much
|
||||
@ -599,7 +600,7 @@ dacp_request_authorize(struct httpd_request *hreq)
|
||||
int32_t id;
|
||||
int ret;
|
||||
|
||||
if (net_peer_address_is_trusted(hreq->peer_address))
|
||||
if (httpd_request_is_trusted(hreq))
|
||||
return 0;
|
||||
|
||||
param = httpd_query_value_find(hreq->query, "session-id");
|
||||
@ -786,7 +787,7 @@ update_fail_cb(void *arg)
|
||||
|
||||
/* Thread: player */
|
||||
static void
|
||||
dacp_playstatus_update_handler(short event_mask)
|
||||
dacp_playstatus_update_handler(short event_mask, void *ctx)
|
||||
{
|
||||
struct dacp_update_request *ur;
|
||||
|
||||
@ -1106,31 +1107,7 @@ dacp_propset_userrating(const char *value, struct httpd_request *hreq)
|
||||
return;
|
||||
}
|
||||
|
||||
ret = db_file_rating_update_byid(itemid, rating);
|
||||
|
||||
/* If no mfi, it may be because we sent an invalid nowplaying itemid. In this
|
||||
* case request the real one from the player and default to that.
|
||||
*/
|
||||
if (ret == 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DACP, "Invalid id %d for rating, defaulting to player id\n", itemid);
|
||||
|
||||
ret = player_playing_now(&itemid);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ret = db_file_rating_update_byid(itemid, rating);
|
||||
if (ret <= 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
library_item_attrib_save(itemid, LIBRARY_ATTRIB_RATING, rating);
|
||||
}
|
||||
|
||||
|
||||
@ -1397,6 +1374,7 @@ dacp_reply_playspec(struct httpd_request *hreq)
|
||||
const char *shuffle;
|
||||
uint32_t plid;
|
||||
uint32_t id;
|
||||
struct query_params qp = { 0 };
|
||||
struct db_queue_item *queue_item = NULL;
|
||||
int ret;
|
||||
|
||||
@ -1482,11 +1460,10 @@ dacp_reply_playspec(struct httpd_request *hreq)
|
||||
|
||||
db_queue_clear(0);
|
||||
|
||||
if (plid > 0)
|
||||
ret = db_queue_add_by_playlistid(plid, status.shuffle, status.item_id, -1, NULL, NULL);
|
||||
else if (id > 0)
|
||||
ret = db_queue_add_by_fileid(id, status.shuffle, status.item_id, -1, NULL, NULL);
|
||||
qp.type = (plid > 0) ? Q_PLITEMS : Q_ITEMS;
|
||||
qp.id = (plid > 0) ? plid : id;
|
||||
|
||||
ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DACP, "Could not build song queue from playlist %d\n", plid);
|
||||
@ -1875,15 +1852,16 @@ dacp_reply_playqueueedit_clear(struct httpd_request *hreq)
|
||||
const char *param;
|
||||
struct player_status status;
|
||||
|
||||
param = httpd_query_value_find(hreq->query, "mode");
|
||||
|
||||
/*
|
||||
* The mode parameter contains the playlist to be cleared.
|
||||
* If mode=0x68697374 (hex representation of the ascii string "hist") clear the history,
|
||||
* otherwise the current playlist.
|
||||
*/
|
||||
if (strcmp(param,"0x68697374") == 0)
|
||||
player_queue_clear_history();
|
||||
param = httpd_query_value_find(hreq->query, "mode");
|
||||
if (param && strcmp(param,"0x68697374") == 0)
|
||||
{
|
||||
player_queue_clear_history();
|
||||
}
|
||||
else
|
||||
{
|
||||
player_get_status(&status);
|
||||
@ -1916,6 +1894,7 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq)
|
||||
const char *querymodifier;
|
||||
const char *sort;
|
||||
const char *param;
|
||||
const char *ptr;
|
||||
char modifiedquery[32];
|
||||
int mode;
|
||||
int plid;
|
||||
@ -1977,7 +1956,8 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq)
|
||||
else
|
||||
{
|
||||
// Modify the query: Take the id from the editquery and use it as a queuefilter playlist id
|
||||
ret = safe_atoi32(strchr(editquery, ':') + 1, &plid);
|
||||
ptr = strchr(editquery, ':');
|
||||
ret = ptr ? safe_atoi32(ptr + 1, &plid) : -1;
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DACP, "Invalid playlist id in request: %s\n", editquery);
|
||||
@ -2051,38 +2031,44 @@ dacp_reply_playqueueedit_move(struct httpd_request *hreq)
|
||||
struct player_status status;
|
||||
int ret;
|
||||
const char *param;
|
||||
const char *ptr;
|
||||
int src;
|
||||
int dst;
|
||||
|
||||
param = httpd_query_value_find(hreq->query, "edit-params");
|
||||
if (param)
|
||||
{
|
||||
ret = safe_atoi32(strchr(param, ':') + 1, &src);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DACP, "Invalid edit-params move-from value in playqueue-edit request\n");
|
||||
if (!param)
|
||||
goto out;
|
||||
|
||||
dacp_send_error(hreq, "cacr", "Invalid request");
|
||||
return -1;
|
||||
}
|
||||
ptr = strchr(param, ':');
|
||||
if (!ptr)
|
||||
goto error;
|
||||
|
||||
ret = safe_atoi32(strchr(param, ',') + 1, &dst);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DACP, "Invalid edit-params move-to value in playqueue-edit request\n");
|
||||
ret = safe_atoi32(ptr + 1, &src);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
dacp_send_error(hreq, "cacr", "Invalid request");
|
||||
return -1;
|
||||
}
|
||||
ptr = strchr(param, ',');
|
||||
if (!ptr)
|
||||
goto error;
|
||||
|
||||
player_get_status(&status);
|
||||
db_queue_move_byposrelativetoitem(src, dst, status.item_id, status.shuffle);
|
||||
}
|
||||
ret = safe_atoi32(ptr + 1, &dst);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
player_get_status(&status);
|
||||
db_queue_move_byposrelativetoitem(src, dst, status.item_id, status.shuffle);
|
||||
|
||||
out:
|
||||
/* 204 No Content is the canonical reply */
|
||||
httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
DPRINTF(E_LOG, L_DACP, "Invalid edit-params in playqueue-edit request: '%s'\n", param);
|
||||
|
||||
dacp_send_error(hreq, "cacr", "Invalid request");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
@ -2271,11 +2257,11 @@ dacp_reply_playstatusupdate(struct httpd_request *hreq)
|
||||
static int
|
||||
dacp_reply_nowplayingartwork(struct httpd_request *hreq)
|
||||
{
|
||||
struct player_status status;
|
||||
char clen[32];
|
||||
const char *param;
|
||||
char *ctype;
|
||||
size_t len;
|
||||
uint32_t id;
|
||||
int max_w;
|
||||
int max_h;
|
||||
int ret;
|
||||
@ -2312,11 +2298,11 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq)
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = player_playing_now(&id);
|
||||
if (ret < 0)
|
||||
player_get_status(&status);
|
||||
if (status.status == PLAY_STOPPED)
|
||||
goto no_artwork;
|
||||
|
||||
ret = artwork_get_item(hreq->out_body, id, max_w, max_h, 0);
|
||||
ret = artwork_get_by_queue_item_id(hreq->out_body, status.item_id, max_w, max_h, 0);
|
||||
len = evbuffer_get_length(hreq->out_body);
|
||||
|
||||
switch (ret)
|
||||
@ -2560,8 +2546,7 @@ dacp_reply_setspeakers(struct httpd_request *hreq)
|
||||
}
|
||||
|
||||
nspk = 1;
|
||||
ptr = param;
|
||||
while ((ptr = strchr(ptr + 1, ',')))
|
||||
for (ptr = param; ptr; ptr = strchr(ptr + 1, ','))
|
||||
nspk++;
|
||||
|
||||
CHECK_NULL(L_DACP, ids = calloc((nspk + 1), sizeof(uint64_t)));
|
||||
@ -2841,7 +2826,7 @@ dacp_init(void)
|
||||
|
||||
CHECK_ERR(L_DACP, mutex_init(&update_request_lck));
|
||||
update_current_rev = 2;
|
||||
listener_add(dacp_playstatus_update_handler, LISTENER_PLAYER | LISTENER_VOLUME | LISTENER_QUEUE);
|
||||
listener_add(dacp_playstatus_update_handler, LISTENER_PLAYER | LISTENER_VOLUME | LISTENER_QUEUE, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -206,6 +206,9 @@ struct httpd_request {
|
||||
void
|
||||
httpd_stream_file(struct httpd_request *hreq, int id);
|
||||
|
||||
int
|
||||
httpd_xcode_profile_get(struct httpd_request *hreq);
|
||||
|
||||
void
|
||||
httpd_request_handler_set(struct httpd_request *hreq);
|
||||
|
||||
@ -224,7 +227,8 @@ httpd_response_not_cachable(struct httpd_request *hreq);
|
||||
* may direct it not to. It will set CORS headers as appropriate. Should be
|
||||
* thread safe.
|
||||
*
|
||||
* @in req The http request struct
|
||||
* @in hreq The http request struct. NOTE: is automatically deallocated if
|
||||
* this is the final reply.
|
||||
* @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
|
||||
@ -248,7 +252,8 @@ httpd_send_reply_end(struct httpd_request *hreq);
|
||||
* which is not possible with evhttp_send_error, because it clears the headers.
|
||||
* Should be thread safe.
|
||||
*
|
||||
* @in req The http request struct
|
||||
* @in hreq The http request struct. NOTE: is automatically deallocated if
|
||||
* this is the final reply.
|
||||
* @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
|
||||
@ -256,12 +261,18 @@ httpd_send_reply_end(struct httpd_request *hreq);
|
||||
void
|
||||
httpd_send_error(struct httpd_request *hreq, int error, const char *reason);
|
||||
|
||||
|
||||
void
|
||||
httpd_redirect_to(struct httpd_request *hreq, const char *path);
|
||||
|
||||
/*
|
||||
* The request either came from a trusted peer (based on ip address) checked by
|
||||
* httpd_request_is_trusted() or was WWW-authenticated via httpd_basic_auth()
|
||||
*/
|
||||
bool
|
||||
httpd_admin_check_auth(struct httpd_request *hreq);
|
||||
httpd_request_is_authorized(struct httpd_request *hreq);
|
||||
|
||||
bool
|
||||
httpd_request_is_trusted(struct httpd_request *hreq);
|
||||
|
||||
int
|
||||
httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passwd, const char *realm);
|
||||
@ -342,6 +353,9 @@ httpd_backend_input_buffer_get(httpd_backend *backend);
|
||||
int
|
||||
httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend, httpd_backend_data *backend_data);
|
||||
|
||||
bool
|
||||
httpd_backend_peer_is_trusted(httpd_backend *backend);
|
||||
|
||||
int
|
||||
httpd_backend_method_get(enum httpd_methods *method, httpd_backend *backend);
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
# include "lastfm.h"
|
||||
#endif
|
||||
#include "library.h"
|
||||
#include "listenbrainz.h"
|
||||
#include "logger.h"
|
||||
#include "misc.h"
|
||||
#include "misc_json.h"
|
||||
@ -59,6 +60,22 @@
|
||||
# include "inputs/spotify.h"
|
||||
#endif
|
||||
|
||||
struct track_attribs
|
||||
{
|
||||
enum library_attrib type;
|
||||
const char *name;
|
||||
};
|
||||
|
||||
// Currently these must all be uint32
|
||||
static const struct track_attribs track_attribs[] =
|
||||
{
|
||||
{ LIBRARY_ATTRIB_PLAY_COUNT, "play_count", },
|
||||
{ LIBRARY_ATTRIB_SKIP_COUNT, "skip_count", },
|
||||
{ LIBRARY_ATTRIB_TIME_PLAYED, "time_played", },
|
||||
{ LIBRARY_ATTRIB_TIME_SKIPPED, "time_skipped", },
|
||||
{ LIBRARY_ATTRIB_RATING, "rating", },
|
||||
{ LIBRARY_ATTRIB_USERMARK, "usermark", },
|
||||
};
|
||||
|
||||
static bool allow_modifying_stored_playlists;
|
||||
static char *default_playlist_directory;
|
||||
@ -322,7 +339,7 @@ track_to_json(struct db_media_file_info *dbmfi)
|
||||
|
||||
safe_json_add_string(item, "path", dbmfi->path);
|
||||
|
||||
ret = snprintf(uri, sizeof(uri), "%s:%s:%s", "library", "track", dbmfi->id);
|
||||
ret = snprintf(uri, sizeof(uri), "library:track:%s", dbmfi->id);
|
||||
if (ret < sizeof(uri))
|
||||
json_object_object_add(item, "uri", json_object_new_string(uri));
|
||||
|
||||
@ -330,28 +347,10 @@ track_to_json(struct db_media_file_info *dbmfi)
|
||||
if (ret < sizeof(artwork_url))
|
||||
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
||||
|
||||
safe_json_add_string(item, "lyrics", dbmfi->lyrics);
|
||||
return item;
|
||||
}
|
||||
|
||||
// TODO Only partially implemented. A full implementation should use a mapping
|
||||
// table, which should also be used above in track_to_json(). It should also
|
||||
// return errors if there are incorrect/mispelled fields, but not sure how to
|
||||
// walk a json object with json-c.
|
||||
static int
|
||||
json_to_track(struct media_file_info *mfi, json_object *json)
|
||||
{
|
||||
if (jparse_contains_key(json, "id", json_type_int))
|
||||
mfi->id = jparse_int_from_obj(json, "id");
|
||||
if (jparse_contains_key(json, "usermark", json_type_int))
|
||||
mfi->usermark = jparse_int_from_obj(json, "usermark");
|
||||
if (jparse_contains_key(json, "rating", json_type_int))
|
||||
mfi->rating = jparse_int_from_obj(json, "rating");
|
||||
if (jparse_contains_key(json, "play_count", json_type_int))
|
||||
mfi->play_count = jparse_int_from_obj(json, "play_count");
|
||||
|
||||
return HTTP_OK;
|
||||
}
|
||||
|
||||
static json_object *
|
||||
playlist_to_json(struct db_playlist_info *dbpli)
|
||||
{
|
||||
@ -543,7 +542,7 @@ fetch_artist(bool *notfound, const char *artist_id)
|
||||
if ((ret = db_query_fetch_group(&dbgri, &query_params)) == 0)
|
||||
{
|
||||
artist = artist_to_json(&dbgri);
|
||||
notfound = false;
|
||||
*notfound = false;
|
||||
}
|
||||
|
||||
error:
|
||||
@ -1285,8 +1284,6 @@ jsonapi_reply_spotify(struct httpd_request *hreq)
|
||||
CHECK_NULL(L_WEB, jreply = json_object_new_object());
|
||||
|
||||
#ifdef SPOTIFY
|
||||
int httpd_port;
|
||||
char redirect_uri[256];
|
||||
char *oauth_uri;
|
||||
struct spotify_status sp_status;
|
||||
struct spotifywebapi_status_info webapi_info;
|
||||
@ -1294,10 +1291,7 @@ jsonapi_reply_spotify(struct httpd_request *hreq)
|
||||
|
||||
json_object_object_add(jreply, "enabled", json_object_new_boolean(true));
|
||||
|
||||
httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port");
|
||||
snprintf(redirect_uri, sizeof(redirect_uri), "http://owntone.local:%d/oauth/spotify", httpd_port);
|
||||
|
||||
oauth_uri = spotifywebapi_oauth_uri_get(redirect_uri);
|
||||
oauth_uri = spotifywebapi_oauth_uri_get();
|
||||
if (!oauth_uri)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Cannot display Spotify oauth interface (http_form_uriencode() failed)\n");
|
||||
@ -1319,6 +1313,8 @@ jsonapi_reply_spotify(struct httpd_request *hreq)
|
||||
safe_json_add_string(jreply, "webapi_country", webapi_info.country);
|
||||
safe_json_add_string(jreply, "webapi_granted_scope", webapi_info.granted_scope);
|
||||
safe_json_add_string(jreply, "webapi_required_scope", webapi_info.required_scope);
|
||||
safe_json_add_string(jreply, "webapi_client_id", webapi_info.client_id);
|
||||
safe_json_add_string(jreply, "webapi_client_secret", webapi_info.client_secret);
|
||||
|
||||
spotifywebapi_access_token_get(&webapi_token);
|
||||
safe_json_add_string(jreply, "webapi_token", webapi_token.token);
|
||||
@ -1449,6 +1445,77 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq)
|
||||
return HTTP_NOCONTENT;
|
||||
}
|
||||
|
||||
static int
|
||||
jsonapi_reply_listenbrainz(struct httpd_request *hreq)
|
||||
{
|
||||
struct listenbrainz_status status;
|
||||
json_object *jreply;
|
||||
|
||||
listenbrainz_status_get(&status);
|
||||
|
||||
CHECK_NULL(L_WEB, jreply = json_object_new_object());
|
||||
|
||||
json_object_object_add(jreply, "enabled", json_object_new_boolean(!status.disabled));
|
||||
json_object_object_add(jreply, "token_valid", json_object_new_boolean(status.token_valid));
|
||||
if (status.user_name)
|
||||
json_object_object_add(jreply, "user_name", json_object_new_string(status.user_name));
|
||||
if (status.message)
|
||||
json_object_object_add(jreply, "message", json_object_new_string(status.message));
|
||||
|
||||
|
||||
CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply)));
|
||||
|
||||
jparse_free(jreply);
|
||||
listenbrainz_status_free(&status, true);
|
||||
|
||||
return HTTP_OK;
|
||||
}
|
||||
|
||||
static int
|
||||
jsonapi_reply_listenbrainz_token_add(struct httpd_request *hreq)
|
||||
{
|
||||
json_object *request;
|
||||
const char *token;
|
||||
int ret;
|
||||
|
||||
request = jparse_obj_from_evbuffer(hreq->in_body);
|
||||
if (!request)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
|
||||
return HTTP_BADREQUEST;
|
||||
}
|
||||
|
||||
token = jparse_str_from_obj(request, "token");
|
||||
|
||||
ret = listenbrainz_token_set(token);
|
||||
|
||||
jparse_free(request);
|
||||
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to set ListenBrainz token\n");
|
||||
return HTTP_INTERNAL;
|
||||
}
|
||||
|
||||
return HTTP_NOCONTENT;
|
||||
}
|
||||
|
||||
static int
|
||||
jsonapi_reply_listenbrainz_token_delete(struct httpd_request *hreq)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = listenbrainz_token_delete();
|
||||
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to delete ListenBrainz token\n");
|
||||
return HTTP_INTERNAL;
|
||||
}
|
||||
|
||||
return HTTP_NOCONTENT;
|
||||
}
|
||||
|
||||
/*
|
||||
* Kicks off pairing of a daap/dacp client
|
||||
*
|
||||
@ -1544,10 +1611,19 @@ static json_object *
|
||||
speaker_to_json(struct player_speaker_info *spk)
|
||||
{
|
||||
json_object *output;
|
||||
json_object *supported_formats;
|
||||
char output_id[21];
|
||||
enum media_format format;
|
||||
|
||||
output = json_object_new_object();
|
||||
|
||||
supported_formats = json_object_new_array();
|
||||
for (format = MEDIA_FORMAT_FIRST; format <= MEDIA_FORMAT_LAST; format = MEDIA_FORMAT_NEXT(format))
|
||||
{
|
||||
if (format & spk->supported_formats)
|
||||
json_object_array_add(supported_formats, json_object_new_string(media_format_to_string(format)));
|
||||
}
|
||||
|
||||
snprintf(output_id, sizeof(output_id), "%" PRIu64, spk->id);
|
||||
json_object_object_add(output, "id", json_object_new_string(output_id));
|
||||
json_object_object_add(output, "name", json_object_new_string(spk->name));
|
||||
@ -1557,6 +1633,8 @@ speaker_to_json(struct player_speaker_info *spk)
|
||||
json_object_object_add(output, "requires_auth", json_object_new_boolean(spk->requires_auth));
|
||||
json_object_object_add(output, "needs_auth_key", json_object_new_boolean(spk->needs_auth_key));
|
||||
json_object_object_add(output, "volume", json_object_new_int(spk->absvol));
|
||||
json_object_object_add(output, "format", json_object_new_string(media_format_to_string(spk->format)));
|
||||
json_object_object_add(output, "supported_formats", supported_formats);
|
||||
|
||||
return output;
|
||||
}
|
||||
@ -1616,58 +1694,66 @@ static int
|
||||
jsonapi_reply_outputs_put_byid(struct httpd_request *hreq)
|
||||
{
|
||||
uint64_t output_id;
|
||||
json_object* request;
|
||||
json_object *request = NULL;
|
||||
bool selected;
|
||||
int volume;
|
||||
const char *pin;
|
||||
const char *format;
|
||||
int ret;
|
||||
|
||||
ret = safe_atou64(hreq->path_parts[2], &output_id);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->path);
|
||||
|
||||
return HTTP_BADREQUEST;
|
||||
goto error;
|
||||
}
|
||||
|
||||
request = jparse_obj_from_evbuffer(hreq->in_body);
|
||||
if (!request)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
|
||||
|
||||
return HTTP_BADREQUEST;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
|
||||
if (jparse_contains_key(request, "selected", json_type_boolean))
|
||||
{
|
||||
selected = jparse_bool_from_obj(request, "selected");
|
||||
if (selected)
|
||||
ret = player_speaker_enable(output_id);
|
||||
else
|
||||
ret = player_speaker_disable(output_id);
|
||||
ret = selected ? player_speaker_enable(output_id) : player_speaker_disable(output_id);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (ret == 0 && jparse_contains_key(request, "volume", json_type_int))
|
||||
if (jparse_contains_key(request, "volume", json_type_int))
|
||||
{
|
||||
volume = jparse_int_from_obj(request, "volume");
|
||||
ret = player_volume_setabs_speaker(output_id, volume);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (ret == 0 && jparse_contains_key(request, "pin", json_type_string))
|
||||
if (jparse_contains_key(request, "pin", json_type_string))
|
||||
{
|
||||
pin = jparse_str_from_obj(request, "pin");
|
||||
if (pin)
|
||||
ret = player_speaker_authorize(output_id, pin);
|
||||
ret = pin ? player_speaker_authorize(output_id, pin) : 0;
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
}
|
||||
|
||||
if (jparse_contains_key(request, "format", json_type_string))
|
||||
{
|
||||
format = jparse_str_from_obj(request, "format");
|
||||
ret = format ? player_speaker_format_set(output_id, media_format_from_string(format)) : 0;
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
jparse_free(request);
|
||||
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
|
||||
return HTTP_NOCONTENT;
|
||||
|
||||
error:
|
||||
jparse_free(request);
|
||||
return HTTP_BADREQUEST;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -2227,199 +2313,59 @@ queue_item_to_json(struct db_queue_item *queue_item, char shuffle)
|
||||
}
|
||||
|
||||
static int
|
||||
queue_tracks_add_artist(const char *id, int pos)
|
||||
queue_tracks_add_byuris(const char *param, char shuffle, uint32_t item_id, int pos, int *total_count, int *new_item_id)
|
||||
{
|
||||
struct query_params query_params;
|
||||
struct player_status status;
|
||||
int count = 0;
|
||||
int ret = 0;
|
||||
|
||||
memset(&query_params, 0, sizeof(struct query_params));
|
||||
|
||||
query_params.type = Q_ITEMS;
|
||||
query_params.sort = S_ALBUM;
|
||||
query_params.idx_type = I_NONE;
|
||||
query_params.filter = db_mprintf("(f.songartistid = %q)", id);
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, &count, NULL);
|
||||
|
||||
free(query_params.filter);
|
||||
|
||||
if (ret == 0)
|
||||
return count;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_tracks_add_album(const char *id, int pos)
|
||||
{
|
||||
struct query_params query_params;
|
||||
struct player_status status;
|
||||
int count = 0;
|
||||
int ret = 0;
|
||||
|
||||
memset(&query_params, 0, sizeof(struct query_params));
|
||||
|
||||
query_params.type = Q_ITEMS;
|
||||
query_params.sort = S_ALBUM;
|
||||
query_params.idx_type = I_NONE;
|
||||
query_params.filter = db_mprintf("(f.songalbumid = %q)", id);
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, &count, NULL);
|
||||
|
||||
free(query_params.filter);
|
||||
|
||||
if (ret == 0)
|
||||
return count;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_tracks_add_track(const char *id, int pos)
|
||||
{
|
||||
struct query_params query_params;
|
||||
struct player_status status;
|
||||
int count = 0;
|
||||
int ret = 0;
|
||||
|
||||
memset(&query_params, 0, sizeof(struct query_params));
|
||||
|
||||
query_params.type = Q_ITEMS;
|
||||
query_params.sort = S_ALBUM;
|
||||
query_params.idx_type = I_NONE;
|
||||
query_params.filter = db_mprintf("(f.id = %q)", id);
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, &count, NULL);
|
||||
|
||||
free(query_params.filter);
|
||||
|
||||
if (ret == 0)
|
||||
return count;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_tracks_add_playlist(const char *id, int pos)
|
||||
{
|
||||
struct player_status status;
|
||||
int playlist_id;
|
||||
int count = 0;
|
||||
char *uris;
|
||||
const char *uri;
|
||||
char *ptr;
|
||||
int count;
|
||||
int new;
|
||||
int ret;
|
||||
|
||||
ret = safe_atoi32(id, &playlist_id);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", id);
|
||||
|
||||
return HTTP_BADREQUEST;
|
||||
}
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
ret = db_queue_add_by_playlistid(playlist_id, status.shuffle, status.item_id, pos, &count, NULL);
|
||||
|
||||
if (ret == 0)
|
||||
return count;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_tracks_add_byuris(const char *param, int pos, int *total_count)
|
||||
{
|
||||
struct player_status status;
|
||||
char *uris;
|
||||
char *uri;
|
||||
char *ptr;
|
||||
const char *id;
|
||||
int count = 0;
|
||||
int ret = 0;
|
||||
|
||||
*total_count = 0;
|
||||
*new_item_id = -1;
|
||||
|
||||
CHECK_NULL(L_WEB, uris = strdup(param));
|
||||
uri = strtok_r(uris, ",", &ptr);
|
||||
|
||||
uri = strtok_r(uris, ",", &ptr);
|
||||
if (!uri)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Empty query parameter 'uris'\n");
|
||||
free(uris);
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
do
|
||||
for (; uri; uri = strtok_r(NULL, ",", &ptr))
|
||||
{
|
||||
count = 0;
|
||||
|
||||
if (strncmp(uri, "library:artist:", strlen("library:artist:")) == 0)
|
||||
ret = library_queue_item_add(uri, pos, shuffle, item_id, &count, &new);
|
||||
if (ret != LIBRARY_OK)
|
||||
{
|
||||
id = uri + (strlen("library:artist:"));
|
||||
count = queue_tracks_add_artist(id, pos);
|
||||
DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri);
|
||||
goto error;
|
||||
}
|
||||
else if (strncmp(uri, "library:album:", strlen("library:album:")) == 0)
|
||||
{
|
||||
id = uri + (strlen("library:album:"));
|
||||
count = queue_tracks_add_album(id, pos);
|
||||
}
|
||||
else if (strncmp(uri, "library:track:", strlen("library:track:")) == 0)
|
||||
{
|
||||
id = uri + (strlen("library:track:"));
|
||||
count = queue_tracks_add_track(id, pos);
|
||||
}
|
||||
else if (strncmp(uri, "library:playlist:", strlen("library:playlist:")) == 0)
|
||||
{
|
||||
id = uri + (strlen("library:playlist:"));
|
||||
count = queue_tracks_add_playlist(id, pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
player_get_status(&status);
|
||||
|
||||
ret = library_queue_item_add(uri, pos, status.shuffle, status.item_id, &count, NULL);
|
||||
if (ret != LIBRARY_OK)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri);
|
||||
break;
|
||||
}
|
||||
pos += count;
|
||||
}
|
||||
|
||||
if (pos >= 0)
|
||||
pos += count;
|
||||
|
||||
*total_count += count;
|
||||
if (pos >= 0)
|
||||
pos += count;
|
||||
if (*new_item_id == -1)
|
||||
*new_item_id = new;
|
||||
}
|
||||
while ((uri = strtok_r(NULL, ",", &ptr)));
|
||||
|
||||
free(uris);
|
||||
return 0;
|
||||
|
||||
return ret;
|
||||
error:
|
||||
free(uris);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
queue_tracks_add_byexpression(const char *param, int pos, int limit, int *total_count)
|
||||
queue_tracks_add_byexpression(const char *param, char shuffle, uint32_t item_id, int pos, int limit, int *total_count, int *new_item_id)
|
||||
{
|
||||
struct query_params query_params = { .type = Q_ITEMS, .sort = S_NAME };
|
||||
struct smartpl smartpl_expression = { 0 };
|
||||
char *expression;
|
||||
struct smartpl smartpl_expression;
|
||||
struct query_params query_params;
|
||||
struct player_status status;
|
||||
int ret;
|
||||
|
||||
memset(&query_params, 0, sizeof(struct query_params));
|
||||
|
||||
query_params.type = Q_ITEMS;
|
||||
query_params.sort = S_NAME;
|
||||
|
||||
memset(&smartpl_expression, 0, sizeof(struct smartpl));
|
||||
expression = safe_asprintf("\"query\" { %s }", param);
|
||||
ret = smartpl_query_parse_string(&smartpl_expression, expression);
|
||||
free(expression);
|
||||
@ -2430,18 +2376,60 @@ queue_tracks_add_byexpression(const char *param, int pos, int limit, int *total_
|
||||
query_params.filter = strdup(smartpl_expression.query_where);
|
||||
query_params.order = safe_strdup(smartpl_expression.order);
|
||||
query_params.limit = limit > 0 ? limit : smartpl_expression.limit;
|
||||
query_params.idx_type = query_params.limit > 0 ? I_FIRST : I_NONE;
|
||||
free_smartpl(&smartpl_expression, 1);
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
query_params.idx_type = query_params.limit > 0 ? I_FIRST : I_NONE;
|
||||
|
||||
ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, total_count, NULL);
|
||||
ret = db_queue_add_by_query(&query_params, shuffle, item_id, pos, total_count, new_item_id);
|
||||
|
||||
free_query_params(&query_params, 1);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
create_reply_queue_tracks_add(struct evbuffer *evbuf, int count, int new_item_id, char shuffle)
|
||||
{
|
||||
json_object *reply = json_object_new_object();
|
||||
json_object *items = json_object_new_array();
|
||||
json_object *item;
|
||||
struct query_params query_params = { 0 };
|
||||
struct db_queue_item queue_item;
|
||||
int version = 0;
|
||||
int ret;
|
||||
|
||||
db_admin_getint(&version, DB_ADMIN_QUEUE_VERSION);
|
||||
|
||||
json_object_object_add(reply, "version", json_object_new_int(version));
|
||||
json_object_object_add(reply, "count", json_object_new_int(count));
|
||||
json_object_object_add(reply, "items", items);
|
||||
|
||||
ret = db_queue_enum_start(&query_params);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0)
|
||||
{
|
||||
if (queue_item.id < new_item_id)
|
||||
continue;
|
||||
|
||||
item = queue_item_to_json(&queue_item, shuffle);
|
||||
if (!item)
|
||||
goto error;
|
||||
|
||||
json_object_array_add(items, item);
|
||||
}
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
db_queue_enum_end(&query_params);
|
||||
jparse_free(reply);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
db_queue_enum_end(&query_params);
|
||||
jparse_free(reply);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
@ -2451,11 +2439,12 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq)
|
||||
const char *param_uris;
|
||||
const char *param_expression;
|
||||
const char *param;
|
||||
struct player_status status;
|
||||
int pos;
|
||||
int limit;
|
||||
bool shuffle;
|
||||
int total_count = 0;
|
||||
json_object *reply;
|
||||
int new_item_id = 0;
|
||||
int ret = 0;
|
||||
|
||||
|
||||
@ -2469,7 +2458,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq)
|
||||
return HTTP_BADREQUEST;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "Add tracks starting at position '%d\n", pos);
|
||||
DPRINTF(E_DBG, L_WEB, "Add tracks starting at position %d\n", pos);
|
||||
}
|
||||
else
|
||||
pos = -1;
|
||||
@ -2500,29 +2489,25 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq)
|
||||
player_shuffle_set(shuffle);
|
||||
}
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
if (param_uris)
|
||||
{
|
||||
ret = queue_tracks_add_byuris(param_uris, pos, &total_count);
|
||||
ret = queue_tracks_add_byuris(param_uris, status.shuffle, status.item_id, pos, &total_count, &new_item_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This overrides the value specified in query
|
||||
param = httpd_query_value_find(hreq->query, "limit");
|
||||
if (param && safe_atoi32(param, &limit) == 0)
|
||||
ret = queue_tracks_add_byexpression(param_expression, pos, limit, &total_count);
|
||||
ret = queue_tracks_add_byexpression(param_expression, status.shuffle, status.item_id, pos, limit, &total_count, &new_item_id);
|
||||
else
|
||||
ret = queue_tracks_add_byexpression(param_expression, pos, -1, &total_count);
|
||||
}
|
||||
|
||||
if (ret == 0)
|
||||
{
|
||||
reply = json_object_new_object();
|
||||
json_object_object_add(reply, "count", json_object_new_int(total_count));
|
||||
|
||||
ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply));
|
||||
jparse_free(reply);
|
||||
ret = queue_tracks_add_byexpression(param_expression, status.shuffle, status.item_id, pos, -1, &total_count, &new_item_id);
|
||||
}
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
|
||||
ret = create_reply_queue_tracks_add(hreq->out_body, total_count, new_item_id, status.shuffle);
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
|
||||
@ -2635,6 +2620,7 @@ static int
|
||||
jsonapi_reply_queue_tracks_delete(struct httpd_request *hreq)
|
||||
{
|
||||
uint32_t item_id;
|
||||
uint32_t count;
|
||||
int ret;
|
||||
|
||||
ret = safe_atou32(hreq->path_parts[3], &item_id);
|
||||
@ -2651,6 +2637,13 @@ jsonapi_reply_queue_tracks_delete(struct httpd_request *hreq)
|
||||
return HTTP_INTERNAL;
|
||||
}
|
||||
|
||||
db_queue_get_count(&count);
|
||||
if (count == 0)
|
||||
{
|
||||
player_playback_stop();
|
||||
db_queue_clear(0);
|
||||
}
|
||||
|
||||
return HTTP_NOCONTENT;
|
||||
}
|
||||
|
||||
@ -3317,11 +3310,11 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq)
|
||||
json_object *request = NULL;
|
||||
json_object *tracks;
|
||||
json_object *track = NULL;
|
||||
struct media_file_info *mfi = NULL;
|
||||
int ret;
|
||||
int err;
|
||||
int32_t track_id;
|
||||
int i;
|
||||
int j;
|
||||
|
||||
request = jparse_obj_from_evbuffer(hreq->in_body);
|
||||
if (!request)
|
||||
@ -3351,30 +3344,26 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq)
|
||||
goto error;
|
||||
}
|
||||
|
||||
mfi = db_file_fetch_byid(track_id);
|
||||
if (!mfi)
|
||||
if (!db_file_id_exists(track_id))
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Unknown track_id %d in json tracks request\n", track_id);
|
||||
err = HTTP_NOTFOUND;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = json_to_track(mfi, track);
|
||||
if (ret != HTTP_OK)
|
||||
for (j = 0; j < ARRAY_SIZE(track_attribs); j++)
|
||||
{
|
||||
err = ret;
|
||||
goto error;
|
||||
if (!jparse_contains_key(track, track_attribs[j].name, json_type_int))
|
||||
continue;
|
||||
|
||||
ret = jparse_int_from_obj(track, track_attribs[j].name);
|
||||
if (ret < 0)
|
||||
continue;
|
||||
|
||||
// async, so no error check
|
||||
library_item_attrib_save(track_id, track_attribs[j].type, ret);
|
||||
}
|
||||
|
||||
ret = library_media_save(mfi);
|
||||
if (ret < 0)
|
||||
{
|
||||
err = HTTP_INTERNAL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
free_mfi(mfi, 0);
|
||||
mfi = NULL;
|
||||
i++;
|
||||
}
|
||||
|
||||
@ -3386,7 +3375,6 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq)
|
||||
jparse_free(request);
|
||||
if (track)
|
||||
db_transaction_rollback();
|
||||
free_mfi(mfi, 0);
|
||||
return err;
|
||||
}
|
||||
|
||||
@ -3397,58 +3385,41 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq)
|
||||
const char *param;
|
||||
uint32_t val;
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
ret = safe_atoi32(hreq->path_parts[3], &track_id);
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
|
||||
param = httpd_query_value_find(hreq->query, "play_count");
|
||||
if (param)
|
||||
if (ret < 0 || !db_file_id_exists(track_id))
|
||||
{
|
||||
if (strcmp(param, "increment") == 0)
|
||||
DPRINTF(E_WARN, L_WEB, "Invalid or unknown track id in request '%s'\n", hreq->path);
|
||||
return HTTP_NOTFOUND;
|
||||
}
|
||||
|
||||
for (i = 0; i < ARRAY_SIZE(track_attribs); i++)
|
||||
{
|
||||
param = httpd_query_value_find(hreq->query, track_attribs[i].name);
|
||||
if (!param)
|
||||
continue;
|
||||
|
||||
// Special cases
|
||||
if (track_attribs[i].type == LIBRARY_ATTRIB_PLAY_COUNT && strcmp(param, "increment") == 0)
|
||||
{
|
||||
db_file_inc_playcount(track_id);
|
||||
continue;
|
||||
}
|
||||
else if (strcmp(param, "reset") == 0)
|
||||
if (track_attribs[i].type == LIBRARY_ATTRIB_PLAY_COUNT && strcmp(param, "reset") == 0)
|
||||
{
|
||||
db_file_reset_playskip_count(track_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
DPRINTF(E_WARN, L_WEB, "Ignoring invalid play_count value '%s' for track '%d'.\n", param, track_id);
|
||||
return HTTP_BADREQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
param = httpd_query_value_find(hreq->query, "rating");
|
||||
if (param)
|
||||
{
|
||||
ret = safe_atou32(param, &val);
|
||||
if (ret < 0 || val > DB_FILES_RATING_MAX)
|
||||
{
|
||||
DPRINTF(E_WARN, L_WEB, "Invalid rating value '%s' for track '%d'.\n", param, track_id);
|
||||
return HTTP_BADREQUEST;
|
||||
continue;
|
||||
}
|
||||
|
||||
ret = db_file_rating_update_byid(track_id, val);
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
}
|
||||
|
||||
// Retreive marked tracks via "/api/search?type=tracks&expression=usermark+=+1"
|
||||
param = httpd_query_value_find(hreq->query, "usermark");
|
||||
if (param)
|
||||
{
|
||||
ret = safe_atou32(param, &val);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_WEB, "Invalid usermark value '%s' for track '%d'.\n", param, track_id);
|
||||
DPRINTF(E_WARN, L_WEB, "Invalid %s value '%s' for track '%d'.\n", track_attribs[i].name, param, track_id);
|
||||
return HTTP_BADREQUEST;
|
||||
}
|
||||
|
||||
ret = db_file_usermark_update_byid(track_id, val);
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
library_item_attrib_save(track_id, track_attribs[i].type, val);
|
||||
}
|
||||
|
||||
return HTTP_OK;
|
||||
@ -3851,7 +3822,7 @@ jsonapi_reply_queue_save(struct httpd_request *hreq)
|
||||
if (!allow_modifying_stored_playlists)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Modifying stored playlists is not enabled in the config file\n");
|
||||
return 403;
|
||||
return 403;
|
||||
}
|
||||
|
||||
if (access(default_playlist_directory, W_OK) < 0)
|
||||
@ -4457,6 +4428,68 @@ search_composers(json_object *reply, struct httpd_request *hreq, const char *par
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
search_genres(json_object *reply, struct httpd_request *hreq, const char *param_query, struct smartpl *smartpl_expression, enum media_kind media_kind)
|
||||
{
|
||||
json_object *type;
|
||||
json_object *items;
|
||||
struct query_params query_params;
|
||||
int total;
|
||||
int ret;
|
||||
|
||||
memset(&query_params, 0, sizeof(struct query_params));
|
||||
|
||||
ret = query_params_limit_set(&query_params, hreq);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
type = json_object_new_object();
|
||||
json_object_object_add(reply, "genres", type);
|
||||
items = json_object_new_array();
|
||||
json_object_object_add(type, "items", items);
|
||||
|
||||
query_params.type = Q_BROWSE_GENRES;
|
||||
query_params.sort = S_GENRE;
|
||||
|
||||
ret = query_params_limit_set(&query_params, hreq);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
if (param_query)
|
||||
{
|
||||
if (media_kind)
|
||||
query_params.filter = db_mprintf("(f.genre LIKE '%%%q%%' AND f.media_kind = %d)", param_query, media_kind);
|
||||
else
|
||||
query_params.filter = db_mprintf("(f.genre LIKE '%%%q%%')", param_query);
|
||||
}
|
||||
else
|
||||
{
|
||||
query_params.filter = strdup(smartpl_expression->query_where);
|
||||
query_params.having = safe_strdup(smartpl_expression->having);
|
||||
query_params.order = safe_strdup(smartpl_expression->order);
|
||||
|
||||
if (smartpl_expression->limit > 0)
|
||||
{
|
||||
query_params.idx_type = I_SUB;
|
||||
query_params.limit = smartpl_expression->limit;
|
||||
query_params.offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ret = fetch_browse_info(&query_params, items, &total);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
json_object_object_add(type, "total", json_object_new_int(total));
|
||||
json_object_object_add(type, "offset", json_object_new_int(query_params.offset));
|
||||
json_object_object_add(type, "limit", json_object_new_int(query_params.limit));
|
||||
|
||||
out:
|
||||
free_query_params(&query_params, 1);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
search_playlists(json_object *reply, struct httpd_request *hreq, const char *param_query)
|
||||
{
|
||||
@ -4579,6 +4612,13 @@ jsonapi_reply_search(struct httpd_request *hreq)
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (strstr(param_type, "genre"))
|
||||
{
|
||||
ret = search_genres(reply, hreq, param_query, &smartpl_expression, media_kind);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (strstr(param_type, "playlist") && param_query)
|
||||
{
|
||||
ret = search_playlists(reply, hreq, param_query);
|
||||
@ -4695,6 +4735,10 @@ static struct httpd_uri_map adm_handlers[] =
|
||||
|
||||
{ HTTPD_METHOD_GET, "^/api/search$", jsonapi_reply_search },
|
||||
|
||||
{ HTTPD_METHOD_GET, "^/api/listenbrainz$", jsonapi_reply_listenbrainz },
|
||||
{ HTTPD_METHOD_POST, "^/api/listenbrainz/token$", jsonapi_reply_listenbrainz_token_add },
|
||||
{ HTTPD_METHOD_DELETE, "^/api/listenbrainz/token$", jsonapi_reply_listenbrainz_token_delete },
|
||||
|
||||
{ 0, NULL, NULL }
|
||||
};
|
||||
|
||||
@ -4706,7 +4750,7 @@ jsonapi_request(struct httpd_request *hreq)
|
||||
{
|
||||
int status_code;
|
||||
|
||||
if (!httpd_admin_check_auth(hreq))
|
||||
if (!httpd_request_is_authorized(hreq))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -4762,7 +4806,7 @@ jsonapi_init(void)
|
||||
default_playlist_directory = NULL;
|
||||
allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "library"), "allow_modifying_stored_playlists");
|
||||
if (allow_modifying_stored_playlists)
|
||||
{
|
||||
{
|
||||
temp_path = cfg_getstr(cfg_getsec(cfg, "library"), "default_playlist_directory");
|
||||
if (temp_path)
|
||||
{
|
||||
|
||||
@ -20,21 +20,27 @@
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <json.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/queue.h> // TAILQ_FOREACH
|
||||
#include <sys/socket.h> // listen()
|
||||
#include <arpa/inet.h> // inet_pton()
|
||||
|
||||
#include <event2/http.h>
|
||||
#include <event2/http_struct.h> // flags in struct evhttp
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#ifdef HAVE_LIBEVENT22
|
||||
#include <event2/ws.h>
|
||||
#endif
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "conffile.h"
|
||||
#include "misc.h"
|
||||
#include "listener.h"
|
||||
#include "logger.h"
|
||||
#include "commands.h"
|
||||
#include "httpd_internal.h"
|
||||
@ -94,6 +100,294 @@ static void
|
||||
closecb_worker(evutil_socket_t fd, short event, void *arg);
|
||||
|
||||
|
||||
#ifdef HAVE_LIBEVENT22
|
||||
|
||||
/*
|
||||
* Each session of the "notify" protocol holds this event mask
|
||||
*
|
||||
* The client sends the events it wants to be notified of and the event mask is
|
||||
* set accordingly translating them to the LISTENER enum (see listener.h)
|
||||
*/
|
||||
struct ws_client
|
||||
{
|
||||
struct evws_connection *evws;
|
||||
char name[INET6_ADDRSTRLEN];
|
||||
short requested_events;
|
||||
struct ws_client *next;
|
||||
};
|
||||
static struct ws_client *ws_clients = NULL;
|
||||
|
||||
/*
|
||||
* Notify clients of the notify-protocol about occurred events
|
||||
*
|
||||
* Sends a JSON message of the form:
|
||||
*
|
||||
* {
|
||||
* "notify": [ "update" ]
|
||||
* }
|
||||
*/
|
||||
static char *
|
||||
ws_create_notify_reply(short events, short *requested_events)
|
||||
{
|
||||
char *json_response;
|
||||
json_object *reply;
|
||||
json_object *notify;
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "notify callback reply: %d\n", events);
|
||||
|
||||
notify = json_object_new_array();
|
||||
if (events & LISTENER_UPDATE)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("update"));
|
||||
}
|
||||
if (events & LISTENER_DATABASE)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("database"));
|
||||
}
|
||||
if (events & LISTENER_PAIRING)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("pairing"));
|
||||
}
|
||||
if (events & LISTENER_SPOTIFY)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("spotify"));
|
||||
}
|
||||
if (events & LISTENER_LASTFM)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("lastfm"));
|
||||
}
|
||||
if (events & LISTENER_SPEAKER)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("outputs"));
|
||||
}
|
||||
if (events & LISTENER_PLAYER)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("player"));
|
||||
}
|
||||
if (events & LISTENER_OPTIONS)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("options"));
|
||||
}
|
||||
if (events & LISTENER_VOLUME)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("volume"));
|
||||
}
|
||||
if (events & LISTENER_QUEUE)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("queue"));
|
||||
}
|
||||
|
||||
reply = json_object_new_object();
|
||||
json_object_object_add(reply, "notify", notify);
|
||||
|
||||
json_response = strdup(json_object_to_json_string(reply));
|
||||
|
||||
json_object_put(reply);
|
||||
|
||||
return json_response;
|
||||
}
|
||||
|
||||
/* Thread: library, player, etc. (the thread the event occurred) */
|
||||
static enum command_state
|
||||
ws_listener_cb(void *arg, int *ret)
|
||||
{
|
||||
struct ws_client *client = NULL;
|
||||
char *reply = NULL;
|
||||
short *event_mask = arg;
|
||||
|
||||
for (client = ws_clients; client; client = client->next)
|
||||
{
|
||||
reply = ws_create_notify_reply(*event_mask, &client->requested_events);
|
||||
evws_send_text(client->evws, reply);
|
||||
free(reply);
|
||||
}
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static void
|
||||
listener_cb(short event_mask, void *ctx)
|
||||
{
|
||||
httpd_server *server = ctx;
|
||||
commands_exec_sync(server->cmdbase, ws_listener_cb, NULL, &event_mask);
|
||||
}
|
||||
|
||||
/*
|
||||
* Processes client requests to the notify-protocol
|
||||
*
|
||||
* Expects the message in "in" to be a JSON string of the form:
|
||||
*
|
||||
* {
|
||||
* "notify": [ "update" ]
|
||||
* }
|
||||
*/
|
||||
static int
|
||||
ws_process_notify_request(short *requested_events, const char *in, size_t len)
|
||||
{
|
||||
json_tokener *tokener;
|
||||
json_object *request;
|
||||
json_object *item;
|
||||
int count, i;
|
||||
enum json_tokener_error jerr;
|
||||
json_object *needle;
|
||||
const char *event_type;
|
||||
|
||||
*requested_events = 0;
|
||||
|
||||
tokener = json_tokener_new();
|
||||
request = json_tokener_parse_ex(tokener, in, len);
|
||||
jerr = json_tokener_get_error(tokener);
|
||||
|
||||
if (jerr != json_tokener_success)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request: %s\n", json_tokener_error_desc(jerr));
|
||||
json_tokener_free(tokener);
|
||||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "notify callback request: %s\n", json_object_to_json_string(request));
|
||||
|
||||
if (json_object_object_get_ex(request, "notify", &needle) && json_object_get_type(needle) == json_type_array)
|
||||
{
|
||||
count = json_object_array_length(needle);
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
item = json_object_array_get_idx(needle, i);
|
||||
|
||||
if (json_object_get_type(item) == json_type_string)
|
||||
{
|
||||
event_type = json_object_get_string(item);
|
||||
DPRINTF(E_SPAM, L_WEB, "notify callback event received: %s\n", event_type);
|
||||
|
||||
if (0 == strcmp(event_type, "update"))
|
||||
{
|
||||
*requested_events |= LISTENER_UPDATE;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "database"))
|
||||
{
|
||||
*requested_events |= LISTENER_DATABASE;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "pairing"))
|
||||
{
|
||||
*requested_events |= LISTENER_PAIRING;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "spotify"))
|
||||
{
|
||||
*requested_events |= LISTENER_SPOTIFY;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "lastfm"))
|
||||
{
|
||||
*requested_events |= LISTENER_LASTFM;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "outputs"))
|
||||
{
|
||||
*requested_events |= LISTENER_SPEAKER;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "player"))
|
||||
{
|
||||
*requested_events |= LISTENER_PLAYER;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "options"))
|
||||
{
|
||||
*requested_events |= LISTENER_OPTIONS;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "volume"))
|
||||
{
|
||||
*requested_events |= LISTENER_VOLUME;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "queue"))
|
||||
{
|
||||
*requested_events |= LISTENER_QUEUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_tokener_free(tokener);
|
||||
json_object_put(request);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
ws_client_msg_cb(struct evws_connection *evws, int type, const unsigned char *data, size_t len, void *arg)
|
||||
{
|
||||
struct ws_client *self = arg;
|
||||
const char *msg = (const char *)data;
|
||||
|
||||
ws_process_notify_request(&self->requested_events, msg, len);
|
||||
}
|
||||
|
||||
static void
|
||||
ws_client_close_cb(struct evws_connection *evws, void *arg)
|
||||
{
|
||||
struct ws_client *client = NULL;
|
||||
struct ws_client *prev = NULL;
|
||||
|
||||
for (client = ws_clients; client && client != arg; client = ws_clients->next)
|
||||
{
|
||||
prev = client;
|
||||
}
|
||||
|
||||
if (client)
|
||||
{
|
||||
if (prev)
|
||||
prev->next = client->next;
|
||||
else
|
||||
ws_clients = client->next;
|
||||
|
||||
free(client);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
ws_gencb(struct evhttp_request *req, void *arg)
|
||||
{
|
||||
struct ws_client *client;
|
||||
|
||||
client = calloc(1, sizeof(*client));
|
||||
|
||||
client->evws = evws_new_session(req, ws_client_msg_cb, client, 0);
|
||||
if (!client->evws)
|
||||
{
|
||||
free(client);
|
||||
return;
|
||||
}
|
||||
|
||||
evws_connection_set_closecb(client->evws, ws_client_close_cb, client);
|
||||
client->next = ws_clients;
|
||||
ws_clients = client;
|
||||
}
|
||||
|
||||
static int
|
||||
ws_init(httpd_server *server)
|
||||
{
|
||||
int websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
|
||||
|
||||
if (websocket_port > 0)
|
||||
{
|
||||
DPRINTF(E_DBG, L_WEB,
|
||||
"Libevent websocket disabled, using libwebsockets instead. Set "
|
||||
"websocket_port to 0 to enable it.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
evhttp_set_cb(server->evhttp, "/ws", ws_gencb, NULL);
|
||||
|
||||
listener_add(listener_cb, LISTENER_UPDATE | LISTENER_DATABASE | LISTENER_PAIRING | LISTENER_SPOTIFY | LISTENER_LASTFM
|
||||
| LISTENER_SPEAKER | LISTENER_PLAYER | LISTENER_OPTIONS | LISTENER_VOLUME
|
||||
| LISTENER_QUEUE, server);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
ws_deinit(void)
|
||||
{
|
||||
listener_remove(listener_cb);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
const char *
|
||||
httpd_query_value_find(httpd_query *query, const char *key)
|
||||
{
|
||||
@ -105,7 +399,8 @@ httpd_query_iterate(httpd_query *query, httpd_query_iteratecb cb, void *arg)
|
||||
{
|
||||
struct evkeyval *param;
|
||||
|
||||
TAILQ_FOREACH(param, query, next)
|
||||
// musl libc doesn't have sys/queue.h so don't use TAILQ_FOREACH
|
||||
for (param = query->tqh_first; param; param = param->next.tqe_next)
|
||||
{
|
||||
cb(param->key, param->value, arg);
|
||||
}
|
||||
@ -303,9 +598,11 @@ gencb_httpd(httpd_backend *backend, void *arg)
|
||||
struct httpd_request *hreq;
|
||||
struct bufferevent *bufev;
|
||||
|
||||
#ifndef HAVE_LIBEVENT22
|
||||
// Clear the proxy request flag set by evhttp if the request URI was absolute.
|
||||
// It has side-effects on Connection: keep-alive
|
||||
backend->flags &= ~EVHTTP_PROXY_REQUEST;
|
||||
#endif
|
||||
|
||||
// This is a workaround for some versions of libevent (2.0 and 2.1) that don't
|
||||
// detect if the client hangs up, and thus don't clean up and never call the
|
||||
@ -340,7 +637,12 @@ httpd_server_free(httpd_server *server)
|
||||
close(server->fd);
|
||||
|
||||
if (server->evhttp)
|
||||
evhttp_free(server->evhttp);
|
||||
{
|
||||
#ifdef HAVE_LIBEVENT22
|
||||
ws_deinit();
|
||||
#endif
|
||||
evhttp_free(server->evhttp);
|
||||
}
|
||||
|
||||
commands_base_free(server->cmdbase);
|
||||
free(server);
|
||||
@ -359,7 +661,7 @@ httpd_server_new(struct event_base *evbase, unsigned short port, httpd_request_c
|
||||
server->request_cb = cb;
|
||||
server->request_cb_arg = arg;
|
||||
|
||||
server->fd = net_bind_with_reuseport(&port, SOCK_STREAM | SOCK_NONBLOCK, "httpd");
|
||||
server->fd = net_bind_with_reuseport(&port, SOCK_STREAM, "httpd");
|
||||
if (server->fd <= 0)
|
||||
goto error;
|
||||
|
||||
@ -373,6 +675,9 @@ httpd_server_new(struct event_base *evbase, unsigned short port, httpd_request_c
|
||||
goto error;
|
||||
|
||||
evhttp_set_gencb(server->evhttp, gencb_httpd, server);
|
||||
#ifdef HAVE_LIBEVENT22
|
||||
ws_init(server);
|
||||
#endif
|
||||
|
||||
return server;
|
||||
|
||||
@ -525,6 +830,7 @@ httpd_backend_output_buffer_get(httpd_backend *backend)
|
||||
int
|
||||
httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend, httpd_backend_data *backend_data)
|
||||
{
|
||||
#define IPV4_MAPPED_IPV6_PREFIX "::ffff:"
|
||||
httpd_connection *conn = evhttp_request_get_connection(backend);
|
||||
if (!conn)
|
||||
return -1;
|
||||
@ -534,9 +840,57 @@ httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend
|
||||
#else
|
||||
evhttp_connection_get_peer(conn, (char **)addr, port);
|
||||
#endif
|
||||
|
||||
// Just use the pure ipv4 address if it's mapped
|
||||
if (strncmp(*addr, IPV4_MAPPED_IPV6_PREFIX, strlen(IPV4_MAPPED_IPV6_PREFIX)) == 0)
|
||||
*addr += strlen(IPV4_MAPPED_IPV6_PREFIX);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// When removing this workaround then also remove the include of arpa/inet.h
|
||||
static bool
|
||||
address_is_trusted_workaround(httpd_backend *backend)
|
||||
{
|
||||
union net_sockaddr naddr = { 0 };
|
||||
const char *saddr;
|
||||
uint16_t port;
|
||||
|
||||
DPRINTF(E_DBG, L_HTTPD, "Detected libevent version with buggy evhttp_connection_get_addr()\n");
|
||||
|
||||
if (httpd_backend_peer_get(&saddr, &port, backend, NULL) < 0)
|
||||
return false;
|
||||
|
||||
if (inet_pton(AF_INET, saddr, &naddr.sin.sin_addr) == 1)
|
||||
naddr.sa.sa_family = AF_INET;
|
||||
else if (inet_pton(AF_INET6, saddr, &naddr.sin6.sin6_addr) == 1)
|
||||
naddr.sa.sa_family = AF_INET6;
|
||||
else
|
||||
return false;
|
||||
|
||||
return net_peer_address_is_trusted(&naddr);
|
||||
}
|
||||
|
||||
bool
|
||||
httpd_backend_peer_is_trusted(httpd_backend *backend)
|
||||
{
|
||||
const struct sockaddr *addr;
|
||||
|
||||
httpd_connection *conn = evhttp_request_get_connection(backend);
|
||||
if (!conn)
|
||||
return false;
|
||||
|
||||
addr = evhttp_connection_get_addr(conn);
|
||||
if (!addr)
|
||||
return false;
|
||||
|
||||
// Workaround for bug in libevent 2.1.6 and .8, see #1775
|
||||
if (addr->sa_family == AF_UNSPEC)
|
||||
return address_is_trusted_workaround(backend);
|
||||
|
||||
return net_peer_address_is_trusted((union net_sockaddr *)addr);
|
||||
}
|
||||
|
||||
int
|
||||
httpd_backend_method_get(enum httpd_methods *method, httpd_backend *backend)
|
||||
{
|
||||
|
||||
@ -43,15 +43,10 @@
|
||||
static int
|
||||
oauth_reply_spotify(struct httpd_request *hreq)
|
||||
{
|
||||
char redirect_uri[256];
|
||||
const char *errmsg;
|
||||
int httpd_port;
|
||||
int ret;
|
||||
|
||||
httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port");
|
||||
|
||||
snprintf(redirect_uri, sizeof(redirect_uri), "http://owntone.local:%d/oauth/spotify", httpd_port);
|
||||
ret = spotifywebapi_oauth_callback(hreq->query, redirect_uri, &errmsg);
|
||||
ret = spotifywebapi_oauth_callback(hreq->query, &errmsg);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Could not parse Spotify OAuth callback '%s': %s\n", hreq->uri, errmsg);
|
||||
|
||||
406
src/httpd_rsp.c
406
src/httpd_rsp.c
@ -30,18 +30,17 @@
|
||||
#include <sys/types.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "mxml-compat.h"
|
||||
|
||||
#include "httpd_internal.h"
|
||||
#include "logger.h"
|
||||
#include "db.h"
|
||||
#include "conffile.h"
|
||||
#include "misc.h"
|
||||
#include "misc_xml.h"
|
||||
#include "transcode.h"
|
||||
#include "parsers/rsp_parser.h"
|
||||
|
||||
#define RSP_VERSION "1.0"
|
||||
#define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?"
|
||||
#define RSP_XML_DECLARATION "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>"
|
||||
|
||||
#define F_FULL (1 << 0)
|
||||
#define F_BROWSE (1 << 1)
|
||||
@ -120,12 +119,12 @@ static const struct field_map rsp_fields[] =
|
||||
/* -------------------------------- HELPERS --------------------------------- */
|
||||
|
||||
static int
|
||||
mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree)
|
||||
xml_to_evbuf(struct evbuffer *evbuf, xml_node *tree)
|
||||
{
|
||||
char *xml;
|
||||
int ret;
|
||||
|
||||
xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK);
|
||||
xml = xml_to_string(tree, RSP_XML_DECLARATION);
|
||||
if (!xml)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n");
|
||||
@ -143,48 +142,53 @@ mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
rsp_xml_response_new(xml_node **response_ptr, int errorcode, const char *errorstring, int records, int totalrecords)
|
||||
{
|
||||
xml_node *node;
|
||||
xml_node *response = xml_new_node(NULL, "response", NULL);
|
||||
xml_node *status = xml_new_node(response, "status", NULL);
|
||||
|
||||
if (!response || !status)
|
||||
return -1;
|
||||
|
||||
xml_new_node_textf(status, "errorcode", "%d", errorcode);
|
||||
node = xml_new_node(status, "errorstring", errorstring);
|
||||
if (errorstring && *errorstring == '\0')
|
||||
xml_new_text(node, ""); // Prevents sending <errorstring/> which the Soundbridge may not understand
|
||||
|
||||
xml_new_node_textf(status, "records", "%d", records);
|
||||
xml_new_node_textf(status, "totalrecords", "%d", totalrecords);
|
||||
|
||||
if (response_ptr)
|
||||
*response_ptr = response;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
rsp_send_error(struct httpd_request *hreq, char *errmsg)
|
||||
{
|
||||
mxml_node_t *reply;
|
||||
mxml_node_t *status;
|
||||
mxml_node_t *node;
|
||||
xml_node *response = NULL;
|
||||
int ret;
|
||||
|
||||
/* We'd use mxmlNewXML(), but then we can't put any attributes
|
||||
* on the root node and we need some.
|
||||
*/
|
||||
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
|
||||
|
||||
node = mxmlNewElement(reply, "response");
|
||||
status = mxmlNewElement(node, "status");
|
||||
|
||||
/* Status block */
|
||||
node = mxmlNewElement(status, "errorcode");
|
||||
mxmlNewText(node, 0, "1");
|
||||
|
||||
node = mxmlNewElement(status, "errorstring");
|
||||
mxmlNewText(node, 0, errmsg);
|
||||
|
||||
node = mxmlNewElement(status, "records");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
node = mxmlNewElement(status, "totalrecords");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
ret = mxml_to_evbuf(hreq->out_body, reply);
|
||||
mxmlDelete(reply);
|
||||
CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 1, errmsg, 0, 0));
|
||||
|
||||
ret = xml_to_evbuf(hreq->out_body, response);
|
||||
if (ret < 0)
|
||||
{
|
||||
httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error");
|
||||
return;
|
||||
}
|
||||
goto error;
|
||||
|
||||
httpd_header_add(hreq->out_headers, "Content-Type", "text/xml; charset=utf-8");
|
||||
httpd_header_add(hreq->out_headers, "Connection", "close");
|
||||
|
||||
httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP);
|
||||
|
||||
xml_free(response);
|
||||
return;
|
||||
|
||||
error:
|
||||
httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error");
|
||||
xml_free(response);
|
||||
}
|
||||
|
||||
static int
|
||||
@ -259,12 +263,12 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq)
|
||||
}
|
||||
|
||||
static void
|
||||
rsp_send_reply(struct httpd_request *hreq, mxml_node_t *reply)
|
||||
rsp_send_reply(struct httpd_request *hreq, xml_node *reply)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = mxml_to_evbuf(hreq->out_body, reply);
|
||||
mxmlDelete(reply);
|
||||
ret = xml_to_evbuf(hreq->out_body, reply);
|
||||
xml_free(reply);
|
||||
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -284,7 +288,7 @@ rsp_request_authorize(struct httpd_request *hreq)
|
||||
char *passwd;
|
||||
int ret;
|
||||
|
||||
if (net_peer_address_is_trusted(hreq->peer_address))
|
||||
if (httpd_request_is_trusted(hreq))
|
||||
return 0;
|
||||
|
||||
passwd = cfg_getstr(cfg_getsec(cfg, "library"), "password");
|
||||
@ -310,10 +314,8 @@ rsp_request_authorize(struct httpd_request *hreq)
|
||||
static int
|
||||
rsp_reply_info(struct httpd_request *hreq)
|
||||
{
|
||||
mxml_node_t *reply;
|
||||
mxml_node_t *status;
|
||||
mxml_node_t *info;
|
||||
mxml_node_t *node;
|
||||
xml_node *response;
|
||||
xml_node *info;
|
||||
cfg_t *lib;
|
||||
char *library;
|
||||
uint32_t songcount;
|
||||
@ -323,43 +325,16 @@ rsp_reply_info(struct httpd_request *hreq)
|
||||
lib = cfg_getsec(cfg, "library");
|
||||
library = cfg_getstr(lib, "name");
|
||||
|
||||
/* We'd use mxmlNewXML(), but then we can't put any attributes
|
||||
* on the root node and we need some.
|
||||
*/
|
||||
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
|
||||
CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", 0, 0));
|
||||
|
||||
node = mxmlNewElement(reply, "response");
|
||||
status = mxmlNewElement(node, "status");
|
||||
info = mxmlNewElement(node, "info");
|
||||
info = xml_new_node(response, "info", NULL);
|
||||
|
||||
/* Status block */
|
||||
node = mxmlNewElement(status, "errorcode");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
node = mxmlNewElement(status, "errorstring");
|
||||
mxmlNewText(node, 0, "");
|
||||
|
||||
node = mxmlNewElement(status, "records");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
node = mxmlNewElement(status, "totalrecords");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
/* Info block */
|
||||
node = mxmlNewElement(info, "count");
|
||||
mxmlNewTextf(node, 0, "%d", (int)songcount);
|
||||
|
||||
node = mxmlNewElement(info, "rsp-version");
|
||||
mxmlNewText(node, 0, RSP_VERSION);
|
||||
|
||||
node = mxmlNewElement(info, "server-version");
|
||||
mxmlNewText(node, 0, VERSION);
|
||||
|
||||
node = mxmlNewElement(info, "name");
|
||||
mxmlNewText(node, 0, library);
|
||||
|
||||
rsp_send_reply(hreq, reply);
|
||||
xml_new_node_textf(info, "count", "%d", (int)songcount);
|
||||
xml_new_node(info, "rsp-version", RSP_VERSION);
|
||||
xml_new_node(info, "server-version", VERSION);
|
||||
xml_new_node(info, "name", library);
|
||||
|
||||
rsp_send_reply(hreq, response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -369,11 +344,9 @@ rsp_reply_db(struct httpd_request *hreq)
|
||||
struct query_params qp;
|
||||
struct db_playlist_info dbpli;
|
||||
char **strval;
|
||||
mxml_node_t *reply;
|
||||
mxml_node_t *status;
|
||||
mxml_node_t *pls;
|
||||
mxml_node_t *pl;
|
||||
mxml_node_t *node;
|
||||
xml_node *response;
|
||||
xml_node *pls;
|
||||
xml_node *pl;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
@ -391,27 +364,9 @@ rsp_reply_db(struct httpd_request *hreq)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* We'd use mxmlNewXML(), but then we can't put any attributes
|
||||
* on the root node and we need some.
|
||||
*/
|
||||
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
|
||||
CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", qp.results, qp.results));
|
||||
|
||||
node = mxmlNewElement(reply, "response");
|
||||
status = mxmlNewElement(node, "status");
|
||||
pls = mxmlNewElement(node, "playlists");
|
||||
|
||||
/* Status block */
|
||||
node = mxmlNewElement(status, "errorcode");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
node = mxmlNewElement(status, "errorstring");
|
||||
mxmlNewText(node, 0, "");
|
||||
|
||||
node = mxmlNewElement(status, "records");
|
||||
mxmlNewTextf(node, 0, "%d", qp.results);
|
||||
|
||||
node = mxmlNewElement(status, "totalrecords");
|
||||
mxmlNewTextf(node, 0, "%d", qp.results);
|
||||
pls = xml_new_node(response, "playlists", NULL);
|
||||
|
||||
/* Playlists block (all playlists) */
|
||||
while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id))
|
||||
@ -421,7 +376,7 @@ rsp_reply_db(struct httpd_request *hreq)
|
||||
continue;
|
||||
|
||||
/* Playlist block (one playlist) */
|
||||
pl = mxmlNewElement(pls, "playlist");
|
||||
pl = xml_new_node(pls, "playlist", NULL);
|
||||
|
||||
for (i = 0; pl_fields[i].field; i++)
|
||||
{
|
||||
@ -429,8 +384,7 @@ rsp_reply_db(struct httpd_request *hreq)
|
||||
{
|
||||
strval = (char **) ((char *)&dbpli + pl_fields[i].offset);
|
||||
|
||||
node = mxmlNewElement(pl, pl_fields[i].field);
|
||||
mxmlNewText(node, 0, *strval);
|
||||
xml_new_node(pl, pl_fields[i].field, *strval);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -439,7 +393,7 @@ rsp_reply_db(struct httpd_request *hreq)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
|
||||
|
||||
mxmlDelete(reply);
|
||||
xml_free(response);
|
||||
db_query_end(&qp);
|
||||
rsp_send_error(hreq, "Error fetching query results");
|
||||
return -1;
|
||||
@ -447,15 +401,83 @@ rsp_reply_db(struct httpd_request *hreq)
|
||||
|
||||
/* HACK
|
||||
* Add a dummy empty string to the playlists element if there is no data
|
||||
* to return - this prevents mxml from sending out an empty <playlists/>
|
||||
* to return - this prevents us from sending out an empty <playlists/>
|
||||
* tag that the SoundBridge does not handle. It's hackish, but it works.
|
||||
*/
|
||||
if (qp.results == 0)
|
||||
mxmlNewText(pls, 0, "");
|
||||
xml_new_text(pls, "");
|
||||
|
||||
db_query_end(&qp);
|
||||
|
||||
rsp_send_reply(hreq, reply);
|
||||
rsp_send_reply(hreq, response);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
item_add(xml_node *parent, struct query_params *qp, enum transcode_profile spk_profile, const char *user_agent, const char *accept_codecs, int mode)
|
||||
{
|
||||
struct media_quality quality = { 0 };
|
||||
struct db_media_file_info dbmfi;
|
||||
struct transcode_metadata_string xcode_metadata;
|
||||
enum transcode_profile profile;
|
||||
const char *orgcodec = NULL;
|
||||
uint32_t len_ms;
|
||||
xml_node *item;
|
||||
char **strval;
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
ret = db_query_fetch_file(&dbmfi, qp);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
profile = transcode_needed(user_agent, accept_codecs, dbmfi.codectype);
|
||||
if (profile == XCODE_UNKNOWN)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
|
||||
}
|
||||
else if (profile != XCODE_NONE)
|
||||
{
|
||||
if (spk_profile != XCODE_NONE)
|
||||
profile = spk_profile; // User has configured a specific transcode format for this speaker
|
||||
|
||||
orgcodec = dbmfi.codectype;
|
||||
|
||||
if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
|
||||
len_ms = 3 * 60 * 1000; // just a fallback default
|
||||
|
||||
safe_atoi32(dbmfi.samplerate, &quality.sample_rate);
|
||||
safe_atoi32(dbmfi.bits_per_sample, &quality.bits_per_sample);
|
||||
safe_atoi32(dbmfi.channels, &quality.channels);
|
||||
quality.bit_rate = cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
|
||||
|
||||
transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
|
||||
dbmfi.type = xcode_metadata.type;
|
||||
dbmfi.codectype = xcode_metadata.codectype;
|
||||
dbmfi.description = xcode_metadata.description;
|
||||
dbmfi.file_size = xcode_metadata.file_size;
|
||||
dbmfi.bitrate = xcode_metadata.bitrate;
|
||||
}
|
||||
|
||||
// Now add block with content
|
||||
item = xml_new_node(parent, "item", NULL);
|
||||
|
||||
for (i = 0; rsp_fields[i].field; i++)
|
||||
{
|
||||
if (!(rsp_fields[i].flags & mode))
|
||||
continue;
|
||||
|
||||
strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset);
|
||||
if (!(*strval) || (strlen(*strval) == 0))
|
||||
continue;
|
||||
|
||||
xml_new_node(item, rsp_fields[i].field, *strval);
|
||||
|
||||
// In case we are transcoding
|
||||
if (rsp_fields[i].offset == dbmfi_offsetof(codectype) && orgcodec)
|
||||
xml_new_node(item, "original_codec", orgcodec);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -464,25 +486,20 @@ static int
|
||||
rsp_reply_playlist(struct httpd_request *hreq)
|
||||
{
|
||||
struct query_params qp;
|
||||
struct db_media_file_info dbmfi;
|
||||
const char *param;
|
||||
const char *ua;
|
||||
const char *client_codecs;
|
||||
char **strval;
|
||||
mxml_node_t *reply;
|
||||
mxml_node_t *status;
|
||||
mxml_node_t *items;
|
||||
mxml_node_t *item;
|
||||
mxml_node_t *node;
|
||||
const char *accept_codecs;
|
||||
enum transcode_profile spk_profile;
|
||||
xml_node *response;
|
||||
xml_node *items;
|
||||
int mode;
|
||||
int records;
|
||||
int transcode;
|
||||
int32_t bitrate;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
memset(&qp, 0, sizeof(struct query_params));
|
||||
|
||||
accept_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
spk_profile = httpd_xcode_profile_get(hreq);
|
||||
|
||||
ret = safe_atoi32(hreq->path_parts[2], &qp.id);
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -537,99 +554,23 @@ rsp_reply_playlist(struct httpd_request *hreq)
|
||||
if (qp.limit && (records > qp.limit))
|
||||
records = qp.limit;
|
||||
|
||||
/* We'd use mxmlNewXML(), but then we can't put any attributes
|
||||
* on the root node and we need some.
|
||||
*/
|
||||
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
|
||||
CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", records, qp.results));
|
||||
|
||||
node = mxmlNewElement(reply, "response");
|
||||
status = mxmlNewElement(node, "status");
|
||||
items = mxmlNewElement(node, "items");
|
||||
|
||||
/* Status block */
|
||||
node = mxmlNewElement(status, "errorcode");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
node = mxmlNewElement(status, "errorstring");
|
||||
mxmlNewText(node, 0, "");
|
||||
|
||||
node = mxmlNewElement(status, "records");
|
||||
mxmlNewTextf(node, 0, "%d", records);
|
||||
|
||||
node = mxmlNewElement(status, "totalrecords");
|
||||
mxmlNewTextf(node, 0, "%d", qp.results);
|
||||
|
||||
/* Items block (all items) */
|
||||
while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
|
||||
// Add a parent items block (all items), and then one item per file
|
||||
items = xml_new_node(response, "items", NULL);
|
||||
do
|
||||
{
|
||||
ua = httpd_header_find(hreq->in_headers, "User-Agent");
|
||||
client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
|
||||
transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
|
||||
|
||||
/* Item block (one item) */
|
||||
item = mxmlNewElement(items, "item");
|
||||
|
||||
for (i = 0; rsp_fields[i].field; i++)
|
||||
{
|
||||
if (!(rsp_fields[i].flags & mode))
|
||||
continue;
|
||||
|
||||
strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset);
|
||||
|
||||
if (!(*strval) || (strlen(*strval) == 0))
|
||||
continue;
|
||||
|
||||
node = mxmlNewElement(item, rsp_fields[i].field);
|
||||
|
||||
if (!transcode)
|
||||
mxmlNewText(node, 0, *strval);
|
||||
else
|
||||
{
|
||||
switch (rsp_fields[i].offset)
|
||||
{
|
||||
case dbmfi_offsetof(type):
|
||||
mxmlNewText(node, 0, "wav");
|
||||
break;
|
||||
|
||||
case dbmfi_offsetof(bitrate):
|
||||
bitrate = 0;
|
||||
ret = safe_atoi32(dbmfi.samplerate, &bitrate);
|
||||
if ((ret < 0) || (bitrate == 0))
|
||||
bitrate = 1411;
|
||||
else
|
||||
bitrate = (bitrate * 8) / 250;
|
||||
|
||||
mxmlNewTextf(node, 0, "%d", bitrate);
|
||||
break;
|
||||
|
||||
case dbmfi_offsetof(description):
|
||||
mxmlNewText(node, 0, "wav audio file");
|
||||
break;
|
||||
|
||||
case dbmfi_offsetof(codectype):
|
||||
mxmlNewText(node, 0, "wav");
|
||||
|
||||
node = mxmlNewElement(item, "original_codec");
|
||||
mxmlNewText(node, 0, *strval);
|
||||
break;
|
||||
|
||||
default:
|
||||
mxmlNewText(node, 0, *strval);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ret = item_add(items, &qp, spk_profile, hreq->user_agent, accept_codecs, mode);
|
||||
}
|
||||
while (ret == 0);
|
||||
|
||||
if (qp.filter)
|
||||
free(qp.filter);
|
||||
free(qp.filter);
|
||||
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
|
||||
|
||||
mxmlDelete(reply);
|
||||
xml_free(response);
|
||||
db_query_end(&qp);
|
||||
rsp_send_error(hreq, "Error fetching query results");
|
||||
return -1;
|
||||
@ -637,15 +578,15 @@ rsp_reply_playlist(struct httpd_request *hreq)
|
||||
|
||||
/* HACK
|
||||
* Add a dummy empty string to the items element if there is no data
|
||||
* to return - this prevents mxml from sending out an empty <items/>
|
||||
* to return - this prevents us from sending out an empty <items/>
|
||||
* tag that the SoundBridge does not handle. It's hackish, but it works.
|
||||
*/
|
||||
if (qp.results == 0)
|
||||
mxmlNewText(items, 0, "");
|
||||
xml_new_text(items, "");
|
||||
|
||||
db_query_end(&qp);
|
||||
|
||||
rsp_send_reply(hreq, reply);
|
||||
rsp_send_reply(hreq, response);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -655,10 +596,8 @@ rsp_reply_browse(struct httpd_request *hreq)
|
||||
{
|
||||
struct query_params qp;
|
||||
char *browse_item;
|
||||
mxml_node_t *reply;
|
||||
mxml_node_t *status;
|
||||
mxml_node_t *items;
|
||||
mxml_node_t *node;
|
||||
xml_node *response;
|
||||
xml_node *items;
|
||||
int records;
|
||||
int ret;
|
||||
|
||||
@ -719,33 +658,14 @@ rsp_reply_browse(struct httpd_request *hreq)
|
||||
if (qp.limit && (records > qp.limit))
|
||||
records = qp.limit;
|
||||
|
||||
/* We'd use mxmlNewXML(), but then we can't put any attributes
|
||||
* on the root node and we need some.
|
||||
*/
|
||||
reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT);
|
||||
CHECK_ERR(L_RSP, rsp_xml_response_new(&response, 0, "", records, qp.results));
|
||||
|
||||
node = mxmlNewElement(reply, "response");
|
||||
status = mxmlNewElement(node, "status");
|
||||
items = mxmlNewElement(node, "items");
|
||||
|
||||
/* Status block */
|
||||
node = mxmlNewElement(status, "errorcode");
|
||||
mxmlNewText(node, 0, "0");
|
||||
|
||||
node = mxmlNewElement(status, "errorstring");
|
||||
mxmlNewText(node, 0, "");
|
||||
|
||||
node = mxmlNewElement(status, "records");
|
||||
mxmlNewTextf(node, 0, "%d", records);
|
||||
|
||||
node = mxmlNewElement(status, "totalrecords");
|
||||
mxmlNewTextf(node, 0, "%d", qp.results);
|
||||
items = xml_new_node(response, "items", NULL);
|
||||
|
||||
/* Items block (all items) */
|
||||
while (((ret = db_query_fetch_string(&browse_item, &qp)) == 0) && (browse_item))
|
||||
{
|
||||
node = mxmlNewElement(items, "item");
|
||||
mxmlNewText(node, 0, browse_item);
|
||||
xml_new_node(items, "item", browse_item);
|
||||
}
|
||||
|
||||
if (qp.filter)
|
||||
@ -755,7 +675,7 @@ rsp_reply_browse(struct httpd_request *hreq)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RSP, "Error fetching results\n");
|
||||
|
||||
mxmlDelete(reply);
|
||||
xml_free(response);
|
||||
db_query_end(&qp);
|
||||
rsp_send_error(hreq, "Error fetching query results");
|
||||
return -1;
|
||||
@ -763,15 +683,15 @@ rsp_reply_browse(struct httpd_request *hreq)
|
||||
|
||||
/* HACK
|
||||
* Add a dummy empty string to the items element if there is no data
|
||||
* to return - this prevents mxml from sending out an empty <items/>
|
||||
* to return - this prevents us from sending out an empty <items/>
|
||||
* tag that the SoundBridge does not handle. It's hackish, but it works.
|
||||
*/
|
||||
if (qp.results == 0)
|
||||
mxmlNewText(items, 0, "");
|
||||
xml_new_text(items, "");
|
||||
|
||||
db_query_end(&qp);
|
||||
|
||||
rsp_send_reply(hreq, reply);
|
||||
rsp_send_reply(hreq, response);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -805,6 +725,16 @@ rsp_stream(struct httpd_request *hreq)
|
||||
// /rsp/stream/36364
|
||||
// /rsp/db/0?query=id%3D36365&type=full
|
||||
// /rsp/stream/36365
|
||||
//
|
||||
// Headers sent from Roku M2000 and M1001 in stream requests (and other?):
|
||||
//
|
||||
// 'User-Agent': 'Roku SoundBridge/3.0'
|
||||
// 'Host': '192.168.1.119:3689'
|
||||
// 'Accept': '*/*'
|
||||
// 'Pragma': 'no-cache'
|
||||
// 'accept-codecs': 'wma,mpeg,wav,mp4a,alac'
|
||||
// 'rsp-version': '0.1'
|
||||
// 'transcode-codecs': 'wav,mp3'
|
||||
static struct httpd_uri_map rsp_handlers[] =
|
||||
{
|
||||
{
|
||||
|
||||
@ -228,7 +228,7 @@ session_free(struct streaming_session *session)
|
||||
}
|
||||
|
||||
static struct streaming_session *
|
||||
session_new(struct httpd_request *hreq, bool icy_is_requested, enum player_format format, struct media_quality quality)
|
||||
session_new(struct httpd_request *hreq, bool icy_is_requested, enum media_format format, struct media_quality quality)
|
||||
{
|
||||
struct streaming_session *session;
|
||||
int audio_fd;
|
||||
@ -279,7 +279,7 @@ streaming_mp3_handler(struct httpd_request *hreq)
|
||||
httpd_header_add(hreq->out_headers, "icy-metaint", buf);
|
||||
}
|
||||
|
||||
session = session_new(hreq, icy_is_requested, PLAYER_FORMAT_MP3, streaming_default_quality);
|
||||
session = session_new(hreq, icy_is_requested, MEDIA_FORMAT_MP3, streaming_default_quality);
|
||||
if (!session)
|
||||
return -1; // Error sent by caller
|
||||
|
||||
|
||||
@ -36,9 +36,11 @@
|
||||
static int
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
struct transcode_decode_setup_args decode_args = { .profile = XCODE_PCM_NATIVE, .path = source->path, .len_ms = source->len_ms };
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_PCM_NATIVE, };
|
||||
struct transcode_ctx *ctx;
|
||||
|
||||
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL);
|
||||
ctx = transcode_setup(decode_args, encode_args);
|
||||
if (!ctx)
|
||||
return -1;
|
||||
|
||||
|
||||
@ -173,8 +173,9 @@ streamurl_process(struct input_metadata *metadata, const char *url)
|
||||
{
|
||||
struct http_client_ctx client = { 0 };
|
||||
struct keyval kv = { 0 };
|
||||
struct evbuffer *evbuf;
|
||||
struct evbuffer *evbuf = NULL;
|
||||
const char *content_type;
|
||||
const char *artwork_url;
|
||||
char *body;
|
||||
int ret;
|
||||
|
||||
@ -186,6 +187,21 @@ streamurl_process(struct input_metadata *metadata, const char *url)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If the StreamUrl contains a keyword followed by the actual url, e.g. http://metadata.cdnstream1.com/?yadayada&ALBUM_ART=https%3A%2F%2Fis1-ssl.mzstatic.com%2Fimage%2Fthumb%2FMusic%2F11%2Fcc%2F21%2Fmzi.nepwiuir.jpg
|
||||
if (streamurl_map[0].words)
|
||||
{
|
||||
ret = http_form_urldecode(&kv, url);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
artwork_url = keyval_get(&kv, streamurl_map[0].words);
|
||||
metadata->artwork_url = safe_strdup(artwork_url);
|
||||
keyval_clear(&kv);
|
||||
|
||||
if (metadata->artwork_url)
|
||||
goto out;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Downloading StreamUrl resource '%s'\n", url);
|
||||
|
||||
CHECK_NULL(L_PLAYER, evbuf = evbuffer_new());
|
||||
@ -219,7 +235,8 @@ streamurl_process(struct input_metadata *metadata, const char *url)
|
||||
|
||||
out:
|
||||
keyval_clear(&kv);
|
||||
evbuffer_free(evbuf);
|
||||
if (evbuf)
|
||||
evbuffer_free(evbuf);
|
||||
streamurl_settings_unload();
|
||||
return ret;
|
||||
}
|
||||
@ -276,11 +293,14 @@ metadata_prepare(struct input_source *source)
|
||||
// Note we map title to album, because clients should show stream name as title
|
||||
swap_pointers(&prepared_metadata.parsed.album, &m->title);
|
||||
|
||||
// In this case we have to go async to download the url and process the content
|
||||
if (m->url && !artwork_extension_is_artwork(m->url))
|
||||
worker_execute(streamurl_cb, m->url, strlen(m->url) + 1, 0);
|
||||
else
|
||||
swap_pointers(&prepared_metadata.parsed.artwork_url, &m->url);
|
||||
if (! SETTINGS_GETBOOL("artwork", "streamurl_ignore"))
|
||||
{
|
||||
// In this case we have to go async to download the url and process the content
|
||||
if (m->url && !artwork_extension_is_artwork(m->url))
|
||||
worker_execute(streamurl_cb, m->url, strlen(m->url) + 1, 0);
|
||||
else
|
||||
swap_pointers(&prepared_metadata.parsed.artwork_url, &m->url);
|
||||
}
|
||||
|
||||
http_icy_metadata_free(m, 0);
|
||||
return 0;
|
||||
@ -295,6 +315,8 @@ metadata_prepare(struct input_source *source)
|
||||
static int
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
struct transcode_decode_setup_args decode_args = { .profile = XCODE_PCM_NATIVE, .is_http = true, .len_ms = source->len_ms };
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_PCM_NATIVE, };
|
||||
struct transcode_ctx *ctx;
|
||||
char *url;
|
||||
|
||||
@ -303,8 +325,9 @@ setup(struct input_source *source)
|
||||
|
||||
free(source->path);
|
||||
source->path = url;
|
||||
decode_args.path = url;
|
||||
|
||||
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL);
|
||||
ctx = transcode_setup(decode_args, encode_args);
|
||||
if (!ctx)
|
||||
return -1;
|
||||
|
||||
|
||||
@ -11,16 +11,36 @@ PROTO_SRC = \
|
||||
src/proto/mercury.pb-c.c src/proto/mercury.pb-c.h \
|
||||
src/proto/metadata.pb-c.c src/proto/metadata.pb-c.h
|
||||
|
||||
HTTP_PROTO_SRC = \
|
||||
src/proto/connectivity.pb-c.c src/proto/connectivity.pb-c.h \
|
||||
src/proto/clienttoken.pb-c.c src/proto/clienttoken.pb-c.h \
|
||||
src/proto/login5_user_info.pb-c.h src/proto/login5_user_info.pb-c.c \
|
||||
src/proto/login5.pb-c.h src/proto/login5.pb-c.c \
|
||||
src/proto/login5_identifiers.pb-c.h src/proto/login5_identifiers.pb-c.c \
|
||||
src/proto/login5_credentials.pb-c.h src/proto/login5_credentials.pb-c.c \
|
||||
src/proto/login5_client_info.pb-c.h src/proto/login5_client_info.pb-c.c \
|
||||
src/proto/login5_challenges_hashcash.pb-c.h src/proto/login5_challenges_hashcash.pb-c.c \
|
||||
src/proto/login5_challenges_code.pb-c.h src/proto/login5_challenges_code.pb-c.c \
|
||||
src/proto/google_duration.pb-c.h src/proto/google_duration.pb-c.c \
|
||||
src/proto/storage_resolve.pb-c.h src/proto/storage_resolve.pb-c.c \
|
||||
src/proto/extended_metadata.pb-c.h src/proto/extended_metadata.pb-c.c \
|
||||
src/proto/extension_kind.pb-c.h src/proto/extension_kind.pb-c.c \
|
||||
src/proto/entity_extension_data.pb-c.h src/proto/entity_extension_data.pb-c.c \
|
||||
src/proto/google_any.pb-c.h src/proto/google_any.pb-c.c
|
||||
|
||||
|
||||
CORE_SRC = \
|
||||
src/librespot-c.c src/connection.c src/channel.c src/crypto.c src/commands.c
|
||||
src/librespot-c.c src/connection.c src/channel.c src/crypto.c src/commands.c \
|
||||
src/http.c
|
||||
|
||||
librespot_c_a_SOURCES = \
|
||||
$(CORE_SRC) \
|
||||
$(SHANNON_SRC) \
|
||||
$(PROTO_SRC)
|
||||
$(PROTO_SRC) \
|
||||
$(HTTP_PROTO_SRC)
|
||||
|
||||
noinst_HEADERS = \
|
||||
librespot-c.h src/librespot-c-internal.h src/connection.h \
|
||||
src/channel.h src/crypto.h src/commands.h
|
||||
src/channel.h src/crypto.h src/commands.h src/http.h
|
||||
|
||||
EXTRA_DIST = README.md LICENSE
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
AC_INIT([librespot-c], [0.1])
|
||||
AC_INIT([librespot-c], [0.6])
|
||||
AC_CONFIG_AUX_DIR([.])
|
||||
AM_INIT_AUTOMAKE([foreign subdir-objects])
|
||||
AM_SILENT_RULES([yes])
|
||||
|
||||
dnl Defines _GNU_SOURCE globally when needed
|
||||
AC_USE_SYSTEM_EXTENSIONS
|
||||
|
||||
AC_PROG_CC
|
||||
AM_PROG_AR
|
||||
AC_PROG_RANLIB
|
||||
|
||||
AM_CPPFLAGS="-Wall"
|
||||
AC_SUBST([AM_CPPFLAGS])
|
||||
|
||||
AC_CHECK_HEADERS_ONCE([sys/utsname.h])
|
||||
|
||||
AC_CHECK_HEADERS([endian.h sys/endian.h libkern/OSByteOrder.h], [found_endian_headers=yes; break;])
|
||||
@ -17,6 +25,7 @@ PKG_CHECK_MODULES([JSON_C], [json-c])
|
||||
PKG_CHECK_MODULES([LIBGCRYPT], [libgcrypt], [], [
|
||||
AM_PATH_LIBGCRYPT([], [], [AC_MSG_ERROR([[libgcrypt is required]])])
|
||||
])
|
||||
|
||||
PKG_CHECK_MODULES([LIBCURL], [libcurl])
|
||||
PKG_CHECK_MODULES([LIBPROTOBUF_C], [libprotobuf-c])
|
||||
|
||||
|
||||
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