Compare commits
930 Commits
1.1.1
...
l10n_maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
610b458196 | ||
|
|
ad285a91f2 | ||
|
|
6400f903e8 | ||
|
|
83d5f3d8f2 | ||
|
|
582b8e8076 | ||
|
|
030908f6ac | ||
|
|
34ae443548 | ||
|
|
1c9e99e776 | ||
|
|
4e6459b977 | ||
|
|
d2b5359fa9 | ||
|
|
1d82977441 | ||
|
|
17519076f5 | ||
|
|
e8d9e1172f | ||
|
|
aac8d4e68b | ||
|
|
0e28e800b3 | ||
|
|
cf84bc29ab | ||
|
|
afc3effc9d | ||
|
|
370bebff5f | ||
|
|
db410cc257 | ||
|
|
20a9946e51 | ||
|
|
b6c8ff19af | ||
|
|
80d058af10 | ||
|
|
da2f28811a | ||
|
|
0bfa736081 | ||
|
|
1392bcbbe1 | ||
|
|
11705889f1 | ||
|
|
604dd2dbde | ||
|
|
25065ba98f | ||
|
|
7b16ec62bb | ||
|
|
d8f31592b9 | ||
|
|
80bb0f476d | ||
|
|
b7222ac85c | ||
|
|
241bca0828 | ||
|
|
90d86b10a3 | ||
|
|
4130c6670f | ||
|
|
8d262959c1 | ||
|
|
b9b70399d8 | ||
|
|
527ccd212a | ||
|
|
4a5afbeb1e | ||
|
|
63c14e014b | ||
|
|
801658c6b9 | ||
|
|
16fe665295 | ||
|
|
2bb0dbada2 | ||
|
|
2cd9498469 | ||
|
|
d1ee27fff9 | ||
|
|
91adf5ba32 | ||
|
|
d68f464269 | ||
|
|
c684a95f89 | ||
|
|
1d03bb2178 | ||
|
|
39f9128ecf | ||
|
|
ca2e802239 | ||
|
|
9a513a9a56 | ||
|
|
1c2e87b741 | ||
|
|
fe4d9979ce | ||
|
|
d8ae790ebf | ||
|
|
ac31d79294 | ||
|
|
4ffebd77b1 | ||
|
|
6682efae2f | ||
|
|
480161c6b7 | ||
|
|
a8ba420d72 | ||
|
|
fc0ec91652 | ||
|
|
0701b97324 | ||
|
|
3867932e1e | ||
|
|
e2907f6051 | ||
|
|
0827ec7f16 | ||
|
|
24d2adf363 | ||
|
|
592729d00b | ||
|
|
c4a564bb56 | ||
|
|
812a02a3a1 | ||
|
|
944936914b | ||
|
|
e7c901d4f3 | ||
|
|
8e996119af | ||
|
|
4348a654ca | ||
|
|
f0be1c782a | ||
|
|
e9898d08bc | ||
|
|
1ad13cd3b0 | ||
|
|
5c640e0e36 | ||
|
|
059def8d0c | ||
|
|
cf15a1f423 | ||
|
|
5d35b0eedd | ||
|
|
5fcb71d08f | ||
|
|
15c2237d4a | ||
|
|
93af866185 | ||
|
|
109ff90401 | ||
|
|
d4b06289c3 | ||
|
|
4351c555a0 | ||
|
|
ce4db40983 | ||
|
|
d37fb7410c | ||
|
|
f1cdd71494 | ||
|
|
000ba997fb | ||
|
|
579efffd14 | ||
|
|
3a098c8a0c | ||
|
|
5bce0ae87f | ||
|
|
afe6967c46 | ||
|
|
e91bab6d42 | ||
|
|
5a64247761 | ||
|
|
9ed89afdb2 | ||
|
|
0ac338026c | ||
|
|
4e5f84a7b7 | ||
|
|
320a3c6815 | ||
|
|
72dd1d783a | ||
|
|
d2205cfe81 | ||
|
|
5830f247f6 | ||
|
|
8b4c57d933 | ||
|
|
67cec09176 | ||
|
|
2df658e1f3 | ||
|
|
f3bc9b151c | ||
|
|
b06b59d0c5 | ||
|
|
99d75ade06 | ||
|
|
3f63246068 | ||
|
|
b205a5f964 | ||
|
|
aeaef12dd4 | ||
|
|
02d76f17f7 | ||
|
|
e4e12c6fa6 | ||
|
|
270ae6085b | ||
|
|
7065a405a5 | ||
|
|
d8c72c3dd9 | ||
|
|
b65502e167 | ||
|
|
132f8df853 | ||
|
|
12e3cffe63 | ||
|
|
56a637682d | ||
|
|
d9b105f89e | ||
|
|
bd6b45e43f | ||
|
|
539172fb70 | ||
|
|
ebd92b3a7f | ||
|
|
b00ae5b210 | ||
|
|
c8e3cf981b | ||
|
|
038f69779f | ||
|
|
a4de7559ac | ||
|
|
0537b072fe | ||
|
|
2657c9f96a | ||
|
|
7e178b1f1a | ||
|
|
dd8513d02c | ||
|
|
5f0175094b | ||
|
|
b4c5b9e1e1 | ||
|
|
2ce0ed2ef8 | ||
|
|
176768f7f8 | ||
|
|
50b954034c | ||
|
|
cab7b6c335 | ||
|
|
fce1dacafc | ||
|
|
94aa6fb940 | ||
|
|
0cfd4aaad1 | ||
|
|
9e72b4fe80 | ||
|
|
1151443372 | ||
|
|
8f6993e7c8 | ||
|
|
a6ab1a7689 | ||
|
|
098b21d818 | ||
|
|
d61adeb595 | ||
|
|
8bfc3bc41c | ||
|
|
0dda2feec3 | ||
|
|
1d0d03ed83 | ||
|
|
330284f03e | ||
|
|
fc3ed3a2ce | ||
|
|
6a656036fe | ||
|
|
5c76c633a5 | ||
|
|
d487c3ea07 | ||
|
|
83dca405af | ||
|
|
acd0b6d3ea | ||
|
|
159242aff4 | ||
|
|
4b014253cf | ||
|
|
1ec6b5582e | ||
|
|
08b8d04500 | ||
|
|
54679b1d57 | ||
|
|
8d648e668e | ||
|
|
5897e786dc | ||
|
|
7f549aa991 | ||
|
|
792e7b6274 | ||
|
|
92c58b0b60 | ||
|
|
5fac9a1c8d | ||
|
|
7f4f715003 | ||
|
|
75d1d4098e | ||
|
|
30e80068a3 | ||
|
|
5fe9a1528f | ||
|
|
7777eda115 | ||
|
|
ce4f2ece93 | ||
|
|
52399d73fe | ||
|
|
6e98166148 | ||
|
|
c658a77b05 | ||
|
|
1880dc8153 | ||
|
|
b5fd3d5717 | ||
|
|
3c3480fb84 | ||
|
|
f628914173 | ||
|
|
c100fb1bb8 | ||
|
|
8c804c4fba | ||
|
|
912a7c7da9 | ||
|
|
0a5815c82e | ||
|
|
6513b3032b | ||
|
|
8c51401bdc | ||
|
|
45fc9c83d4 | ||
|
|
be57d8147a | ||
|
|
a97908fb6b | ||
|
|
c0417d4bb3 | ||
|
|
062e2cfb84 | ||
|
|
700f7dbe36 | ||
|
|
0487118dad | ||
|
|
f3d088e48b | ||
|
|
f8afd49fcf | ||
|
|
363fcb5aba | ||
|
|
183aba4181 | ||
|
|
742be01aa6 | ||
|
|
38c8054873 | ||
|
|
da9e9840b8 | ||
|
|
c4646531b0 | ||
|
|
65d9b6a9e9 | ||
|
|
046f40fbca | ||
|
|
a0ca50ac30 | ||
|
|
d939733675 | ||
|
|
61a8a3a84a | ||
|
|
d4858a338c | ||
|
|
31380a5bd4 | ||
|
|
e45b9aabeb | ||
|
|
27e782d8cf | ||
|
|
0bfc2ee198 | ||
|
|
e7fc4b1706 | ||
|
|
6dea1a2149 | ||
|
|
7844a2b932 | ||
|
|
96a53bfbe5 | ||
|
|
fe5fbae4b4 | ||
|
|
a9140232e5 | ||
|
|
835090dd96 | ||
|
|
af5590dcb1 | ||
|
|
26b5588d7d | ||
|
|
390bf049f2 | ||
|
|
321272b695 | ||
|
|
342805e0f3 | ||
|
|
e614626913 | ||
|
|
2ddacf2f98 | ||
|
|
a47531d4ce | ||
|
|
84b758e395 | ||
|
|
51b69a85c4 | ||
|
|
52774a3222 | ||
|
|
9030b2567b | ||
|
|
ee7bb449a5 | ||
|
|
d901258f11 | ||
|
|
6372c5ee7d | ||
|
|
75f0402793 | ||
|
|
20e5c014ef | ||
|
|
1ebc32c3aa | ||
|
|
a5f94b608b | ||
|
|
e0d61223a4 | ||
|
|
459eea5bc4 | ||
|
|
09d02c53a3 | ||
|
|
61a701554e | ||
|
|
d280e6426f | ||
|
|
5b9bb3efa7 | ||
|
|
b8cbe49f8c | ||
|
|
633e5707ef | ||
|
|
d54290c3a7 | ||
|
|
3ef2b53e46 | ||
|
|
d3a4dd6da6 | ||
|
|
0158f7f08a | ||
|
|
8cea020fac | ||
|
|
f6b38fecb0 | ||
|
|
5e2729fafe | ||
|
|
19dce1c25d | ||
|
|
00bb722e25 | ||
|
|
cbaf4d3121 | ||
|
|
4b5370044b | ||
|
|
ffbe1ec9fd | ||
|
|
53e43db91b | ||
|
|
2858cdabc2 | ||
|
|
cf74eeb120 | ||
|
|
790a1b4dbf | ||
|
|
ee6332af1e | ||
|
|
bf0704f6b2 | ||
|
|
ae13fe7f52 | ||
|
|
90678e72ac | ||
|
|
a0ec244008 | ||
|
|
fba4f84fb6 | ||
|
|
950774f1c8 | ||
|
|
340bc21537 | ||
|
|
a86ba4dffc | ||
|
|
d6bc6e33c0 | ||
|
|
7e128a9af5 | ||
|
|
0f0746be9d | ||
|
|
bec3fe9fd5 | ||
|
|
83c666baf9 | ||
|
|
b9b54e6e96 | ||
|
|
b2ff6240eb | ||
|
|
26a7c74a24 | ||
|
|
a34954ec4a | ||
|
|
349ab62e75 | ||
|
|
65e960f2c5 | ||
|
|
e22fef8ca4 | ||
|
|
3e99045e2c | ||
|
|
4fcade273e | ||
|
|
5eaff0d26e | ||
|
|
5b22f12b4a | ||
|
|
5f85c2e7a5 | ||
|
|
4fb5a7b6bc | ||
|
|
04c6c862c4 | ||
|
|
baec45f742 | ||
|
|
9efdbd2c10 | ||
|
|
d8800b80d5 | ||
|
|
ec715abb0d | ||
|
|
1485801efb | ||
|
|
4f9ac3d33a | ||
|
|
1577ce4d67 | ||
|
|
7eee74a2e9 | ||
|
|
d9e38fb3be | ||
|
|
81cc90e54a | ||
|
|
bd9771a88f | ||
|
|
f5cd81fe09 | ||
|
|
277e2cff59 | ||
|
|
6fa9514059 | ||
|
|
c5e38b71f7 | ||
|
|
3746915ae7 | ||
|
|
21bdf88d09 | ||
|
|
ff032c3cd7 | ||
|
|
c083110051 | ||
|
|
a7dbeb5d76 | ||
|
|
634f6ea9f5 | ||
|
|
f9e4f9a09a | ||
|
|
aab9889174 | ||
|
|
3b560e4e4f | ||
|
|
9e327c9556 | ||
|
|
1ec640e088 | ||
|
|
463aaf6942 | ||
|
|
71287dd77e | ||
|
|
b66c0f5573 | ||
|
|
a9f2c384fa | ||
|
|
ae9584c213 | ||
|
|
4db1c5ceb8 | ||
|
|
1738259467 | ||
|
|
fcee02edc1 | ||
|
|
4fff5820c5 | ||
|
|
9aa6da2faf | ||
|
|
279934411c | ||
|
|
fd829551e8 | ||
|
|
be8f515388 | ||
|
|
dd12462fbb | ||
|
|
5c3ded0099 | ||
|
|
2c9b14f5f2 | ||
|
|
8c195382c4 | ||
|
|
6e5198799c | ||
|
|
389d04bbec | ||
|
|
6f0f88dd01 | ||
|
|
3dce84c73c | ||
|
|
e798349aca | ||
|
|
41ecf8e535 | ||
|
|
70c96ded28 | ||
|
|
d9cc6bf238 | ||
|
|
0a70ac1c95 | ||
|
|
e3a3f44513 | ||
|
|
c2e42ebf3a | ||
|
|
d71b344e77 | ||
|
|
d5f7a4b883 | ||
|
|
34fb289e33 | ||
|
|
6d4d8251ad | ||
|
|
fc69ef27e8 | ||
|
|
bbd8a24b75 | ||
|
|
9fa9012c70 | ||
|
|
2a4fc185ac | ||
|
|
fa0703246b | ||
|
|
954c21e21e | ||
|
|
8954dfb0aa | ||
|
|
5e031be42c | ||
|
|
d5281abb22 | ||
|
|
f5368d8108 | ||
|
|
e2dc22c2c8 | ||
|
|
817ca828e6 | ||
|
|
61dc2cc640 | ||
|
|
cd4adf6f89 | ||
|
|
91ebcc948e | ||
|
|
e7e6d59172 | ||
|
|
95b12a01a4 | ||
|
|
5947aeae24 | ||
|
|
0298fa0b73 | ||
|
|
05d72c8bd6 | ||
|
|
70b7c4560d | ||
|
|
2687dc31cc | ||
|
|
cabf1cb78d | ||
|
|
d9f68ab944 | ||
|
|
8630c5329d | ||
|
|
66e175f6d1 | ||
|
|
1173d5f865 | ||
|
|
b02b114caf | ||
|
|
cd516c37b9 | ||
|
|
7de8a44709 | ||
|
|
bb345b14de | ||
|
|
baa82966d8 | ||
|
|
f85d60f5cd | ||
|
|
6f731fcf4a | ||
|
|
bdbe66b116 | ||
|
|
5ae0320911 | ||
|
|
31671af5f5 | ||
|
|
27184eb001 | ||
|
|
2e30f5c585 | ||
|
|
109d3f9ec3 | ||
|
|
e9d413c7dc | ||
|
|
ee60191b6c | ||
|
|
a6d8627129 | ||
|
|
d317c9158b | ||
|
|
3716e8c3ef | ||
|
|
ce0e1e900e | ||
|
|
f1aa92ec9f | ||
|
|
25bdfcdb76 | ||
|
|
6a3de3937a | ||
|
|
5f775e87ae | ||
|
|
1fd83c55ee | ||
|
|
e6e9edef7d | ||
|
|
1bae665f76 | ||
|
|
5bfc8b74f5 | ||
|
|
e588896729 | ||
|
|
0cd0f7f2e7 | ||
|
|
6db540a3a7 | ||
|
|
82679e0cea | ||
|
|
d571bc3305 | ||
|
|
2b52553864 | ||
|
|
215627b0e4 | ||
|
|
b17cae6ec7 | ||
|
|
30ac9697ea | ||
|
|
61e3ea249d | ||
|
|
d1986eeae2 | ||
|
|
ba354207d2 | ||
|
|
eac5674891 | ||
|
|
8349a8b0ee | ||
|
|
b9b4e9f831 | ||
|
|
98ff2525f0 | ||
|
|
3fd29c6dcc | ||
|
|
4429e9973f | ||
|
|
1572d241d5 | ||
|
|
eae7e2a9e6 | ||
|
|
251e5b379b | ||
|
|
0db082fca0 | ||
|
|
2799e55076 | ||
|
|
440ee43a91 | ||
|
|
98c72ec1f8 | ||
|
|
759488ae1a | ||
|
|
055cb413c9 | ||
|
|
f760b87b58 | ||
|
|
39f228f862 | ||
|
|
a683a279f5 | ||
|
|
e800926f57 | ||
|
|
dd9f80d539 | ||
|
|
8484cac4ed | ||
|
|
e24097582f | ||
|
|
58fc8c82bb | ||
|
|
02bb875bb3 | ||
|
|
5db01482eb | ||
|
|
719fa6ffb3 | ||
|
|
159be5d79e | ||
|
|
911237e281 | ||
|
|
ae89ca8123 | ||
|
|
b832675893 | ||
|
|
b4cfe636c9 | ||
|
|
e6a0945dfa | ||
|
|
726c105ed6 | ||
|
|
d73cbc3a1d | ||
|
|
121f45d3b6 | ||
|
|
3a9ea81929 | ||
|
|
b919472241 | ||
|
|
e8c8b39410 | ||
|
|
6904efef47 | ||
|
|
fafa89baff | ||
|
|
d9062446f5 | ||
|
|
9256b92d8f | ||
|
|
f66459f3cb | ||
|
|
f8ea9631ca | ||
|
|
ab558f87b5 | ||
|
|
ab73eda2be | ||
|
|
92f34ff36e | ||
|
|
d0bf2d7a9c | ||
|
|
b2cd3afe55 | ||
|
|
decd0a1dc6 | ||
|
|
3e0a9fa388 | ||
|
|
e5b6c5959f | ||
|
|
8a9db5440d | ||
|
|
28a25c5763 | ||
|
|
ea49fbcbee | ||
|
|
d9807b358e | ||
|
|
3e53d2b237 | ||
|
|
9122881f74 | ||
|
|
9c36d7fd43 | ||
|
|
a4a365cbee | ||
|
|
00f06b22b8 | ||
|
|
e2d8838fca | ||
|
|
9427691f39 | ||
|
|
bebdcc4e7f | ||
|
|
041f761921 | ||
|
|
1435ae6dc0 | ||
|
|
33ae53a90f | ||
|
|
01c28867b7 | ||
|
|
08fb2ae331 | ||
|
|
99970f9e52 | ||
|
|
bc206f43b4 | ||
|
|
018448159c | ||
|
|
81fe90bdef | ||
|
|
319558c535 | ||
|
|
9095b0d6b2 | ||
|
|
558eae1ca1 | ||
|
|
2c64f05cea | ||
|
|
c80e7071a1 | ||
|
|
038e679000 | ||
|
|
72447fecfb | ||
|
|
c7830f6f05 | ||
|
|
af525e42b6 | ||
|
|
eb83f23125 | ||
|
|
7527d2ea9a | ||
|
|
69d38879d2 | ||
|
|
cbce9f7191 | ||
|
|
f938129d81 | ||
|
|
415a40ea04 | ||
|
|
6e7aaed4ee | ||
|
|
7afae70bb0 | ||
|
|
be8097919b | ||
|
|
1990a42e1d | ||
|
|
8fcee4511d | ||
|
|
24af1be666 | ||
|
|
8302a95bc1 | ||
|
|
36a8ab49a0 | ||
|
|
5c64dc9c4d | ||
|
|
c271743208 | ||
|
|
ee49b1ddc8 | ||
|
|
bf98633f16 | ||
|
|
e2a928f2dc | ||
|
|
47d3312a6b | ||
|
|
4f97325953 | ||
|
|
91e8fe0943 | ||
|
|
dc5894b38a | ||
|
|
52ee50a2a4 | ||
|
|
f545b028ee | ||
|
|
a96627c5a9 | ||
|
|
a13fc31f83 | ||
|
|
9ee5c8dc17 | ||
|
|
82cd425ece | ||
|
|
3b02d364ba | ||
|
|
73e7947487 | ||
|
|
cfc9a43b88 | ||
|
|
969500023a | ||
|
|
53c72d4f8e | ||
|
|
f971c92f32 | ||
|
|
82156e8a13 | ||
|
|
6a6285861e | ||
|
|
dae8c8730b | ||
|
|
20b47eae8f | ||
|
|
2ce8220d88 | ||
|
|
35b0b5df57 | ||
|
|
63631d6b0c | ||
|
|
21ab2ef1a7 | ||
|
|
a3f96d2b85 | ||
|
|
e2c1cb0116 | ||
|
|
07e295776b | ||
|
|
8604a39d94 | ||
|
|
315240fec7 | ||
|
|
fbc6d326f8 | ||
|
|
c082377e7a | ||
|
|
448445a38a | ||
|
|
18f835d7e5 | ||
|
|
fd427dac29 | ||
|
|
e1afe03d51 | ||
|
|
1b49653974 | ||
|
|
d66126f998 | ||
|
|
0fff5f672a | ||
|
|
2726f01fb3 | ||
|
|
9a74fce53d | ||
|
|
b3be8387f1 | ||
|
|
d396cb515d | ||
|
|
2548b4648e | ||
|
|
df0ec6b709 | ||
|
|
376af26f0e | ||
|
|
9bff55e1ee | ||
|
|
8f7e29f503 | ||
|
|
c3aa885a0f | ||
|
|
77ea5729c3 | ||
|
|
fac323a4a5 | ||
|
|
a26066d70f | ||
|
|
d500d38e63 | ||
|
|
295d4d9d05 | ||
|
|
01aaa0ba0b | ||
|
|
7b23118475 | ||
|
|
0c7806ab0a | ||
|
|
93c10fd77d | ||
|
|
14e8cc511a | ||
|
|
7674bd1926 | ||
|
|
be351f3118 | ||
|
|
47aa0b05b6 | ||
|
|
6d32cc9c4e | ||
|
|
fd9f4575eb | ||
|
|
c62fd2b58a | ||
|
|
712db598f7 | ||
|
|
0cccc30c98 | ||
|
|
19e6ebe9c4 | ||
|
|
24ed5413b2 | ||
|
|
a239c127bd | ||
|
|
61daec6597 | ||
|
|
544db75eb4 | ||
|
|
6a60eff5cd | ||
|
|
63aa04241c | ||
|
|
7a7550388d | ||
|
|
f2845b6632 | ||
|
|
deaeab3cbb | ||
|
|
16c9a0f974 | ||
|
|
18000b1b2c | ||
|
|
f1b56028b7 | ||
|
|
3d2315f754 | ||
|
|
07c898581c | ||
|
|
6612eeb9e3 | ||
|
|
04064ebf68 | ||
|
|
3de3c52c01 | ||
|
|
93929c73ee | ||
|
|
d68bede374 | ||
|
|
b659b27f95 | ||
|
|
70d0772e04 | ||
|
|
0a361bfb3b | ||
|
|
f9f47458d5 | ||
|
|
218dd439b6 | ||
|
|
7f3293609b | ||
|
|
356b7d8e64 | ||
|
|
d26c291a2a | ||
|
|
82d34eea7b | ||
|
|
4b0b0aa989 | ||
|
|
975d0dff25 | ||
|
|
b75410abc6 | ||
|
|
c0f5b53aaf | ||
|
|
ba285925ca | ||
|
|
a0dd2c66e4 | ||
|
|
64a9d557a4 | ||
|
|
3a5e5d4aaa | ||
|
|
f59c6c356e | ||
|
|
65b6e6d540 | ||
|
|
0d2e933ed1 | ||
|
|
b16fbb3040 | ||
|
|
638fc2881c | ||
|
|
2656cfd3ad | ||
|
|
80c5829792 | ||
|
|
957a850adc | ||
|
|
2a5e425b71 | ||
|
|
904ffb1417 | ||
|
|
777fec9a92 | ||
|
|
a2017c003e | ||
|
|
95842225fb | ||
|
|
7cfb175a45 | ||
|
|
b7165e0124 | ||
|
|
7132b06d6a | ||
|
|
4f79b8692c | ||
|
|
a8e307bb6a | ||
|
|
a30aca4759 | ||
|
|
4c0220d10a | ||
|
|
5fede2551e | ||
|
|
e83b521ee0 | ||
|
|
8da2b9cd94 | ||
|
|
dfcf715291 | ||
|
|
24c89b8ca3 | ||
|
|
b106e94494 | ||
|
|
28222b1832 | ||
|
|
c818dabe92 | ||
|
|
60f4a57425 | ||
|
|
a9ea686577 | ||
|
|
756f7cf6af | ||
|
|
ef9ef63f02 | ||
|
|
722035913e | ||
|
|
50aa2dcc2b | ||
|
|
b6cbebcc8a | ||
|
|
ba127c57d8 | ||
|
|
ef261455a2 | ||
|
|
1b1ab2e833 | ||
|
|
fbf7fa51e5 | ||
|
|
b0d2da04ac | ||
|
|
0ab16c9ebf | ||
|
|
d930ee205f | ||
|
|
0e330b81db | ||
|
|
6931538ebf | ||
|
|
b166396ef3 | ||
|
|
026c2677f9 | ||
|
|
34e2e01992 | ||
|
|
7b2d8ac1a2 | ||
|
|
256cc7d15c | ||
|
|
1d9b8f464a | ||
|
|
a8d1bf7e73 | ||
|
|
c58207dd2f | ||
|
|
1720ddc808 | ||
|
|
056d8817b2 | ||
|
|
e12f27a945 | ||
|
|
39fd89cb90 | ||
|
|
756215544a | ||
|
|
0768298b95 | ||
|
|
525ebbb9b7 | ||
|
|
c47ec3e70a | ||
|
|
20394271c7 | ||
|
|
731f670a2b | ||
|
|
d35c8e5b93 | ||
|
|
b1161d3542 | ||
|
|
47a01fc659 | ||
|
|
d72694ce06 | ||
|
|
85af736bfd | ||
|
|
b50da3eba4 | ||
|
|
4479daeaf1 | ||
|
|
a123de06c6 | ||
|
|
62f2aee00c | ||
|
|
b59cc4d038 | ||
|
|
3468737e14 | ||
|
|
5292e53b4a | ||
|
|
bae3fcfaba | ||
|
|
e4a57aa768 | ||
|
|
25451d361c | ||
|
|
daaacf4663 | ||
|
|
3cb0f60900 | ||
|
|
f2e28d18bc | ||
|
|
6cb54b9f9f | ||
|
|
ce3db115a3 | ||
|
|
dae4943593 | ||
|
|
ca8aa40034 | ||
|
|
a239374c4b | ||
|
|
1c833a28dc | ||
|
|
c30de78bf1 | ||
|
|
19b3adeb96 | ||
|
|
4b55150515 | ||
|
|
5bcfd5dd25 | ||
|
|
722f93b090 | ||
|
|
1ef50333a6 | ||
|
|
fa6ac4df41 | ||
|
|
517a8baea2 | ||
|
|
5473a6f8ac | ||
|
|
4d4dd6b249 | ||
|
|
2e6408a4f7 | ||
|
|
3c984ac8cf | ||
|
|
e52ea2a255 | ||
|
|
cf22b183a5 | ||
|
|
11d4151a82 | ||
|
|
c5f9e43d10 | ||
|
|
3329bc7d76 | ||
|
|
4c8ada2b80 | ||
|
|
6b446eb693 | ||
|
|
1663fa3ef1 | ||
|
|
7da0df2178 | ||
|
|
254c442a69 | ||
|
|
0568c47a37 | ||
|
|
c9c6c2ad12 | ||
|
|
f8ce2f1705 | ||
|
|
f9c35edd7c | ||
|
|
4cd490d871 | ||
|
|
936521981b | ||
|
|
fa96cdfaea | ||
|
|
2c13942ed5 | ||
|
|
af8a7d5093 | ||
|
|
04eb97ef93 | ||
|
|
0b49dcaa2d | ||
|
|
c3008b4179 | ||
|
|
28a29d219e | ||
|
|
0919d5d3de | ||
|
|
167e0d73d1 | ||
|
|
d87560b73e | ||
|
|
e6607fef6e | ||
|
|
f815b2b699 | ||
|
|
247c0ae46a | ||
|
|
f62d22f94a | ||
|
|
78a9cb35a3 | ||
|
|
e72b001d02 | ||
|
|
5bdbb9f13f | ||
|
|
0e7ab1cab8 | ||
|
|
f11de5bfc2 | ||
|
|
6d68f7f008 | ||
|
|
2bed827eb6 | ||
|
|
98cfef3306 | ||
|
|
f44839137c | ||
|
|
0decbdc2fb | ||
|
|
23b5ceb6c9 | ||
|
|
4b36beec40 | ||
|
|
b60ca9f6fa | ||
|
|
718c24ee47 | ||
|
|
86dd2886d2 | ||
|
|
23fb4651be | ||
|
|
877c9ca41a | ||
|
|
99fb775e30 | ||
|
|
1bb764a2d2 | ||
|
|
a3eab902ff | ||
|
|
e9684cd1a1 | ||
|
|
9ae0afaaf7 | ||
|
|
48bb5a3e9f | ||
|
|
f642513587 | ||
|
|
255623bbfd | ||
|
|
4270b12cd1 | ||
|
|
e3e6a22172 | ||
|
|
eb30c654c5 | ||
|
|
3b925c24ad | ||
|
|
beefdabc64 | ||
|
|
91e06cadf3 | ||
|
|
4ea5eb8292 | ||
|
|
af06f8e70a | ||
|
|
3238e295ba | ||
|
|
26fbd5c144 | ||
|
|
e64a2b98c4 | ||
|
|
42417e5554 | ||
|
|
82e613206e | ||
|
|
f1038152e9 | ||
|
|
2597a8faed | ||
|
|
8008ec895a | ||
|
|
d85d25b931 | ||
|
|
de62552ad1 | ||
|
|
155485173b | ||
|
|
82079fcf70 | ||
|
|
3b2eea5292 | ||
|
|
1f2ad9c177 | ||
|
|
cc6e5a6c69 | ||
|
|
c77c7a247a | ||
|
|
552440f50e | ||
|
|
2a9ccd7480 | ||
|
|
f265c055a5 | ||
|
|
837e5388ea | ||
|
|
9883dc6925 | ||
|
|
e3ad00e930 | ||
|
|
6428ae8b3a | ||
|
|
07c182d5b8 | ||
|
|
64aa15842c | ||
|
|
19c8da06e6 | ||
|
|
7dd959f4a1 | ||
|
|
148ae530d8 | ||
|
|
e75698ee9a | ||
|
|
80bea31b98 | ||
|
|
4ea57d1181 | ||
|
|
854847ca8a | ||
|
|
bd39e7cb0d | ||
|
|
20ef621a20 | ||
|
|
145c276c97 | ||
|
|
1c5d0dceb1 | ||
|
|
359f320b06 | ||
|
|
108d522dcf | ||
|
|
96e746c508 | ||
|
|
b2c862e7d5 | ||
|
|
9334fe9f24 | ||
|
|
b964385024 | ||
|
|
3f3059c98b | ||
|
|
8da616491d | ||
|
|
cb0db8750f | ||
|
|
08224443e3 | ||
|
|
5c2989196f | ||
|
|
4c4c84e104 | ||
|
|
232399ea28 | ||
|
|
9d22e4ec07 | ||
|
|
ee5bc16e47 | ||
|
|
74f0f885b9 | ||
|
|
b914d9aaba | ||
|
|
136f150d67 | ||
|
|
5b50cbb61b | ||
|
|
cb3a9bf195 | ||
|
|
cb847951e6 | ||
|
|
11228f0634 | ||
|
|
a0889d60f1 | ||
|
|
0055ebe8a7 | ||
|
|
58c7a9b9cc | ||
|
|
6afc081ff0 | ||
|
|
2c0ad2fc88 | ||
|
|
77e934beab | ||
|
|
368022ec43 | ||
|
|
69f8ca95bc | ||
|
|
dde8661e93 | ||
|
|
2604e1a0ff | ||
|
|
e8471bcc66 | ||
|
|
d230dd7365 | ||
|
|
74dce24e91 | ||
|
|
bc667a6474 | ||
|
|
a2cae06582 | ||
|
|
5212587055 | ||
|
|
efd42bc68f | ||
|
|
ebaa2e7918 | ||
|
|
7ebcc73a49 | ||
|
|
be09011bb7 | ||
|
|
2778a55e8e | ||
|
|
9b5fe3bfd6 | ||
|
|
91eef0d695 | ||
|
|
88704efad8 | ||
|
|
f4e4483392 | ||
|
|
63102bce23 | ||
|
|
8890a3dd0f | ||
|
|
3d9dec2c27 | ||
|
|
c96ad61c80 | ||
|
|
acd74d5b54 | ||
|
|
4808188964 | ||
|
|
1bb045b3b0 | ||
|
|
bdca60c0ad | ||
|
|
8d9c135498 | ||
|
|
0f76482916 | ||
|
|
38eb86bdee | ||
|
|
cbce9892d5 | ||
|
|
f624b7a331 | ||
|
|
1ebcd61a75 | ||
|
|
358da72ffe | ||
|
|
9666feca37 | ||
|
|
03eb52eac8 | ||
|
|
6562cc710c | ||
|
|
222001bc13 | ||
|
|
e93f14248a | ||
|
|
7119f1bc81 | ||
|
|
548fa3f6ee | ||
|
|
8ddd309d5d | ||
|
|
8c8acbb546 | ||
|
|
fe30f27af3 | ||
|
|
d5d2eaba8a | ||
|
|
e8f64bfe8f | ||
|
|
79b03993b0 | ||
|
|
e3b8f9cb8c | ||
|
|
819463a865 | ||
|
|
c69777ca39 | ||
|
|
40f3e828aa | ||
|
|
dc8520ebec | ||
|
|
7ffbedf0dd | ||
|
|
826fad1ad4 | ||
|
|
1c4d3aebad | ||
|
|
ff6e93fc15 | ||
|
|
17e88bb97d | ||
|
|
d3dd26c596 | ||
|
|
c2a7509e0e | ||
|
|
079040b721 | ||
|
|
b80d239820 | ||
|
|
c3ff43dac2 | ||
|
|
4ac51afd8f | ||
|
|
26eab51698 | ||
|
|
388dcf2a1f | ||
|
|
9bfd14d067 | ||
|
|
061e0562d1 | ||
|
|
2ba20b013d | ||
|
|
dc36f87a6e | ||
|
|
e82ecb48b8 | ||
|
|
c56a134179 | ||
|
|
11028456ad | ||
|
|
81fcb02974 | ||
|
|
5ef4976c53 | ||
|
|
160356072f | ||
|
|
c10df5b634 | ||
|
|
70fdd5b0b3 | ||
|
|
6c1ec51fab | ||
|
|
66b1c22174 | ||
|
|
97138fd6b9 | ||
|
|
03c69be421 | ||
|
|
3a9a952cb0 | ||
|
|
3bd0331aa3 | ||
|
|
3ecf224d91 | ||
|
|
37743606a2 | ||
|
|
58587f4a9a |
@@ -130,7 +130,10 @@ InsertBraces: false
|
||||
InsertTrailingCommas: None
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
KeepEmptyLines:
|
||||
AtEndOfFile: true
|
||||
AtStartOfBlock: true
|
||||
AtStartOfFile: true
|
||||
LambdaBodyIndentation: Signature
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
594
.github/workflows/build.yml
vendored
594
.github/workflows/build.yml
vendored
File diff suppressed because it is too large
Load Diff
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/build
|
||||
/bin
|
||||
/CMakeLists.txt.user
|
||||
/.qtcreator
|
||||
/.kdev4
|
||||
/strawberry.kdev4
|
||||
/.vscode
|
||||
@@ -11,6 +12,5 @@
|
||||
/out
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/dist/unix/strawberry.spec
|
||||
/dist/windows/strawberry.nsi
|
||||
src/translations/translations.pot
|
||||
/debian/changelog
|
||||
_codeql_detected_source_root
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "3rdparty/kdsingleapplication/KDSingleApplication"]
|
||||
path = 3rdparty/kdsingleapplication/KDSingleApplication
|
||||
url = https://github.com/KDAB/KDSingleApplication.git
|
||||
branch = master
|
||||
|
||||
21
3rdparty/README.md
vendored
21
3rdparty/README.md
vendored
@@ -1,21 +0,0 @@
|
||||
3rdparty libraries located in this directory
|
||||
============================================
|
||||
|
||||
KDSingleApplication
|
||||
-----------------
|
||||
This is a small static library used by Strawberry to prevent it from starting twice per user session.
|
||||
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
|
||||
It is also used to pass command-line options through to the first instance.
|
||||
|
||||
URL: https://github.com/KDAB/KDSingleApplication/
|
||||
|
||||
|
||||
SPMediaKeyTap
|
||||
-------------
|
||||
Used on macOS to exclusively enable strawberry to grab global media shortcuts.
|
||||
Can safely be deleted on other platforms.
|
||||
|
||||
|
||||
getopt
|
||||
------
|
||||
getopt included only when compiling on Windows.
|
||||
11
3rdparty/SPMediaKeyTap/CMakeLists.txt
vendored
11
3rdparty/SPMediaKeyTap/CMakeLists.txt
vendored
@@ -1,11 +0,0 @@
|
||||
set(SPMEDIAKEY-SOURCES
|
||||
SPMediaKeyTap.m
|
||||
)
|
||||
|
||||
set(SPMEDIAKEY-HEADERS
|
||||
SPMediaKeyTap.h
|
||||
)
|
||||
|
||||
ADD_LIBRARY(SPMediaKeyTap STATIC
|
||||
${SPMEDIAKEY-SOURCES}
|
||||
)
|
||||
8
3rdparty/SPMediaKeyTap/LICENSE
vendored
8
3rdparty/SPMediaKeyTap/LICENSE
vendored
@@ -1,8 +0,0 @@
|
||||
Copyright (c) 2011, Joachim Bengtsson
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
12
3rdparty/SPMediaKeyTap/README.md
vendored
12
3rdparty/SPMediaKeyTap/README.md
vendored
@@ -1,12 +0,0 @@
|
||||
SPMediaKeyTap
|
||||
=============
|
||||
|
||||
`SPMediaKeyTap` abstracts a `CGEventHook` and other nastiness in order to give you a relatively simple API to receive media key events (prev/next/playpause, on F7 to F9 on modern MacBook Pros) exclusively, without them reaching other applications like iTunes. `SPMediaKeyTap` is clever enough to resign its exclusive lock on media keys by looking for which application was active most recently: if that application is in `SPMediaKeyTap`'s whitelist, it will resign the keys. This is similar to the behavior of Apple's applications collaborating on media key handling exclusivity, but unfortunately, Apple is not exposing any APIs allowing third-parties to join in on this collaboration.
|
||||
|
||||
For now, the whitelist is just a hardcoded array in `+[SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers]`. If your app starts using `SPMediaKeyTap`, please [mail me](mailto:nevyn@spotify.com) your bundle ID, and I'll include it in the canonical repository. This is a bad solution; a better solution would be to use distributed notifications to collaborate in creating this whitelist at runtime. Hopefully someone'll have the time and energy to write this soon.
|
||||
|
||||
In `Example/SPMediaKeyTapExampleAppDelegate.m` is an example of both how you use `SPMediaKeyTap`, and how you handle the semi-private `NSEvent` subtypes involved in media keys, including on how to fall back to non-event tap handling of these events.
|
||||
|
||||
`SPMediaKeyTap` and other `CGEventHook`s on the event type `NSSystemDefined` is known to interfere with each other and applications doing weird stuff with mouse input, because mouse clicks are also part of the `NSSystemDefined` category. The single issue we have had reported here at Spotify is Adobe Fireworks, in which item selection stops working with `SPMediaKeyTap` is active.
|
||||
|
||||
`SPMediaKeyTap` requires 10.5 to work, and disables itself on 10.4.
|
||||
53
3rdparty/SPMediaKeyTap/SPMediaKeyTap.h
vendored
53
3rdparty/SPMediaKeyTap/SPMediaKeyTap.h
vendored
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2011, Joachim Bengtsson
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Neither the name of the organization nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <IOKit/hidsystem/ev_keymap.h>
|
||||
|
||||
// http://overooped.com/post/2593597587/mediakeys
|
||||
|
||||
#define SPSystemDefinedEventMediaKeys 8
|
||||
|
||||
@interface SPMediaKeyTap : NSObject
|
||||
|
||||
- (id)initWithDelegate:(id)delegate;
|
||||
|
||||
+ (BOOL)usesGlobalMediaKeyTap;
|
||||
- (BOOL)startWatchingMediaKeys;
|
||||
- (void)stopWatchingMediaKeys;
|
||||
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event;
|
||||
@end
|
||||
|
||||
@interface NSObject (SPMediaKeyTapDelegate)
|
||||
- (void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event;
|
||||
@end
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern NSString *kIgnoreMediaKeysDefaultsKey;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
361
3rdparty/SPMediaKeyTap/SPMediaKeyTap.m
vendored
361
3rdparty/SPMediaKeyTap/SPMediaKeyTap.m
vendored
@@ -1,361 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2011, Joachim Bengtsson
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Neither the name of the organization nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
// Copyright (c) 2010 Spotify AB
|
||||
#import "SPMediaKeyTap.h"
|
||||
|
||||
// Define to enable app list debug output
|
||||
// #define DEBUG_SPMEDIAKEY_APPLIST 1
|
||||
|
||||
NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
|
||||
|
||||
@interface SPMediaKeyTap () {
|
||||
CFMachPortRef _eventPort;
|
||||
CFRunLoopSourceRef _eventPortSource;
|
||||
CFRunLoopRef _tapThreadRL;
|
||||
NSThread *_tapThread;
|
||||
BOOL _shouldInterceptMediaKeyEvents;
|
||||
id _delegate;
|
||||
// The app that is frontmost in this list owns media keys
|
||||
NSMutableArray<NSRunningApplication *> *_mediaKeyAppList;
|
||||
}
|
||||
|
||||
- (BOOL)shouldInterceptMediaKeyEvents;
|
||||
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
|
||||
- (void)startWatchingAppSwitching;
|
||||
- (void)stopWatchingAppSwitching;
|
||||
- (void)eventTapThread;
|
||||
@end
|
||||
|
||||
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
|
||||
|
||||
|
||||
// Inspired by http://gist.github.com/546311
|
||||
|
||||
@implementation SPMediaKeyTap
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setup and teardown
|
||||
|
||||
- (id)initWithDelegate:(id)delegate
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_delegate = delegate;
|
||||
[self startWatchingAppSwitching];
|
||||
_mediaKeyAppList = [NSMutableArray new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self stopWatchingMediaKeys];
|
||||
[self stopWatchingAppSwitching];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)startWatchingAppSwitching
|
||||
{
|
||||
// Listen to "app switched" event, so that we don't intercept media keys if we
|
||||
// weren't the last "media key listening" app to be active
|
||||
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||
selector:@selector(frontmostAppChanged:)
|
||||
name:NSWorkspaceDidActivateApplicationNotification
|
||||
object:nil];
|
||||
|
||||
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||
selector:@selector(appTerminated:)
|
||||
name:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)stopWatchingAppSwitching
|
||||
{
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (BOOL)startWatchingMediaKeys
|
||||
{
|
||||
// Prevent having multiple mediaKeys threads
|
||||
[self stopWatchingMediaKeys];
|
||||
|
||||
[self setShouldInterceptMediaKeyEvents:YES];
|
||||
|
||||
// Add an event tap to intercept the system defined media key events
|
||||
_eventPort = CGEventTapCreate(kCGSessionEventTap,
|
||||
kCGHeadInsertEventTap,
|
||||
kCGEventTapOptionDefault,
|
||||
CGEventMaskBit(NX_SYSDEFINED),
|
||||
tapEventCallback,
|
||||
(__bridge void * __nullable)(self));
|
||||
|
||||
// Can be NULL if the app has no accessibility access permission
|
||||
if (_eventPort == NULL)
|
||||
return NO;
|
||||
|
||||
_eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
|
||||
assert(_eventPortSource != NULL);
|
||||
|
||||
if (_eventPortSource == NULL)
|
||||
return NO;
|
||||
|
||||
// Let's do this in a separate thread so that a slow app doesn't lag the event tap
|
||||
_tapThread = [[NSThread alloc] initWithTarget:self
|
||||
selector:@selector(eventTapThread)
|
||||
object:nil];
|
||||
[_tapThread start];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)stopWatchingMediaKeys
|
||||
{
|
||||
// Shut down tap thread
|
||||
if(_tapThreadRL){
|
||||
CFRunLoopStop(_tapThreadRL);
|
||||
_tapThreadRL = nil;
|
||||
}
|
||||
|
||||
// Remove tap port
|
||||
if(_eventPort){
|
||||
CFMachPortInvalidate(_eventPort);
|
||||
CFRelease(_eventPort);
|
||||
_eventPort = nil;
|
||||
}
|
||||
|
||||
// Remove tap source
|
||||
if(_eventPortSource){
|
||||
CFRelease(_eventPortSource);
|
||||
_eventPortSource = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Accessors
|
||||
|
||||
+ (BOOL)usesGlobalMediaKeyTap
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
// breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
|
||||
return NO;
|
||||
#else
|
||||
// XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
|
||||
return
|
||||
![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
|
||||
&& floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (NSArray*)mediaKeyUserBundleIdentifiers
|
||||
{
|
||||
return [NSArray arrayWithObjects:
|
||||
[[NSBundle mainBundle] bundleIdentifier], // your app
|
||||
@"com.spotify.client",
|
||||
@"com.apple.iTunes",
|
||||
@"com.apple.QuickTimePlayerX",
|
||||
@"com.apple.quicktimeplayer",
|
||||
@"com.apple.iWork.Keynote",
|
||||
@"com.apple.iPhoto",
|
||||
@"org.videolan.vlc",
|
||||
@"com.apple.Aperture",
|
||||
@"com.plexsquared.Plex",
|
||||
@"com.soundcloud.desktop",
|
||||
@"org.niltsh.MPlayerX",
|
||||
@"com.ilabs.PandorasHelper",
|
||||
@"com.mahasoftware.pandabar",
|
||||
@"com.bitcartel.pandorajam",
|
||||
@"org.clementine-player.clementine",
|
||||
@"fm.last.Last.fm",
|
||||
@"fm.last.Scrobbler",
|
||||
@"com.beatport.BeatportPro",
|
||||
@"com.Timenut.SongKey",
|
||||
@"com.macromedia.fireworks", // the tap messes up their mouse input
|
||||
@"at.justp.Theremin",
|
||||
@"ru.ya.themblsha.YandexMusic",
|
||||
@"com.jriver.MediaCenter18",
|
||||
@"com.jriver.MediaCenter19",
|
||||
@"com.jriver.MediaCenter20",
|
||||
@"co.rackit.mate",
|
||||
@"com.ttitt.b-music",
|
||||
@"com.beardedspice.BeardedSpice",
|
||||
@"com.plug.Plug",
|
||||
@"com.plug.Plug2",
|
||||
@"com.netease.163music",
|
||||
@"org.quodlibet.quodlibet",
|
||||
nil
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)shouldInterceptMediaKeyEvents
|
||||
{
|
||||
BOOL shouldIntercept = NO;
|
||||
@synchronized(self) {
|
||||
shouldIntercept = _shouldInterceptMediaKeyEvents;
|
||||
}
|
||||
return shouldIntercept;
|
||||
}
|
||||
|
||||
- (void)pauseTapOnTapThread:(NSNumber *)yeahno
|
||||
{
|
||||
CGEventTapEnable(self->_eventPort, [yeahno boolValue]);
|
||||
}
|
||||
|
||||
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting
|
||||
{
|
||||
BOOL oldSetting;
|
||||
@synchronized(self) {
|
||||
oldSetting = _shouldInterceptMediaKeyEvents;
|
||||
_shouldInterceptMediaKeyEvents = newSetting;
|
||||
}
|
||||
if(_tapThreadRL && oldSetting != newSetting) {
|
||||
[self performSelector:@selector(pauseTapOnTapThread:)
|
||||
onThread:_tapThread
|
||||
withObject:@(newSetting)
|
||||
waitUntilDone:NO];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Event tap callbacks
|
||||
|
||||
// Note: method called on background thread
|
||||
|
||||
static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
||||
{
|
||||
#pragma unused(proxy)
|
||||
SPMediaKeyTap *self = (__bridge SPMediaKeyTap *)refcon;
|
||||
|
||||
if(type == kCGEventTapDisabledByTimeout) {
|
||||
NSLog(@"Media key event tap was disabled by timeout");
|
||||
CGEventTapEnable(self->_eventPort, TRUE);
|
||||
return event;
|
||||
} else if(type == kCGEventTapDisabledByUserInput) {
|
||||
// Was disabled manually by -[pauseTapOnTapThread]
|
||||
return event;
|
||||
}
|
||||
NSEvent *nsEvent = nil;
|
||||
@try {
|
||||
nsEvent = [NSEvent eventWithCGEvent:event];
|
||||
}
|
||||
@catch (NSException * e) {
|
||||
NSLog(@"Strange CGEventType: %d: %@", type, e);
|
||||
assert(0);
|
||||
return event;
|
||||
}
|
||||
|
||||
if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
|
||||
return event;
|
||||
|
||||
int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
|
||||
if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
|
||||
return event;
|
||||
|
||||
if (![self shouldInterceptMediaKeyEvents])
|
||||
return event;
|
||||
|
||||
[self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
||||
{
|
||||
@autoreleasepool {
|
||||
CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event
|
||||
{
|
||||
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
|
||||
}
|
||||
|
||||
- (void)eventTapThread
|
||||
{
|
||||
_tapThreadRL = CFRunLoopGetCurrent();
|
||||
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
|
||||
CFRunLoopRun();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Task switching callbacks
|
||||
|
||||
- (void)mediaKeyAppListChanged
|
||||
{
|
||||
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
||||
[self debugPrintAppList];
|
||||
#endif
|
||||
|
||||
if([_mediaKeyAppList count] == 0)
|
||||
return;
|
||||
|
||||
NSRunningApplication *thisApp = [NSRunningApplication currentApplication];
|
||||
NSRunningApplication *otherApp = [_mediaKeyAppList firstObject];
|
||||
|
||||
BOOL isCurrent = [thisApp isEqual:otherApp];
|
||||
|
||||
[self setShouldInterceptMediaKeyEvents:isCurrent];
|
||||
}
|
||||
|
||||
- (void)frontmostAppChanged:(NSNotification *)notification
|
||||
{
|
||||
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
||||
if (app.bundleIdentifier == nil)
|
||||
return;
|
||||
|
||||
if (![[SPMediaKeyTap mediaKeyUserBundleIdentifiers] containsObject:app.bundleIdentifier])
|
||||
return;
|
||||
|
||||
[_mediaKeyAppList removeObject:app];
|
||||
[_mediaKeyAppList insertObject:app atIndex:0];
|
||||
[self mediaKeyAppListChanged];
|
||||
}
|
||||
|
||||
- (void)appTerminated:(NSNotification *)notification
|
||||
{
|
||||
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
||||
[_mediaKeyAppList removeObject:app];
|
||||
|
||||
[self mediaKeyAppListChanged];
|
||||
}
|
||||
|
||||
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
||||
- (void)debugPrintAppList
|
||||
{
|
||||
NSMutableString *list = [NSMutableString stringWithCapacity:255];
|
||||
for (NSRunningApplication *app in _mediaKeyAppList) {
|
||||
[list appendFormat:@" - %@\n", app.bundleIdentifier];
|
||||
}
|
||||
NSLog(@"List: \n%@", list);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
41
3rdparty/discord-rpc/CMakeLists.txt
vendored
Normal file
41
3rdparty/discord-rpc/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
set(DISCORD_RPC_SOURCES
|
||||
discord_rpc.h
|
||||
discord_register.h
|
||||
discord_rpc.cpp
|
||||
discord_rpc_connection.h
|
||||
discord_rpc_connection.cpp
|
||||
discord_serialization.h
|
||||
discord_serialization.cpp
|
||||
discord_connection.h
|
||||
discord_backoff.h
|
||||
discord_msg_queue.h
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
|
||||
if(APPLE)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
|
||||
add_definitions(-DDISCORD_OSX)
|
||||
else()
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
|
||||
add_definitions(-DDISCORD_LINUX)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
|
||||
add_definitions(-DDISCORD_WINDOWS)
|
||||
endif()
|
||||
|
||||
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
19
3rdparty/discord-rpc/LICENSE
vendored
Normal file
19
3rdparty/discord-rpc/LICENSE
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2017 Discord, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
162
3rdparty/discord-rpc/README.md
vendored
Normal file
162
3rdparty/discord-rpc/README.md
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
# Discord RPC
|
||||
|
||||
## Fork Notice
|
||||
|
||||
This library was slightly modified for Strawberry Music Player with some extra features from the new API and shared library support/more unnecessary components removed. The original repository is [here](https://github.com/discord/discord-rpc)
|
||||
|
||||
## Deprecation Notice
|
||||
|
||||
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
|
||||
|
||||
---
|
||||
|
||||
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
|
||||
|
||||
Included here are some quick demos that implement the very minimal subset to show current status, and
|
||||
have callbacks for where a more complete game would do more things (joining, spectating, etc).
|
||||
|
||||
## Documentation
|
||||
|
||||
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Zeroith, you should be set up to build things because you are a game developer, right?
|
||||
|
||||
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
|
||||
|
||||
### Unreal Engine 4 Setup
|
||||
|
||||
To use the Rich Presense plugin with Unreal Engine Projects:
|
||||
|
||||
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
|
||||
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
|
||||
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
|
||||
4. Follow the steps below for each OS
|
||||
5. Build your UE4 project
|
||||
6. Launch the editor, and enable the Discord plugin.
|
||||
|
||||
#### Windows
|
||||
|
||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
|
||||
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
|
||||
|
||||
#### Mac
|
||||
|
||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
|
||||
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
|
||||
|
||||
#### Linux
|
||||
|
||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
|
||||
- Inside, create another folder `x86_64-unknown-linux-gnu`
|
||||
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
|
||||
|
||||
### Unity Setup
|
||||
|
||||
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
|
||||
|
||||
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
|
||||
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
|
||||
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
|
||||
|
||||
We've got our `Plugins` folder ready, so let's get platform-specific!
|
||||
|
||||
#### Windows
|
||||
|
||||
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
|
||||
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
|
||||
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
|
||||
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
|
||||
8. Done!
|
||||
|
||||
#### MacOS
|
||||
|
||||
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
|
||||
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
|
||||
6. Done!
|
||||
|
||||
#### Linux
|
||||
|
||||
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
|
||||
5. Done!
|
||||
|
||||
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
|
||||
|
||||
### From package
|
||||
|
||||
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
|
||||
|
||||
### From repo
|
||||
|
||||
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
|
||||
|
||||
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
|
||||
|
||||
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
|
||||
|
||||
```sh
|
||||
cd <path to discord-rpc>
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
|
||||
cmake --build . --config Release --target install
|
||||
```
|
||||
|
||||
There is a wrapper build script `build.py` that runs `cmake` with a few different options.
|
||||
|
||||
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
|
||||
|
||||
There are some CMake options you might care about:
|
||||
|
||||
| flag | default | does |
|
||||
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. |
|
||||
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) |
|
||||
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL |
|
||||
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
|
||||
|
||||
## Continuous Builds
|
||||
|
||||
Why do we have three of these? Three times the fun!
|
||||
|
||||
| CI | badge |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| TravisCI | [](https://travis-ci.org/discordapp/discord-rpc) |
|
||||
| AppVeyor | [](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
|
||||
| Buildkite (internal) | [](https://buildkite.com/discord/discord-rpc) |
|
||||
|
||||
## Sample: send-presence
|
||||
|
||||
This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command.
|
||||
|
||||
## Sample: button-clicker
|
||||
|
||||
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders.
|
||||
|
||||
## Sample: unrealstatus
|
||||
|
||||
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders.
|
||||
|
||||
## Wrappers and Implementations
|
||||
|
||||
Below is a table of unofficial, community-developed wrappers for and implementations of Rich Presence in various languages. If you would like to have yours added, please make a pull request adding your repository to the table. The repository should include:
|
||||
|
||||
- The code
|
||||
- A brief ReadMe of how to use it
|
||||
- A working example
|
||||
|
||||
###### Rich Presence Wrappers and Implementations
|
||||
|
||||
| Name | Language |
|
||||
| ------------------------------------------------------------------------- | --------------------------------- |
|
||||
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# |
|
||||
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
|
||||
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
|
||||
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
|
||||
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
|
||||
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
|
||||
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
|
||||
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
|
||||
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
|
||||
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |
|
||||
63
3rdparty/discord-rpc/discord_backoff.h
vendored
Normal file
63
3rdparty/discord-rpc/discord_backoff.h
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_BACKOFF_H
|
||||
#define DISCORD_BACKOFF_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
struct Backoff {
|
||||
int64_t minAmount;
|
||||
int64_t maxAmount;
|
||||
int64_t current;
|
||||
int fails;
|
||||
std::mt19937_64 randGenerator;
|
||||
std::uniform_real_distribution<> randDistribution;
|
||||
|
||||
double rand01() { return randDistribution(randGenerator); }
|
||||
|
||||
Backoff(int64_t min, int64_t max)
|
||||
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
fails = 0;
|
||||
current = minAmount;
|
||||
}
|
||||
|
||||
int64_t nextDelay() {
|
||||
++fails;
|
||||
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
|
||||
current = std::min(current + delay, maxAmount);
|
||||
return current;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_BACKOFF_H
|
||||
48
3rdparty/discord-rpc/discord_connection.h
vendored
Normal file
48
3rdparty/discord-rpc/discord_connection.h
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_CONNECTION_H
|
||||
#define DISCORD_CONNECTION_H
|
||||
|
||||
// This is to wrap the platform specific kinds of connect/read/write.
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// not really connectiony, but need per-platform
|
||||
int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection*&);
|
||||
bool isOpen = false;
|
||||
bool Open();
|
||||
bool Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(void *data, size_t length);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_CONNECTION_H
|
||||
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
Normal file
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_connection.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
int GetProcessId() {
|
||||
return ::getpid();
|
||||
}
|
||||
|
||||
struct BaseConnectionUnix : public BaseConnection {
|
||||
int sock{ -1 };
|
||||
};
|
||||
|
||||
static BaseConnectionUnix Connection;
|
||||
static sockaddr_un PipeAddr{};
|
||||
#ifdef MSG_NOSIGNAL
|
||||
static int MsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
static int MsgFlags = 0;
|
||||
#endif
|
||||
|
||||
static const char *GetTempPath() {
|
||||
|
||||
const char *temp = getenv("XDG_RUNTIME_DIR");
|
||||
temp = temp ? temp : getenv("TMPDIR");
|
||||
temp = temp ? temp : getenv("TMP");
|
||||
temp = temp ? temp : getenv("TEMP");
|
||||
temp = temp ? temp : "/tmp";
|
||||
|
||||
return temp;
|
||||
|
||||
}
|
||||
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
PipeAddr.sun_family = AF_UNIX;
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
const char *tempPath = GetTempPath();
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
fcntl(self->sock, F_SETFL, O_NONBLOCK);
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int optval = 1;
|
||||
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
||||
#endif
|
||||
|
||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
||||
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
||||
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
|
||||
if (err == 0) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self->Close();
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
close(self->sock);
|
||||
self->sock = -1;
|
||||
self->isOpen = false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void *data, size_t length) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
|
||||
if (sentBytes < 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return sentBytes == static_cast<ssize_t>(length);
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void *data, size_t length) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long res = recv(self->sock, data, length, MsgFlags);
|
||||
if (res < 0) {
|
||||
if (errno == EAGAIN) {
|
||||
return false;
|
||||
}
|
||||
Close();
|
||||
}
|
||||
else if (res == 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return static_cast<size_t>(res) == length;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
Normal file
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_connection.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <cassert>
|
||||
#include <windows.h>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
int GetProcessId() {
|
||||
return static_cast<int>(::GetCurrentProcessId());
|
||||
}
|
||||
|
||||
struct BaseConnectionWin : public BaseConnection {
|
||||
HANDLE pipe{ INVALID_HANDLE_VALUE };
|
||||
};
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto lastError = GetLastError();
|
||||
if (lastError == ERROR_FILE_NOT_FOUND) {
|
||||
if (pipeName[pipeDigit] < L'9') {
|
||||
pipeName[pipeDigit]++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (lastError == ERROR_PIPE_BUSY) {
|
||||
if (!WaitNamedPipeW(pipeName, 10000)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void *data, size_t length) {
|
||||
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
}
|
||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const DWORD bytesLength = static_cast<DWORD>(length);
|
||||
DWORD bytesWritten = 0;
|
||||
|
||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void *data, size_t length) {
|
||||
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
}
|
||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
DWORD bytesAvailable = 0;
|
||||
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
|
||||
if (bytesAvailable >= length) {
|
||||
DWORD bytesToRead = static_cast<DWORD>(length);
|
||||
DWORD bytesRead = 0;
|
||||
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
|
||||
assert(bytesToRead == bytesRead);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
Normal file
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_MSG_QUEUE_H
|
||||
#define DISCORD_MSG_QUEUE_H
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
||||
// a consumer. Mutex up as needed.
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template<typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_{ 0 };
|
||||
std::atomic_uint nextSend_{ 0 };
|
||||
std::atomic_uint pendingSends_{ 0 };
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
ElementType *GetNextAddMessage() {
|
||||
// if we are falling behind, bail
|
||||
if (pendingSends_.load() >= QueueSize) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = (nextAdd_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitAdd() { ++pendingSends_; }
|
||||
|
||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
||||
ElementType *GetNextSendMessage() {
|
||||
auto index = (nextSend_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitSend() { --pendingSends_; }
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_MSG_QUEUE_H
|
||||
37
3rdparty/discord-rpc/discord_register.h
vendored
Normal file
37
3rdparty/discord-rpc/discord_register.h
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_REGISTER_H
|
||||
#define DISCORD_REGISTER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void Discord_Register(const char *applicationId, const char *command);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_REGISTER_H
|
||||
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
Normal file
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <errno.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static bool Mkdir(const char *path) {
|
||||
int result = mkdir(path, 0755);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (errno == EEXIST) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// We want to register games so we can run them from Discord client as discord-<appid>://
|
||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
char exePath[1024]{};
|
||||
if (!command || !command[0]) {
|
||||
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
||||
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
|
||||
return;
|
||||
}
|
||||
exePath[size] = '\0';
|
||||
command = exePath;
|
||||
}
|
||||
|
||||
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
|
||||
"Name=Game %s\n"
|
||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
||||
"Type=Application\n"
|
||||
"NoDisplay=true\n"
|
||||
"Categories=Discord;Games;\n"
|
||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
||||
char desktopFile[2048]{};
|
||||
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
||||
if (fileLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char desktopFilename[256]{};
|
||||
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
||||
|
||||
char desktopFilePath[1024]{};
|
||||
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/share");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/applications");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, desktopFilename);
|
||||
|
||||
FILE *fp = fopen(desktopFilePath, "w");
|
||||
if (fp) {
|
||||
fwrite(desktopFile, 1, fileLen, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
char xdgMimeCommand[1024]{};
|
||||
snprintf(xdgMimeCommand,
|
||||
sizeof(xdgMimeCommand),
|
||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
||||
applicationId,
|
||||
applicationId);
|
||||
if (system(xdgMimeCommand) < 0) {
|
||||
fprintf(stderr, "Failed to register mime handler\n");
|
||||
}
|
||||
|
||||
}
|
||||
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
Normal file
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include "discord_register.h"
|
||||
|
||||
static void RegisterCommand(const char *applicationId, const char *command) {
|
||||
|
||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
||||
|
||||
// Note: will not work for sandboxed apps
|
||||
NSString *home = NSHomeDirectory();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
||||
stringByAppendingPathComponent:@"Application Support"]
|
||||
stringByAppendingPathComponent:@"discord"]
|
||||
stringByAppendingPathComponent:@"games"]
|
||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
||||
stringByAppendingPathExtension:@"json"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
|
||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
||||
|
||||
}
|
||||
|
||||
static void RegisterURL(const char *applicationId) {
|
||||
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
||||
|
||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!myBundleId) {
|
||||
fprintf(stderr, "No bundle id found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
||||
if (!myURL) {
|
||||
fprintf(stderr, "No bundle url found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
if (command) {
|
||||
RegisterCommand(applicationId, command);
|
||||
}
|
||||
else {
|
||||
// raii lite
|
||||
@autoreleasepool {
|
||||
RegisterURL(applicationId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
165
3rdparty/discord-rpc/discord_register_win.cpp
vendored
Normal file
165
3rdparty/discord-rpc/discord_register_win.cpp
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <cstdio>
|
||||
|
||||
/**
|
||||
* Updated fixes for MinGW and WinXP
|
||||
* This block is written the way it does not involve changing the rest of the code
|
||||
* Checked to be compiling
|
||||
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
|
||||
* #include guarded, functions redirected to <string.h> substitutes
|
||||
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
|
||||
* The entire function is rewritten
|
||||
*/
|
||||
#ifdef __MINGW32__
|
||||
# include <wchar.h>
|
||||
/// strsafe.h fixes
|
||||
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) {
|
||||
HRESULT ret;
|
||||
va_list va;
|
||||
va_start(va, pszFormat);
|
||||
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
|
||||
// othervise
|
||||
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
|
||||
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
|
||||
va_end(va);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
# include <cwchar>
|
||||
# include <strsafe.h>
|
||||
#endif // __MINGW32__
|
||||
|
||||
/// winreg.h fixes
|
||||
#ifndef LSTATUS
|
||||
# define LSTATUS LONG
|
||||
#endif
|
||||
#ifdef RegSetKeyValueW
|
||||
# undefine RegSetKeyValueW
|
||||
#endif
|
||||
#define RegSetKeyValueW regset
|
||||
|
||||
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
|
||||
|
||||
HKEY htkey = hkey, hsubkey = nullptr;
|
||||
LSTATUS ret;
|
||||
if (subkey && subkey[0]) {
|
||||
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
|
||||
ERROR_SUCCESS)
|
||||
return ret;
|
||||
htkey = hsubkey;
|
||||
}
|
||||
ret = RegSetValueExW(htkey, name, 0, type, static_cast<const BYTE*>(data), len);
|
||||
if (hsubkey && hsubkey != hkey)
|
||||
RegCloseKey(hsubkey);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
|
||||
// we want to register games so we can run them as discord-<appid>://
|
||||
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
|
||||
|
||||
wchar_t exeFilePath[MAX_PATH]{};
|
||||
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
||||
wchar_t openCommand[1024]{};
|
||||
|
||||
if (command && command[0]) {
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
||||
}
|
||||
else {
|
||||
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
||||
}
|
||||
|
||||
wchar_t protocolName[64]{};
|
||||
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
||||
wchar_t protocolDescription[128]{};
|
||||
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||
wchar_t urlProtocol = 0;
|
||||
|
||||
wchar_t keyName[256]{};
|
||||
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
||||
HKEY key;
|
||||
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error creating key\n");
|
||||
return;
|
||||
}
|
||||
DWORD len;
|
||||
LSTATUS result;
|
||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing icon\n");
|
||||
}
|
||||
|
||||
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
|
||||
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing command\n");
|
||||
}
|
||||
RegCloseKey(key);
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
wchar_t appId[32]{};
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t openCommand[1024]{};
|
||||
const wchar_t *wcommand = nullptr;
|
||||
if (command && command[0]) {
|
||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
||||
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
|
||||
wcommand = openCommand;
|
||||
}
|
||||
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
|
||||
}
|
||||
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
Normal file
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
Normal file
@@ -0,0 +1,510 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_backoff.h"
|
||||
#include "discord_register.h"
|
||||
#include "discord_msg_queue.h"
|
||||
#include "discord_rpc_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
using namespace discord_rpc;
|
||||
|
||||
static void Discord_UpdateConnection();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxMessageSize{ 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize{ 8 };
|
||||
constexpr size_t JoinQueueSize{ 8 };
|
||||
|
||||
struct QueuedMessage {
|
||||
size_t length;
|
||||
char buffer[MaxMessageSize];
|
||||
|
||||
void Copy(const QueuedMessage &other) {
|
||||
length = other.length;
|
||||
if (length) {
|
||||
memcpy(buffer, other.buffer, length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct User {
|
||||
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
|
||||
// terminator = 21
|
||||
char userId[32];
|
||||
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
|
||||
// terminator = 129
|
||||
char username[344];
|
||||
// 4 decimal digits + 1 null terminator = 5
|
||||
char discriminator[8];
|
||||
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
|
||||
char avatar[128];
|
||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||
};
|
||||
|
||||
static RpcConnection *Connection{ nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers{};
|
||||
static DiscordEventHandlers Handlers{};
|
||||
static std::atomic_bool WasJustConnected{ false };
|
||||
static std::atomic_bool WasJustDisconnected{ false };
|
||||
static std::atomic_bool GotErrorMessage{ false };
|
||||
static std::atomic_bool WasJoinGame{ false };
|
||||
static std::atomic_bool WasSpectateGame{ false };
|
||||
static std::atomic_bool UpdatePresence{ false };
|
||||
static char JoinGameSecret[256];
|
||||
static char SpectateGameSecret[256];
|
||||
static int LastErrorCode{ 0 };
|
||||
static char LastErrorMessage[256];
|
||||
static int LastDisconnectErrorCode{ 0 };
|
||||
static char LastDisconnectErrorMessage[256];
|
||||
static std::mutex PresenceMutex;
|
||||
static std::mutex HandlerMutex;
|
||||
static QueuedMessage QueuedPresence{};
|
||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid{ 0 };
|
||||
static int Nonce{ 1 };
|
||||
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning{ true };
|
||||
std::mutex waitForIOMutex;
|
||||
std::condition_variable waitForIOActivity;
|
||||
std::thread ioThread;
|
||||
|
||||
public:
|
||||
void Start() {
|
||||
keepRunning.store(true);
|
||||
ioThread = std::thread([&]() {
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Notify() { waitForIOActivity.notify_all(); }
|
||||
|
||||
void Stop() {
|
||||
keepRunning.exchange(false);
|
||||
Notify();
|
||||
if (ioThread.joinable()) {
|
||||
ioThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread{ nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
|
||||
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
||||
|
||||
}
|
||||
|
||||
static void SignalIOActivity() {
|
||||
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Notify();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static bool RegisterForEvent(const char *evtName) {
|
||||
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
static bool DeregisterForEvent(const char *evtName) {
|
||||
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void Discord_UpdateConnection() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Connection->IsOpen()) {
|
||||
if (std::chrono::system_clock::now() >= NextConnect) {
|
||||
UpdateReconnectTime();
|
||||
Connection->Open();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// reads
|
||||
|
||||
for (;;) {
|
||||
JsonDocument message;
|
||||
|
||||
if (!Connection->Read(message)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const char *evtName = GetStrMember(&message, "evt");
|
||||
const char *nonce = GetStrMember(&message, "nonce");
|
||||
|
||||
if (nonce) {
|
||||
// in responses only -- should use to match up response when needed.
|
||||
|
||||
if (evtName && strcmp(evtName, "ERROR") == 0) {
|
||||
auto data = GetObjMember(&message, "data");
|
||||
LastErrorCode = GetIntMember(data, "code");
|
||||
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
|
||||
GotErrorMessage.store(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// should have evt == name of event, optional data
|
||||
if (evtName == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto data = GetObjMember(&message, "data");
|
||||
|
||||
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
|
||||
auto secret = GetStrMember(data, "secret");
|
||||
if (secret) {
|
||||
StringCopy(JoinGameSecret, secret);
|
||||
WasJoinGame.store(true);
|
||||
}
|
||||
}
|
||||
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
|
||||
auto secret = GetStrMember(data, "secret");
|
||||
if (secret) {
|
||||
StringCopy(SpectateGameSecret, secret);
|
||||
WasSpectateGame.store(true);
|
||||
}
|
||||
}
|
||||
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
|
||||
auto user = GetObjMember(data, "user");
|
||||
auto userId = GetStrMember(user, "id");
|
||||
auto username = GetStrMember(user, "username");
|
||||
auto avatar = GetStrMember(user, "avatar");
|
||||
auto joinReq = JoinAskQueue.GetNextAddMessage();
|
||||
if (userId && username && joinReq) {
|
||||
StringCopy(joinReq->userId, userId);
|
||||
StringCopy(joinReq->username, username);
|
||||
auto discriminator = GetStrMember(user, "discriminator");
|
||||
if (discriminator) {
|
||||
StringCopy(joinReq->discriminator, discriminator);
|
||||
}
|
||||
if (avatar) {
|
||||
StringCopy(joinReq->avatar, avatar);
|
||||
}
|
||||
else {
|
||||
joinReq->avatar[0] = 0;
|
||||
}
|
||||
JoinAskQueue.CommitAdd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writes
|
||||
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
|
||||
QueuedMessage local;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
local.Copy(QueuedPresence);
|
||||
}
|
||||
if (!Connection->Write(local.buffer, local.length)) {
|
||||
// if we fail to send, requeue
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.Copy(local);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
}
|
||||
|
||||
while (SendQueue.HavePendingSends()) {
|
||||
auto qmessage = SendQueue.GetNextSendMessage();
|
||||
Connection->Write(qmessage->buffer, qmessage->length);
|
||||
SendQueue.CommitSend();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
||||
|
||||
IoThread = new (std::nothrow) IoThreadHolder();
|
||||
if (IoThread == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRegister) {
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
|
||||
Pid = GetProcessId();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
|
||||
if (handlers) {
|
||||
QueuedHandlers = *handlers;
|
||||
}
|
||||
else {
|
||||
QueuedHandlers = {};
|
||||
}
|
||||
|
||||
Handlers = {};
|
||||
}
|
||||
|
||||
if (Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
Connection = RpcConnection::Create(applicationId);
|
||||
Connection->onConnect = [](JsonDocument &readyMessage) {
|
||||
Discord_UpdateHandlers(&QueuedHandlers);
|
||||
if (QueuedPresence.length > 0) {
|
||||
UpdatePresence.exchange(true);
|
||||
SignalIOActivity();
|
||||
}
|
||||
auto data = GetObjMember(&readyMessage, "data");
|
||||
auto user = GetObjMember(data, "user");
|
||||
auto userId = GetStrMember(user, "id");
|
||||
auto username = GetStrMember(user, "username");
|
||||
auto avatar = GetStrMember(user, "avatar");
|
||||
if (userId && username) {
|
||||
StringCopy(connectedUser.userId, userId);
|
||||
StringCopy(connectedUser.username, username);
|
||||
auto discriminator = GetStrMember(user, "discriminator");
|
||||
if (discriminator) {
|
||||
StringCopy(connectedUser.discriminator, discriminator);
|
||||
}
|
||||
if (avatar) {
|
||||
StringCopy(connectedUser.avatar, avatar);
|
||||
}
|
||||
else {
|
||||
connectedUser.avatar[0] = 0;
|
||||
}
|
||||
}
|
||||
WasJustConnected.exchange(true);
|
||||
ReconnectTimeMs.reset();
|
||||
};
|
||||
Connection->onDisconnect = [](int err, const char *message) {
|
||||
LastDisconnectErrorCode = err;
|
||||
StringCopy(LastDisconnectErrorMessage, message);
|
||||
WasJustDisconnected.exchange(true);
|
||||
UpdateReconnectTime();
|
||||
};
|
||||
|
||||
IoThread->Start();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Shutdown() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
Connection->onConnect = nullptr;
|
||||
Connection->onDisconnect = nullptr;
|
||||
Handlers = {};
|
||||
QueuedPresence.length = 0;
|
||||
UpdatePresence.exchange(false);
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Stop();
|
||||
delete IoThread;
|
||||
IoThread = nullptr;
|
||||
}
|
||||
|
||||
RpcConnection::Destroy(Connection);
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
|
||||
SignalIOActivity();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_ClearPresence(void) {
|
||||
Discord_UpdatePresence(nullptr);
|
||||
}
|
||||
|
||||
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
|
||||
|
||||
// if we are not connected, let's not batch up stale messages for later
|
||||
if (!Connection || !Connection->IsOpen()) {
|
||||
return;
|
||||
}
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_RunCallbacks() {
|
||||
|
||||
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
|
||||
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
|
||||
// signals are book-ended by calls to ready and disconnect.
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
const bool isConnected = Connection->IsOpen();
|
||||
|
||||
if (isConnected) {
|
||||
// if we are connected, disconnect cb first
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (wasDisconnected && Handlers.disconnected) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
|
||||
if (GotErrorMessage.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.errored) {
|
||||
Handlers.errored(LastErrorCode, LastErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasJoinGame.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinGame) {
|
||||
Handlers.joinGame(JoinGameSecret);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasSpectateGame.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.spectateGame) {
|
||||
Handlers.spectateGame(SpectateGameSecret);
|
||||
}
|
||||
}
|
||||
|
||||
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
|
||||
// where the implementer would rather sequentially accept/reject each one before the next invite
|
||||
// is sent. I left it this way because I could also imagine wanting to process these all and
|
||||
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
|
||||
// not it should be trivial for the implementer to make a queue themselves.
|
||||
while (JoinAskQueue.HavePendingSends()) {
|
||||
const auto req = JoinAskQueue.GetNextSendMessage();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
|
||||
Handlers.joinRequest(&du);
|
||||
}
|
||||
}
|
||||
JoinAskQueue.CommitSend();
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
// if we are not connected, disconnect message last
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (wasDisconnected && Handlers.disconnected) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
|
||||
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
|
||||
|
||||
#undef HANDLE_EVENT_REGISTRATION
|
||||
|
||||
Handlers = *newHandlers;
|
||||
}
|
||||
else {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
Handlers = {};
|
||||
}
|
||||
|
||||
}
|
||||
94
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
94
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_H
|
||||
#define DISCORD_RPC_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
int type;
|
||||
int status_display_type;
|
||||
const char *name; /* max 128 bytes */
|
||||
const char *state; /* max 128 bytes */
|
||||
const char *details; /* max 128 bytes */
|
||||
int64_t startTimestamp;
|
||||
int64_t endTimestamp;
|
||||
const char *largeImageKey; /* max 32 bytes */
|
||||
const char *largeImageText; /* max 128 bytes */
|
||||
const char *smallImageKey; /* max 32 bytes */
|
||||
const char *smallImageText; /* max 128 bytes */
|
||||
const char *partyId; /* max 128 bytes */
|
||||
int partySize;
|
||||
int partyMax;
|
||||
int partyPrivacy;
|
||||
const char *matchSecret; /* max 128 bytes */
|
||||
const char *joinSecret; /* max 128 bytes */
|
||||
const char *spectateSecret; /* max 128 bytes */
|
||||
int8_t instance;
|
||||
} DiscordRichPresence;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char *userId;
|
||||
const char *username;
|
||||
const char *discriminator;
|
||||
const char *avatar;
|
||||
} DiscordUser;
|
||||
|
||||
typedef struct DiscordEventHandlers {
|
||||
void (*ready)(const DiscordUser *request);
|
||||
void (*disconnected)(int errorCode, const char *message);
|
||||
void (*errored)(int errorCode, const char *message);
|
||||
void (*joinGame)(const char *joinSecret);
|
||||
void (*spectateGame)(const char *spectateSecret);
|
||||
void (*joinRequest)(const DiscordUser *request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#define DISCORD_PARTY_PRIVATE 0
|
||||
#define DISCORD_PARTY_PUBLIC 1
|
||||
|
||||
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
|
||||
void Discord_Shutdown(void);
|
||||
|
||||
// checks for incoming messages, dispatches callbacks
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence *presence);
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_RPC_H
|
||||
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
Normal file
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
static const int RpcVersion = 1;
|
||||
static RpcConnection Instance;
|
||||
|
||||
RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||
|
||||
Instance.connection = BaseConnection::Create();
|
||||
StringCopy(Instance.appId, applicationId);
|
||||
return &Instance;
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Destroy(RpcConnection *&c) {
|
||||
|
||||
c->Close();
|
||||
BaseConnection::Destroy(c->connection);
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Open() {
|
||||
|
||||
if (state == State::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::Disconnected && !connection->Open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::SentHandshake) {
|
||||
JsonDocument message;
|
||||
if (Read(message)) {
|
||||
auto cmd = GetStrMember(&message, "cmd");
|
||||
auto evt = GetStrMember(&message, "evt");
|
||||
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
|
||||
state = State::Connected;
|
||||
if (onConnect) {
|
||||
onConnect(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendFrame.opcode = Opcode::Handshake;
|
||||
sendFrame.length = static_cast<uint32_t>(JsonWriteHandshakeObj(sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId));
|
||||
|
||||
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
|
||||
state = State::SentHandshake;
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Close() {
|
||||
|
||||
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
||||
onDisconnect(lastErrorCode, lastErrorMessage);
|
||||
}
|
||||
connection->Close();
|
||||
state = State::Disconnected;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Write(const void *data, size_t length) {
|
||||
|
||||
sendFrame.opcode = Opcode::Frame;
|
||||
memcpy(sendFrame.message, data, length);
|
||||
sendFrame.length = static_cast<uint32_t>(length);
|
||||
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Read(JsonDocument &message) {
|
||||
|
||||
if (state != State::Connected && state != State::SentHandshake) {
|
||||
return false;
|
||||
}
|
||||
MessageFrame readFrame{};
|
||||
for (;;) {
|
||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
||||
if (!didRead) {
|
||||
if (!connection->isOpen) {
|
||||
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
|
||||
StringCopy(lastErrorMessage, "Pipe closed");
|
||||
Close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readFrame.length > 0) {
|
||||
didRead = connection->Read(readFrame.message, readFrame.length);
|
||||
if (!didRead) {
|
||||
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
|
||||
StringCopy(lastErrorMessage, "Partial data in frame");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
readFrame.message[readFrame.length] = 0;
|
||||
}
|
||||
|
||||
switch (readFrame.opcode) {
|
||||
case Opcode::Close: {
|
||||
message.ParseInsitu(readFrame.message);
|
||||
lastErrorCode = GetIntMember(&message, "code");
|
||||
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
case Opcode::Frame:
|
||||
message.ParseInsitu(readFrame.message);
|
||||
return true;
|
||||
case Opcode::Ping:
|
||||
readFrame.opcode = Opcode::Pong;
|
||||
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
|
||||
Close();
|
||||
}
|
||||
break;
|
||||
case Opcode::Pong:
|
||||
break;
|
||||
case Opcode::Handshake:
|
||||
default:
|
||||
// something bad happened
|
||||
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
|
||||
StringCopy(lastErrorMessage, "Bad ipc frame");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
Normal file
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_CONNECTION_H
|
||||
#define DISCORD_RPC_CONNECTION_H
|
||||
|
||||
#include "discord_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
|
||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
||||
|
||||
struct RpcConnection {
|
||||
enum class ErrorCode : int {
|
||||
Success = 0,
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
};
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
struct MessageFrameHeader {
|
||||
Opcode opcode;
|
||||
uint32_t length;
|
||||
};
|
||||
|
||||
struct MessageFrame : public MessageFrameHeader {
|
||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
||||
};
|
||||
|
||||
enum class State : uint32_t {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
AwaitingResponse,
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection{ nullptr };
|
||||
State state{ State::Disconnected };
|
||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||
char appId[64]{};
|
||||
int lastErrorCode{ 0 };
|
||||
char lastErrorMessage[256]{};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection*&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(JsonDocument &message);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_RPC_CONNECTION_H
|
||||
285
3rdparty/discord-rpc/discord_serialization.cpp
vendored
Normal file
285
3rdparty/discord-rpc/discord_serialization.cpp
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_serialization.h"
|
||||
#include "discord_connection.h"
|
||||
#include "discord_rpc.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template<typename T>
|
||||
void NumberToString(char *dest, T number) {
|
||||
|
||||
if (!number) {
|
||||
*dest++ = '0';
|
||||
*dest++ = 0;
|
||||
return;
|
||||
}
|
||||
if (number < 0) {
|
||||
*dest++ = '-';
|
||||
number = -number;
|
||||
}
|
||||
char temp[32];
|
||||
int place = 0;
|
||||
while (number) {
|
||||
auto digit = number % 10;
|
||||
number = number / 10;
|
||||
temp[place++] = '0' + static_cast<char>(digit);
|
||||
}
|
||||
for (--place; place >= 0; --place) {
|
||||
*dest++ = temp[place];
|
||||
}
|
||||
*dest = 0;
|
||||
|
||||
}
|
||||
|
||||
// it's ever so slightly faster to not have to strlen the key
|
||||
template<typename T>
|
||||
void WriteKey(JsonWriter &w, T &k) {
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
}
|
||||
|
||||
struct WriteObject {
|
||||
JsonWriter &writer;
|
||||
WriteObject(JsonWriter &w)
|
||||
: writer(w) {
|
||||
writer.StartObject();
|
||||
}
|
||||
template<typename T>
|
||||
WriteObject(JsonWriter &w, T &name)
|
||||
: writer(w) {
|
||||
WriteKey(writer, name);
|
||||
writer.StartObject();
|
||||
}
|
||||
~WriteObject() { writer.EndObject(); }
|
||||
};
|
||||
|
||||
struct WriteArray {
|
||||
JsonWriter &writer;
|
||||
template<typename T>
|
||||
WriteArray(JsonWriter &w, T &name)
|
||||
: writer(w) {
|
||||
WriteKey(writer, name);
|
||||
writer.StartArray();
|
||||
}
|
||||
~WriteArray() { writer.EndArray(); }
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
|
||||
|
||||
if (value && value[0]) {
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
w.String(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
|
||||
|
||||
WriteKey(writer, "nonce");
|
||||
char nonceBuffer[32];
|
||||
NumberToString(nonceBuffer, nonce);
|
||||
writer.String(nonceBuffer);
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject top(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("SET_ACTIVITY");
|
||||
|
||||
{
|
||||
WriteObject args(writer, "args");
|
||||
|
||||
WriteKey(writer, "pid");
|
||||
writer.Int(pid);
|
||||
|
||||
if (presence != nullptr) {
|
||||
WriteObject activity(writer, "activity");
|
||||
|
||||
if (presence->type >= 0 && presence->type <= 5) {
|
||||
WriteKey(writer, "type");
|
||||
writer.Int(presence->type);
|
||||
|
||||
WriteKey(writer, "status_display_type");
|
||||
writer.Int(presence->status_display_type);
|
||||
}
|
||||
|
||||
WriteOptionalString(writer, "name", presence->name);
|
||||
WriteOptionalString(writer, "state", presence->state);
|
||||
WriteOptionalString(writer, "details", presence->details);
|
||||
|
||||
if (presence->startTimestamp || presence->endTimestamp) {
|
||||
WriteObject timestamps(writer, "timestamps");
|
||||
|
||||
if (presence->startTimestamp) {
|
||||
WriteKey(writer, "start");
|
||||
writer.Int64(presence->startTimestamp);
|
||||
}
|
||||
|
||||
if (presence->endTimestamp) {
|
||||
WriteKey(writer, "end");
|
||||
writer.Int64(presence->endTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
|
||||
(presence->largeImageText && presence->largeImageText[0]) ||
|
||||
(presence->smallImageKey && presence->smallImageKey[0]) ||
|
||||
(presence->smallImageText && presence->smallImageText[0])) {
|
||||
WriteObject assets(writer, "assets");
|
||||
WriteOptionalString(writer, "large_image", presence->largeImageKey);
|
||||
WriteOptionalString(writer, "large_text", presence->largeImageText);
|
||||
WriteOptionalString(writer, "small_image", presence->smallImageKey);
|
||||
WriteOptionalString(writer, "small_text", presence->smallImageText);
|
||||
}
|
||||
|
||||
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
|
||||
presence->partyMax || presence->partyPrivacy) {
|
||||
WriteObject party(writer, "party");
|
||||
WriteOptionalString(writer, "id", presence->partyId);
|
||||
if (presence->partySize && presence->partyMax) {
|
||||
WriteArray size(writer, "size");
|
||||
writer.Int(presence->partySize);
|
||||
writer.Int(presence->partyMax);
|
||||
}
|
||||
|
||||
if (presence->partyPrivacy) {
|
||||
WriteKey(writer, "privacy");
|
||||
writer.Int(presence->partyPrivacy);
|
||||
}
|
||||
}
|
||||
|
||||
if ((presence->matchSecret && presence->matchSecret[0]) ||
|
||||
(presence->joinSecret && presence->joinSecret[0]) ||
|
||||
(presence->spectateSecret && presence->spectateSecret[0])) {
|
||||
WriteObject secrets(writer, "secrets");
|
||||
WriteOptionalString(writer, "match", presence->matchSecret);
|
||||
WriteOptionalString(writer, "join", presence->joinSecret);
|
||||
WriteOptionalString(writer, "spectate", presence->spectateSecret);
|
||||
}
|
||||
|
||||
writer.Key("instance");
|
||||
writer.Bool(presence->instance != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
WriteKey(writer, "v");
|
||||
writer.Int(version);
|
||||
WriteKey(writer, "client_id");
|
||||
writer.String(applicationId);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("SUBSCRIBE");
|
||||
|
||||
WriteKey(writer, "evt");
|
||||
writer.String(evtName);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("UNSUBSCRIBE");
|
||||
|
||||
WriteKey(writer, "evt");
|
||||
writer.String(evtName);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
if (reply == DISCORD_REPLY_YES) {
|
||||
writer.String("SEND_ACTIVITY_JOIN_INVITE");
|
||||
}
|
||||
else {
|
||||
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
|
||||
}
|
||||
|
||||
WriteKey(writer, "args");
|
||||
{
|
||||
WriteObject args(writer);
|
||||
|
||||
WriteKey(writer, "user_id");
|
||||
writer.String(userId);
|
||||
}
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
213
3rdparty/discord-rpc/discord_serialization.h
vendored
Normal file
213
3rdparty/discord-rpc/discord_serialization.h
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_SERIALIZATION_H
|
||||
#define DISCORD_SERIALIZATION_H
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/writer.h>
|
||||
|
||||
struct DiscordRichPresence;
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// if only there was a standard library function for this
|
||||
template<size_t Len>
|
||||
inline size_t StringCopy(char (&dest)[Len], const char *src) {
|
||||
if (!src || !Len) {
|
||||
return 0;
|
||||
}
|
||||
size_t copied;
|
||||
char *out = dest;
|
||||
for (copied = 1; *src && copied < Len; ++copied) {
|
||||
*out++ = *src++;
|
||||
}
|
||||
*out = 0;
|
||||
return copied - 1;
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
||||
|
||||
// Commands
|
||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
|
||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
||||
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
|
||||
|
||||
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
|
||||
// to supply some of your own allocators for stuff rather than use the defaults
|
||||
|
||||
class LinearAllocator {
|
||||
public:
|
||||
char *buffer_;
|
||||
char *end_;
|
||||
LinearAllocator() {
|
||||
assert(0); // needed for some default case in rapidjson, should not use
|
||||
}
|
||||
LinearAllocator(char *buffer, size_t size)
|
||||
: buffer_(buffer), end_(buffer + size) {
|
||||
}
|
||||
static const bool kNeedFree = false;
|
||||
void *Malloc(size_t size) {
|
||||
char *res = buffer_;
|
||||
buffer_ += size;
|
||||
if (buffer_ > end_) {
|
||||
buffer_ = res;
|
||||
return nullptr;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
|
||||
if (newSize == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// allocate how much you need in the first place
|
||||
assert(!originalPtr && !originalSize);
|
||||
// unused parameter warning
|
||||
(void)(originalPtr);
|
||||
(void)(originalSize);
|
||||
return Malloc(newSize);
|
||||
}
|
||||
static void Free(void *ptr) {
|
||||
/* shrug */
|
||||
(void)ptr;
|
||||
}
|
||||
};
|
||||
|
||||
template<size_t Size>
|
||||
class FixedLinearAllocator : public LinearAllocator {
|
||||
public:
|
||||
char fixedBuffer_[Size];
|
||||
FixedLinearAllocator()
|
||||
: LinearAllocator(fixedBuffer_, Size) {
|
||||
}
|
||||
static const bool kNeedFree = false;
|
||||
};
|
||||
|
||||
// wonder why this isn't a thing already, maybe I missed it
|
||||
class DirectStringBuffer {
|
||||
public:
|
||||
using Ch = char;
|
||||
char *buffer_;
|
||||
char *end_;
|
||||
char *current_;
|
||||
|
||||
DirectStringBuffer(char *buffer, size_t maxLen)
|
||||
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
|
||||
}
|
||||
|
||||
void Put(char c) {
|
||||
if (current_ < end_) {
|
||||
*current_++ = c;
|
||||
}
|
||||
}
|
||||
void Flush() {}
|
||||
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
|
||||
};
|
||||
|
||||
using MallocAllocator = rapidjson::CrtAllocator;
|
||||
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
|
||||
using UTF8 = rapidjson::UTF8<char>;
|
||||
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
|
||||
using StackAllocator = FixedLinearAllocator<2048>;
|
||||
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
|
||||
using JsonWriterBase =
|
||||
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
|
||||
class JsonWriter : public JsonWriterBase {
|
||||
public:
|
||||
DirectStringBuffer stringBuffer_;
|
||||
StackAllocator stackAlloc_;
|
||||
|
||||
JsonWriter(char *dest, size_t maxLen)
|
||||
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
|
||||
}
|
||||
|
||||
size_t Size() const { return stringBuffer_.GetSize(); }
|
||||
};
|
||||
|
||||
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
|
||||
class JsonDocument : public JsonDocumentBase {
|
||||
public:
|
||||
static const int kDefaultChunkCapacity = 32 * 1024;
|
||||
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
|
||||
// send any messages that would use all of this, though.
|
||||
char parseBuffer_[32 * 1024];
|
||||
MallocAllocator mallocAllocator_;
|
||||
PoolAllocator poolAllocator_;
|
||||
StackAllocator stackAllocator_;
|
||||
JsonDocument()
|
||||
: JsonDocumentBase(rapidjson::kObjectType,
|
||||
&poolAllocator_,
|
||||
sizeof(stackAllocator_.fixedBuffer_),
|
||||
&stackAllocator_),
|
||||
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
|
||||
}
|
||||
};
|
||||
|
||||
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
|
||||
|
||||
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
|
||||
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsObject()) {
|
||||
return &member->value;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
|
||||
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsInt()) {
|
||||
return member->value.GetInt();
|
||||
}
|
||||
}
|
||||
|
||||
return notFoundDefault;
|
||||
|
||||
}
|
||||
|
||||
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
|
||||
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsString()) {
|
||||
return member->value.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return notFoundDefault;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_SERIALIZATION_H
|
||||
3
3rdparty/getopt/CMakeLists.txt
vendored
3
3rdparty/getopt/CMakeLists.txt
vendored
@@ -1,3 +0,0 @@
|
||||
add_library(getopt STATIC getopt.cpp)
|
||||
target_compile_definitions(getopt PRIVATE -DSTATIC_GETOPT -D_UNICODE)
|
||||
target_include_directories(getopt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
753
3rdparty/getopt/getopt.cpp
vendored
753
3rdparty/getopt/getopt.cpp
vendored
@@ -1,753 +0,0 @@
|
||||
/* Getopt for Microsoft C
|
||||
This code is a modification of the Free Software Foundation, Inc.
|
||||
Getopt library for parsing command line argument the purpose was
|
||||
to provide a Microsoft Visual C friendly derivative. This code
|
||||
provides functionality for both Unicode and Multibyte builds.
|
||||
|
||||
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
Version: 1.1
|
||||
Comment: Supports getopt, getopt_long, and getopt_long_only
|
||||
and POSIXLY_CORRECT environment flag
|
||||
License: LGPL
|
||||
|
||||
Revisions:
|
||||
|
||||
02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
|
||||
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
|
||||
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
|
||||
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
|
||||
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
|
||||
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
|
||||
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
|
||||
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
|
||||
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
|
||||
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
|
||||
|
||||
**DISCLAIMER**
|
||||
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
|
||||
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
|
||||
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
|
||||
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
|
||||
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
|
||||
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
|
||||
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
|
||||
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
*/
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <malloc.h>
|
||||
#include "getopt.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
# define _GETOPT_THROW throw()
|
||||
#else
|
||||
# define _GETOPT_THROW
|
||||
#endif
|
||||
|
||||
int optind = 1;
|
||||
int opterr = 1;
|
||||
int optopt = '?';
|
||||
enum ENUM_ORDERING {
|
||||
REQUIRE_ORDER,
|
||||
PERMUTE,
|
||||
RETURN_IN_ORDER
|
||||
};
|
||||
|
||||
//
|
||||
//
|
||||
// Ansi structures and functions follow
|
||||
//
|
||||
//
|
||||
|
||||
static struct _getopt_data_a {
|
||||
int optind;
|
||||
int opterr;
|
||||
int optopt;
|
||||
char *optarg;
|
||||
int __initialized;
|
||||
char *__nextchar;
|
||||
enum ENUM_ORDERING __ordering;
|
||||
int __first_nonopt;
|
||||
int __last_nonopt;
|
||||
} getopt_data_a;
|
||||
char *optarg_a;
|
||||
|
||||
static void exchange_a(char **argv, struct _getopt_data_a *d) {
|
||||
int bottom = d->__first_nonopt;
|
||||
int middle = d->__last_nonopt;
|
||||
int top = d->optind;
|
||||
char *tem;
|
||||
while (top > middle && middle > bottom) {
|
||||
if (top - middle > middle - bottom) {
|
||||
int len = middle - bottom;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[top - (middle - bottom) + i];
|
||||
argv[top - (middle - bottom) + i] = tem;
|
||||
}
|
||||
top -= len;
|
||||
}
|
||||
else {
|
||||
int len = top - middle;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[middle + i];
|
||||
argv[middle + i] = tem;
|
||||
}
|
||||
bottom += len;
|
||||
}
|
||||
}
|
||||
d->__first_nonopt += (d->optind - d->__last_nonopt);
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
|
||||
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix);
|
||||
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix) {
|
||||
assert(longopts != NULL);
|
||||
char *nameend;
|
||||
size_t namelen;
|
||||
const struct option_a *p;
|
||||
const struct option_a *pfound = NULL;
|
||||
int n_options;
|
||||
int option_index = 0;
|
||||
for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++)
|
||||
;
|
||||
namelen = nameend - d->__nextchar;
|
||||
for (p = longopts, n_options = 0; p->name; p++, n_options++)
|
||||
if (!strncmp(p->name, d->__nextchar, namelen) && namelen == strlen(p->name)) {
|
||||
pfound = p;
|
||||
option_index = n_options;
|
||||
break;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
unsigned char *ambig_set = NULL;
|
||||
int ambig_fallback = 0;
|
||||
int indfound = -1;
|
||||
for (p = longopts, option_index = 0; p->name; p++, option_index++)
|
||||
if (!strncmp(p->name, d->__nextchar, namelen)) {
|
||||
if (pfound == NULL) {
|
||||
pfound = p;
|
||||
indfound = option_index;
|
||||
}
|
||||
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
|
||||
if (!ambig_fallback) {
|
||||
if (!print_errors)
|
||||
ambig_fallback = 1;
|
||||
|
||||
else if (!ambig_set) {
|
||||
if ((ambig_set = reinterpret_cast<unsigned char *>(malloc(n_options * sizeof(char)))) == NULL)
|
||||
ambig_fallback = 1;
|
||||
|
||||
if (ambig_set) {
|
||||
memset(ambig_set, 0, n_options * sizeof(char));
|
||||
ambig_set[indfound] = 1;
|
||||
}
|
||||
}
|
||||
if (ambig_set)
|
||||
ambig_set[option_index] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ambig_set || ambig_fallback) {
|
||||
if (print_errors) {
|
||||
if (ambig_fallback)
|
||||
fprintf(stderr, "%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
|
||||
else {
|
||||
_lock_file(stderr);
|
||||
fprintf(stderr, "%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
|
||||
for (option_index = 0; option_index < n_options; option_index++)
|
||||
if (ambig_set[option_index])
|
||||
fprintf(stderr, " '%s%s'", prefix, longopts[option_index].name);
|
||||
fprintf(stderr, "\n");
|
||||
_unlock_file(stderr);
|
||||
}
|
||||
}
|
||||
free(ambig_set);
|
||||
d->__nextchar += strlen(d->__nextchar);
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return '?';
|
||||
}
|
||||
option_index = indfound;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
if (!long_only || argv[d->optind][1] == '-' || strchr(optstring, *d->__nextchar) == NULL) {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
|
||||
d->__nextchar = NULL;
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return '?';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
d->optind++;
|
||||
d->__nextchar = NULL;
|
||||
if (*nameend) {
|
||||
if (pfound->has_arg)
|
||||
d->optarg = nameend + 1;
|
||||
else {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
else if (pfound->has_arg == 1) {
|
||||
if (d->optind < argc)
|
||||
d->optarg = argv[d->optind++];
|
||||
else {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return optstring[0] == ':' ? ':' : '?';
|
||||
}
|
||||
}
|
||||
if (longind != NULL)
|
||||
*longind = option_index;
|
||||
|
||||
if (pfound->flag) {
|
||||
*(pfound->flag) = pfound->val;
|
||||
return 0;
|
||||
}
|
||||
return pfound->val;
|
||||
}
|
||||
|
||||
static const char *_getopt_initialize_a(const char *optstring, struct _getopt_data_a *d, int posixly_correct) {
|
||||
if (d->optind == 0)
|
||||
d->optind = 1;
|
||||
|
||||
d->__first_nonopt = d->__last_nonopt = d->optind;
|
||||
d->__nextchar = NULL;
|
||||
|
||||
if (optstring[0] == '-') {
|
||||
d->__ordering = RETURN_IN_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (optstring[0] == '+') {
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (posixly_correct | !!getenv("POSIXLY_CORRECT"))
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
else
|
||||
d->__ordering = PERMUTE;
|
||||
|
||||
d->__initialized = 1;
|
||||
return optstring;
|
||||
}
|
||||
|
||||
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct);
|
||||
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct) {
|
||||
int print_errors = d->opterr;
|
||||
if (argc < 1)
|
||||
return -1;
|
||||
d->optarg = NULL;
|
||||
if (d->optind == 0 || !d->__initialized)
|
||||
optstring = _getopt_initialize_a(optstring, d, posixly_correct);
|
||||
else if (optstring[0] == '-' || optstring[0] == '+')
|
||||
optstring++;
|
||||
if (optstring[0] == ':')
|
||||
print_errors = 0;
|
||||
|
||||
if (d->__nextchar == NULL || *d->__nextchar == '\0') {
|
||||
if (d->__last_nonopt > d->optind)
|
||||
d->__last_nonopt = d->optind;
|
||||
if (d->__first_nonopt > d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
if (d->__ordering == PERMUTE) {
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_a(const_cast<char **>(argv), d);
|
||||
else if (d->__last_nonopt != d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
while (d->optind < argc && (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0'))
|
||||
d->optind++;
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
if (d->optind != argc && !strcmp(argv[d->optind], "--")) {
|
||||
d->optind++;
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_a(const_cast<char **>(argv), d);
|
||||
else if (d->__first_nonopt == d->__last_nonopt)
|
||||
d->__first_nonopt = d->optind;
|
||||
d->__last_nonopt = argc;
|
||||
d->optind = argc;
|
||||
}
|
||||
if (d->optind == argc) {
|
||||
if (d->__first_nonopt != d->__last_nonopt)
|
||||
d->optind = d->__first_nonopt;
|
||||
return -1;
|
||||
}
|
||||
if (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0') {
|
||||
if (d->__ordering == REQUIRE_ORDER)
|
||||
return -1;
|
||||
d->optarg = argv[d->optind++];
|
||||
return 1;
|
||||
}
|
||||
if (longopts) {
|
||||
if (argv[d->optind][1] == '-') {
|
||||
d->__nextchar = argv[d->optind] + 2;
|
||||
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, long_only, d, print_errors, "--");
|
||||
}
|
||||
if (long_only && (argv[d->optind][2] || !strchr(optstring, argv[d->optind][1]))) {
|
||||
int code;
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
code = process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts,
|
||||
longind, long_only, d,
|
||||
print_errors, "-");
|
||||
if (code != -1)
|
||||
return code;
|
||||
}
|
||||
}
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
}
|
||||
{
|
||||
char c = *d->__nextchar++;
|
||||
const char *temp = strchr(optstring, c);
|
||||
if (*d->__nextchar == '\0')
|
||||
++d->optind;
|
||||
if (temp == NULL || c == ':' || c == ';') {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
return '?';
|
||||
}
|
||||
if (temp[0] == 'W' && temp[1] == ';' && longopts != NULL) {
|
||||
if (*d->__nextchar != '\0')
|
||||
d->optarg = d->__nextchar;
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == ':')
|
||||
c = ':';
|
||||
else
|
||||
c = '?';
|
||||
return c;
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind];
|
||||
d->__nextchar = d->optarg;
|
||||
d->optarg = NULL;
|
||||
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, 0, d, print_errors, "-W ");
|
||||
}
|
||||
if (temp[1] == ':') {
|
||||
if (temp[2] == ':') {
|
||||
if (*d->__nextchar != '\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else
|
||||
d->optarg = NULL;
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
else {
|
||||
if (*d->__nextchar != '\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == ':')
|
||||
c = ':';
|
||||
else
|
||||
c = '?';
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind++];
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct);
|
||||
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct) {
|
||||
int result;
|
||||
getopt_data_a.optind = optind;
|
||||
getopt_data_a.opterr = opterr;
|
||||
result = _getopt_internal_r_a(argc, argv, optstring, longopts, longind, long_only, &getopt_data_a, posixly_correct);
|
||||
optind = getopt_data_a.optind;
|
||||
optarg_a = getopt_data_a.optarg;
|
||||
optopt = getopt_data_a.optopt;
|
||||
return result;
|
||||
}
|
||||
|
||||
int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW {
|
||||
return _getopt_internal_a(argc, argv, optstring, static_cast<const struct option_a *>(0), static_cast<int *>(0), 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 1, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
|
||||
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
|
||||
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 0, d, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
|
||||
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
|
||||
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 1, d, 0);
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Unicode Structures and Functions
|
||||
//
|
||||
//
|
||||
|
||||
static struct _getopt_data_w {
|
||||
int optind;
|
||||
int opterr;
|
||||
int optopt;
|
||||
wchar_t *optarg;
|
||||
int __initialized;
|
||||
wchar_t *__nextchar;
|
||||
enum ENUM_ORDERING __ordering;
|
||||
int __first_nonopt;
|
||||
int __last_nonopt;
|
||||
} getopt_data_w;
|
||||
wchar_t *optarg_w;
|
||||
|
||||
static void exchange_w(wchar_t **argv, struct _getopt_data_w *d) {
|
||||
int bottom = d->__first_nonopt;
|
||||
int middle = d->__last_nonopt;
|
||||
int top = d->optind;
|
||||
wchar_t *tem;
|
||||
while (top > middle && middle > bottom) {
|
||||
if (top - middle > middle - bottom) {
|
||||
int len = middle - bottom;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[top - (middle - bottom) + i];
|
||||
argv[top - (middle - bottom) + i] = tem;
|
||||
}
|
||||
top -= len;
|
||||
}
|
||||
else {
|
||||
int len = top - middle;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[middle + i];
|
||||
argv[middle + i] = tem;
|
||||
}
|
||||
bottom += len;
|
||||
}
|
||||
}
|
||||
d->__first_nonopt += (d->optind - d->__last_nonopt);
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
|
||||
static int process_long_option_w(int argc, wchar_t **argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int print_errors, const wchar_t *prefix) {
|
||||
assert(longopts != NULL);
|
||||
wchar_t *nameend;
|
||||
size_t namelen;
|
||||
const struct option_w *p;
|
||||
const struct option_w *pfound = NULL;
|
||||
int n_options;
|
||||
int option_index = 0;
|
||||
for (nameend = d->__nextchar; *nameend && *nameend != L'='; nameend++)
|
||||
;
|
||||
namelen = nameend - d->__nextchar;
|
||||
for (p = longopts, n_options = 0; p->name; p++, n_options++)
|
||||
if (!wcsncmp(p->name, d->__nextchar, namelen) && namelen == wcslen(p->name)) {
|
||||
pfound = p;
|
||||
option_index = n_options;
|
||||
break;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
wchar_t *ambig_set = NULL;
|
||||
int ambig_fallback = 0;
|
||||
int indfound = -1;
|
||||
for (p = longopts, option_index = 0; p->name; p++, option_index++)
|
||||
if (!wcsncmp(p->name, d->__nextchar, namelen)) {
|
||||
if (pfound == NULL) {
|
||||
pfound = p;
|
||||
indfound = option_index;
|
||||
}
|
||||
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
|
||||
if (!ambig_fallback) {
|
||||
if (!print_errors)
|
||||
ambig_fallback = 1;
|
||||
|
||||
else if (!ambig_set) {
|
||||
if ((ambig_set = reinterpret_cast<wchar_t *>(malloc(n_options * sizeof(wchar_t)))) == NULL)
|
||||
ambig_fallback = 1;
|
||||
|
||||
if (ambig_set) {
|
||||
memset(ambig_set, 0, n_options * sizeof(wchar_t));
|
||||
ambig_set[indfound] = 1;
|
||||
}
|
||||
}
|
||||
if (ambig_set)
|
||||
ambig_set[option_index] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ambig_set || ambig_fallback) {
|
||||
if (print_errors) {
|
||||
if (ambig_fallback)
|
||||
fwprintf(stderr, L"%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
|
||||
else {
|
||||
_lock_file(stderr);
|
||||
fwprintf(stderr, L"%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
|
||||
for (option_index = 0; option_index < n_options; option_index++)
|
||||
if (ambig_set[option_index])
|
||||
fwprintf(stderr, L" '%s%s'", prefix, longopts[option_index].name);
|
||||
fwprintf(stderr, L"\n");
|
||||
_unlock_file(stderr);
|
||||
}
|
||||
}
|
||||
free(ambig_set);
|
||||
d->__nextchar += wcslen(d->__nextchar);
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return L'?';
|
||||
}
|
||||
option_index = indfound;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
if (!long_only || argv[d->optind][1] == L'-' || wcschr(optstring, *d->__nextchar) == NULL) {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
|
||||
d->__nextchar = NULL;
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return L'?';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
d->optind++;
|
||||
d->__nextchar = NULL;
|
||||
if (*nameend) {
|
||||
if (pfound->has_arg)
|
||||
d->optarg = nameend + 1;
|
||||
else {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return L'?';
|
||||
}
|
||||
}
|
||||
else if (pfound->has_arg == 1) {
|
||||
if (d->optind < argc)
|
||||
d->optarg = argv[d->optind++];
|
||||
else {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return optstring[0] == L':' ? L':' : L'?';
|
||||
}
|
||||
}
|
||||
if (longind != NULL)
|
||||
*longind = option_index;
|
||||
if (pfound->flag) {
|
||||
*(pfound->flag) = pfound->val;
|
||||
return 0;
|
||||
}
|
||||
return pfound->val;
|
||||
}
|
||||
|
||||
static const wchar_t *_getopt_initialize_w(const wchar_t *optstring, struct _getopt_data_w *d, int posixly_correct) {
|
||||
if (d->optind == 0)
|
||||
d->optind = 1;
|
||||
|
||||
d->__first_nonopt = d->__last_nonopt = d->optind;
|
||||
d->__nextchar = NULL;
|
||||
|
||||
if (optstring[0] == L'-') {
|
||||
d->__ordering = RETURN_IN_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (optstring[0] == L'+') {
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (posixly_correct | !!_wgetenv(L"POSIXLY_CORRECT"))
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
else
|
||||
d->__ordering = PERMUTE;
|
||||
|
||||
d->__initialized = 1;
|
||||
return optstring;
|
||||
}
|
||||
|
||||
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct);
|
||||
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct) {
|
||||
int print_errors = d->opterr;
|
||||
if (argc < 1)
|
||||
return -1;
|
||||
d->optarg = NULL;
|
||||
if (d->optind == 0 || !d->__initialized)
|
||||
optstring = _getopt_initialize_w(optstring, d, posixly_correct);
|
||||
else if (optstring[0] == L'-' || optstring[0] == L'+')
|
||||
optstring++;
|
||||
if (optstring[0] == L':')
|
||||
print_errors = 0;
|
||||
#define NONOPTION_P (argv[d->optind][0] != L'-' || argv[d->optind][1] == L'\0')
|
||||
|
||||
if (d->__nextchar == NULL || *d->__nextchar == L'\0') {
|
||||
if (d->__last_nonopt > d->optind)
|
||||
d->__last_nonopt = d->optind;
|
||||
if (d->__first_nonopt > d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
if (d->__ordering == PERMUTE) {
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_w(const_cast<wchar_t **>(argv), d);
|
||||
else if (d->__last_nonopt != d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
while (d->optind < argc && NONOPTION_P)
|
||||
d->optind++;
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
if (d->optind != argc && !wcscmp(argv[d->optind], L"--")) {
|
||||
d->optind++;
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_w(const_cast<wchar_t **>(argv), d);
|
||||
else if (d->__first_nonopt == d->__last_nonopt)
|
||||
d->__first_nonopt = d->optind;
|
||||
d->__last_nonopt = argc;
|
||||
d->optind = argc;
|
||||
}
|
||||
if (d->optind == argc) {
|
||||
if (d->__first_nonopt != d->__last_nonopt)
|
||||
d->optind = d->__first_nonopt;
|
||||
return -1;
|
||||
}
|
||||
if (NONOPTION_P) {
|
||||
if (d->__ordering == REQUIRE_ORDER)
|
||||
return -1;
|
||||
d->optarg = argv[d->optind++];
|
||||
return 1;
|
||||
}
|
||||
if (longopts) {
|
||||
if (argv[d->optind][1] == L'-') {
|
||||
d->__nextchar = argv[d->optind] + 2;
|
||||
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"--");
|
||||
}
|
||||
if (long_only && (argv[d->optind][2] || !wcschr(optstring, argv[d->optind][1]))) {
|
||||
int code;
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
code = process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"-");
|
||||
if (code != -1)
|
||||
return code;
|
||||
}
|
||||
}
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
}
|
||||
{
|
||||
wchar_t c = *d->__nextchar++;
|
||||
const wchar_t *temp = wcschr(optstring, c);
|
||||
if (*d->__nextchar == L'\0')
|
||||
++d->optind;
|
||||
if (temp == NULL || c == L':' || c == L';') {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: invalid option -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
return L'?';
|
||||
}
|
||||
if (temp[0] == L'W' && temp[1] == L';' && longopts != NULL) {
|
||||
if (*d->__nextchar != L'\0')
|
||||
d->optarg = d->__nextchar;
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == L':')
|
||||
c = L':';
|
||||
else
|
||||
c = L'?';
|
||||
return c;
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind];
|
||||
d->__nextchar = d->optarg;
|
||||
d->optarg = NULL;
|
||||
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind,
|
||||
0, d, print_errors, L"-W ");
|
||||
}
|
||||
if (temp[1] == L':') {
|
||||
if (temp[2] == L':') {
|
||||
if (*d->__nextchar != L'\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else
|
||||
d->optarg = NULL;
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
else {
|
||||
if (*d->__nextchar != L'\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == L':')
|
||||
c = L':';
|
||||
else
|
||||
c = L'?';
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind++];
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct);
|
||||
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct) {
|
||||
int result;
|
||||
getopt_data_w.optind = optind;
|
||||
getopt_data_w.opterr = opterr;
|
||||
result = _getopt_internal_r_w(argc, argv, optstring, longopts, longind, long_only, &getopt_data_w, posixly_correct);
|
||||
optind = getopt_data_w.optind;
|
||||
optarg_w = getopt_data_w.optarg;
|
||||
optopt = getopt_data_w.optopt;
|
||||
return result;
|
||||
}
|
||||
|
||||
int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW {
|
||||
return _getopt_internal_w(argc, argv, optstring, static_cast<const struct option_w *>(0), static_cast<int *>(0), 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 1, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
|
||||
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
|
||||
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 0, d, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
|
||||
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
|
||||
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 1, d, 0);
|
||||
}
|
||||
135
3rdparty/getopt/getopt.h
vendored
135
3rdparty/getopt/getopt.h
vendored
@@ -1,135 +0,0 @@
|
||||
/* Getopt for Microsoft C
|
||||
This code is a modification of the Free Software Foundation, Inc.
|
||||
Getopt library for parsing command line argument the purpose was
|
||||
to provide a Microsoft Visual C friendly derivative. This code
|
||||
provides functionality for both Unicode and Multibyte builds.
|
||||
|
||||
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
Version: 1.1
|
||||
Comment: Supports getopt, getopt_long, and getopt_long_only
|
||||
and POSIXLY_CORRECT environment flag
|
||||
License: LGPL
|
||||
|
||||
Revisions:
|
||||
|
||||
02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
|
||||
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
|
||||
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
|
||||
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
|
||||
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
|
||||
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
|
||||
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
|
||||
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
|
||||
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
|
||||
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
|
||||
|
||||
**DISCLAIMER**
|
||||
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
|
||||
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
|
||||
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
|
||||
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
|
||||
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
|
||||
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
|
||||
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
|
||||
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
*/
|
||||
#ifndef __GETOPT_H_
|
||||
#define __GETOPT_H_
|
||||
|
||||
#ifdef _GETOPT_API
|
||||
# undef _GETOPT_API
|
||||
#endif
|
||||
|
||||
#if defined(EXPORTS_GETOPT) && defined(STATIC_GETOPT)
|
||||
# error "The preprocessor definitions of EXPORTS_GETOPT and STATIC_GETOPT can only be used individually"
|
||||
#elif defined(STATIC_GETOPT)
|
||||
# define _GETOPT_API
|
||||
#elif defined(EXPORTS_GETOPT)
|
||||
# pragma message("Exporting getopt library")
|
||||
# define _GETOPT_API __declspec(dllexport)
|
||||
#else
|
||||
# pragma message("Importing getopt library")
|
||||
# define _GETOPT_API __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
// Change behavior for C\C++
|
||||
#ifdef __cplusplus
|
||||
# define _BEGIN_EXTERN_C extern "C" {
|
||||
# define _END_EXTERN_C }
|
||||
# define _GETOPT_THROW throw()
|
||||
#else
|
||||
# define _BEGIN_EXTERN_C
|
||||
# define _END_EXTERN_C
|
||||
# define _GETOPT_THROW
|
||||
#endif
|
||||
|
||||
// Standard GNU options
|
||||
#define null_argument 0 /*Argument Null*/
|
||||
#define no_argument 0 /*Argument Switch Only*/
|
||||
#define required_argument 1 /*Argument Required*/
|
||||
#define optional_argument 2 /*Argument Optional*/
|
||||
|
||||
// Shorter Options
|
||||
#define ARG_NULL 0 /*Argument Null*/
|
||||
#define ARG_NONE 0 /*Argument Switch Only*/
|
||||
#define ARG_REQ 1 /*Argument Required*/
|
||||
#define ARG_OPT 2 /*Argument Optional*/
|
||||
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
|
||||
_BEGIN_EXTERN_C
|
||||
|
||||
extern _GETOPT_API int optind;
|
||||
extern _GETOPT_API int opterr;
|
||||
extern _GETOPT_API int optopt;
|
||||
|
||||
// Ansi
|
||||
struct option_a {
|
||||
const char *name;
|
||||
int has_arg;
|
||||
int *flag;
|
||||
int val;
|
||||
};
|
||||
extern _GETOPT_API char *optarg_a;
|
||||
extern _GETOPT_API int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
|
||||
|
||||
// Unicode
|
||||
struct option_w {
|
||||
const wchar_t *name;
|
||||
int has_arg;
|
||||
int *flag;
|
||||
int val;
|
||||
};
|
||||
extern _GETOPT_API wchar_t *optarg_w;
|
||||
extern _GETOPT_API int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
|
||||
|
||||
_END_EXTERN_C
|
||||
|
||||
#undef _BEGIN_EXTERN_C
|
||||
#undef _END_EXTERN_C
|
||||
#undef _GETOPT_THROW
|
||||
#undef _GETOPT_API
|
||||
|
||||
#ifdef _UNICODE
|
||||
# define getopt getopt_w
|
||||
# define getopt_long getopt_long_w
|
||||
# define getopt_long_only getopt_long_only_w
|
||||
# define option option_w
|
||||
# define optarg optarg_w
|
||||
#else
|
||||
# define getopt getopt_a
|
||||
# define getopt_long getopt_long_a
|
||||
# define getopt_long_only getopt_long_only_a
|
||||
# define option option_a
|
||||
# define optarg optarg_a
|
||||
#endif
|
||||
#endif // __GETOPT_H_
|
||||
8
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
8
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
@@ -1,8 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/src/kdsingleapplication_localsocket.cpp)
|
||||
set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h)
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
|
||||
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
|
||||
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(kdsingleapplication PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
|
||||
Submodule 3rdparty/kdsingleapplication/KDSingleApplication deleted from cb0c664b40
1727
CMakeLists.txt
1727
CMakeLists.txt
File diff suppressed because it is too large
Load Diff
269
Changelog
269
Changelog
@@ -2,6 +2,275 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.16 (2025.12.16):
|
||||
|
||||
* Make Discord Rich presence use filename if song title is missing
|
||||
* Added better error message when a GStreamer plugin is missing
|
||||
* Preserve track order in album shuffle mode when restarting playback (#1623)
|
||||
* Possible fixes for context word wrap
|
||||
* Added lyrics from lrclib.net
|
||||
* Added option to turn off the use of sort tags for the collection
|
||||
* Fixed Spotify login
|
||||
* Fixed error dialog shown minimized if another Strawberry window than the mainwindow was active
|
||||
* Fixed seeking to the end of the track and back causing seeking to stop working (#1675)
|
||||
* Set current index when automatically selecting track (#1825)
|
||||
* Make icon size for shuffle and repeat buttons adjust to screen resolution (#1838)
|
||||
* Fixed song being removed from playlist when dragging to another application (#1815)
|
||||
* Don't automatically scroll on dynamic playlists (#1427)
|
||||
|
||||
Version 1.2.15 (2025.11.25):
|
||||
|
||||
* Fixed system default language not respected
|
||||
* Fixed length filter search
|
||||
* Fixed playlist parser converting Spotify URL's
|
||||
* Removed use of deprecated QStyle::State_Editing
|
||||
* Ignore connection closed errors for ListenBrainz
|
||||
* (Windows) Support building with vcpkg unofficial::getopt-win32::getopt
|
||||
|
||||
Version 1.2.14 (2025.10.25):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed showing error dialog minimized when main window is not current active window (#1739)
|
||||
* Fixed Discord timestamp update when seeking (#1813)
|
||||
* Fixed CD metadata lookup to respect MusicBrainz rate limiting
|
||||
* Fixed Tidal Open API cover provider
|
||||
* (Windows) Fixed device selection with WASAPI2
|
||||
|
||||
Enhancements/Other:
|
||||
* Removed libre.fm support
|
||||
* Rewrote MusicBrainzClient to use Json instead of XML
|
||||
* Subsonic will now use cover art from album when available
|
||||
* Added option to remove "Remastered", etc from song titles for Tidal, Qobuz and Spotify
|
||||
* Added webm to supported file extensions
|
||||
* (Windows|MinGW) Added WASAPI2 support
|
||||
* (Windows) Added experimental exclusive mode for WASAPI2
|
||||
|
||||
Version 1.2.13 (2025.08.31):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed playlist alternating row colors no longer working with some styles (#1806)
|
||||
* Fixed "Open Audio CD" no longer working (#1803)
|
||||
* Fixed systemtray icon playback status not working with scaling (#1782)
|
||||
* Fixed build without MusicBrainz (#1799)
|
||||
* Fixed build without MTP (#1804)
|
||||
|
||||
Enhancements:
|
||||
* Added Discord status text option (#1796)
|
||||
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
|
||||
|
||||
Version 1.2.12 (2025.08.12):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed scrobbling for radio streams.
|
||||
* Fixed CDDA memory leaks.
|
||||
* Fixed device view CDDA loading (#1676).
|
||||
* Fixed collection directory editing (#1767).
|
||||
* Fixed devices sometimes being duplicated in the database.
|
||||
* Fixed alternating playlist row colors with Windows 11 style.
|
||||
* Fixed broken file filter for GME formats.
|
||||
* Fixed collection scanning for GME formats.
|
||||
* Fixed Chartlyrics.
|
||||
* Fixed network cache file descriptor leak on lyrics search with workaround for QTBUG-135641.
|
||||
* Fixed parsing Tidal urls with certain stream URL replies.
|
||||
* Fixed pixelated window icon on Wayland (#1753).
|
||||
* Fixed saving collection grouping with special characters in the name (#1758).
|
||||
* Fixed Spotify token not automatically updated on renewal when playing (#1769).
|
||||
* (macOS/Windows) Fixed network cache file descriptor leak with patch for QTBUG-135641.
|
||||
* (Windows|MSVC) Fixed installer to not restart the computer after installing Visual C++ Redistributable.
|
||||
|
||||
Enhancements:
|
||||
* Implemented edit tag dialog reset for year, track, disc and rating.
|
||||
* Added ALAC to supported filetypes for iPods.
|
||||
* Added CD-TEXT support.
|
||||
* Added back Genius lyrics.
|
||||
* Added support for reporting more info to ListenBrainz.
|
||||
* Added support for BPM, mood and initial key tags.
|
||||
* Added support for sort tags to collection, playlists and smart playlists.
|
||||
|
||||
Version 1.2.11 (2025.05.15):
|
||||
|
||||
* Fixed playlist songs sometimes not updated with new cover.
|
||||
* Fixed context album cover showing even when it's disabled in the setting (#1744).
|
||||
* Fixed crash when dragging songs to a closed playlist (#1741).
|
||||
* Enable startup notify in desktop file.
|
||||
* (Windows|MSVC) Add experimental support for native ARM64 builds.
|
||||
* (Windows|MinGW) Fixed crash on exit.
|
||||
|
||||
Version 1.2.10 (2025.04.18):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed Discord rich presence showing bogus artist and album.
|
||||
* Fixed incorrect ID3v2 comment tag.
|
||||
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
|
||||
|
||||
Enhancements:
|
||||
* Removed Genius lyrics (longer working properly because of website changes).
|
||||
* (macOS|Windows MSVC) Added back Spotify
|
||||
|
||||
Version 1.2.9 (2025.04.08):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed subsonic parse error (#1719).
|
||||
* Fixed Deezer cover provider parse error (#1716).
|
||||
* Fixed last.fm import progress.
|
||||
* (Windows|MinGW) Switched from winpthreads to win32 threads, winpthreads are no longer working with Qt as of version 6.9 (QTBUG-131892).
|
||||
|
||||
Enhancements:
|
||||
* Added option to disable playbin3.
|
||||
|
||||
Version 1.2.8 (2025.04.05):
|
||||
|
||||
Bugfixes:
|
||||
* Added "HI_RES_LOSSLESS" for Tidal quality setting.
|
||||
* Increased backend settings device lineedit height.
|
||||
* Possible fix for KGlobalAccel shortcuts sometimes not working.
|
||||
|
||||
Enhancements:
|
||||
* Removed deprecated Tidal username/password login.
|
||||
* Turned off "Grey out unavailable songs in playlists on startup" by default.
|
||||
* Added support for reading tags from streams.
|
||||
* Added tooltips in equalizer, backend and appearance settings.
|
||||
* Added full tag support for AIFF including embedded covers.
|
||||
* Use card ID instead of index for ALSA devices.
|
||||
* Removed KDSingleApplication from 3rdparty.
|
||||
* Support arbitrarily large EBU R 128 loudness normalization.
|
||||
|
||||
New features:
|
||||
* Added Discord rich presence support.
|
||||
|
||||
Version 1.2.7 (2025.01.31):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed strawberry exiting when clicking tray icon.
|
||||
* Fixed Clementine import script errors.
|
||||
* Disabled OSD Pretty on Wayland since it's not working properly.
|
||||
|
||||
Enhancements:
|
||||
* Only maximize error dialog if Strawberry is the active window (#1627).
|
||||
* Added QPA Platform Native Interface as optional component.
|
||||
|
||||
Version 1.2.6 (2025.01.17):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed dragging songs from playlist to queue.
|
||||
|
||||
Version 1.2.5 (2025.01.17):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed crash when saving playcount or rating to file (#1633).
|
||||
* Fixed QFile::open failing in unit tests.
|
||||
* Fixed playlist sequence settings saved to wrong configuration file (#1649).
|
||||
|
||||
Enhancements:
|
||||
* Fixed use of deprecated GIO functions with GLib 2.84 and newer.
|
||||
* (macOS) Added back Sparkle updater to check for new releases.
|
||||
|
||||
Version 1.2.4 (2025.01.10):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed Spotify songs not being available for scrobbling.
|
||||
* Fixed leading "A" and "The" articles being skipped for album sort text.
|
||||
* Fixed thread safety issue when validating playlist songs on startup.
|
||||
* Fixed filter search not ignoring space after colon when using column based search.
|
||||
* Fixed KGlobalAccel to use capitalized application name.
|
||||
* Fixed slash not properly handled when saving a playlist (#1624).
|
||||
* (Unix) Fixed collection scanner so it ignores special filesystem paths (/sys, /proc, /run, etc) (#1615).
|
||||
* (Windows) Fixed smart playlist wizard not respecting dark mode with Windows 11 style (#1639).
|
||||
|
||||
Enhancements:
|
||||
* Use XSPF "title" as playlist name when loading and saving playlists (#1624).
|
||||
* Added support for using album ID when receving album covers for Subsonic songs (#1636).
|
||||
* Added option for preserving directory structure when trascoding songs (#1637).
|
||||
* (Windows) Always run MSVC runtime installer to possible fix issues when there is an older runtime installed.
|
||||
|
||||
Version 1.2.3 (2024.12.08):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed libcdio NULL related compilation error on FreeBSD (#1610).
|
||||
* Fixed missing seek when starting playback of a CUE song (#1568).
|
||||
* Fixed "QDBusObjectPath: invalid path" error.
|
||||
|
||||
Version 1.2.2 (2024.11.23):
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Fixed crash when creating a new smart playlist (#1609).
|
||||
* Fixed last playlist column being added when dragging a song and switching playlists.
|
||||
|
||||
Version 1.2.1 (2024.11.21):
|
||||
|
||||
This release features major restructuring of the codebase, moving source files,
|
||||
rewriting CMake build files, dropping Qt 5 support, external tagreader,
|
||||
and dropping some unmaintained parts such as VLC.
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Fixed playback of CUE continuing to play from the same file after the song has finished playing (#1568).
|
||||
* Fixed updating collection song sort text when disc is changed.
|
||||
* Fixed current playing file left open when the next track errored (#1582).
|
||||
* Fixed filter search not finding song containing uppercase "A" (#1599).
|
||||
* Fixed crash when removing album from playlist when using shuffle albums (#1588).
|
||||
* Fixed IDv3 MBID's tags with multiple entries being ignored.
|
||||
* Fixed crash when enabling Tidal, Spotify, Qobuz or Subsonic services.
|
||||
* Fixed passing filenames to strawberry on command line not resolving to absolute paths.
|
||||
* (macOS) Fixed program not starting for users with long usernames.
|
||||
* (macoS) Fixed crash when pressing caps lock (#1606).
|
||||
* (macOS) Remove "song progress on taskbar" option in behaviour settings.
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Resolve symbolic links when dragging files to the playlist to match collection song.
|
||||
* Replaced Spotify username/password with access token.
|
||||
* Require Qt 6.4 or higher and drop support for Qt 5.
|
||||
* Require TagLib 1.12 or higher.
|
||||
* Use Qt stringliterals.
|
||||
* Move gstfastspectrum to src.
|
||||
* Use standard user temp location for current album cover.
|
||||
* Removed old MacFSListener.
|
||||
* Removed external tagreader and protobuf dependency.
|
||||
* Removed VLC support.
|
||||
* Ported to Qt translation (.ts) files and removed gettext dependency.
|
||||
* Removed deprecated Gnome/Mate SettingsDaemon global shortcuts.
|
||||
|
||||
Version 1.1.3 (2024.09.21):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed gstreamer registry lookup leak in Spotify settings.
|
||||
* Fixed all songs in a CUE sheet starting playback at the zero position (#1549).
|
||||
* Fixed playback going to pause and back to play on song change.
|
||||
* Fixed Genius Lyrics login not working (#1554).
|
||||
* Fixed slow collection filter search.
|
||||
|
||||
Version 1.1.2 (2024.09.12):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed Tidal Open API cover provider to only login when needed instead of on startup.
|
||||
* Fixed KDE added keyboard accelerator characters (ampersands) appearing in sidebar (#1400, #1389, #1476).
|
||||
* Fixed KDE added keyboard accelerator characters (ampersands) appearing when editing playlist name (#1499).
|
||||
* Fixed collection "Search for this" adding prefix without value (#1510).
|
||||
* Fixed play (-p) command line option not working on startup (#1465).
|
||||
* Fixed scan transaction being started when "Update the collection when Strawberry starts" option is unchecked (#1469)
|
||||
* Fixed Spotify bitrate being limited 128kbit/s.
|
||||
* Fixed Spotify returning too many artists and albums.
|
||||
* Fixed manually switching Spotify songs blocking UI.
|
||||
* Fixed analyzer not being set.
|
||||
* Fixed context top text being updated causing selected text to be unselected.
|
||||
* Fixed filter search to use filename for songs with empty title.
|
||||
* Fixed missing developer in Appstream appdata file.
|
||||
* Fixed MPRIS2 DesktopEntry to return desktop file entry without ".desktop" (#1516)
|
||||
* Fixed WavPack .wvc accepted as valid audio files (#1525).
|
||||
* Fixed dynamic playlist controls not following system colors (#1483).
|
||||
* Fixed freeze on playlist right click (#1478).
|
||||
* Fixed copying songs to a iPod device keeping too many files open (#1527).
|
||||
* Fixed MBIDs from MP4 being parsed incorrectly causing ListenBrainz errors (#1531).
|
||||
* Fixed playlist sorting after filename (#1538).
|
||||
|
||||
Enhancements:
|
||||
* Improved volume adjustment and track seeking using touchpad (#1498).
|
||||
* Use own thread for lyrics parsing.
|
||||
* Added url and filename columns to collection and playlist filter search.
|
||||
* (macOS) Added Spotify.
|
||||
|
||||
Version 1.1.1 (2024.07.22):
|
||||
|
||||
Bugfixes:
|
||||
|
||||
185
README.md
185
README.md
@@ -1,122 +1,137 @@
|
||||
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
=======================
|
||||
# :strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
[](https://github.com/sponsors/jonaski)
|
||||
[](https://patreon.com/jonaskvinge)
|
||||
[](https://paypal.me/jonaskvinge)
|
||||
|
||||
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt toolkit.
|
||||
Strawberry is a **music player and music collection organizer**, originally forked from *Clementine* in 2018.
|
||||
It’s written in **C++ using the Qt framework**, designed for **audiophiles and music collectors**.
|
||||
|
||||

|
||||

|
||||
|
||||
Resources:
|
||||
---
|
||||
|
||||
* Website: https://www.strawberrymusicplayer.org/
|
||||
* Wiki: https://wiki.strawberrymusicplayer.org/
|
||||
* Forum: https://forum.strawberrymusicplayer.org/
|
||||
* Github: https://github.com/strawberrymusicplayer/strawberry
|
||||
* Buildbot: https://buildbot.strawberrymusicplayer.org/
|
||||
* Latest builds: https://builds.strawberrymusicplayer.org/
|
||||
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
## :globe_with_meridians: Resources
|
||||
|
||||
### :bangbang: Opening an issue
|
||||
- **Website:** https://www.strawberrymusicplayer.org
|
||||
- **Wiki:** https://wiki.strawberrymusicplayer.org
|
||||
- **Forum:** https://forum.strawberrymusicplayer.org
|
||||
- **GitHub:** https://github.com/strawberrymusicplayer/strawberry
|
||||
- **Latest builds:** https://builds.strawberrymusicplayer.org
|
||||
- **openSUSE Build Service:**
|
||||
- Stable: https://build.opensuse.org/package/show/home:jonaski:strawberry/strawberry
|
||||
- Unstable: https://build.opensuse.org/package/show/home:jonaski:strawberry-dev/strawberry
|
||||
- **Ubuntu PPAs:**
|
||||
- Stable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
- Unstable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
- **Translations:** https://crowdin.com/project/strawberrymusicplayer
|
||||
|
||||
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
|
||||
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
|
||||
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
|
||||
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
|
||||
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
|
||||
---
|
||||
|
||||
### :moneybag: Sponsoring
|
||||
## :warning: Opening an Issue
|
||||
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||
There are currently 4 options for sponsoring:
|
||||
Before creating a new GitHub issue:
|
||||
|
||||
1. [GitHub](https://github.com/sponsors/jonaski)
|
||||
2. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
1. **Read the [FAQ](https://wiki.strawberrymusicplayer.org/wiki/FAQ)**.
|
||||
2. **Search existing issues** to avoid duplicates. If one already exists, comment there with any additional information.
|
||||
3. **Use the [forum](https://forum.strawberrymusicplayer.org/)** for technical problems, discussions or feature suggestions — it’s better suited for back-and-forth conversation.
|
||||
4. **Feature requests are not accepted on GitHub.** Issues created for feature requests will be closed. You can still discuss ideas on the forum.
|
||||
5. **Flatpak users:** We do **not** maintain the Flatpak package. Report Flatpak-specific issues via [Flatpak support](https://flatpak.org/about/).
|
||||
|
||||
---
|
||||
|
||||
## :moneybag: Sponsoring
|
||||
|
||||
Strawberry is **free software released under the GPL**.
|
||||
If you enjoy using it, please consider **supporting development** through sponsorship or donation.
|
||||
|
||||
**Sponsorship options:**
|
||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||
|
||||
Funding developers is a way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
|
||||
Supporting open-source developers helps ensure continued maintenance and improvements.
|
||||
|
||||
### :heavy_check_mark: Features
|
||||
---
|
||||
|
||||
* Play and organize music
|
||||
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
|
||||
* Audio CD playback
|
||||
* Native desktop notifications
|
||||
* Playlist management
|
||||
* Smart and dynamic playlists
|
||||
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
* In-player song loudness analysis and song playback loudness normalization, as per EBU R 128
|
||||
* Edit tags on audio files
|
||||
* Fetch tags from MusicBrainz
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
|
||||
* Support for multiple backends
|
||||
* Audio analyzer
|
||||
* Audio equalizer
|
||||
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
* Subsonic, Tidal, Spotify and Qobuz streaming support
|
||||
## :white_check_mark: Features
|
||||
|
||||
- Play and organize your music collection
|
||||
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
||||
- Audio CD playback
|
||||
- Bit-perfect playback on Linux
|
||||
- Native desktop notifications
|
||||
- Advanced playlist management
|
||||
- Smart and dynamic playlists
|
||||
- Loudness analysis and EBU R128 normalization
|
||||
- Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/)
|
||||
- Album art from: [Last.fm](https://www.last.fm/), [MusicBrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/), [Spotify](https://www.spotify.com/)
|
||||
- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com) and [lrclib.net](https://lrclib.net/)
|
||||
- Audio analyzer and equalizer
|
||||
- Transfer music to USB, MTP and iPod devices
|
||||
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
- Streaming from Subsonic-compatible servers
|
||||
- Unofficial integrations: Tidal, Spotify, and Qobuz
|
||||
- Discord Rich Presence
|
||||
|
||||
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
||||
---
|
||||
|
||||
**Access to macOS and Windows releases are currently restricted to sponsors, a 5 USD monthly sponsorship is required. You can sponsor strawberry through <a href="https://www.patreon.com/jonaskvinge">Patreon</a> for direct access to new releases. If you are sponsoring through GitHub, Ko-fi or PayPal, please e-mail support AT strawberrymusicplayer.org for access to downloads.**
|
||||
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
||||
|
||||
### :heavy_exclamation_mark: Requirements
|
||||
> **Note:** macOS and Windows releases are currently **available to sponsors only**.
|
||||
> A monthly sponsorship via [Patreon](https://www.patreon.com/jonaskvinge) grants direct access to new releases.
|
||||
|
||||
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
|
||||
---
|
||||
|
||||
* [CMake](https://cmake.org/)
|
||||
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
* [Boost](https://www.boost.org/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||
* [Protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
* [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/) or [VLC](https://www.videolan.org)
|
||||
* [TagLib 1.11.1 or higher](https://www.taglib.org/) or [TagParser](https://github.com/Martchus/tagparser)
|
||||
* [ICU](https://unicode-org.github.io/icu/)
|
||||
## :gear: Requirements
|
||||
|
||||
Optional dependencies:
|
||||
To build Strawberry from source, you’ll need:
|
||||
|
||||
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
||||
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
||||
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
|
||||
**Dependencies:**
|
||||
- [CMake ≥= 3.13](https://cmake.org/)
|
||||
- C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/), or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||
- [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
- [Boost](https://www.boost.org/)
|
||||
- [GLib](https://developer.gnome.org/glib/)
|
||||
- [Qt ≥= 6.4](https://www.qt.io/) (Core, Concurrent, Gui, Widgets, Network, SQL, D-Bus)
|
||||
- [SQLite ≥= 3.9](https://www.sqlite.org)
|
||||
- [ALSA (Linux only)](https://www.alsa-project.org/)
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [TagLib ≥= 1.12](https://www.taglib.org/)
|
||||
- [ICU](https://unicode-org.github.io/icu/)
|
||||
- [KDSingleApplication ≥= 1.1.0](https://github.com/KDAB/KDSingleApplication)
|
||||
|
||||
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
|
||||
**Dependencies for optional features:**
|
||||
- Fingerprinting & tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||
- Moodbar: [FFTW3](http://www.fftw.org/)
|
||||
- PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/)
|
||||
- Audio CD support: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||
- MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||
- iPod Classic: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||
- EBU R128 normalization: [libebur128](https://github.com/jiixyj/libebur128)
|
||||
- Discord presence: [RapidJSON](https://rapidjson.org/)
|
||||
|
||||
### :wrench: Compiling from source
|
||||
Also install GStreamer plugins **base**, **good**, and optionally **bad**, **ugly** and **libav** for full codec support.
|
||||
|
||||
### Get the code:
|
||||
---
|
||||
|
||||
## :wrench: Build from Source
|
||||
|
||||
**Get the code:**
|
||||
|
||||
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
### Compile and install:
|
||||
**Build and install:**
|
||||
|
||||
cd strawberry
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DBUILD_WITH_QT6=ON
|
||||
make -j $(nproc)
|
||||
sudo make install
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel $(nproc)
|
||||
sudo cmake --install build
|
||||
|
||||
Strawberry is backwards compatible with Qt 5, to compile with Qt 5 use:
|
||||
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
cmake .. -DBUILD_WITH_QT5=ON
|
||||
---
|
||||
|
||||
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
## :package: Packaging status
|
||||
|
||||
### :penguin: Packaging status
|
||||
|
||||
[](https://repology.org/metapackage/strawberry/versions)
|
||||
[](https://repology.org/metapackage/strawberry/versions)
|
||||
|
||||
@@ -2,5 +2,5 @@ find_program(LSB_RELEASE_EXEC lsb_release)
|
||||
find_program(DPKG_BUILDPACKAGE dpkg-buildpackage)
|
||||
|
||||
if (LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
||||
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us)
|
||||
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us -nc -j4)
|
||||
endif()
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin /usr/local/opt/qt5/bin REQUIRED)
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeployqt executable.")
|
||||
endif()
|
||||
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeploycheck executable.")
|
||||
endif()
|
||||
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||
@@ -24,17 +31,18 @@ if(MACDEPLOYQT_EXECUTABLE)
|
||||
|
||||
add_custom_target(deploy
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS strawberry strawberry-tagreader
|
||||
)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${CMAKE_BINARY_DIR}/ext/macdeploycheck/macdeploycheck strawberry.app
|
||||
DEPENDS macdeploycheck
|
||||
DEPENDS strawberry
|
||||
)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||
)
|
||||
endif()
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
add_custom_target(dmg
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# - Try to find the libcppunit libraries
|
||||
# Once done this will define
|
||||
#
|
||||
# CppUnit_FOUND - system has libcppunit
|
||||
# CPPUNIT_INCLUDE_DIR - the libcppunit include directory
|
||||
# CPPUNIT_LIBRARIES - libcppunit library
|
||||
|
||||
#include (MacroEnsureVersion)
|
||||
|
||||
if(NOT CPPUNIT_MIN_VERSION)
|
||||
SET(CPPUNIT_MIN_VERSION 1.12.0)
|
||||
endif(NOT CPPUNIT_MIN_VERSION)
|
||||
|
||||
FIND_PROGRAM(CPPUNIT_CONFIG_EXECUTABLE cppunit-config )
|
||||
|
||||
IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
# in cache already
|
||||
SET(CppUnit_FOUND TRUE)
|
||||
|
||||
ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
SET(CPPUNIT_INCLUDE_DIR)
|
||||
SET(CPPUNIT_LIBRARIES)
|
||||
|
||||
IF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --cflags RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_CFLAGS)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --libs RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_LIBRARIES)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION)
|
||||
STRING(REGEX REPLACE "-I(.+)" "\\1" CPPUNIT_CFLAGS "${CPPUNIT_CFLAGS}")
|
||||
ELSE(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
# in case win32 needs to find it the old way?
|
||||
FIND_PATH(CPPUNIT_CFLAGS cppunit/TestRunner.h PATHS /usr/include /usr/local/include )
|
||||
FIND_LIBRARY(CPPUNIT_LIBRARIES NAMES cppunit PATHS /usr/lib /usr/local/lib )
|
||||
# how can we find cppunit version?
|
||||
MESSAGE (STATUS "Ensure you cppunit installed version is at least ${CPPUNIT_MIN_VERSION}")
|
||||
SET (CPPUNIT_INSTALLED_VERSION ${CPPUNIT_MIN_VERSION})
|
||||
ENDIF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
|
||||
SET(CPPUNIT_INCLUDE_DIR ${CPPUNIT_CFLAGS} "${CPPUNIT_CFLAGS}/cppunit")
|
||||
|
||||
ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
SET(CppUnit_FOUND TRUE)
|
||||
|
||||
if(NOT CppUnit_FIND_QUIETLY)
|
||||
MESSAGE (STATUS "Found cppunit: ${CPPUNIT_LIBRARIES}")
|
||||
endif(NOT CppUnit_FIND_QUIETLY)
|
||||
|
||||
IF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION)
|
||||
ENDIF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
|
||||
#macro_ensure_version( ${CPPUNIT_MIN_VERSION} ${CPPUNIT_INSTALLED_VERSION} CPPUNIT_INSTALLED_VERSION_OK )
|
||||
|
||||
#IF(NOT CPPUNIT_INSTALLED_VERSION_OK)
|
||||
# MESSAGE ("** CppUnit version is too old: found ${CPPUNIT_INSTALLED_VERSION} installed, ${CPPUNIT_MIN_VERSION} or major is required")
|
||||
# SET(CppUnit_FOUND FALSE)
|
||||
#ENDIF(NOT CPPUNIT_INSTALLED_VERSION_OK)
|
||||
|
||||
ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
SET(CppUnit_FOUND FALSE CACHE BOOL "Not found cppunit library")
|
||||
|
||||
ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
MARK_AS_ADVANCED(CPPUNIT_INCLUDE_DIR CPPUNIT_LIBRARIES)
|
||||
@@ -1,132 +0,0 @@
|
||||
#
|
||||
# Try to find FFTW3 library
|
||||
# (see www.fftw.org)
|
||||
# Once run this will define:
|
||||
#
|
||||
# FFTW3_FOUND
|
||||
# FFTW3_INCLUDE_DIR
|
||||
# FFTW3_LIBRARIES
|
||||
# FFTW3_LINK_DIRECTORIES
|
||||
#
|
||||
# You may set one of these options before including this file:
|
||||
# FFTW3_USE_SSE2
|
||||
#
|
||||
# TODO: _F_ versions.
|
||||
#
|
||||
# Jan Woetzel 05/2004
|
||||
# www.mip.informatik.uni-kiel.de
|
||||
# --------------------------------
|
||||
|
||||
FIND_PATH(FFTW3_INCLUDE_DIR fftw3.h
|
||||
${FFTW3_DIR}/include
|
||||
${FFTW3_HOME}/include
|
||||
${FFTW3_DIR}
|
||||
${FFTW3_HOME}
|
||||
$ENV{FFTW3_DIR}/include
|
||||
$ENV{FFTW3_HOME}/include
|
||||
$ENV{FFTW3_DIR}
|
||||
$ENV{FFTW3_HOME}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
$ENV{SOURCE_DIR}/fftw3
|
||||
$ENV{SOURCE_DIR}/fftw3/include
|
||||
$ENV{SOURCE_DIR}/fftw
|
||||
$ENV{SOURCE_DIR}/fftw/include
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_INCLUDE_DIR=${FFTW3_INCLUDE_DIR}")
|
||||
|
||||
|
||||
SET(FFTW3_POSSIBLE_LIBRARY_PATH
|
||||
${FFTW3_DIR}/lib
|
||||
${FFTW3_HOME}/lib
|
||||
${FFTW3_DIR}
|
||||
${FFTW3_HOME}
|
||||
$ENV{FFTW3_DIR}/lib
|
||||
$ENV{FFTW3_HOME}/lib
|
||||
$ENV{FFTW3_DIR}
|
||||
$ENV{FFTW3_HOME}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
$ENV{SOURCE_DIR}/fftw3
|
||||
$ENV{SOURCE_DIR}/fftw3/lib
|
||||
$ENV{SOURCE_DIR}/fftw
|
||||
$ENV{SOURCE_DIR}/fftw/lib
|
||||
)
|
||||
|
||||
|
||||
# The lib prefix is contained in filename of W32, unfortunately. In the "general" lib:
|
||||
FIND_LIBRARY(FFTW3_FFTW_LIBRARY
|
||||
NAMES fftw3 libfftw libfftw3 libfftw3-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTW_LIBRARY=${FFTW3_FFTW_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWF_LIBRARY
|
||||
NAMES fftwf3 fftw3f fftwf libfftwf libfftwf3 libfftw3f-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWF_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWL_LIBRARY
|
||||
NAMES fftwl3 fftw3l fftwl libfftwl libfftwl3 libfftw3l-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWL_LIBRARY}")
|
||||
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTW_SSE2_LIBRARY
|
||||
NAMES fftw_sse2 fftw3_sse2 libfftw_sse2 libfftw3_sse2
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTW_SSE2_LIBRARY=${FFTW3_FFTW_SSE2_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWF_SSE_LIBRARY
|
||||
NAMES fftwf_sse fftwf3_sse libfftwf_sse libfftwf3_sse
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_SSE_LIBRARY=${FFTW3_FFTWF_SSE_LIBRARY}")
|
||||
|
||||
|
||||
# --------------------------------
|
||||
# select one of the above
|
||||
# default:
|
||||
IF (FFTW3_FFTW_LIBRARY)
|
||||
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_LIBRARY})
|
||||
ENDIF (FFTW3_FFTW_LIBRARY)
|
||||
# specialized:
|
||||
IF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_SSE2_LIBRARY})
|
||||
ENDIF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||
|
||||
# --------------------------------
|
||||
|
||||
IF(FFTW3_LIBRARIES)
|
||||
IF (FFTW3_INCLUDE_DIR)
|
||||
|
||||
# OK, found all we need
|
||||
SET(FFTW3_FOUND TRUE)
|
||||
GET_FILENAME_COMPONENT(FFTW3_LINK_DIRECTORIES ${FFTW3_LIBRARIES} PATH)
|
||||
|
||||
ELSE (FFTW3_INCLUDE_DIR)
|
||||
MESSAGE("FFTW3 include dir not found. Set FFTW3_DIR to find it.")
|
||||
ENDIF(FFTW3_INCLUDE_DIR)
|
||||
ELSE(FFTW3_LIBRARIES)
|
||||
MESSAGE("FFTW3 lib not found. Set FFTW3_DIR to find it.")
|
||||
ENDIF(FFTW3_LIBRARIES)
|
||||
|
||||
|
||||
MARK_AS_ADVANCED(
|
||||
FFTW3_INCLUDE_DIR
|
||||
FFTW3_LIBRARIES
|
||||
FFTW3_FFTW_LIBRARY
|
||||
FFTW3_FFTW_SSE2_LIBRARY
|
||||
FFTW3_FFTWF_LIBRARY
|
||||
FFTW3_FFTWF_SSE_LIBRARY
|
||||
FFTW3_FFTWL_LIBRARY
|
||||
FFTW3_LINK_DIRECTORIES
|
||||
)
|
||||
@@ -1,15 +1,15 @@
|
||||
set(summary_willbuild "")
|
||||
set(summary_willnotbuild "")
|
||||
|
||||
macro(summary_add name test)
|
||||
macro(optional_component_summary_add name test)
|
||||
if (${test})
|
||||
list(APPEND summary_willbuild ${name})
|
||||
else (${test})
|
||||
list(APPEND summary_willnotbuild "${name}")
|
||||
endif (${test})
|
||||
endmacro(summary_add)
|
||||
endmacro(optional_component_summary_add)
|
||||
|
||||
macro(summary_show_part variable title)
|
||||
macro(optional_component_summary_show_part variable title)
|
||||
list(LENGTH ${variable} _len)
|
||||
if (_len)
|
||||
message("")
|
||||
@@ -18,19 +18,20 @@ macro(summary_show_part variable title)
|
||||
message(" ${_item}")
|
||||
endforeach (_item)
|
||||
endif (_len)
|
||||
endmacro(summary_show_part)
|
||||
endmacro(optional_component_summary_show_part)
|
||||
|
||||
macro(summary_show)
|
||||
macro(optional_component_summary_show)
|
||||
list(SORT summary_willbuild)
|
||||
list(SORT summary_willnotbuild)
|
||||
message("")
|
||||
message("Building strawberry version: ${STRAWBERRY_VERSION_DISPLAY}, Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION}")
|
||||
summary_show_part(summary_willbuild "The following components will be built:")
|
||||
summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
|
||||
optional_component_summary_show_part(summary_willbuild "The following components will be built:")
|
||||
optional_component_summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
|
||||
message("")
|
||||
endmacro(summary_show)
|
||||
endmacro(optional_component_summary_show)
|
||||
|
||||
function(optional_component name default description)
|
||||
|
||||
set(option_variable "ENABLE_${name}")
|
||||
set(have_variable "HAVE_${name}")
|
||||
set(${have_variable} OFF)
|
||||
@@ -79,6 +80,9 @@ function(optional_component name default description)
|
||||
set(text "${description} (missing ${deplist_text})")
|
||||
|
||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
|
||||
else()
|
||||
set(${have_variable} ON PARENT_SCOPE)
|
||||
set(summary_willbuild "${summary_willbuild};${description}" PARENT_SCOPE)
|
||||
@@ -64,7 +64,7 @@ if (LSB_RELEASE_EXEC AND RPMBUILD_EXEC)
|
||||
add_custom_target(rpm
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/scripts/maketarball.sh
|
||||
COMMAND ${CMAKE_COMMAND} -E copy strawberry-${STRAWBERRY_VERSION_PACKAGE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
|
||||
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
|
||||
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_BINARY_DIR}/strawberry.spec
|
||||
)
|
||||
|
||||
endif()
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext REQUIRED)
|
||||
find_program(CAT_EXECUTABLE cat REQUIRED)
|
||||
|
||||
list(APPEND XGETTEXT_OPTIONS
|
||||
--qt
|
||||
--keyword=tr:1,2c
|
||||
--keyword=tr
|
||||
--flag=tr:1:pass-c-format
|
||||
--flag=tr:1:pass-qt-format
|
||||
--keyword=trUtf8
|
||||
--flag=tr:1:pass-c-format
|
||||
--flag=tr:1:pass-qt-format
|
||||
--keyword=translate:2,3c
|
||||
--keyword=translate:2
|
||||
--flag=translate:2:pass-c-format
|
||||
--flag=translate:2:pass-qt-format
|
||||
--keyword=QT_TR_NOOP
|
||||
--flag=QT_TR_NOOP:1:pass-c-format
|
||||
--flag=QT_TR_NOOP:1:pass-qt-format
|
||||
--keyword=QT_TRANSLATE_NOOP:2
|
||||
--flag=QT_TRANSLATE_NOOP:2:pass-c-format
|
||||
--flag=QT_TRANSLATE_NOOP:2:pass-qt-format
|
||||
--keyword=_
|
||||
--flag=_:1:pass-c-format
|
||||
--flag=_:1:pass-qt-format
|
||||
--keyword=N_
|
||||
--flag=N_:1:pass-c-format
|
||||
--flag=N_:1:pass-qt-format
|
||||
--from-code=utf-8
|
||||
)
|
||||
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/translations)
|
||||
|
||||
macro(add_pot outfiles header pot)
|
||||
# Make relative filenames for all source files
|
||||
set(add_pot_sources)
|
||||
foreach(_filename ${ARGN})
|
||||
get_filename_component(_absolute_filename ${_filename} ABSOLUTE)
|
||||
file(RELATIVE_PATH _relative_filename ${CMAKE_CURRENT_SOURCE_DIR} ${_absolute_filename})
|
||||
list(APPEND add_pot_sources ${_relative_filename})
|
||||
endforeach(_filename)
|
||||
|
||||
# Generate the .pot
|
||||
add_custom_command(
|
||||
OUTPUT ${pot}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMAND ${GETTEXT_XGETTEXT_EXECUTABLE} ${XGETTEXT_OPTIONS} -s -C --omit-header --output="${CMAKE_CURRENT_BINARY_DIR}/pot.temp" ${add_pot_sources}
|
||||
COMMAND cat ${header} ${CMAKE_CURRENT_BINARY_DIR}/pot.temp > ${pot}
|
||||
DEPENDS ${add_pot_sources} ${header}
|
||||
)
|
||||
|
||||
list(APPEND ${outfiles} ${pot})
|
||||
endmacro(add_pot)
|
||||
|
||||
# Syntax is:
|
||||
# add_po(sources_var po_prefix LANGUAGES language1 language2 ... DIRECTORY dir)
|
||||
|
||||
macro(add_po outfiles po_prefix)
|
||||
parse_arguments(ADD_PO
|
||||
"LANGUAGES;DIRECTORY"
|
||||
""
|
||||
${ARGN}
|
||||
)
|
||||
|
||||
foreach (_lang ${ADD_PO_LANGUAGES})
|
||||
set(_po_filename "${_lang}.po")
|
||||
set(_po_filepath "${CMAKE_CURRENT_SOURCE_DIR}/${ADD_PO_DIRECTORY}/${_po_filename}")
|
||||
set(_qm_filename "strawberry_${_lang}.qm")
|
||||
set(_qm_filepath "${CMAKE_CURRENT_BINARY_DIR}/${ADD_PO_DIRECTORY}/${_qm_filename}")
|
||||
|
||||
# Convert the .po files to .qm files
|
||||
add_custom_command(
|
||||
OUTPUT ${_qm_filepath}
|
||||
COMMAND ${QT_LCONVERT_EXECUTABLE} ARGS ${_po_filepath} -o ${_qm_filepath} -of qm -target-language ${_lang}
|
||||
DEPENDS ${_po_filepath} ${_po_filepath}
|
||||
)
|
||||
|
||||
list(APPEND ${outfiles} ${_qm_filepath})
|
||||
list(APPEND INSTALL_TRANSLATIONS_FILES ${_qm_filepath})
|
||||
endforeach (_lang)
|
||||
|
||||
# Generate a qrc file for the translations
|
||||
if(NOT INSTALL_TRANSLATIONS)
|
||||
set(_qrc ${CMAKE_CURRENT_BINARY_DIR}/${ADD_PO_DIRECTORY}/translations.qrc)
|
||||
file(WRITE ${_qrc} "<RCC><qresource prefix=\"/${ADD_PO_DIRECTORY}\">")
|
||||
foreach(_lang ${ADD_PO_LANGUAGES})
|
||||
file(APPEND ${_qrc} "<file>${po_prefix}${_lang}.qm</file>")
|
||||
endforeach(_lang)
|
||||
file(APPEND ${_qrc} "</qresource></RCC>")
|
||||
qt_add_resources(${outfiles} ${_qrc})
|
||||
endif()
|
||||
endmacro(add_po)
|
||||
@@ -1,9 +1,9 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 1)
|
||||
set(STRAWBERRY_VERSION_PATCH 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 16)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
set(INCLUDE_GIT_REVISION ON)
|
||||
|
||||
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")
|
||||
|
||||
|
||||
3
crowdin.yml
Normal file
3
crowdin.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
files:
|
||||
- source: /src/translations/strawberry_en_US.ts
|
||||
translation: /src/translations/strawberry_%locale_with_underscore%.ts
|
||||
@@ -12,6 +12,7 @@
|
||||
<file>schema/schema-18.sql</file>
|
||||
<file>schema/schema-19.sql</file>
|
||||
<file>schema/schema-20.sql</file>
|
||||
<file>schema/schema-21.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
@@ -45,5 +46,6 @@
|
||||
<file>mood/sample.mood</file>
|
||||
<file>text/ghosts.txt</file>
|
||||
<file>pictures/sidebar-background.png</file>
|
||||
<file>style/dynamicplaylistcontrols.css</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
|
||||
CREATE TABLE device_%deviceid_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -94,4 +104,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
|
||||
|
||||
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
||||
|
||||
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;
|
||||
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
|
||||
|
||||
43
data/schema/schema-21.sql
Normal file
43
data/schema/schema-21.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
DROP INDEX IF EXISTS idx_albumartistsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_albumsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_artistsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_composersort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_performersort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_titlesort;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||
|
||||
UPDATE schema_version SET version=21;
|
||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
|
||||
DELETE FROM schema_version;
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (20);
|
||||
INSERT INTO schema_version (version) VALUES (21);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
path TEXT NOT NULL,
|
||||
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
|
||||
CREATE TABLE IF NOT EXISTS songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -192,7 +216,9 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -273,7 +307,9 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -435,7 +489,9 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -516,7 +580,9 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -678,7 +762,9 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -759,7 +853,9 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -931,9 +1041,13 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
playlist_url TEXT,
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER,
|
||||
disc INTEGER,
|
||||
year INTEGER,
|
||||
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
genre TEXT,
|
||||
compilation INTEGER DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -1032,10 +1152,22 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||
|
||||
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
||||
|
||||
13
data/style/dynamicplaylistcontrols.css
Normal file
13
data/style/dynamicplaylistcontrols.css
Normal file
@@ -0,0 +1,13 @@
|
||||
#container {
|
||||
background: %background;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(200, 200, 200, 75%);
|
||||
}
|
||||
|
||||
#label1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#label2 {
|
||||
font-size: 7.5pt;
|
||||
}
|
||||
12
debian/CMakeLists.txt
vendored
12
debian/CMakeLists.txt
vendored
@@ -4,19 +4,7 @@ if(LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
||||
execute_process(COMMAND /bin/sh "-c" "${LSB_RELEASE_EXEC} -cs" OUTPUT_VARIABLE DEB_CODENAME OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
if(DEB_CODENAME AND DEB_DATE)
|
||||
|
||||
if(QT_VERSION_MAJOR EQUAL 5)
|
||||
set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qtbase5-dev,qtbase5-dev-tools,qttools5-dev,qttools5-dev-tools,libqt5x11extras5-dev)
|
||||
set(DEBIAN_DEPENDS_QT_PACKAGES libqt5sql5-sqlite)
|
||||
endif()
|
||||
if(QT_VERSION_MAJOR EQUAL 6)
|
||||
set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qt6-base-dev,qt6-base-dev-tools,qt6-tools-dev,qt6-tools-dev-tools,qt6-l10n-tools)
|
||||
set(DEBIAN_DEPENDS_QT_PACKAGES libqt6sql6-sqlite,qt6-qpa-plugins)
|
||||
endif()
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/control.in ${CMAKE_CURRENT_SOURCE_DIR}/control @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/changelog.in ${CMAKE_CURRENT_SOURCE_DIR}/changelog)
|
||||
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
3
debian/clean
vendored
Normal file
3
debian/clean
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/scripts/maketarball.sh
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
1
debian/compat
vendored
1
debian/compat
vendored
@@ -1 +0,0 @@
|
||||
11
|
||||
27
debian/control.in → debian/control
vendored
27
debian/control.in → debian/control
vendored
@@ -2,23 +2,27 @@ Source: strawberry
|
||||
Section: sound
|
||||
Priority: optional
|
||||
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
|
||||
Build-Depends: debhelper (>= 11),
|
||||
Build-Depends: debhelper-compat (= 12),
|
||||
git,
|
||||
make,
|
||||
cmake,
|
||||
gcc,
|
||||
g++,
|
||||
protobuf-compiler,
|
||||
libglib2.0-dev,
|
||||
libdbus-1-dev,
|
||||
libprotobuf-dev,
|
||||
libboost-dev,
|
||||
libsqlite3-dev,
|
||||
libasound2-dev,
|
||||
libpulse-dev,
|
||||
libtag1-dev,
|
||||
libicu-dev,
|
||||
@DEBIAN_BUILD_DEPENDS_QT_PACKAGES@,
|
||||
libxkbcommon-dev,
|
||||
qt6-base-dev,
|
||||
qt6-base-private-dev,
|
||||
qt6-base-dev-tools,
|
||||
qt6-tools-dev,
|
||||
qt6-tools-dev-tools,
|
||||
qt6-l10n-tools,
|
||||
libkdsingleapplication-qt6-dev,
|
||||
libgstreamer1.0-dev,
|
||||
libgstreamer-plugins-base1.0-dev,
|
||||
libcdio-dev,
|
||||
@@ -26,14 +30,17 @@ Build-Depends: debhelper (>= 11),
|
||||
libmtp-dev,
|
||||
libchromaprint-dev,
|
||||
libfftw3-dev,
|
||||
libebur128-dev
|
||||
Standards-Version: 4.6.1
|
||||
libebur128-dev,
|
||||
libsparsehash-dev,
|
||||
rapidjson-dev
|
||||
Standards-Version: 4.7.0
|
||||
|
||||
Package: strawberry
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends},
|
||||
${misc:Depends},
|
||||
@DEBIAN_DEPENDS_QT_PACKAGES@,
|
||||
libqt6sql6-sqlite,
|
||||
qt6-qpa-plugins,
|
||||
gstreamer1.0-plugins-base,
|
||||
gstreamer1.0-plugins-good,
|
||||
gstreamer1.0-alsa,
|
||||
@@ -53,11 +60,11 @@ Description: music player and music collection organizer
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Lyrics from multiple sources
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
.
|
||||
61
debian/copyright
vendored
61
debian/copyright
vendored
@@ -5,14 +5,12 @@ Source: https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2015, David Sansome <me@davidsansome.com>
|
||||
2012-2014, 2017-2023 Jonas Kvinge <jonas@jkvinge.net>
|
||||
2012-2014, 2017-2024 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/utilities/timeconstants.h
|
||||
ext/libstrawberry-common/core/logging.cpp
|
||||
ext/libstrawberry-common/core/logging.h
|
||||
ext/libstrawberry-common/core/messagehandler.cpp
|
||||
ext/libstrawberry-common/core/messagehandler.h
|
||||
src/core/logging.cpp
|
||||
src/core/logging.h
|
||||
Copyright: 2011, 2012, David Sansome <me@davidsansome.com>
|
||||
2018-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: Apache-2.0
|
||||
@@ -31,18 +29,20 @@ Files: src/core/main.h
|
||||
src/engine/alsapcmdevicefinder.h
|
||||
src/engine/mmdevicefinder.cpp
|
||||
src/engine/mmdevicefinder.h
|
||||
src/engine/uwpdevicefinder.cpp
|
||||
src/engine/uwpdevicefinder.h
|
||||
src/engine/devicefinder.cpp
|
||||
src/engine/devicefinder.h
|
||||
src/engine/enginedevice.cpp
|
||||
src/engine/enginedevice.h
|
||||
src/engine/enginemetadata.cpp
|
||||
src/engine/enginemetadata.h
|
||||
src/internet/internetservice.cpp
|
||||
src/internet/internetservice.h
|
||||
src/internet/internettabsview.cpp
|
||||
src/internet/internettabsview.h
|
||||
src/internet/internetsongsview.cpp
|
||||
src/internet/internetsongsview.h
|
||||
src/streaming/streamingservice.cpp
|
||||
src/streaming/streamingservice.h
|
||||
src/streaming/streamingtabsview.cpp
|
||||
src/streaming/streamingtabsview.h
|
||||
src/streaming/streamingsongsview.cpp
|
||||
src/streaming/streamingsongsview.h
|
||||
src/settings/backendsettingspage.cpp
|
||||
src/settings/backendsettingspage.h
|
||||
src/settings/coverssettingspage.cpp
|
||||
@@ -55,6 +55,8 @@ Files: src/core/main.h
|
||||
src/settings/subsonicsettingspage.h
|
||||
src/settings/tidalsettingspage.cpp
|
||||
src/settings/tidalsettingspage.h
|
||||
src/settings/spotifysettingspage.cpp
|
||||
src/settings/spotifysettingspage.h
|
||||
src/covermanager/jsoncoverprovider.cpp
|
||||
src/covermanager/jsoncoverprovider.h
|
||||
src/covermanager/lastfmcoverprovider.cpp
|
||||
@@ -65,16 +67,16 @@ Files: src/core/main.h
|
||||
src/covermanager/deezercoverprovider.h
|
||||
src/covermanager/tidalcoverprovider.cpp
|
||||
src/covermanager/tidalcoverprovider.h
|
||||
src/covermanager/opentidalcoverprovider.cpp
|
||||
src/covermanager/opentidalcoverprovider.h
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
src/covermanager/musixmatchcoverprovider.cpp
|
||||
src/covermanager/musixmatchcoverprovider.h
|
||||
src/globalshortcuts/globalshortcutsbackend-kde.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-kde.h
|
||||
src/globalshortcuts/globalshortcutsbackend-mate.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-mate.h
|
||||
src/globalshortcuts/globalshortcutsbackend-kglobalaccel.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-kglobalaccel.h
|
||||
src/globalshortcuts/globalshortcutsbackend-x11.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-x11.h
|
||||
src/globalshortcuts/globalshortcutsbackend-win.cpp
|
||||
@@ -91,14 +93,14 @@ Files: src/core/main.h
|
||||
src/tidal/*
|
||||
src/qobuz/*
|
||||
src/radios/*
|
||||
src/spotify/*
|
||||
src/transcoder/transcoderoptionswavpack.cpp
|
||||
src/transcoder/transcoderoptionswavpack.h
|
||||
ext/libstrawberry-tagreader/tagreadertagparser.cpp
|
||||
ext/libstrawberry-tagreader/tagreadertagparser.h
|
||||
ext/macdeploycheck/*
|
||||
src/widgets/resizabletextedit.cpp
|
||||
src/widgets/resizabletextedit.h
|
||||
Copyright: 2012-2014, 2017-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
src/widgets/fancytabdata.cpp
|
||||
src/widgets/fancytabdata.h
|
||||
Copyright: 2012-2014, 2017-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/engine/enginebase.cpp
|
||||
@@ -119,6 +121,8 @@ License: GPL-2+
|
||||
|
||||
Files: src/widgets/fancytabwidget.cpp
|
||||
src/widgets/fancytabwidget.h
|
||||
src/widgets/fancytabbar.cpp
|
||||
src/widgets/fancytabbar.h
|
||||
Copyright: 2018, Vikram Ambrose <ambroseworks@gmail.com>
|
||||
2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
@@ -208,8 +212,8 @@ Files: src/device/udisks2lister.cpp
|
||||
Copyright: 2016, Valeriy Malov <jazzvoid@gmail.com>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/internet/localredirectserver.cpp
|
||||
src/internet/localredirectserver.h
|
||||
Files: src/core/localredirectserver.cpp
|
||||
src/core/localredirectserver.h
|
||||
Copyright: 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
@@ -248,25 +252,24 @@ Files: src/core/stylehelper.cpp
|
||||
Copyright: 2016 The Qt Company Ltd.
|
||||
License: GPL-3+
|
||||
|
||||
Files: ext/gstmoodbar/gstfastspectrum.cpp
|
||||
ext/gstmoodbar/gstfastspectrum.h
|
||||
Files: 3rdparty/gstfastspectrum/gstfastspectrum.cpp
|
||||
3rdparty/gstfastspectrum/gstfastspectrum.h
|
||||
Copyright: 1999 Erik Walthinsen <omega@cse.ogi.edu>
|
||||
2006,2011 Stefan Kost <ensonic@users.sf.net>
|
||||
2007-2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
2018-2024 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-2+
|
||||
|
||||
Files: src/widgets/qsearchfield_nonmac.cpp
|
||||
Files: src/widgets/qsearchfield_qt.cpp
|
||||
src/widgets/qsearchfield_mac.mm
|
||||
src/widgets/qsearchfield.h
|
||||
src/widgets/qocoa_mac.h
|
||||
src/widgets/searchfield_qt_private.cpp
|
||||
src/widgets/searchfield_qt_private.h
|
||||
Copyright: 2011, Mike McQuaid <mike@mikemcquaid.com>
|
||||
2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: Expat
|
||||
|
||||
Files: 3rdparty/SPMediaKeyTap/*
|
||||
Copyright: 2010, Spotify AB
|
||||
2011, Joachim Bengtsson
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: 3rdparty/kdsingleapplication/*
|
||||
Copyright: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||
License: MIT
|
||||
|
||||
17
debian/rules
vendored
17
debian/rules
vendored
@@ -1,17 +1,14 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --buildsystem=cmake -builddirectory=build
|
||||
export DH_VERBOSE=1
|
||||
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
|
||||
|
||||
override_dh_auto_clean:
|
||||
rm -f dist/macos/Info.plist
|
||||
rm -f dist/unix/strawberry.spec
|
||||
rm -f dist/scripts/maketarball.sh
|
||||
rm -f dist/windows/strawberry.nsi
|
||||
rm -f src/translations/translations.pot
|
||||
dh_auto_clean
|
||||
override_dh_auto_configure:
|
||||
dh_auto_configure -- \
|
||||
-DBUILD_WERROR=ON
|
||||
|
||||
override_dh_installchangelogs:
|
||||
dh_installchangelogs Changelog
|
||||
|
||||
override_dh_auto_test:
|
||||
%:
|
||||
dh $@
|
||||
|
||||
19
dist/CMakeLists.txt
vendored
19
dist/CMakeLists.txt
vendored
@@ -1,7 +1,7 @@
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh.in ${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh @ONLY)
|
||||
if(RPM_DISTRO AND RPM_DATE)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec @ONLY)
|
||||
endif(RPM_DISTRO AND RPM_DATE)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_BINARY_DIR}/strawberry.spec @ONLY)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
|
||||
@@ -9,12 +9,13 @@ if(APPLE)
|
||||
else()
|
||||
set(LSMinimumSystemVersion 12.0)
|
||||
endif()
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist)
|
||||
endif(APPLE)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi @ONLY)
|
||||
endif(WIN32)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/windres.rc.in ${CMAKE_BINARY_DIR}/windres.rc)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_BINARY_DIR}/strawberry.nsi @ONLY)
|
||||
endif()
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
install(FILES ../data/icons/48x48/strawberry.png DESTINATION share/icons/hicolor/48x48/apps/)
|
||||
@@ -22,10 +23,10 @@ if(UNIX AND NOT APPLE)
|
||||
install(FILES ../data/icons/128x128/strawberry.png DESTINATION share/icons/hicolor/128x128/apps/)
|
||||
install(FILES unix/org.strawberrymusicplayer.strawberry.desktop DESTINATION share/applications)
|
||||
install(FILES unix/org.strawberrymusicplayer.strawberry.appdata.xml DESTINATION share/metainfo)
|
||||
install(FILES unix/strawberry.1 unix/strawberry-tagreader.1 DESTINATION share/man/man1)
|
||||
endif(UNIX AND NOT APPLE)
|
||||
install(FILES unix/strawberry.1 DESTINATION share/man/man1)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/strawberry.icns" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources")
|
||||
endif()
|
||||
|
||||
4
dist/macos/Info.plist.in
vendored
4
dist/macos/Info.plist.in
vendored
@@ -35,9 +35,9 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>@LSMinimumSystemVersion@</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos</string>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>3IRScV8YtNVnx7zoeJAXvg28Kh1gN/Pyl2iPM467pG8=</string>
|
||||
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
1
dist/macos/macgstcopy.sh
vendored
1
dist/macos/macgstcopy.sh
vendored
@@ -111,6 +111,7 @@ libgstrtsp
|
||||
libgstsoup
|
||||
libgstspectrum
|
||||
libgstspeex
|
||||
libgstspotify
|
||||
libgsttaglib
|
||||
libgsttcp
|
||||
libgsttwolame
|
||||
|
||||
22
dist/macos/macversion.sh
vendored
22
dist/macos/macversion.sh
vendored
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
macos_version=$(sw_vers -productVersion)
|
||||
macos_version_major=$(echo $macos_version | awk -F '[.]' '{print $1}')
|
||||
macos_version_minor=$(echo $macos_version | awk -F '[.]' '{print $2}')
|
||||
|
||||
if [ "${macos_version_major}" = "10" ]; then
|
||||
macos_codenames=(
|
||||
["13"]="highsierra"
|
||||
["14"]="mojave"
|
||||
["15"]="catalina"
|
||||
)
|
||||
if [[ -n "${macos_codenames[$macos_version_minor]}" ]]; then
|
||||
echo "${macos_codenames[$macos_version_minor]}"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
elif [ "${macos_version_major}" = "11" ]; then
|
||||
echo "bigsur"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
29
dist/scripts/import-from-clementine.sh
vendored
29
dist/scripts/import-from-clementine.sh
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
# Strawberry Music Player
|
||||
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
# 2021 Alexey Vazhnov
|
||||
# Copyright 2021, Alexey Vazhnov
|
||||
#
|
||||
# Strawberry is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
# Based on https://github.com/strawberrymusicplayer/strawberry/wiki/Import-collection-library-and-playlists-data-from-Clementine
|
||||
# Based on https://wiki.strawberrymusicplayer.org/wiki/Import_collection_library_and_playlists_from_Clementine
|
||||
|
||||
set -o nounset
|
||||
set -o errexit
|
||||
@@ -35,8 +35,8 @@ test -f "$FILE_DST" || { echo "No such file: $FILE_DST"; exit 1; }
|
||||
|
||||
echo "Will try to copy information from $FILE_SRC to $FILE_DST."
|
||||
echo
|
||||
echo 'This script will **delete all information** from Strawberry database!'
|
||||
read -r -p 'Do you want to continue? (the only YES is accepted) ' answer
|
||||
echo 'This script will **delete all data** from the Strawberry database!'
|
||||
read -r -p 'Do you want to continue? (Only YES is accepted) ' answer
|
||||
if [ "$answer" != "YES" ]; then exit 1; fi
|
||||
|
||||
# 'heredoc' with substitution of variables, see `man bash`, "Here Documents":
|
||||
@@ -62,9 +62,9 @@ INSERT INTO strawberry.subdirectories (directory_id, path, mtime) SELECT directo
|
||||
INSERT INTO strawberry.songs (ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory_id, url, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, compilation_detected, compilation_on, compilation_off, compilation_effective, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating)
|
||||
SELECT ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory, filename, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, sampler, forced_compilation_on, forced_compilation_off, effective_compilation, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating FROM clementine.songs WHERE unavailable = 0;
|
||||
UPDATE strawberry.songs SET source = 2;
|
||||
UPDATE strawberry.songs SET artist_id = "";
|
||||
UPDATE strawberry.songs SET album_id = "";
|
||||
UPDATE strawberry.songs SET song_id = "";
|
||||
UPDATE strawberry.songs SET artist_id = '';
|
||||
UPDATE strawberry.songs SET album_id = '';
|
||||
UPDATE strawberry.songs SET song_id = '';
|
||||
|
||||
/* Import playlists */
|
||||
|
||||
@@ -140,7 +140,7 @@ SELECT ROWID,
|
||||
bitrate,
|
||||
samplerate,
|
||||
directory,
|
||||
filename,
|
||||
CASE WHEN filename IS NULL THEN '' ELSE filename END,
|
||||
filetype,
|
||||
filesize,
|
||||
mtime,
|
||||
@@ -162,16 +162,9 @@ SELECT ROWID,
|
||||
|
||||
UPDATE strawberry.playlist_items SET source = 2;
|
||||
UPDATE strawberry.playlist_items SET type = 2;
|
||||
UPDATE strawberry.playlist_items SET artist_id = "";
|
||||
UPDATE strawberry.playlist_items SET album_id = "";
|
||||
UPDATE strawberry.playlist_items SET song_id = "";
|
||||
|
||||
/* Recreate the FTS tables */
|
||||
|
||||
DELETE FROM strawberry.songs_fts;
|
||||
INSERT INTO strawberry.songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
|
||||
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment
|
||||
FROM strawberry.songs;
|
||||
UPDATE strawberry.playlist_items SET artist_id = '';
|
||||
UPDATE strawberry.playlist_items SET album_id = '';
|
||||
UPDATE strawberry.playlist_items SET song_id = '';
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<provides>
|
||||
<binary>strawberry</binary>
|
||||
<binary>strawberry-tagreader</binary>
|
||||
</provides>
|
||||
<name>Strawberry Music Player</name>
|
||||
<summary>A music player and collection organizer</summary>
|
||||
<url type="homepage">https://www.strawberrymusicplayer.org/</url>
|
||||
<url type="bugtracker">https://github.com/strawberrymusicplayer/strawberry/</url>
|
||||
<developer id="net.jkvinge.jonas">
|
||||
<name>Jonas Kvinge</name>
|
||||
</developer>
|
||||
<translation type="qt">strawberry</translation>
|
||||
<content_rating type="oars-1.1" />
|
||||
<description>
|
||||
<p>
|
||||
Strawberry is a music player and music collection organizer. It is aimed at music collectors and audiophiles. With Strawberry you can play and manage your digital music collection, or stream your favorite radios. It also has unofficial streaming support for Tidal and Qobuz. Strawberry is free software released under GPL. The source code is available on GitHub. It's written in C++ using the Qt toolkit and GStreamer. Strawberry is compatible with both Qt version 5 and 6.
|
||||
Strawberry is a music player and music collection organizer. It is aimed at music collectors and audiophiles. Strawberry is free software released under GPL. It's written in C++ using the Qt framework and GStreamer.
|
||||
</p>
|
||||
<p>Features:</p>
|
||||
<ul>
|
||||
@@ -29,13 +31,12 @@
|
||||
<li>Edit tags on audio files</li>
|
||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
||||
<li>Support for multiple backends</li>
|
||||
<li>Lyrics from multiple sources</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
||||
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||
<li>Streaming support for Subsonic-compatible servers</li>
|
||||
<li>Unofficial streaming support for Tidal and Qobuz</li>
|
||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||
</ul>
|
||||
</description>
|
||||
<screenshots>
|
||||
@@ -50,6 +51,24 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.16" date="2025-12-16"/>
|
||||
<release version="1.2.15" date="2025-11-25"/>
|
||||
<release version="1.2.14" date="2025-10-25"/>
|
||||
<release version="1.2.13" date="2025-08-31"/>
|
||||
<release version="1.2.12" date="2025-08-12"/>
|
||||
<release version="1.2.11" date="2025-05-15"/>
|
||||
<release version="1.2.10" date="2025-04-18"/>
|
||||
<release version="1.2.9" date="2025-04-08"/>
|
||||
<release version="1.2.8" date="2025-04-05"/>
|
||||
<release version="1.2.7" date="2025-01-31"/>
|
||||
<release version="1.2.6" date="2025-01-17"/>
|
||||
<release version="1.2.5" date="2025-01-17"/>
|
||||
<release version="1.2.4" date="2025-01-10"/>
|
||||
<release version="1.2.3" date="2024-12-08"/>
|
||||
<release version="1.2.2" date="2024-11-23"/>
|
||||
<release version="1.2.1" date="2024-11-21"/>
|
||||
<release version="1.1.3" date="2024-09-21"/>
|
||||
<release version="1.1.2" date="2024-09-12"/>
|
||||
<release version="1.1.1" date="2024-07-22"/>
|
||||
<release version="1.1.0" date="2024-07-14"/>
|
||||
<release version="1.0.23" date="2024-01-11"/>
|
||||
|
||||
@@ -13,8 +13,7 @@ TryExec=strawberry
|
||||
Icon=strawberry
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Player;Qt;Audio;
|
||||
Keywords=Audio;Player;
|
||||
StartupNotify=false
|
||||
Keywords=Audio;Player;Clementine;
|
||||
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
||||
StartupWMClass=strawberry
|
||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||
|
||||
10
dist/unix/strawberry-tagreader.1
vendored
10
dist/unix/strawberry-tagreader.1
vendored
@@ -1,10 +0,0 @@
|
||||
.TH STRAWBERRY-TAGREADER "1"
|
||||
.SH NAME
|
||||
strawberry-tagreader \- internal tag reader for strawberry
|
||||
.SH SYNOPSIS
|
||||
.B strawberry-tagreader
|
||||
.SH DESCRIPTION
|
||||
This program is used internally by Strawberry to parse tags in music files without exposing the whole application to crashes caused by malformed files. It is not meant to be run on its own.
|
||||
.SH "AUTHORS"
|
||||
.PP
|
||||
Strawberry main developer is Jonas Kvinge <jonas@jkvinge.net>.
|
||||
6
dist/unix/strawberry.1
vendored
6
dist/unix/strawberry.1
vendored
@@ -29,9 +29,7 @@ Features:
|
||||
.br
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
.br
|
||||
- Support for multiple backends
|
||||
- Lyrics from multiple sources
|
||||
.br
|
||||
- Audio analyzer
|
||||
.br
|
||||
@@ -39,7 +37,7 @@ Features:
|
||||
.br
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
.br
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
.br
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
.br
|
||||
|
||||
45
dist/unix/strawberry.spec.in
vendored
45
dist/unix/strawberry.spec.in
vendored
@@ -21,7 +21,6 @@ BuildRequires: gcc-c++
|
||||
BuildRequires: hicolor-icon-theme
|
||||
BuildRequires: make
|
||||
BuildRequires: git
|
||||
BuildRequires: gettext
|
||||
BuildRequires: desktop-file-utils
|
||||
%if 0%{?suse_version}
|
||||
BuildRequires: update-desktop-files
|
||||
@@ -38,9 +37,7 @@ BuildRequires: pkgconfig(glib-2.0)
|
||||
BuildRequires: pkgconfig(gio-2.0)
|
||||
BuildRequires: pkgconfig(gio-unix-2.0)
|
||||
BuildRequires: pkgconfig(gthread-2.0)
|
||||
BuildRequires: pkgconfig(dbus-1)
|
||||
BuildRequires: pkgconfig(alsa)
|
||||
BuildRequires: pkgconfig(protobuf)
|
||||
BuildRequires: pkgconfig(sqlite3) >= 3.9
|
||||
BuildRequires: pkgconfig(taglib)
|
||||
BuildRequires: pkgconfig(fftw3)
|
||||
@@ -55,9 +52,6 @@ BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Gui)
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Widgets)
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Test)
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@LinguistTools)
|
||||
%if "@QT_VERSION_MAJOR@" == "5"
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@X11Extras)
|
||||
%endif
|
||||
BuildRequires: pkgconfig(gstreamer-1.0)
|
||||
BuildRequires: pkgconfig(gstreamer-app-1.0)
|
||||
BuildRequires: pkgconfig(gstreamer-audio-1.0)
|
||||
@@ -69,18 +63,18 @@ BuildRequires: pkgconfig(libcdio)
|
||||
BuildRequires: pkgconfig(libebur128)
|
||||
BuildRequires: pkgconfig(libgpod-1.0)
|
||||
BuildRequires: pkgconfig(libmtp)
|
||||
%if 0%{?suse_version} || 0%{?fedora_version}
|
||||
BuildRequires: pkgconfig(libvlc)
|
||||
BuildRequires: pkgconfig(libsparsehash)
|
||||
BuildRequires: cmake(GTest)
|
||||
BuildRequires: pkgconfig(gmock)
|
||||
BuildRequires: cmake(RapidJSON)
|
||||
|
||||
%if 0%{?fedora} || (0%{?suse_version} && 0%{?suse_version} > 1600) || "%{?_vendor}" == "openmandriva"
|
||||
BuildRequires: cmake(KDSingleApplication-qt6)
|
||||
%endif
|
||||
|
||||
%if 0%{?suse_version}
|
||||
%if "@QT_VERSION_MAJOR@" == "6"
|
||||
Requires: qt6-sql-sqlite
|
||||
Requires: qt6-network-tls
|
||||
%endif
|
||||
%if "@QT_VERSION_MAJOR@" == "5"
|
||||
Requires: libQt5Sql5-sqlite
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%description
|
||||
@@ -99,16 +93,15 @@ Features:
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Support for multiple backends
|
||||
- Lyrics from multiple sources
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
|
||||
%if 0%{?suse_version}
|
||||
%if 0%{?suse_version} && 0%{?suse_version} < 1600
|
||||
%debug_package
|
||||
%endif
|
||||
|
||||
@@ -117,13 +110,13 @@ Features:
|
||||
|
||||
%build
|
||||
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
|
||||
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
|
||||
export CXXFLAGS="-fPIC -Wno-maybe-uninitialized $RPM_OPT_FLAGS"
|
||||
%endif
|
||||
%if "%{?_vendor}" == "openmandriva"
|
||||
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@ -DENABLE_TRANSLATIONS=OFF
|
||||
%{cmake} -DBUILD_WERROR=ON
|
||||
%make_build
|
||||
%else
|
||||
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@
|
||||
%{cmake} -DBUILD_WERROR=ON
|
||||
%cmake_build
|
||||
%endif
|
||||
|
||||
@@ -134,11 +127,13 @@ Features:
|
||||
%cmake_install
|
||||
%endif
|
||||
|
||||
%if 0%{?suse_version}
|
||||
%suse_update_desktop_file org.strawberrymusicplayer.strawberry Qt AudioVideo Audio Player
|
||||
%endif
|
||||
|
||||
%check
|
||||
export QT_QPA_PLATFORM="offscreen"
|
||||
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
|
||||
%{cmake_build} -t strawberry_tests
|
||||
%else
|
||||
%{make_build} -j $(nproc) -C build strawberry_tests
|
||||
%endif
|
||||
desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicplayer.strawberry.desktop
|
||||
%if 0%{?suse_version}
|
||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml
|
||||
@@ -151,11 +146,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicpl
|
||||
%doc README.md Changelog
|
||||
%license COPYING
|
||||
%{_bindir}/strawberry
|
||||
%{_bindir}/strawberry-tagreader
|
||||
%{_datadir}/applications/*.desktop
|
||||
%{_datadir}/icons/hicolor/*/apps/strawberry.*
|
||||
%{_mandir}/man1/%{name}.1.*
|
||||
%{_mandir}/man1/%{name}-tagreader.1.*
|
||||
%if 0%{?suse_version}
|
||||
%{_datadir}/metainfo/*.appdata.xml
|
||||
%else
|
||||
|
||||
277
dist/windows/strawberry.nsi.in
vendored
277
dist/windows/strawberry.nsi.in
vendored
@@ -21,6 +21,10 @@
|
||||
!define arch_x64
|
||||
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
||||
!define arch_x64
|
||||
!else if "@ARCH@" == "arm64"
|
||||
!define arch_arm64
|
||||
!else
|
||||
!error "Missing ARCH"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -31,6 +35,10 @@
|
||||
!define arch "x64"
|
||||
!endif
|
||||
|
||||
!ifdef arch_arm64
|
||||
!define arch "arm64"
|
||||
!endif
|
||||
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||
!define release
|
||||
@@ -38,6 +46,8 @@
|
||||
!define release
|
||||
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||
!define debug
|
||||
!else
|
||||
!error "Missing CMAKE_BUILD_TYPE"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
@@ -70,7 +80,7 @@
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!ifdef arch_x64 || arch_arm64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!else
|
||||
@@ -80,7 +90,7 @@
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!ifdef arch_x64 || arch_arm64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
||||
!endif
|
||||
!endif
|
||||
@@ -208,14 +218,16 @@ FunctionEnd
|
||||
!ifdef msvc
|
||||
!define vc_redist_file "vc_redist.${arch}.exe"
|
||||
Function InstallMSVCRuntime
|
||||
${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
|
||||
${If} $R0 == ""
|
||||
SetOutPath "$TEMP"
|
||||
File "${vc_redist_file}"
|
||||
; ${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
|
||||
; ${If} $R0 == ""
|
||||
SetDetailsView hide
|
||||
inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
|
||||
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive /norestart'
|
||||
Delete "$TEMP\${vc_redist_file}"
|
||||
SetDetailsView show
|
||||
${EndIf}
|
||||
; ${EndIf}
|
||||
FunctionEnd
|
||||
!endif
|
||||
|
||||
@@ -236,7 +248,6 @@ Section "Strawberry" Strawberry
|
||||
; Common executables
|
||||
|
||||
File "strawberry.exe"
|
||||
File "strawberry-tagreader.exe"
|
||||
File "strawberry.ico"
|
||||
File "sqlite3.exe"
|
||||
File "gst-launch-1.0.exe"
|
||||
@@ -258,9 +269,10 @@ Section "Strawberry" Strawberry
|
||||
File "libssl-3-x64.dll"
|
||||
!endif
|
||||
|
||||
File "libFLAC-12.dll"
|
||||
File "libFLAC-14.dll"
|
||||
File "libbrotlicommon.dll"
|
||||
File "libbrotlidec.dll"
|
||||
File "libbrotlienc.dll"
|
||||
File "libbs2b-0.dll"
|
||||
File "libbz2.dll"
|
||||
File "libchromaprint.dll"
|
||||
@@ -272,6 +284,7 @@ Section "Strawberry" Strawberry
|
||||
File "libffi-8.dll"
|
||||
File "libfreetype-6.dll"
|
||||
File "libgcrypt-20.dll"
|
||||
File "libgetopt.dll"
|
||||
File "libgio-2.0-0.dll"
|
||||
File "libglib-2.0-0.dll"
|
||||
File "libgme.dll"
|
||||
@@ -336,48 +349,6 @@ Section "Strawberry" Strawberry
|
||||
File "libzstd.dll"
|
||||
File "zlib1.dll"
|
||||
|
||||
File "libabsl_base.dll"
|
||||
File "libabsl_city.dll"
|
||||
File "libabsl_cord.dll"
|
||||
File "libabsl_cord_internal.dll"
|
||||
File "libabsl_cordz_handle.dll"
|
||||
File "libabsl_cordz_info.dll"
|
||||
File "libabsl_crc32c.dll"
|
||||
File "libabsl_crc_cord_state.dll"
|
||||
File "libabsl_crc_internal.dll"
|
||||
File "libabsl_die_if_null.dll"
|
||||
File "libabsl_examine_stack.dll"
|
||||
File "libabsl_hash.dll"
|
||||
File "libabsl_int128.dll"
|
||||
File "libabsl_kernel_timeout_internal.dll"
|
||||
File "libabsl_log_globals.dll"
|
||||
File "libabsl_log_internal_check_op.dll"
|
||||
File "libabsl_log_internal_conditions.dll"
|
||||
File "libabsl_log_internal_format.dll"
|
||||
File "libabsl_log_internal_globals.dll"
|
||||
File "libabsl_log_internal_log_sink_set.dll"
|
||||
File "libabsl_log_internal_message.dll"
|
||||
File "libabsl_log_internal_nullguard.dll"
|
||||
File "libabsl_log_internal_proto.dll"
|
||||
File "libabsl_log_sink.dll"
|
||||
File "libabsl_low_level_hash.dll"
|
||||
File "libabsl_malloc_internal.dll"
|
||||
File "libabsl_raw_hash_set.dll"
|
||||
File "libabsl_raw_logging_internal.dll"
|
||||
File "libabsl_spinlock_wait.dll"
|
||||
File "libabsl_stacktrace.dll"
|
||||
File "libabsl_status.dll"
|
||||
File "libabsl_statusor.dll"
|
||||
File "libabsl_strerror.dll"
|
||||
File "libabsl_str_format_internal.dll"
|
||||
File "libabsl_strings.dll"
|
||||
File "libabsl_strings_internal.dll"
|
||||
File "libabsl_symbolize.dll"
|
||||
File "libabsl_synchronization.dll"
|
||||
File "libabsl_throw_delegate.dll"
|
||||
File "libabsl_time.dll"
|
||||
File "libabsl_time_zone.dll"
|
||||
|
||||
!ifdef debug
|
||||
File "gdb.exe"
|
||||
File "libexpat-1.dll"
|
||||
@@ -387,7 +358,6 @@ Section "Strawberry" Strawberry
|
||||
File "libpcre2-16d.dll"
|
||||
File "libreadline8.dll"
|
||||
File "libtermcap.dll"
|
||||
File "libabsl_graphcycles_internal.dll"
|
||||
!else
|
||||
File "libpcre2-8.dll"
|
||||
File "libpcre2-16.dll"
|
||||
@@ -407,6 +377,10 @@ Section "Strawberry" Strawberry
|
||||
File "libcrypto-3-x64.dll"
|
||||
File "libssl-3-x64.dll"
|
||||
!endif
|
||||
!ifdef arch_arm64
|
||||
File "libcrypto-3-arm64.dll"
|
||||
File "libssl-3-arm64.dll"
|
||||
!endif
|
||||
|
||||
File "FLAC.dll"
|
||||
File "brotlicommon.dll"
|
||||
@@ -416,6 +390,7 @@ Section "Strawberry" Strawberry
|
||||
File "faad-2.dll"
|
||||
File "fdk-aac.dll"
|
||||
File "ffi-7.dll"
|
||||
File "getopt.dll"
|
||||
File "gio-2.0-0.dll"
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
@@ -440,14 +415,11 @@ Section "Strawberry" Strawberry
|
||||
File "gsttag-1.0-0.dll"
|
||||
File "gsturidownloader-1.0-0.dll"
|
||||
File "gstvideo-1.0-0.dll"
|
||||
File "gstwinrt-1.0-0.dll"
|
||||
File "harfbuzz.dll"
|
||||
File "intl-8.dll"
|
||||
File "jpeg62.dll"
|
||||
File "kdsingleapplication-qt6.dll"
|
||||
File "libbs2b.dll"
|
||||
File "libfaac_dll.dll"
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
File "mpcdec.dll"
|
||||
@@ -464,7 +436,11 @@ Section "Strawberry" Strawberry
|
||||
File "vorbis.dll"
|
||||
File "vorbisfile.dll"
|
||||
File "wavpackdll.dll"
|
||||
File "abseil_dll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
File "freetype.dll"
|
||||
@@ -473,8 +449,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeex.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
File "zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolame.dll"
|
||||
File "zlib.dll"
|
||||
!endif
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
@@ -483,8 +461,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeexd.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
File "zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolamed.dll"
|
||||
File "zlibd.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||
@@ -497,16 +477,15 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt75.dll"
|
||||
File "libfftw3-3.dll"
|
||||
!ifdef debug
|
||||
File "libprotobufd.dll"
|
||||
File "icudt78.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
File "fftw3.dll"
|
||||
!else
|
||||
File "libprotobuf.dll"
|
||||
File "libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin75d.dll"
|
||||
File "icuuc75d.dll"
|
||||
File "icuin78d.dll"
|
||||
File "icuuc78d.dll"
|
||||
File "libxml2d.dll"
|
||||
File "Qt6Concurrentd.dll"
|
||||
File "Qt6Cored.dll"
|
||||
@@ -515,8 +494,8 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Sqld.dll"
|
||||
File "Qt6Widgetsd.dll"
|
||||
!else
|
||||
File "icuin75.dll"
|
||||
File "icuuc75.dll"
|
||||
File "icuin78.dll"
|
||||
File "icuuc78.dll"
|
||||
File "libxml2.dll"
|
||||
File "Qt6Concurrent.dll"
|
||||
File "Qt6Core.dll"
|
||||
@@ -526,13 +505,22 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Widgets.dll"
|
||||
!endif
|
||||
|
||||
File "avcodec-60.dll"
|
||||
File "avfilter-9.dll"
|
||||
File "avformat-60.dll"
|
||||
File "avutil-58.dll"
|
||||
File "postproc-57.dll"
|
||||
File "swresample-4.dll"
|
||||
File "swscale-7.dll"
|
||||
!ifdef msvc && arch_x86
|
||||
File "avcodec-61.dll"
|
||||
File "avfilter-10.dll"
|
||||
File "avformat-61.dll"
|
||||
File "avutil-59.dll"
|
||||
File "postproc-58.dll"
|
||||
File "swresample-5.dll"
|
||||
File "swscale-8.dll"
|
||||
!else
|
||||
File "avcodec-62.dll"
|
||||
File "avfilter-11.dll"
|
||||
File "avformat-62.dll"
|
||||
File "avutil-60.dll"
|
||||
File "swresample-6.dll"
|
||||
File "swscale-9.dll"
|
||||
!endif
|
||||
|
||||
; Register Strawberry with Default Programs
|
||||
Var /GLOBAL AppIcon
|
||||
@@ -570,11 +558,13 @@ Section "GIO modules" gio-modules
|
||||
SetOutPath "$INSTDIR\gio-modules"
|
||||
!ifdef mingw
|
||||
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
|
||||
File "/oname=libgioopenssl.dll" "gio-modules\libgioopenssl.dll"
|
||||
!endif
|
||||
!ifdef msvc
|
||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||
!ifdef arch_arm64
|
||||
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
||||
!else
|
||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||
!endif
|
||||
!endif
|
||||
SectionEnd
|
||||
|
||||
@@ -640,6 +630,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgstapp.dll" "gstreamer-plugins\libgstapp.dll"
|
||||
File "/oname=libgstasf.dll" "gstreamer-plugins\libgstasf.dll"
|
||||
File "/oname=libgstasfmux.dll" "gstreamer-plugins\libgstasfmux.dll"
|
||||
File "/oname=libgstasio.dll" "gstreamer-plugins\libgstasio.dll"
|
||||
File "/oname=libgstaudioconvert.dll" "gstreamer-plugins\libgstaudioconvert.dll"
|
||||
File "/oname=libgstaudiofx.dll" "gstreamer-plugins\libgstaudiofx.dll"
|
||||
File "/oname=libgstaudioparsers.dll" "gstreamer-plugins\libgstaudioparsers.dll"
|
||||
@@ -690,6 +681,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
||||
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
File "/oname=libgstwasapi2.dll" "gstreamer-plugins\libgstwasapi2.dll"
|
||||
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
||||
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
||||
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
|
||||
@@ -717,7 +709,6 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
|
||||
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
|
||||
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.dll"
|
||||
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
|
||||
File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
|
||||
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
||||
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
|
||||
@@ -750,7 +741,6 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
|
||||
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
|
||||
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll"
|
||||
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
|
||||
File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
|
||||
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
|
||||
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
|
||||
@@ -762,6 +752,10 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
|
||||
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
|
||||
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
|
||||
!ifndef arch_arm64
|
||||
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
|
||||
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
||||
!endif
|
||||
@@ -805,7 +799,6 @@ Section "Uninstall"
|
||||
; Delete all the files
|
||||
|
||||
Delete "$INSTDIR\strawberry.exe"
|
||||
Delete "$INSTDIR\strawberry-tagreader.exe"
|
||||
Delete "$INSTDIR\strawberry.ico"
|
||||
Delete "$INSTDIR\sqlite3.exe"
|
||||
Delete "$INSTDIR\gst-launch-1.0.exe"
|
||||
@@ -828,9 +821,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\libFLAC-12.dll"
|
||||
Delete "$INSTDIR\libFLAC-14.dll"
|
||||
Delete "$INSTDIR\libbrotlicommon.dll"
|
||||
Delete "$INSTDIR\libbrotlidec.dll"
|
||||
Delete "$INSTDIR\libbrotlienc.dll"
|
||||
Delete "$INSTDIR\libbs2b-0.dll"
|
||||
Delete "$INSTDIR\libbz2.dll"
|
||||
Delete "$INSTDIR\libchromaprint.dll"
|
||||
@@ -842,6 +836,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libffi-8.dll"
|
||||
Delete "$INSTDIR\libfreetype-6.dll"
|
||||
Delete "$INSTDIR\libgcrypt-20.dll"
|
||||
Delete "$INSTDIR\libgetopt.dll"
|
||||
Delete "$INSTDIR\libgio-2.0-0.dll"
|
||||
Delete "$INSTDIR\libglib-2.0-0.dll"
|
||||
Delete "$INSTDIR\libgme.dll"
|
||||
@@ -906,48 +901,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libzstd.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
|
||||
Delete "$INSTDIR\libabsl_base.dll"
|
||||
Delete "$INSTDIR\libabsl_city.dll"
|
||||
Delete "$INSTDIR\libabsl_cord.dll"
|
||||
Delete "$INSTDIR\libabsl_cord_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_cordz_handle.dll"
|
||||
Delete "$INSTDIR\libabsl_cordz_info.dll"
|
||||
Delete "$INSTDIR\libabsl_crc32c.dll"
|
||||
Delete "$INSTDIR\libabsl_crc_cord_state.dll"
|
||||
Delete "$INSTDIR\libabsl_crc_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_die_if_null.dll"
|
||||
Delete "$INSTDIR\libabsl_examine_stack.dll"
|
||||
Delete "$INSTDIR\libabsl_hash.dll"
|
||||
Delete "$INSTDIR\libabsl_int128.dll"
|
||||
Delete "$INSTDIR\libabsl_kernel_timeout_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_log_globals.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_check_op.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_conditions.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_format.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_globals.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_log_sink_set.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_message.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_nullguard.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_proto.dll"
|
||||
Delete "$INSTDIR\libabsl_log_sink.dll"
|
||||
Delete "$INSTDIR\libabsl_low_level_hash.dll"
|
||||
Delete "$INSTDIR\libabsl_malloc_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_raw_hash_set.dll"
|
||||
Delete "$INSTDIR\libabsl_raw_logging_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_spinlock_wait.dll"
|
||||
Delete "$INSTDIR\libabsl_stacktrace.dll"
|
||||
Delete "$INSTDIR\libabsl_status.dll"
|
||||
Delete "$INSTDIR\libabsl_statusor.dll"
|
||||
Delete "$INSTDIR\libabsl_strerror.dll"
|
||||
Delete "$INSTDIR\libabsl_str_format_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_strings.dll"
|
||||
Delete "$INSTDIR\libabsl_strings_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_symbolize.dll"
|
||||
Delete "$INSTDIR\libabsl_synchronization.dll"
|
||||
Delete "$INSTDIR\libabsl_throw_delegate.dll"
|
||||
Delete "$INSTDIR\libabsl_time.dll"
|
||||
Delete "$INSTDIR\libabsl_time_zone.dll"
|
||||
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\gdb.exe"
|
||||
Delete "$INSTDIR\libexpat-1.dll"
|
||||
@@ -957,7 +910,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libpcre2-16d.dll"
|
||||
Delete "$INSTDIR\libreadline8.dll"
|
||||
Delete "$INSTDIR\libtermcap.dll"
|
||||
Delete "$INSTDIR\libabsl_graphcycles_internal.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libpcre2-8.dll"
|
||||
Delete "$INSTDIR\libpcre2-16.dll"
|
||||
@@ -977,6 +929,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libcrypto-3-x64.dll"
|
||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||
!endif
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\libcrypto-3-arm64.dll"
|
||||
Delete "$INSTDIR\libssl-3-arm64.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\FLAC.dll"
|
||||
Delete "$INSTDIR\brotlicommon.dll"
|
||||
@@ -986,6 +942,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\faad-2.dll"
|
||||
Delete "$INSTDIR\fdk-aac.dll"
|
||||
Delete "$INSTDIR\ffi-7.dll"
|
||||
Delete "$INSTDIR\getopt.dll"
|
||||
Delete "$INSTDIR\gio-2.0-0.dll"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
@@ -1010,14 +967,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
||||
Delete "$INSTDIR\harfbuzz.dll"
|
||||
Delete "$INSTDIR\intl-8.dll"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
Delete "$INSTDIR\mpcdec.dll"
|
||||
@@ -1034,7 +988,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\vorbis.dll"
|
||||
Delete "$INSTDIR\vorbisfile.dll"
|
||||
Delete "$INSTDIR\wavpackdll.dll"
|
||||
Delete "$INSTDIR\abseil_dll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
Delete "$INSTDIR\freetype.dll"
|
||||
@@ -1043,8 +1001,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolame.dll"
|
||||
Delete "$INSTDIR\zlib.dll"
|
||||
!endif
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
@@ -1053,8 +1013,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeexd.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolamed.dll"
|
||||
Delete "$INSTDIR\zlibd.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -1066,16 +1028,15 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt75.dll"
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\libprotobufd.dll"
|
||||
Delete "$INSTDIR\icudt78.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
Delete "$INSTDIR\fftw3.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libprotobuf.dll"
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin75d.dll"
|
||||
Delete "$INSTDIR\icuuc75d.dll"
|
||||
Delete "$INSTDIR\icuin78d.dll"
|
||||
Delete "$INSTDIR\icuuc78d.dll"
|
||||
Delete "$INSTDIR\libxml2d.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||
Delete "$INSTDIR\Qt6Cored.dll"
|
||||
@@ -1084,8 +1045,8 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\icuin75.dll"
|
||||
Delete "$INSTDIR\icuuc75.dll"
|
||||
Delete "$INSTDIR\icuin78.dll"
|
||||
Delete "$INSTDIR\icuuc78.dll"
|
||||
Delete "$INSTDIR\libxml2.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||
Delete "$INSTDIR\Qt6Core.dll"
|
||||
@@ -1095,21 +1056,32 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Widgets.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\avcodec-60.dll"
|
||||
Delete "$INSTDIR\avfilter-9.dll"
|
||||
Delete "$INSTDIR\avformat-60.dll"
|
||||
Delete "$INSTDIR\avutil-58.dll"
|
||||
Delete "$INSTDIR\postproc-57.dll"
|
||||
Delete "$INSTDIR\swresample-4.dll"
|
||||
Delete "$INSTDIR\swscale-7.dll"
|
||||
!ifdef msvc && arch_x86
|
||||
Delete "$INSTDIR\avcodec-61.dll"
|
||||
Delete "$INSTDIR\avfilter-10.dll"
|
||||
Delete "$INSTDIR\avformat-61.dll"
|
||||
Delete "$INSTDIR\avutil-59.dll"
|
||||
Delete "$INSTDIR\postproc-58.dll"
|
||||
Delete "$INSTDIR\swresample-5.dll"
|
||||
Delete "$INSTDIR\swscale-8.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\avcodec-62.dll"
|
||||
Delete "$INSTDIR\avfilter-11.dll"
|
||||
Delete "$INSTDIR\avformat-62.dll"
|
||||
Delete "$INSTDIR\avutil-60.dll"
|
||||
Delete "$INSTDIR\swresample-6.dll"
|
||||
Delete "$INSTDIR\swscale-9.dll"
|
||||
!endif
|
||||
|
||||
!ifdef mingw
|
||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
|
||||
!endif
|
||||
!ifdef msvc
|
||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef msvc && debug
|
||||
@@ -1142,6 +1114,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstapp.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstasf.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstasfmux.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstasio.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstaudioconvert.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstaudiofx.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstaudioparsers.dll"
|
||||
@@ -1192,6 +1165,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
||||
@@ -1221,7 +1195,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
||||
@@ -1254,7 +1227,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
|
||||
@@ -1266,9 +1238,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
||||
!ifdef arch_x64
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
||||
!endif
|
||||
|
||||
!endif ; msvc
|
||||
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
|
||||
2
dist/windows/windres.rc.in
vendored
2
dist/windows/windres.rc.in
vendored
@@ -1,4 +1,4 @@
|
||||
strawberry ICON "${CMAKE_CURRENT_SOURCE_DIR}/../dist/windows/strawberry.ico"
|
||||
strawberry ICON "${CMAKE_SOURCE_DIR}/dist/windows/strawberry.ico"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${STRAWBERRY_VERSION_MAJOR},${STRAWBERRY_VERSION_MINOR},${STRAWBERRY_VERSION_PATCH}
|
||||
PRODUCTVERSION ${STRAWBERRY_VERSION_MAJOR},${STRAWBERRY_VERSION_MINOR},${STRAWBERRY_VERSION_PATCH}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(SOURCES gstfastspectrum.cpp gstmoodbarplugin.cpp)
|
||||
|
||||
link_directories(
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${GOBJECT_LIBRARY_DIRS}
|
||||
${GSTREAMER_LIBRARY_DIRS}
|
||||
${GSTREAMER_BASE_LIBRARY_DIRS}
|
||||
${GSTREAMER_AUDIO_LIBRARY_DIRS}
|
||||
${FFTW3_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
add_library(gstmoodbar STATIC ${SOURCES})
|
||||
|
||||
target_include_directories(gstmoodbar SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${GOBJECT_INCLUDE_DIRS}
|
||||
${GSTREAMER_INCLUDE_DIRS}
|
||||
${GSTREAMER_BASE_INCLUDE_DIRS}
|
||||
${GSTREAMER_AUDIO_INCLUDE_DIRS}
|
||||
${FFTW3_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_include_directories(gstmoodbar PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(gstmoodbar PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
${GOBJECT_LIBRARIES}
|
||||
${GSTREAMER_LIBRARIES}
|
||||
${GSTREAMER_BASE_LIBRARIES}
|
||||
${GSTREAMER_AUDIO_LIBRARIES}
|
||||
${FFTW3_FFTW_LIBRARY}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
)
|
||||
@@ -1,520 +0,0 @@
|
||||
/* GStreamer
|
||||
* Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
|
||||
* <2006,2011> Stefan Kost <ensonic@users.sf.net>
|
||||
* <2007-2009> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/audio/gstaudiofilter.h>
|
||||
|
||||
#include <QMutex>
|
||||
|
||||
#include "gstfastspectrum.h"
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC(gst_fastspectrum_debug);
|
||||
|
||||
namespace {
|
||||
|
||||
// Spectrum properties
|
||||
constexpr auto DEFAULT_INTERVAL = (GST_SECOND / 10);
|
||||
constexpr auto DEFAULT_BANDS = 128;
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_INTERVAL,
|
||||
PROP_BANDS
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#define gst_fastspectrum_parent_class parent_class
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
||||
#endif
|
||||
G_DEFINE_TYPE(GstFastSpectrum, gst_fastspectrum, GST_TYPE_AUDIO_FILTER)
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
static void gst_fastspectrum_finalize(GObject *object);
|
||||
static void gst_fastspectrum_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
|
||||
static void gst_fastspectrum_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
|
||||
static gboolean gst_fastspectrum_start(GstBaseTransform *trans);
|
||||
static gboolean gst_fastspectrum_stop(GstBaseTransform *trans);
|
||||
static GstFlowReturn gst_fastspectrum_transform_ip(GstBaseTransform *trans, GstBuffer *buffer);
|
||||
static gboolean gst_fastspectrum_setup(GstAudioFilter *base, const GstAudioInfo *info);
|
||||
|
||||
static void gst_fastspectrum_class_init(GstFastSpectrumClass *klass) {
|
||||
|
||||
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
|
||||
GstElementClass *element_class = GST_ELEMENT_CLASS(klass);
|
||||
GstBaseTransformClass *trans_class = GST_BASE_TRANSFORM_CLASS(klass);
|
||||
GstAudioFilterClass *filter_class = GST_AUDIO_FILTER_CLASS(klass);
|
||||
GstCaps *caps = nullptr;
|
||||
|
||||
gobject_class->set_property = gst_fastspectrum_set_property;
|
||||
gobject_class->get_property = gst_fastspectrum_get_property;
|
||||
gobject_class->finalize = gst_fastspectrum_finalize;
|
||||
|
||||
trans_class->start = GST_DEBUG_FUNCPTR(gst_fastspectrum_start);
|
||||
trans_class->stop = GST_DEBUG_FUNCPTR(gst_fastspectrum_stop);
|
||||
trans_class->transform_ip = GST_DEBUG_FUNCPTR(gst_fastspectrum_transform_ip);
|
||||
trans_class->passthrough_on_same_caps = TRUE;
|
||||
|
||||
filter_class->setup = GST_DEBUG_FUNCPTR(gst_fastspectrum_setup);
|
||||
|
||||
g_object_class_install_property(gobject_class, PROP_INTERVAL, g_param_spec_uint64("interval", "Interval", "Interval of time between message posts (in nanoseconds)", 1, G_MAXUINT64, DEFAULT_INTERVAL, static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
g_object_class_install_property(gobject_class, PROP_BANDS, g_param_spec_uint("bands", "Bands", "Number of frequency bands", 0, G_MAXUINT, DEFAULT_BANDS, static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT(gst_fastspectrum_debug, "spectrum", 0, "audio spectrum analyser element");
|
||||
|
||||
gst_element_class_set_static_metadata(element_class, "Spectrum analyzer",
|
||||
"Filter/Analyzer/Audio",
|
||||
"Run an FFT on the audio signal, output spectrum data",
|
||||
"Erik Walthinsen <omega@cse.ogi.edu>, "
|
||||
"Stefan Kost <ensonic@users.sf.net>, "
|
||||
"Sebastian Dröge <sebastian.droege@collabora.co.uk>");
|
||||
|
||||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||||
caps = gst_caps_from_string(GST_AUDIO_CAPS_MAKE("{ S16LE, S24LE, S32LE, F32LE, F64LE }") ", layout = (string) interleaved, channels = 1");
|
||||
#else
|
||||
caps = gst_caps_from_string(GST_AUDIO_CAPS_MAKE("{ S16BE, S24BE, S32BE, F32BE, F64BE }") ", layout = (string) interleaved, channels = 1");
|
||||
#endif
|
||||
|
||||
gst_audio_filter_class_add_pad_templates(filter_class, caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
klass->fftw_lock = new QMutex;
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_init(GstFastSpectrum *spectrum) {
|
||||
|
||||
spectrum->interval = DEFAULT_INTERVAL;
|
||||
spectrum->bands = DEFAULT_BANDS;
|
||||
|
||||
spectrum->channel_data_initialized = false;
|
||||
|
||||
g_mutex_init(&spectrum->lock);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_alloc_channel_data(GstFastSpectrum *spectrum) {
|
||||
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
|
||||
spectrum->input_ring_buffer = new double[nfft];
|
||||
spectrum->fft_input = reinterpret_cast<double*>(fftw_malloc(sizeof(double) * nfft));
|
||||
spectrum->fft_output = reinterpret_cast<fftw_complex*>(fftw_malloc(sizeof(fftw_complex) * (nfft / 2 + 1)));
|
||||
|
||||
spectrum->spect_magnitude = new double[bands] {};
|
||||
|
||||
GstFastSpectrumClass *klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
spectrum->plan = fftw_plan_dft_r2c_1d(static_cast<int>(nfft), spectrum->fft_input, spectrum->fft_output, FFTW_ESTIMATE);
|
||||
}
|
||||
spectrum->channel_data_initialized = true;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_free_channel_data(GstFastSpectrum *spectrum) {
|
||||
|
||||
GstFastSpectrumClass *klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
if (spectrum->channel_data_initialized) {
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
fftw_destroy_plan(spectrum->plan);
|
||||
}
|
||||
fftw_free(spectrum->fft_input);
|
||||
fftw_free(spectrum->fft_output);
|
||||
delete[] spectrum->input_ring_buffer;
|
||||
delete[] spectrum->spect_magnitude;
|
||||
|
||||
spectrum->channel_data_initialized = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_flush(GstFastSpectrum *spectrum) {
|
||||
|
||||
spectrum->num_frames = 0;
|
||||
spectrum->num_fft = 0;
|
||||
|
||||
spectrum->accumulated_error = 0;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_reset_state(GstFastSpectrum *spectrum) {
|
||||
|
||||
GST_DEBUG_OBJECT(spectrum, "resetting state");
|
||||
|
||||
gst_fastspectrum_free_channel_data(spectrum);
|
||||
gst_fastspectrum_flush(spectrum);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_finalize(GObject *object) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(object);
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
g_mutex_clear(&spectrum->lock);
|
||||
|
||||
G_OBJECT_CLASS(parent_class)->finalize(object);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) {
|
||||
|
||||
GstFastSpectrum *filter = reinterpret_cast<GstFastSpectrum*>(object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_INTERVAL: {
|
||||
guint64 interval = g_value_get_uint64(value);
|
||||
g_mutex_lock(&filter->lock);
|
||||
if (filter->interval != interval) {
|
||||
filter->interval = interval;
|
||||
gst_fastspectrum_reset_state(filter);
|
||||
}
|
||||
g_mutex_unlock(&filter->lock);
|
||||
break;
|
||||
}
|
||||
case PROP_BANDS: {
|
||||
guint bands = g_value_get_uint(value);
|
||||
g_mutex_lock(&filter->lock);
|
||||
if (filter->bands != bands) {
|
||||
filter->bands = bands;
|
||||
gst_fastspectrum_reset_state(filter);
|
||||
}
|
||||
g_mutex_unlock(&filter->lock);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) {
|
||||
|
||||
GstFastSpectrum *filter = reinterpret_cast<GstFastSpectrum*>(object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_INTERVAL:
|
||||
g_value_set_uint64(value, filter->interval);
|
||||
break;
|
||||
case PROP_BANDS:
|
||||
g_value_set_uint(value, filter->bands);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_start(GstBaseTransform *trans) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(trans);
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_stop(GstBaseTransform *trans) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(trans);
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
// Mixing data readers
|
||||
|
||||
static void input_data_mixed_float(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
Q_UNUSED(max_value);
|
||||
|
||||
const gfloat *in = reinterpret_cast<const gfloat*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++];
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_double(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
Q_UNUSED(max_value);
|
||||
|
||||
const gdouble *in = reinterpret_cast<const gdouble*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++];
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int32_max(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
const gint32 *in = reinterpret_cast<const gint32*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++] / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int24_max(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
#if G_BYTE_ORDER == G_BIG_ENDIAN
|
||||
guint32 value = GST_READ_UINT24_BE(_in);
|
||||
#else
|
||||
guint32 value = GST_READ_UINT24_LE(_in);
|
||||
#endif
|
||||
if (value & 0x00800000) {
|
||||
value |= 0xff000000;
|
||||
}
|
||||
|
||||
out[op] = value / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
_in += 3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int16_max(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
const gint16 *in = reinterpret_cast<const gint16*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++] / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_setup(GstAudioFilter *base, const GstAudioInfo *info) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(base);
|
||||
GstFastSpectrumInputData input_data = nullptr;
|
||||
|
||||
g_mutex_lock(&spectrum->lock);
|
||||
switch (GST_AUDIO_INFO_FORMAT(info)) {
|
||||
case GST_AUDIO_FORMAT_S16:
|
||||
input_data = input_data_mixed_int16_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_S24:
|
||||
input_data = input_data_mixed_int24_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_S32:
|
||||
input_data = input_data_mixed_int32_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_F32:
|
||||
input_data = input_data_mixed_float;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_F64:
|
||||
input_data = input_data_mixed_double;
|
||||
break;
|
||||
default:
|
||||
g_assert_not_reached();
|
||||
break;
|
||||
}
|
||||
spectrum->input_data = input_data;
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
g_mutex_unlock(&spectrum->lock);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_run_fft(GstFastSpectrum *spectrum, guint input_pos) {
|
||||
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
|
||||
for (guint i = 0; i < nfft; i++) {
|
||||
spectrum->fft_input[i] = spectrum->input_ring_buffer[(input_pos + i) % nfft];
|
||||
}
|
||||
|
||||
// Should be safe to execute the same plan multiple times in parallel.
|
||||
fftw_execute(spectrum->plan);
|
||||
|
||||
// Calculate magnitude in db
|
||||
for (guint i = 0; i < bands; i++) {
|
||||
gdouble val = spectrum->fft_output[i][0] * spectrum->fft_output[i][0];
|
||||
val += spectrum->fft_output[i][1] * spectrum->fft_output[i][1];
|
||||
val /= nfft * nfft;
|
||||
spectrum->spect_magnitude[i] += val;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static GstFlowReturn gst_fastspectrum_transform_ip(GstBaseTransform *trans, GstBuffer *buffer) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(trans);
|
||||
guint rate = GST_AUDIO_FILTER_RATE(spectrum);
|
||||
guint bps = GST_AUDIO_FILTER_BPS(spectrum);
|
||||
guint bpf = GST_AUDIO_FILTER_BPF(spectrum);
|
||||
double max_value = static_cast<double>((1UL << ((bps << 3) - 1)) - 1);
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
guint input_pos = 0;
|
||||
GstMapInfo map;
|
||||
const guint8 *data = nullptr;
|
||||
gsize size = 0;
|
||||
GstFastSpectrumInputData input_data = nullptr;
|
||||
|
||||
g_mutex_lock(&spectrum->lock);
|
||||
gst_buffer_map(buffer, &map, GST_MAP_READ);
|
||||
data = map.data;
|
||||
size = map.size;
|
||||
|
||||
GST_LOG_OBJECT(spectrum, "input size: %" G_GSIZE_FORMAT " bytes", size);
|
||||
|
||||
if (GST_BUFFER_IS_DISCONT(buffer)) {
|
||||
GST_DEBUG_OBJECT(spectrum, "Discontinuity detected -- flushing");
|
||||
gst_fastspectrum_flush(spectrum);
|
||||
}
|
||||
|
||||
// If we don't have a FFT context yet (or it was reset due to parameter changes) get one and allocate memory for everything
|
||||
if (!spectrum->channel_data_initialized) {
|
||||
GST_DEBUG_OBJECT(spectrum, "allocating for bands %u", bands);
|
||||
|
||||
gst_fastspectrum_alloc_channel_data(spectrum);
|
||||
|
||||
// Number of sample frames we process before posting a message interval is in ns
|
||||
spectrum->frames_per_interval = gst_util_uint64_scale(spectrum->interval, rate, GST_SECOND);
|
||||
spectrum->frames_todo = spectrum->frames_per_interval;
|
||||
// Rounding error for frames_per_interval in ns, aggregated it in accumulated_error
|
||||
spectrum->error_per_interval = (spectrum->interval * rate) % GST_SECOND;
|
||||
if (spectrum->frames_per_interval == 0) {
|
||||
spectrum->frames_per_interval = 1;
|
||||
}
|
||||
|
||||
GST_INFO_OBJECT(spectrum, "interval %" GST_TIME_FORMAT ", fpi %" G_GUINT64_FORMAT ", error %" GST_TIME_FORMAT, GST_TIME_ARGS(spectrum->interval), spectrum->frames_per_interval, GST_TIME_ARGS(spectrum->error_per_interval));
|
||||
|
||||
spectrum->input_pos = 0;
|
||||
|
||||
gst_fastspectrum_flush(spectrum);
|
||||
}
|
||||
|
||||
if (spectrum->num_frames == 0) {
|
||||
spectrum->message_ts = GST_BUFFER_TIMESTAMP(buffer);
|
||||
}
|
||||
|
||||
input_pos = spectrum->input_pos;
|
||||
input_data = spectrum->input_data;
|
||||
|
||||
while (size >= bpf) {
|
||||
// Run input_data for a chunk of data
|
||||
guint fft_todo = nfft - (spectrum->num_frames % nfft);
|
||||
guint msg_todo = spectrum->frames_todo - spectrum->num_frames;
|
||||
GST_LOG_OBJECT(spectrum, "message frames todo: %u, fft frames todo: %u, input frames %" G_GSIZE_FORMAT, msg_todo, fft_todo, (size / bpf));
|
||||
guint block_size = msg_todo;
|
||||
if (block_size > (size / bpf)) {
|
||||
block_size = (size / bpf);
|
||||
}
|
||||
if (block_size > fft_todo) {
|
||||
block_size = fft_todo;
|
||||
}
|
||||
|
||||
// Move the current frames into our ringbuffers
|
||||
input_data(data, spectrum->input_ring_buffer, block_size, max_value, input_pos, nfft);
|
||||
|
||||
data += block_size * bpf;
|
||||
size -= block_size * bpf;
|
||||
input_pos = (input_pos + block_size) % nfft;
|
||||
spectrum->num_frames += block_size;
|
||||
|
||||
gboolean have_full_interval = (spectrum->num_frames == spectrum->frames_todo);
|
||||
|
||||
GST_LOG_OBJECT(spectrum, "size: %" G_GSIZE_FORMAT ", do-fft = %d, do-message = %d", size, (spectrum->num_frames % nfft == 0), have_full_interval);
|
||||
|
||||
// If we have enough frames for an FFT or we have all frames required for the interval and we haven't run a FFT, then run an FFT
|
||||
if ((spectrum->num_frames % nfft == 0) || (have_full_interval && !spectrum->num_fft)) {
|
||||
gst_fastspectrum_run_fft(spectrum, input_pos);
|
||||
spectrum->num_fft++;
|
||||
}
|
||||
|
||||
// Do we have the FFTs for one interval?
|
||||
if (have_full_interval) {
|
||||
GST_DEBUG_OBJECT(spectrum, "nfft: %u frames: %" G_GUINT64_FORMAT " fpi: %" G_GUINT64_FORMAT " error: %" GST_TIME_FORMAT, nfft, spectrum->num_frames, spectrum->frames_per_interval, GST_TIME_ARGS(spectrum->accumulated_error));
|
||||
|
||||
spectrum->frames_todo = spectrum->frames_per_interval;
|
||||
if (spectrum->accumulated_error >= GST_SECOND) {
|
||||
spectrum->accumulated_error -= GST_SECOND;
|
||||
spectrum->frames_todo++;
|
||||
}
|
||||
spectrum->accumulated_error += spectrum->error_per_interval;
|
||||
|
||||
if (spectrum->output_callback) {
|
||||
// Calculate average
|
||||
for (guint i = 0; i < spectrum->bands; i++) {
|
||||
spectrum->spect_magnitude[i] /= static_cast<double>(spectrum->num_fft);
|
||||
}
|
||||
|
||||
spectrum->output_callback(spectrum->spect_magnitude, static_cast<int>(spectrum->bands));
|
||||
|
||||
// Reset spectrum accumulators
|
||||
memset(spectrum->spect_magnitude, 0, spectrum->bands * sizeof(double));
|
||||
}
|
||||
|
||||
if (GST_CLOCK_TIME_IS_VALID(spectrum->message_ts)) {
|
||||
spectrum->message_ts += gst_util_uint64_scale(spectrum->num_frames, GST_SECOND, rate);
|
||||
}
|
||||
|
||||
spectrum->num_frames = 0;
|
||||
spectrum->num_fft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
spectrum->input_pos = input_pos;
|
||||
|
||||
gst_buffer_unmap(buffer, &map);
|
||||
g_mutex_unlock(&spectrum->lock);
|
||||
|
||||
g_assert(size == 0);
|
||||
|
||||
return GST_FLOW_OK;
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "gstfastspectrum.h"
|
||||
#include "gstmoodbarplugin.h"
|
||||
|
||||
static gboolean gst_moodbar_plugin_init(GstPlugin *plugin) {
|
||||
|
||||
if (!gst_element_register(plugin, "fastspectrum", GST_RANK_NONE, GST_TYPE_FASTSPECTRUM)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int gstfastspectrum_register_static() {
|
||||
|
||||
return gst_plugin_register_static(
|
||||
GST_VERSION_MAJOR,
|
||||
GST_VERSION_MINOR,
|
||||
"fastspectrum",
|
||||
"Fast spectrum analyzer for generating Moodbars",
|
||||
gst_moodbar_plugin_init,
|
||||
"0.1",
|
||||
"GPL",
|
||||
"FastSpectrum",
|
||||
"FastSpectrum",
|
||||
"https://www.strawberrymusicplayer.org");
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef GST_MOODBAR_PLUGIN_H
|
||||
#define GST_MOODBAR_PLUGIN_H
|
||||
|
||||
extern "C" {
|
||||
int gstfastspectrum_register_static();
|
||||
}
|
||||
|
||||
#endif // GST_MOODBAR_PLUGIN_H
|
||||
@@ -1,43 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(SOURCES
|
||||
core/logging.cpp
|
||||
core/messagehandler.cpp
|
||||
core/messagereply.cpp
|
||||
core/workerpool.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
core/logging.h
|
||||
core/messagehandler.h
|
||||
core/messagereply.h
|
||||
core/workerpool.h
|
||||
)
|
||||
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
link_directories(${GLIB_LIBRARY_DIRS})
|
||||
|
||||
add_library(libstrawberry-common STATIC ${SOURCES} ${MOC})
|
||||
|
||||
target_include_directories(libstrawberry-common SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS})
|
||||
target_include_directories(libstrawberry-common PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
|
||||
if(Backtrace_FOUND)
|
||||
target_include_directories(libstrawberry-common SYSTEM PRIVATE ${Backtrace_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(libstrawberry-common PRIVATE
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
${GLIB_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
)
|
||||
|
||||
if(Backtrace_FOUND)
|
||||
target_link_libraries(libstrawberry-common PRIVATE ${Backtrace_LIBRARIES})
|
||||
endif()
|
||||
@@ -1,117 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include "messagehandler.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractSocket>
|
||||
#include <QDataStream>
|
||||
#include <QIODevice>
|
||||
#include <QLocalSocket>
|
||||
#include <QByteArray>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
_MessageHandlerBase::_MessageHandlerBase(QIODevice *device, QObject *parent)
|
||||
: QObject(parent),
|
||||
device_(nullptr),
|
||||
flush_abstract_socket_(nullptr),
|
||||
flush_local_socket_(nullptr),
|
||||
reading_protobuf_(false),
|
||||
expected_length_(0),
|
||||
is_device_closed_(false) {
|
||||
if (device) {
|
||||
SetDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::SetDevice(QIODevice *device) {
|
||||
|
||||
device_ = device;
|
||||
|
||||
buffer_.open(QIODevice::ReadWrite);
|
||||
|
||||
QObject::connect(device, &QIODevice::readyRead, this, &_MessageHandlerBase::DeviceReadyRead);
|
||||
|
||||
// Yeah I know.
|
||||
if (QAbstractSocket *abstractsocket = qobject_cast<QAbstractSocket*>(device)) {
|
||||
flush_abstract_socket_ = &QAbstractSocket::flush;
|
||||
QObject::connect(abstractsocket, &QAbstractSocket::disconnected, this, &_MessageHandlerBase::DeviceClosed);
|
||||
}
|
||||
else if (QLocalSocket *localsocket = qobject_cast<QLocalSocket*>(device)) {
|
||||
flush_local_socket_ = &QLocalSocket::flush;
|
||||
QObject::connect(localsocket, &QLocalSocket::disconnected, this, &_MessageHandlerBase::DeviceClosed);
|
||||
}
|
||||
else {
|
||||
qFatal("Unsupported device type passed to _MessageHandlerBase");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::DeviceReadyRead() {
|
||||
|
||||
while (device_->bytesAvailable() > 0) {
|
||||
if (!reading_protobuf_) {
|
||||
// Read the length of the next message
|
||||
QDataStream s(device_);
|
||||
s >> expected_length_;
|
||||
|
||||
reading_protobuf_ = true;
|
||||
}
|
||||
|
||||
// Read some of the message
|
||||
buffer_.write(device_->read(expected_length_ - buffer_.size()));
|
||||
|
||||
// Did we get everything?
|
||||
if (buffer_.size() == expected_length_) {
|
||||
// Parse the message
|
||||
if (!RawMessageArrived(buffer_.data())) {
|
||||
qLog(Error) << "Malformed protobuf message";
|
||||
device_->close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the buffer
|
||||
buffer_.close();
|
||||
buffer_.setData(QByteArray());
|
||||
buffer_.open(QIODevice::ReadWrite);
|
||||
reading_protobuf_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::WriteMessage(const QByteArray &data) {
|
||||
|
||||
QDataStream s(device_);
|
||||
s << static_cast<quint32>(data.length());
|
||||
s.writeRawData(data.data(), static_cast<int>(data.length()));
|
||||
|
||||
// Sorry.
|
||||
if (flush_abstract_socket_) {
|
||||
((qobject_cast<QAbstractSocket*>(device_))->*(flush_abstract_socket_))();
|
||||
}
|
||||
else if (flush_local_socket_) {
|
||||
((qobject_cast<QLocalSocket*>(device_))->*(flush_local_socket_))();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::DeviceClosed() {
|
||||
is_device_closed_ = true;
|
||||
AbortAll();
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef MESSAGEHANDLER_H
|
||||
#define MESSAGEHANDLER_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QBuffer>
|
||||
#include <QByteArray>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QLocalSocket>
|
||||
#include <QAbstractSocket>
|
||||
|
||||
#include "core/messagereply.h"
|
||||
|
||||
class QIODevice;
|
||||
|
||||
// Reads and writes uint32 length encoded protobufs to a socket.
|
||||
// This base QObject is separate from AbstractMessageHandler because moc can't handle templated classes.
|
||||
// Use AbstractMessageHandler instead.
|
||||
class _MessageHandlerBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// device can be nullptr, in which case you must call SetDevice before writing any messages.
|
||||
_MessageHandlerBase(QIODevice *device, QObject *parent);
|
||||
|
||||
void SetDevice(QIODevice *device);
|
||||
|
||||
// After this is true, messages cannot be sent to the handler any more.
|
||||
bool is_device_closed() const { return is_device_closed_; }
|
||||
|
||||
protected slots:
|
||||
void WriteMessage(const QByteArray &data);
|
||||
void DeviceReadyRead();
|
||||
virtual void DeviceClosed();
|
||||
|
||||
protected:
|
||||
virtual bool RawMessageArrived(const QByteArray &data) = 0;
|
||||
virtual void AbortAll() = 0;
|
||||
|
||||
protected:
|
||||
typedef bool (QAbstractSocket::*FlushAbstractSocket)();
|
||||
typedef bool (QLocalSocket::*FlushLocalSocket)();
|
||||
|
||||
QIODevice *device_;
|
||||
FlushAbstractSocket flush_abstract_socket_;
|
||||
FlushLocalSocket flush_local_socket_;
|
||||
|
||||
bool reading_protobuf_;
|
||||
quint32 expected_length_;
|
||||
QBuffer buffer_;
|
||||
|
||||
bool is_device_closed_;
|
||||
};
|
||||
|
||||
// Reads and writes uint32 length encoded MessageType messages to a socket.
|
||||
// You should subclass this and implement the MessageArrived(MessageType) method.
|
||||
template<typename MT>
|
||||
class AbstractMessageHandler : public _MessageHandlerBase {
|
||||
public:
|
||||
AbstractMessageHandler(QIODevice *device, QObject *parent);
|
||||
~AbstractMessageHandler() override { AbstractMessageHandler::AbortAll(); }
|
||||
|
||||
using MessageType = MT;
|
||||
using ReplyType = MessageReply<MT>;
|
||||
|
||||
// Serialises the message and writes it to the socket.
|
||||
// This version MUST be called from the thread in which the AbstractMessageHandler was created.
|
||||
void SendMessage(const MessageType &message);
|
||||
|
||||
// Serialises the message and writes it to the socket.
|
||||
// This version may be called from any thread.
|
||||
void SendMessageAsync(const MessageType &message);
|
||||
|
||||
// Sends the request message inside and takes ownership of the MessageReply.
|
||||
// The MessageReply's Finished() signal will be emitted when a reply arrives with the same ID. Must be called from my thread.
|
||||
void SendRequest(ReplyType *reply);
|
||||
|
||||
// Sets the "id" field of reply to the same as the request, and sends the reply on the socket. Used on the worker side.
|
||||
void SendReply(const MessageType &request, MessageType *reply);
|
||||
|
||||
protected:
|
||||
// Called when a message is received from the socket.
|
||||
virtual void MessageArrived(const MessageType &message) { Q_UNUSED(message); }
|
||||
|
||||
// _MessageHandlerBase
|
||||
bool RawMessageArrived(const QByteArray &data) override;
|
||||
void AbortAll() override;
|
||||
|
||||
private:
|
||||
QMap<int, ReplyType*> pending_replies_;
|
||||
};
|
||||
|
||||
template<typename MT>
|
||||
AbstractMessageHandler<MT>::AbstractMessageHandler(QIODevice *device, QObject *parent)
|
||||
: _MessageHandlerBase(device, parent) {}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendMessage(const MessageType &message) {
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
const std::string data = message.SerializeAsString();
|
||||
WriteMessage(QByteArray(data.data(), data.size()));
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendMessageAsync(const MessageType &message) {
|
||||
const std::string data = message.SerializeAsString();
|
||||
QMetaObject::invokeMethod(this, "WriteMessage", Qt::QueuedConnection, Q_ARG(QByteArray, QByteArray(data.data(), data.size())));
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendRequest(ReplyType *reply) {
|
||||
pending_replies_[reply->id()] = reply;
|
||||
SendMessage(reply->request_message());
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendReply(const MessageType &request, MessageType *reply) {
|
||||
reply->set_id(request.id());
|
||||
SendMessage(*reply);
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
bool AbstractMessageHandler<MT>::RawMessageArrived(const QByteArray &data) {
|
||||
|
||||
MessageType message;
|
||||
if (!message.ParseFromArray(data.constData(), data.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pending_replies_.contains(message.id())) {
|
||||
// This is a reply to a message that we created earlier.
|
||||
ReplyType *reply = pending_replies_.take(message.id());
|
||||
reply->SetReply(message);
|
||||
}
|
||||
else {
|
||||
MessageArrived(message);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::AbortAll() {
|
||||
|
||||
for (ReplyType *reply : pending_replies_) {
|
||||
reply->Abort();
|
||||
}
|
||||
pending_replies_.clear();
|
||||
|
||||
}
|
||||
|
||||
#endif // MESSAGEHANDLER_H
|
||||
@@ -1,48 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "messagereply.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
_MessageReplyBase::_MessageReplyBase(QObject *parent)
|
||||
: QObject(parent), finished_(false), success_(false) {}
|
||||
|
||||
bool _MessageReplyBase::WaitForFinished() {
|
||||
|
||||
qLog(Debug) << "Waiting on ID" << id();
|
||||
semaphore_.acquire();
|
||||
qLog(Debug) << "Acquired ID" << id();
|
||||
return success_;
|
||||
|
||||
}
|
||||
|
||||
void _MessageReplyBase::Abort() {
|
||||
|
||||
Q_ASSERT(!finished_);
|
||||
finished_ = true;
|
||||
success_ = false;
|
||||
|
||||
emit Finished();
|
||||
qLog(Debug) << "Releasing ID" << id() << "(aborted)";
|
||||
semaphore_.release();
|
||||
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef MESSAGEREPLY_H
|
||||
#define MESSAGEREPLY_H
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QSemaphore>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
// Base QObject for a reply future class that is returned immediately for requests that will occur in the background.
|
||||
// Similar to QNetworkReply. Use MessageReply instead.
|
||||
class _MessageReplyBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit _MessageReplyBase(QObject *parent = nullptr);
|
||||
|
||||
virtual int id() const = 0;
|
||||
bool is_finished() const { return finished_; }
|
||||
bool is_successful() const { return success_; }
|
||||
|
||||
// Waits for the reply to finish by waiting on a semaphore. Never call this from the MessageHandler's thread or it will block forever.
|
||||
// Returns true if the call was successful.
|
||||
bool WaitForFinished();
|
||||
|
||||
void Abort();
|
||||
|
||||
signals:
|
||||
void Finished();
|
||||
|
||||
protected:
|
||||
bool finished_;
|
||||
bool success_;
|
||||
|
||||
QSemaphore semaphore_;
|
||||
};
|
||||
|
||||
// A reply future class that is returned immediately for requests that will occur in the background. Similar to QNetworkReply.
|
||||
template<typename MessageType>
|
||||
class MessageReply : public _MessageReplyBase {
|
||||
public:
|
||||
explicit MessageReply(const MessageType &request_message, QObject *parent = nullptr);
|
||||
|
||||
int id() const override { return request_message_.id(); }
|
||||
const MessageType &request_message() const { return request_message_; }
|
||||
const MessageType &message() const { return reply_message_; }
|
||||
|
||||
void SetReply(const MessageType &message);
|
||||
|
||||
private:
|
||||
MessageType request_message_;
|
||||
MessageType reply_message_;
|
||||
};
|
||||
|
||||
|
||||
template<typename MessageType>
|
||||
MessageReply<MessageType>::MessageReply(const MessageType &request_message, QObject *parent) : _MessageReplyBase(parent) {
|
||||
request_message_.MergeFrom(request_message);
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void MessageReply<MessageType>::SetReply(const MessageType &message) {
|
||||
|
||||
Q_ASSERT(!finished_);
|
||||
|
||||
reply_message_.MergeFrom(message);
|
||||
finished_ = true;
|
||||
success_ = true;
|
||||
|
||||
qLog(Debug) << "Releasing ID" << id() << "(finished)";
|
||||
|
||||
// Delay the signal as workaround to fix the signal periodically not emitted.
|
||||
QTimer::singleShot(1, this, &_MessageReplyBase::Finished);
|
||||
|
||||
semaphore_.release();
|
||||
|
||||
}
|
||||
|
||||
#endif // MESSAGEREPLY_H
|
||||
@@ -1,23 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "workerpool.h"
|
||||
|
||||
_WorkerPoolBase::_WorkerPoolBase(QObject *parent) : QObject(parent) {}
|
||||
@@ -1,466 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef WORKERPOOL_H
|
||||
#define WORKERPOOL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QMutex>
|
||||
#include <QLocalServer>
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QAtomicInt>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
class QLocalSocket;
|
||||
|
||||
// Base class containing signals and slots - required because moc doesn't do templated objects.
|
||||
class _WorkerPoolBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit _WorkerPoolBase(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
// Emitted when a worker failed to start. This usually happens when the worker wasn't found, or couldn't be executed.
|
||||
void WorkerFailedToStart();
|
||||
|
||||
protected slots:
|
||||
virtual void DoStart() {}
|
||||
virtual void NewConnection() {}
|
||||
virtual void ProcessReadyReadStandardOutput() {}
|
||||
virtual void ProcessReadyReadStandardError() {}
|
||||
virtual void ProcessError(QProcess::ProcessError) {}
|
||||
virtual void SendQueuedMessages() {}
|
||||
};
|
||||
|
||||
|
||||
// Manages a pool of one or more external processes.
|
||||
// A local socket server is started for each process, and the address is passed to the process as argv[1].
|
||||
// The process is expected to connect back to the socket server, and when it does a HandlerType is created for it.
|
||||
// Instances of HandlerType are created in the WorkerPool's thread.
|
||||
template<typename HandlerType>
|
||||
class WorkerPool : public _WorkerPoolBase {
|
||||
public:
|
||||
explicit WorkerPool(QObject *parent = nullptr);
|
||||
~WorkerPool() override;
|
||||
|
||||
using MessageType = typename HandlerType::MessageType;
|
||||
using ReplyType = typename HandlerType::ReplyType;
|
||||
|
||||
// Sets the name of the worker executable. This is looked for first in the current directory, and then in $PATH.
|
||||
// You must call this before calling Start().
|
||||
void SetExecutableName(const QString &executable_name);
|
||||
|
||||
// Sets the number of worker process to use. Defaults to 1 <= (processors / 2) <= 2.
|
||||
void SetWorkerCount(const int count);
|
||||
|
||||
// Sets the prefix to use for the local server (on unix this is a named pipe in /tmp).
|
||||
// Defaults to QApplication::applicationName().
|
||||
// A random number is appended to this name when creating each server.
|
||||
void SetLocalServerName(const QString &local_server_name);
|
||||
|
||||
// Starts all workers.
|
||||
void Start();
|
||||
|
||||
// Fills in the message's "id" field and creates a reply future.
|
||||
// The message is queued and the WorkerPool's thread will send it to the next available worker.
|
||||
// Can be called from any thread.
|
||||
ReplyType *SendMessageWithReply(MessageType *message);
|
||||
|
||||
protected:
|
||||
// These are all reimplemented slots, they are called on the WorkerPool's thread.
|
||||
void DoStart() override;
|
||||
void NewConnection() override;
|
||||
void ProcessReadyReadStandardOutput() override;
|
||||
void ProcessReadyReadStandardError() override;
|
||||
void ProcessError(QProcess::ProcessError error) override;
|
||||
void SendQueuedMessages() override;
|
||||
|
||||
private:
|
||||
struct Worker {
|
||||
Worker() : local_server_(nullptr), local_socket_(nullptr), process_(nullptr), handler_(nullptr) {}
|
||||
|
||||
QLocalServer *local_server_;
|
||||
QLocalSocket *local_socket_;
|
||||
QProcess *process_;
|
||||
HandlerType *handler_;
|
||||
};
|
||||
|
||||
// Must only ever be called on my thread.
|
||||
void StartOneWorker(Worker *worker);
|
||||
|
||||
template<typename T>
|
||||
Worker *FindWorker(T Worker::*member, T value) {
|
||||
for (typename QList<Worker>::iterator it = workers_.begin(); it != workers_.end(); ++it) {
|
||||
if ((*it).*member == value) {
|
||||
return &(*it);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void DeleteQObjectPointerLater(T **p) {
|
||||
if (*p) {
|
||||
(*p)->deleteLater();
|
||||
*p = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new reply future for the request with the next sequential ID,
|
||||
// and sets the request's ID to the ID of the reply. Can be called from any thread
|
||||
ReplyType *NewReply(MessageType *message);
|
||||
|
||||
// Returns the next handler, or nullptr if there isn't one. Must be called from my thread.
|
||||
HandlerType *NextHandler() const;
|
||||
|
||||
private:
|
||||
QString local_server_name_;
|
||||
QString executable_name_;
|
||||
QString executable_path_;
|
||||
|
||||
int worker_count_;
|
||||
mutable int next_worker_;
|
||||
QList<Worker> workers_;
|
||||
|
||||
QAtomicInt next_id_;
|
||||
|
||||
QMutex message_queue_mutex_;
|
||||
QQueue<ReplyType *> message_queue_;
|
||||
};
|
||||
|
||||
|
||||
template<typename HandlerType>
|
||||
WorkerPool<HandlerType>::WorkerPool(QObject *parent)
|
||||
: _WorkerPoolBase(parent),
|
||||
worker_count_(1),
|
||||
next_worker_(0),
|
||||
next_id_(0) {
|
||||
|
||||
local_server_name_ = qApp->applicationName().toLower();
|
||||
|
||||
if (local_server_name_.isEmpty()) {
|
||||
local_server_name_ = QStringLiteral("workerpool");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
WorkerPool<HandlerType>::~WorkerPool() {
|
||||
|
||||
for (const Worker &worker : workers_) {
|
||||
if (worker.local_socket_ && worker.process_) {
|
||||
QObject::disconnect(worker.process_, &QProcess::errorOccurred, this, &WorkerPool::ProcessError);
|
||||
QObject::disconnect(worker.process_, &QProcess::readyReadStandardOutput, this, &WorkerPool::ProcessReadyReadStandardOutput);
|
||||
QObject::disconnect(worker.process_, &QProcess::readyReadStandardError, this, &WorkerPool::ProcessReadyReadStandardError);
|
||||
|
||||
// The worker is connected. Close his socket and wait for him to exit.
|
||||
qLog(Debug) << "Closing worker socket";
|
||||
worker.local_socket_->close();
|
||||
worker.process_->waitForFinished(500);
|
||||
}
|
||||
|
||||
if (worker.process_ && worker.process_->state() == QProcess::Running) {
|
||||
// The worker is still running - kill it.
|
||||
qLog(Debug) << "Killing worker process";
|
||||
worker.process_->terminate();
|
||||
if (!worker.process_->waitForFinished(500)) {
|
||||
worker.process_->kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ReplyType *reply : message_queue_) {
|
||||
reply->Abort();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetWorkerCount(const int count) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
worker_count_ = count;
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetLocalServerName(const QString &local_server_name) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
local_server_name_ = local_server_name;
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetExecutableName(const QString &executable_name) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
executable_name_ = executable_name;
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::Start() {
|
||||
QMetaObject::invokeMethod(this, &WorkerPool<HandlerType>::DoStart);
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::DoStart() {
|
||||
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
Q_ASSERT(!executable_name_.isEmpty());
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
// Find the executable if we can, default to searching $PATH
|
||||
executable_path_ = executable_name_;
|
||||
|
||||
QStringList search_path;
|
||||
search_path << QCoreApplication::applicationDirPath();
|
||||
#if defined(Q_OS_UNIX)
|
||||
search_path << QStringLiteral("/usr/libexec");
|
||||
search_path << QStringLiteral("/usr/local/libexec");
|
||||
#endif
|
||||
#if defined(Q_OS_MACOS)
|
||||
search_path << QDir::cleanPath(QCoreApplication::applicationDirPath() + QStringLiteral("/../PlugIns"));
|
||||
#endif
|
||||
|
||||
for (const QString &path_prefix : std::as_const(search_path)) {
|
||||
const QString executable_path = path_prefix + QLatin1Char('/') + executable_name_;
|
||||
if (QFile::exists(executable_path)) {
|
||||
executable_path_ = executable_path;
|
||||
qLog(Debug) << "Using worker" << executable_name_ << "from" << path_prefix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (executable_path_ == executable_name_) {
|
||||
qLog(Debug) << "Using worker" << executable_name_;
|
||||
}
|
||||
|
||||
// Start all the workers
|
||||
for (int i = 0; i < worker_count_; ++i) {
|
||||
Worker worker;
|
||||
StartOneWorker(&worker);
|
||||
|
||||
workers_ << worker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
DeleteQObjectPointerLater(&worker->local_server_);
|
||||
DeleteQObjectPointerLater(&worker->local_socket_);
|
||||
DeleteQObjectPointerLater(&worker->process_);
|
||||
DeleteQObjectPointerLater(&worker->handler_);
|
||||
|
||||
worker->local_server_ = new QLocalServer(this);
|
||||
worker->process_ = new QProcess(this);
|
||||
|
||||
QObject::connect(worker->local_server_, &QLocalServer::newConnection, this, &WorkerPool::NewConnection);
|
||||
QObject::connect(worker->process_, &QProcess::errorOccurred, this, &WorkerPool::ProcessError);
|
||||
QObject::connect(worker->process_, &QProcess::readyReadStandardOutput, this, &WorkerPool::ProcessReadyReadStandardOutput);
|
||||
QObject::connect(worker->process_, &QProcess::readyReadStandardError, this, &WorkerPool::ProcessReadyReadStandardError);
|
||||
|
||||
// Create a server, find an unused name and start listening
|
||||
forever {
|
||||
const quint32 unique_number = QRandomGenerator::global()->bounded(static_cast<quint32>(quint64(this) & 0xFFFFFFFF));
|
||||
const QString name = QStringLiteral("%1_%2").arg(local_server_name_).arg(unique_number);
|
||||
|
||||
if (worker->local_server_->listen(name)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qLog(Debug) << "Starting worker" << worker << executable_path_ << worker->local_server_->fullServerName();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
worker->process_->setProcessChannelMode(QProcess::SeparateChannels);
|
||||
#else
|
||||
worker->process_->setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
#endif
|
||||
|
||||
worker->process_->start(executable_path_, QStringList() << worker->local_server_->fullServerName());
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::NewConnection() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QLocalServer *server = qobject_cast<QLocalServer*>(sender());
|
||||
|
||||
// Find the worker with this server.
|
||||
Worker *worker = FindWorker(&Worker::local_server_, server);
|
||||
if (!worker) return;
|
||||
|
||||
qLog(Debug) << "Worker" << worker << "connected to" << server->fullServerName();
|
||||
|
||||
// Accept the connection.
|
||||
worker->local_socket_ = server->nextPendingConnection();
|
||||
|
||||
// We only ever accept one connection per worker, so destroy the server now.
|
||||
worker->local_socket_->setParent(this);
|
||||
worker->local_server_->deleteLater();
|
||||
worker->local_server_ = nullptr;
|
||||
|
||||
// Create the handler.
|
||||
worker->handler_ = new HandlerType(worker->local_socket_, this);
|
||||
|
||||
SendQueuedMessages();
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessError(QProcess::ProcessError error) {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QProcess *process = qobject_cast<QProcess*>(sender());
|
||||
|
||||
// Find the worker with this process.
|
||||
Worker *worker = FindWorker(&Worker::process_, process);
|
||||
if (!worker) return;
|
||||
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
// Failed to start errors are bad - it usually means the worker isn't installed.
|
||||
// Don't restart the process, but tell our owner, who will probably want to do something fatal.
|
||||
qLog(Error) << "Worker failed to start";
|
||||
emit WorkerFailedToStart();
|
||||
break;
|
||||
|
||||
default:
|
||||
// On any other error we just restart the process.
|
||||
qLog(Debug) << "Worker" << worker << "failed with error" << error << "- restarting";
|
||||
StartOneWorker(worker);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessReadyReadStandardOutput() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QProcess *process = qobject_cast<QProcess*>(sender());
|
||||
QByteArray data = process->readAllStandardOutput();
|
||||
|
||||
fprintf(stdout, "%s", data.data());
|
||||
fflush(stdout);
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessReadyReadStandardError() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QProcess *process = qobject_cast<QProcess*>(sender());
|
||||
QByteArray data = process->readAllStandardError();
|
||||
|
||||
fprintf(stderr, "%s", data.data());
|
||||
fflush(stderr);
|
||||
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
typename WorkerPool<HandlerType>::ReplyType*
|
||||
WorkerPool<HandlerType>::NewReply(MessageType *message) {
|
||||
|
||||
const int id = next_id_.fetchAndAddOrdered(1);
|
||||
message->set_id(id);
|
||||
|
||||
return new ReplyType(*message);
|
||||
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
typename WorkerPool<HandlerType>::ReplyType*
|
||||
WorkerPool<HandlerType>::SendMessageWithReply(MessageType *message) {
|
||||
|
||||
ReplyType *reply = NewReply(message);
|
||||
|
||||
// Add the pending reply to the queue
|
||||
{
|
||||
QMutexLocker l(&message_queue_mutex_);
|
||||
message_queue_.enqueue(reply);
|
||||
}
|
||||
|
||||
// Wake up the main thread
|
||||
QMetaObject::invokeMethod(this, &WorkerPool<HandlerType>::SendQueuedMessages, Qt::QueuedConnection);
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SendQueuedMessages() {
|
||||
|
||||
QMutexLocker l(&message_queue_mutex_);
|
||||
|
||||
while (!message_queue_.isEmpty()) {
|
||||
ReplyType *reply = message_queue_.dequeue();
|
||||
|
||||
// Find a worker for this message
|
||||
HandlerType *handler = NextHandler();
|
||||
if (!handler) {
|
||||
// No available handlers - put the message on the front of the queue.
|
||||
message_queue_.prepend(reply);
|
||||
qLog(Debug) << "No available handlers to process request";
|
||||
break;
|
||||
}
|
||||
|
||||
handler->SendRequest(reply);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
HandlerType *WorkerPool<HandlerType>::NextHandler() const {
|
||||
|
||||
for (int i = 0; i < workers_.count(); ++i) {
|
||||
const int worker_index = (next_worker_ + i) % workers_.count();
|
||||
|
||||
if (workers_[worker_index].handler_ && !workers_[worker_index].handler_->is_device_closed()) {
|
||||
next_worker_ = (worker_index + 1) % workers_.count();
|
||||
return workers_[worker_index].handler_;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
#endif // WORKERPOOL_H
|
||||
@@ -1,69 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
# Workaround a bug in protobuf-generate.cmake (https://github.com/protocolbuffers/protobuf/issues/12450)
|
||||
if(NOT protobuf_PROTOC_EXE)
|
||||
set(protobuf_PROTOC_EXE "protobuf::protoc")
|
||||
endif()
|
||||
|
||||
if(NOT Protobuf_LIBRARIES)
|
||||
set(Protobuf_LIBRARIES protobuf::libprotobuf)
|
||||
endif()
|
||||
|
||||
set(SOURCES tagreaderbase.cpp tagreadermessages.proto)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
list(APPEND SOURCES tagreadertaglib.cpp tagreadergme.cpp)
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
list(APPEND SOURCES tagreadertagparser.cpp)
|
||||
endif()
|
||||
|
||||
link_directories(
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${PROTOBUF_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
link_directories(${TAGLIB_LIBRARY_DIRS})
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
link_directories(${TAGPARSER_LIBRARY_DIRS})
|
||||
endif()
|
||||
|
||||
add_library(libstrawberry-tagreader STATIC ${PROTO_SOURCES} ${SOURCES})
|
||||
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${PROTOBUF_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(libstrawberry-tagreader PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-common
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
${Protobuf_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
Qt${QT_VERSION_MAJOR}::Gui
|
||||
libstrawberry-common
|
||||
)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGLIB_INCLUDE_DIRS})
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGLIB_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGPARSER_INCLUDE_DIRS})
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGPARSER_LIBRARIES})
|
||||
endif()
|
||||
|
||||
protobuf_generate(TARGET libstrawberry-tagreader)
|
||||
@@ -1,170 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QBuffer>
|
||||
#include <QImage>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "tagreaderbase.h"
|
||||
|
||||
TagReaderBase::TagReaderBase() = default;
|
||||
|
||||
QString TagReaderBase::ErrorString(const Result &result) {
|
||||
|
||||
switch (result.error_code) {
|
||||
case Result::ErrorCode::Success:
|
||||
return QObject::tr("Success");
|
||||
case Result::ErrorCode::Unsupported:
|
||||
return QObject::tr("File is unsupported");
|
||||
case Result::ErrorCode::FilenameMissing:
|
||||
return QObject::tr("Filename is missing");
|
||||
case Result::ErrorCode::FileDoesNotExist:
|
||||
return QObject::tr("File does not exist");
|
||||
case Result::ErrorCode::FileOpenError:
|
||||
return QObject::tr("File could not be opened");
|
||||
case Result::ErrorCode::FileParseError:
|
||||
return QObject::tr("Could not parse file");
|
||||
case Result::ErrorCode::FileSaveError:
|
||||
return QObject::tr("Could save file");
|
||||
case Result::ErrorCode::CustomError:
|
||||
return result.error;
|
||||
}
|
||||
|
||||
return QObject::tr("Unknown error");
|
||||
|
||||
}
|
||||
|
||||
float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
|
||||
|
||||
if (POPM_rating < 0x01) return 0.0F;
|
||||
if (POPM_rating < 0x40) return 0.20F;
|
||||
if (POPM_rating < 0x80) return 0.40F;
|
||||
if (POPM_rating < 0xC0) return 0.60F;
|
||||
if (POPM_rating < 0xFC) return 0.80F;
|
||||
|
||||
return 1.0F;
|
||||
|
||||
}
|
||||
|
||||
int TagReaderBase::ConvertToPOPMRating(const float rating) {
|
||||
|
||||
if (rating < 0.20) return 0x00;
|
||||
if (rating < 0.40) return 0x01;
|
||||
if (rating < 0.60) return 0x40;
|
||||
if (rating < 0.80) return 0x80;
|
||||
if (rating < 1.0) return 0xC0;
|
||||
|
||||
return 0xFF;
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::WriteFileRequest &request) {
|
||||
|
||||
if (!request.has_save_cover() || !request.save_cover()) {
|
||||
return Cover();
|
||||
}
|
||||
|
||||
QString cover_filename;
|
||||
if (request.has_cover_filename()) {
|
||||
cover_filename = QString::fromStdString(request.cover_filename());
|
||||
}
|
||||
QByteArray cover_data;
|
||||
if (request.has_cover_data()) {
|
||||
cover_data = QByteArray(request.cover_data().data(), static_cast<qint64>(request.cover_data().size()));
|
||||
}
|
||||
QString cover_mime_type;
|
||||
if (request.has_cover_mime_type()) {
|
||||
cover_mime_type = QString::fromStdString(request.cover_mime_type());
|
||||
}
|
||||
|
||||
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::SaveEmbeddedArtRequest &request) {
|
||||
|
||||
QString cover_filename;
|
||||
if (request.has_cover_filename()) {
|
||||
cover_filename = QString::fromStdString(request.cover_filename());
|
||||
}
|
||||
QByteArray cover_data;
|
||||
if (request.has_cover_data()) {
|
||||
cover_data = QByteArray(request.cover_data().data(), static_cast<qint64>(request.cover_data().size()));
|
||||
}
|
||||
QString cover_mime_type;
|
||||
if (request.has_cover_mime_type()) {
|
||||
cover_mime_type = QString::fromStdString(request.cover_mime_type());
|
||||
}
|
||||
|
||||
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type) {
|
||||
|
||||
if (cover_data.isEmpty() && !cover_filename.isEmpty()) {
|
||||
qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename;
|
||||
QFile file(cover_filename);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString();
|
||||
return Cover();
|
||||
}
|
||||
cover_data = file.readAll();
|
||||
file.close();
|
||||
}
|
||||
|
||||
if (!cover_data.isEmpty()) {
|
||||
if (cover_mime_type.isEmpty()) {
|
||||
cover_mime_type = QMimeDatabase().mimeTypeForData(cover_data).name();
|
||||
}
|
||||
if (cover_mime_type == QLatin1String("image/jpeg")) {
|
||||
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
|
||||
return Cover(cover_data, cover_mime_type);
|
||||
}
|
||||
if (cover_mime_type == QLatin1String("image/png")) {
|
||||
qLog(Debug) << "Using cover from PNG data for" << song_filename;
|
||||
return Cover(cover_data, cover_mime_type);
|
||||
}
|
||||
// Convert image to JPEG.
|
||||
qLog(Debug) << "Converting cover to JPEG data for" << song_filename;
|
||||
QImage cover_image;
|
||||
if (!cover_image.loadFromData(cover_data)) {
|
||||
qLog(Error) << "Failed to load image from cover data for" << song_filename;
|
||||
return Cover();
|
||||
}
|
||||
cover_data.clear();
|
||||
QBuffer buffer(&cover_data);
|
||||
if (buffer.open(QIODevice::WriteOnly)) {
|
||||
cover_image.save(&buffer, "JPEG");
|
||||
buffer.close();
|
||||
}
|
||||
return Cover(cover_data, QStringLiteral("image/jpeg"));
|
||||
}
|
||||
|
||||
return Cover();
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERBASE_H
|
||||
#define TAGREADERBASE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
*/
|
||||
class TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderBase();
|
||||
~TagReaderBase() = default;
|
||||
|
||||
class Result {
|
||||
public:
|
||||
enum class ErrorCode {
|
||||
Success,
|
||||
Unsupported,
|
||||
FilenameMissing,
|
||||
FileDoesNotExist,
|
||||
FileOpenError,
|
||||
FileParseError,
|
||||
FileSaveError,
|
||||
CustomError,
|
||||
};
|
||||
Result(const ErrorCode _error_code, const QString &_error = QString()) : error_code(_error_code), error(_error) {}
|
||||
ErrorCode error_code;
|
||||
QString error;
|
||||
bool success() const { return error_code == ErrorCode::Success; }
|
||||
};
|
||||
|
||||
class Cover {
|
||||
public:
|
||||
explicit Cover(const QByteArray &_data = QByteArray(), const QString &_mime_type = QString()) : data(_data), mime_type(_mime_type) {}
|
||||
QByteArray data;
|
||||
QString mime_type;
|
||||
QString error;
|
||||
};
|
||||
|
||||
static QString ErrorString(const Result &result);
|
||||
|
||||
virtual bool IsMediaFile(const QString &filename) const = 0;
|
||||
|
||||
virtual Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
|
||||
virtual Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const = 0;
|
||||
|
||||
virtual Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const = 0;
|
||||
virtual Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const = 0;
|
||||
|
||||
virtual Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const = 0;
|
||||
virtual Result SaveSongRatingToFile(const QString &filename, const float rating) const = 0;
|
||||
|
||||
protected:
|
||||
static float ConvertPOPMRating(const int POPM_rating);
|
||||
static int ConvertToPOPMRating(const float rating);
|
||||
|
||||
static Cover LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::WriteFileRequest &request);
|
||||
static Cover LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::SaveEmbeddedArtRequest &request);
|
||||
|
||||
private:
|
||||
static Cover LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type);
|
||||
|
||||
Q_DISABLE_COPY(TagReaderBase)
|
||||
};
|
||||
|
||||
#endif // TAGREADERBASE_H
|
||||
@@ -1,195 +0,0 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package spb.tagreader;
|
||||
|
||||
message SongMetadata {
|
||||
|
||||
enum FileType {
|
||||
UNKNOWN = 0;
|
||||
WAV = 1;
|
||||
FLAC = 2;
|
||||
WAVPACK = 3;
|
||||
OGGFLAC = 4;
|
||||
OGGVORBIS = 5;
|
||||
OGGOPUS = 6;
|
||||
OGGSPEEX = 7;
|
||||
MPEG = 8;
|
||||
MP4 = 9;
|
||||
ASF = 10;
|
||||
AIFF = 11;
|
||||
MPC = 12;
|
||||
TRUEAUDIO = 13;
|
||||
DSF = 14;
|
||||
DSDIFF = 15;
|
||||
PCM = 16;
|
||||
APE = 17;
|
||||
MOD = 18;
|
||||
S3M = 19;
|
||||
XM = 20;
|
||||
IT = 21;
|
||||
SPC = 22;
|
||||
VGM = 23;
|
||||
CDDA = 90;
|
||||
STREAM = 91;
|
||||
}
|
||||
|
||||
optional bool valid = 1;
|
||||
|
||||
optional string title = 2;
|
||||
optional string album = 3;
|
||||
optional string artist = 4;
|
||||
optional string albumartist = 5;
|
||||
optional int32 track = 6;
|
||||
optional int32 disc = 7;
|
||||
optional int32 year = 8;
|
||||
optional int32 originalyear = 9;
|
||||
optional string genre = 10;
|
||||
optional bool compilation = 11;
|
||||
optional string composer = 12;
|
||||
optional string performer = 13;
|
||||
optional string grouping = 14;
|
||||
optional string comment = 15;
|
||||
optional string lyrics = 16;
|
||||
|
||||
optional uint64 length_nanosec = 17;
|
||||
|
||||
optional int32 bitrate = 18;
|
||||
optional int32 samplerate = 19;
|
||||
optional int32 bitdepth = 20;
|
||||
|
||||
optional string url = 21;
|
||||
optional string basefilename = 22;
|
||||
optional FileType filetype = 23;
|
||||
optional int64 filesize = 24;
|
||||
optional int64 mtime = 25;
|
||||
optional int64 ctime = 26;
|
||||
|
||||
optional uint32 playcount = 27;
|
||||
optional uint32 skipcount = 28;
|
||||
optional int64 lastplayed = 29;
|
||||
optional int64 lastseen = 30;
|
||||
|
||||
optional bool art_embedded = 31;
|
||||
|
||||
optional float rating = 32;
|
||||
|
||||
optional string acoustid_id = 33;
|
||||
optional string acoustid_fingerprint = 34;
|
||||
|
||||
optional string musicbrainz_album_artist_id = 35; // MusicBrainz Release Artist ID (MUSICBRAINZ_ALBUMARTISTID)
|
||||
optional string musicbrainz_artist_id = 36; // MusicBrainz Artist ID (MUSICBRAINZ_ARTISTID)
|
||||
optional string musicbrainz_original_artist_id = 37; // MusicBrainz Original Artist ID (MUSICBRAINZ_ORIGINALARTISTID)
|
||||
optional string musicbrainz_album_id = 38; // MusicBrainz Release ID (MUSICBRAINZ_ALBUMID)
|
||||
optional string musicbrainz_original_album_id = 39; // MusicBrainz Original Release ID (MUSICBRAINZ_ORIGINALALBUMID)
|
||||
optional string musicbrainz_recording_id = 40; // MusicBrainz Recording ID (MUSICBRAINZ_TRACKID)
|
||||
optional string musicbrainz_track_id = 41; // MusicBrainz Track ID (MUSICBRAINZ_RELEASETRACKID)
|
||||
optional string musicbrainz_disc_id = 42; // MusicBrainz Disc ID (MUSICBRAINZ_DISCID)
|
||||
optional string musicbrainz_release_group_id = 43; // MusicBrainz Release Group ID (MUSICBRAINZ_RELEASEGROUPID)
|
||||
optional string musicbrainz_work_id = 44; // MusicBrainz Work ID (MUSICBRAINZ_WORKID)
|
||||
|
||||
optional bool suspicious_tags = 50;
|
||||
|
||||
}
|
||||
|
||||
message IsMediaFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message ReadFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message ReadFileResponse {
|
||||
optional bool success = 1;
|
||||
optional SongMetadata metadata = 2;
|
||||
optional string error = 3;
|
||||
}
|
||||
|
||||
message WriteFileRequest {
|
||||
optional string filename = 1;
|
||||
optional bool save_tags = 2;
|
||||
optional bool save_playcount = 3;
|
||||
optional bool save_rating = 4;
|
||||
optional bool save_cover = 5;
|
||||
optional SongMetadata metadata = 6;
|
||||
optional string cover_filename = 7;
|
||||
optional bytes cover_data = 8;
|
||||
optional string cover_mime_type = 9;
|
||||
}
|
||||
|
||||
message WriteFileResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtResponse {
|
||||
optional bool success = 1;
|
||||
optional bytes data = 2;
|
||||
optional string error = 3;
|
||||
}
|
||||
|
||||
message SaveEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
optional string cover_filename = 2;
|
||||
optional bytes cover_data = 3;
|
||||
optional string cover_mime_type = 4;
|
||||
}
|
||||
|
||||
message SaveEmbeddedArtResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message SaveSongPlaycountToFileRequest {
|
||||
optional string filename = 1;
|
||||
optional uint32 playcount = 2;
|
||||
}
|
||||
|
||||
message SaveSongPlaycountToFileResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message SaveSongRatingToFileRequest {
|
||||
optional string filename = 1;
|
||||
optional float rating = 2;
|
||||
}
|
||||
|
||||
message SaveSongRatingToFileResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message Message {
|
||||
optional int32 id = 1;
|
||||
|
||||
optional ReadFileRequest read_file_request = 2;
|
||||
optional ReadFileResponse read_file_response = 3;
|
||||
|
||||
optional WriteFileRequest write_file_request = 4;
|
||||
optional WriteFileResponse write_file_response = 5;
|
||||
|
||||
optional IsMediaFileRequest is_media_file_request = 6;
|
||||
optional IsMediaFileResponse is_media_file_response = 7;
|
||||
|
||||
optional LoadEmbeddedArtRequest load_embedded_art_request = 8;
|
||||
optional LoadEmbeddedArtResponse load_embedded_art_response = 9;
|
||||
|
||||
optional SaveEmbeddedArtRequest save_embedded_art_request = 10;
|
||||
optional SaveEmbeddedArtResponse save_embedded_art_response = 11;
|
||||
|
||||
optional SaveSongPlaycountToFileRequest save_song_playcount_to_file_request = 12;
|
||||
optional SaveSongPlaycountToFileResponse save_song_playcount_to_file_response = 13;
|
||||
|
||||
optional SaveSongRatingToFileRequest save_song_rating_to_file_request = 14;
|
||||
optional SaveSongRatingToFileResponse save_song_rating_to_file_response = 15;
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERTAGLIB_H
|
||||
#define TAGREADERTAGLIB_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include <taglib/tstring.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/xiphcomment.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/popularimeterframe.h>
|
||||
#include <taglib/mp4tag.h>
|
||||
#include <taglib/asftag.h>
|
||||
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#undef TStringToQString
|
||||
#undef QStringToTString
|
||||
|
||||
class FileRefFactory;
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
*/
|
||||
class TagReaderTagLib : public TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderTagLib();
|
||||
~TagReaderTagLib();
|
||||
|
||||
static inline TagLib::String StdStringToTagLibString(const std::string &s) {
|
||||
return TagLib::String(s.c_str(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
static inline std::string TagLibStringToStdString(const TagLib::String &s) {
|
||||
return std::string(s.toCString(true), s.length());
|
||||
}
|
||||
|
||||
static inline TagLib::String QStringToTagLibString(const QString &s) {
|
||||
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
static inline QString TagLibStringToQString(const TagLib::String &s) {
|
||||
return QString::fromUtf8((s).toCString(true));
|
||||
}
|
||||
|
||||
static inline void AssignTagLibStringToStdString(const TagLib::String &tstr, std::string *output) {
|
||||
|
||||
const QString qstr = TagLibStringToQString(tstr).trimmed();
|
||||
const QByteArray data = qstr.toUtf8();
|
||||
output->assign(data.constData(), data.size());
|
||||
|
||||
}
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const override;
|
||||
|
||||
Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const;
|
||||
Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const override;
|
||||
Result SaveSongRatingToFile(const QString &filename, const float rating) const override;
|
||||
|
||||
private:
|
||||
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
|
||||
|
||||
void ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseVorbisComments(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseAPETags(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseMP4Tags(TagLib::MP4::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseASFTags(TagLib::ASF::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseASFAttribute(const TagLib::ASF::AttributeListMap &attributes_map, const char *attribute, std::string *str) const;
|
||||
|
||||
void SetID3v2Tag(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUserTextFrame(const std::string &description, const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
|
||||
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetASFTag(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetAsfAttribute(TagLib::ASF::Tag *tag, const char *attribute, const std::string &value) const;
|
||||
void SetAsfAttribute(TagLib::ASF::Tag *tag, const char *attribute, const int value) const;
|
||||
|
||||
QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const;
|
||||
|
||||
static TagLib::ID3v2::PopularimeterFrame *GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag);
|
||||
|
||||
void SetPlaycount(TagLib::Ogg::XiphComment *vorbis_comment, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::APE::Tag *tag, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::ID3v2::Tag *tag, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::MP4::Tag *tag, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::ASF::Tag *tag, const uint playcount) const;
|
||||
|
||||
void SetRating(TagLib::Ogg::XiphComment *vorbis_comment, const float rating) const;
|
||||
void SetRating(TagLib::APE::Tag *tag, const float rating) const;
|
||||
void SetRating(TagLib::ID3v2::Tag *tag, const float rating) const;
|
||||
void SetRating(TagLib::MP4::Tag *tag, const float rating) const;
|
||||
void SetRating(TagLib::ASF::Tag *tag, const float rating) const;
|
||||
|
||||
void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *vorbis_comment, const QByteArray &data, const QString &mime_type) const;
|
||||
void SetEmbeddedArt(TagLib::Ogg::XiphComment *vorbis_comment, const QByteArray &data, const QString &mime_type) const;
|
||||
void SetEmbeddedArt(TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const;
|
||||
void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const;
|
||||
|
||||
private:
|
||||
FileRefFactory *factory_;
|
||||
|
||||
Q_DISABLE_COPY(TagReaderTagLib)
|
||||
};
|
||||
|
||||
#endif // TAGREADERTAGLIB_H
|
||||
@@ -1,557 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "tagreadertagparser.h"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
#include <tagparser/diagnostics.h>
|
||||
#include <tagparser/progressfeedback.h>
|
||||
#include <tagparser/tag.h>
|
||||
#include <tagparser/abstracttrack.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
|
||||
TagReaderTagParser::TagReaderTagParser() = default;
|
||||
|
||||
TagReaderTagParser::~TagReaderTagParser() = default;
|
||||
|
||||
bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
|
||||
|
||||
qLog(Debug) << "Checking for valid file" << filename;
|
||||
|
||||
QFileInfo fileinfo(filename);
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare(QLatin1String("bak"), Qt::CaseInsensitive) == 0) return false;
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
taginfo.open(true);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
const auto tracks = taginfo.tracks();
|
||||
for (TagParser::AbstractTrack *track : tracks) {
|
||||
if (track->mediaType() == TagParser::MediaType::Audio) {
|
||||
taginfo.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
taginfo.close();
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
qLog(Debug) << "Reading tags from" << filename;
|
||||
|
||||
const QFileInfo fileinfo(filename);
|
||||
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare(QLatin1String("bak"), Qt::CaseInsensitive) == 0) return Result::ErrorCode::FileParseError;
|
||||
|
||||
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
||||
const QByteArray basefilename = fileinfo.fileName().toUtf8();
|
||||
|
||||
song->set_basefilename(basefilename.constData(), basefilename.size());
|
||||
song->set_url(url.constData(), url.size());
|
||||
song->set_filesize(fileinfo.size());
|
||||
|
||||
song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
||||
song->set_ctime(fileinfo.birthTime().isValid() ? std::max(fileinfo.birthTime().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
||||
|
||||
if (song->ctime() <= 0) {
|
||||
song->set_ctime(song->mtime());
|
||||
}
|
||||
|
||||
song->set_lastseen(QDateTime::currentDateTime().toSecsSinceEpoch());
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
|
||||
taginfo.open(true);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
std::vector<TagParser::AbstractTrack*> tracks = taginfo.tracks();
|
||||
for (TagParser::AbstractTrack *track : tracks) {
|
||||
switch (track->format().general) {
|
||||
case TagParser::GeneralMediaFormat::Flac:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_FLAC);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::WavPack:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_WAVPACK);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::MonkeysAudio:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_APE);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::WindowsMediaAudio:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_ASF);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Vorbis:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_OGGVORBIS);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Opus:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_OGGOPUS);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Speex:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_OGGSPEEX);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Mpeg1Audio:
|
||||
switch (track->format().sub) {
|
||||
case TagParser::SubFormats::Mpeg1Layer3:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_MPEG);
|
||||
break;
|
||||
case TagParser::SubFormats::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Mpc:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_MPC);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Pcm:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_PCM);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
song->set_length_nanosec(track->duration().totalMilliseconds() * kNsecPerMsec);
|
||||
song->set_samplerate(track->samplingFrequency());
|
||||
song->set_bitdepth(track->bitsPerSample());
|
||||
song->set_bitrate(std::max(track->bitrate(), track->maxBitrate()));
|
||||
}
|
||||
|
||||
if (song->filetype() == spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_UNKNOWN) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::Unsupported;
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
song->set_albumartist(tag->value(TagParser::KnownField::AlbumArtist).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_artist(tag->value(TagParser::KnownField::Artist).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_album(tag->value(TagParser::KnownField::Album).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_title(tag->value(TagParser::KnownField::Title).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_genre(tag->value(TagParser::KnownField::Genre).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_composer(tag->value(TagParser::KnownField::Composer).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_performer(tag->value(TagParser::KnownField::Performers).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_grouping(tag->value(TagParser::KnownField::Grouping).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_comment(tag->value(TagParser::KnownField::Comment).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_lyrics(tag->value(TagParser::KnownField::Lyrics).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_year(tag->value(TagParser::KnownField::RecordDate).toInteger());
|
||||
song->set_originalyear(tag->value(TagParser::KnownField::ReleaseDate).toInteger());
|
||||
song->set_track(tag->value(TagParser::KnownField::TrackPosition).toInteger());
|
||||
song->set_disc(tag->value(TagParser::KnownField::DiskPosition).toInteger());
|
||||
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
|
||||
song->set_art_embedded(true);
|
||||
}
|
||||
const float rating = ConvertPOPMRating(tag->value(TagParser::KnownField::Rating));
|
||||
if (song->rating() <= 0 && rating > 0.0 && rating <= 1.0) {
|
||||
song->set_rating(rating);
|
||||
}
|
||||
}
|
||||
|
||||
// Set integer fields to -1 if they're not valid
|
||||
if (song->track() <= 0) { song->set_track(-1); }
|
||||
if (song->disc() <= 0) { song->set_disc(-1); }
|
||||
if (song->year() <= 0) { song->set_year(-1); }
|
||||
if (song->originalyear() <= 0) { song->set_originalyear(-1); }
|
||||
if (song->samplerate() <= 0) { song->set_samplerate(-1); }
|
||||
if (song->bitdepth() <= 0) { song->set_bitdepth(-1); }
|
||||
if (song->bitrate() <= 0) { song->set_bitrate(-1); }
|
||||
if (song->lastplayed() <= 0) { song->set_lastplayed(-1); }
|
||||
|
||||
song->set_valid(true);
|
||||
|
||||
taginfo.close();
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
|
||||
}
|
||||
catch(...) {
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const {
|
||||
|
||||
if (request.filename().empty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
const spb::tagreader::SongMetadata &song = request.metadata();
|
||||
const bool save_tags = request.has_save_tags() && request.save_tags();
|
||||
const bool save_playcount = request.has_save_playcount() && request.save_playcount();
|
||||
const bool save_rating = request.has_save_rating() && request.save_rating();
|
||||
const bool save_cover = request.has_save_cover() && request.save_cover();
|
||||
|
||||
QStringList save_tags_options;
|
||||
if (save_tags) {
|
||||
save_tags_options << QStringLiteral("tags");
|
||||
}
|
||||
if (save_playcount) {
|
||||
save_tags_options << QStringLiteral("playcount");
|
||||
}
|
||||
if (save_rating) {
|
||||
save_tags_options << QStringLiteral("rating");
|
||||
}
|
||||
if (save_cover) {
|
||||
save_tags_options << QStringLiteral("embedded cover");
|
||||
}
|
||||
|
||||
qLog(Debug) << "Saving" << save_tags_options.join(QLatin1String(", ")) << "to" << filename;
|
||||
|
||||
const Cover cover = LoadCoverFromRequest(filename, request);
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
taginfo.open(false);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
if (taginfo.tags().size() <= 0) {
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
if (save_tags) {
|
||||
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
|
||||
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
|
||||
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
|
||||
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
|
||||
}
|
||||
if (save_playcount) {
|
||||
SaveSongPlaycountToFile(tag, song.playcount());
|
||||
}
|
||||
if (save_rating) {
|
||||
SaveSongRatingToFile(tag, song.rating());
|
||||
}
|
||||
if (save_cover) {
|
||||
SaveEmbeddedArt(tag, cover.data);
|
||||
}
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::LoadEmbeddedArt(const QString &filename, QByteArray &data) const {
|
||||
|
||||
if (filename.isEmpty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
qLog(Debug) << "Loading art from" << filename;
|
||||
|
||||
try {
|
||||
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
|
||||
taginfo.open();
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
|
||||
data = QByteArray(tag->value(TagParser::KnownField::Cover).dataPointer(), tag->value(TagParser::KnownField::Cover).dataSize());
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::Success;
|
||||
}
|
||||
}
|
||||
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const {
|
||||
|
||||
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const {
|
||||
|
||||
if (request.filename().empty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
qLog(Debug) << "Saving art to" << filename;
|
||||
|
||||
const Cover cover = LoadCoverFromRequest(filename, request);
|
||||
|
||||
try {
|
||||
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
|
||||
taginfo.open();
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
if (taginfo.tags().size() <= 0) {
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
SaveEmbeddedArt(tag, cover.data);
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveSongPlaycountToFile(TagParser::Tag *tag, const uint playcount) const {
|
||||
|
||||
Q_UNUSED(tag);
|
||||
Q_UNUSED(playcount);
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::SaveSongPlaycountToFile(const QString &filename, const uint playcount) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(playcount);
|
||||
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveSongRatingToFile(TagParser::Tag *tag, const float rating) const {
|
||||
|
||||
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(rating)));
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const float rating) const {
|
||||
|
||||
if (filename.isEmpty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
qLog(Debug) << "Saving song rating to" << filename;
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
taginfo.open(false);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
if (taginfo.tags().size() <= 0) {
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
SaveSongRatingToFile(tag, rating);
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERTAGPARSER_H
|
||||
#define TAGREADERTAGPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <tagparser/tag.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
#include "tagreaderbase.h"
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
*/
|
||||
class TagReaderTagParser : public TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderTagParser();
|
||||
~TagReaderTagParser();
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const override;
|
||||
|
||||
Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const override;
|
||||
Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const override;
|
||||
Result SaveSongRatingToFile(const QString &filename, const float rating) const override;
|
||||
|
||||
private:
|
||||
void SaveSongPlaycountToFile(TagParser::Tag *tag, const uint playcount) const;
|
||||
void SaveSongRatingToFile(TagParser::Tag *tag, const float rating) const;
|
||||
void SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const;
|
||||
|
||||
public:
|
||||
Q_DISABLE_COPY(TagReaderTagParser)
|
||||
};
|
||||
|
||||
#endif // TAGREADERTAGPARSER_H
|
||||
@@ -1,15 +0,0 @@
|
||||
qt_wrap_cpp(MACDEPLOYCHECK_MOC ${CMAKE_SOURCE_DIR}/ext/libstrawberry-common/core/logging.h)
|
||||
link_directories(${GLIB_LIBRARY_DIRS})
|
||||
add_executable(macdeploycheck macdeploycheck.cpp ${CMAKE_SOURCE_DIR}/ext/libstrawberry-common/core/logging.cpp ${MACDEPLOYCHECK_MOC})
|
||||
target_include_directories(macdeploycheck PUBLIC SYSTEM
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
)
|
||||
target_include_directories(macdeploycheck PUBLIC
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-common
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
target_link_libraries(macdeploycheck PUBLIC
|
||||
"-framework AppKit"
|
||||
${GLIB_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
)
|
||||
@@ -1,147 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
int main(int argc, char **argv);
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
logging::Init();
|
||||
|
||||
qLog(Info) << "Running macdeploycheck";
|
||||
|
||||
if (argc < 1) {
|
||||
qLog(Error) << "Usage: macdeploycheck <bundledir>";
|
||||
return 1;
|
||||
}
|
||||
QString bundle_path = QString::fromLocal8Bit(argv[1]);
|
||||
|
||||
bool success = true;
|
||||
|
||||
QDirIterator iter(bundle_path, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories);
|
||||
while (iter.hasNext()) {
|
||||
|
||||
iter.next();
|
||||
|
||||
QString filepath = iter.fileInfo().filePath();
|
||||
|
||||
// Ignore these files.
|
||||
if (filepath.endsWith(".plist") ||
|
||||
filepath.endsWith(".icns") ||
|
||||
filepath.endsWith(".prl") ||
|
||||
filepath.endsWith(".conf") ||
|
||||
filepath.endsWith(".h") ||
|
||||
filepath.endsWith(".nib") ||
|
||||
filepath.endsWith(".strings") ||
|
||||
filepath.endsWith(".css") ||
|
||||
filepath.endsWith("CodeResources") ||
|
||||
filepath.endsWith("PkgInfo") ||
|
||||
filepath.endsWith(".modulemap")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QProcess otool;
|
||||
otool.start("otool", QStringList() << "-L" << filepath);
|
||||
otool.waitForFinished();
|
||||
if (otool.exitStatus() != QProcess::NormalExit || otool.exitCode() != 0) {
|
||||
qLog(Error) << "otool failed for" << filepath << ":" << otool.readAllStandardError();
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
QString output = otool.readAllStandardOutput();
|
||||
QStringList output_lines = output.split("\n", Qt::SkipEmptyParts);
|
||||
if (output_lines.size() < 2) {
|
||||
qLog(Error) << "Could not parse otool output:" << output;
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
QString first_line = output_lines.first();
|
||||
if (first_line.endsWith(':')) first_line.chop(1);
|
||||
if (first_line == filepath) {
|
||||
output_lines.removeFirst();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "First line" << first_line << "does not match" << filepath;
|
||||
success = false;
|
||||
}
|
||||
QRegularExpression regexp(QStringLiteral("^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), current version (\\d+\\.\\d+\\.\\d+)(, weak|, reexport)?\\)$"));
|
||||
for (const QString &output_line : output_lines) {
|
||||
|
||||
//qDebug() << "Final check on" << filepath << output_line;
|
||||
|
||||
QRegularExpressionMatch match = regexp.match(output_line);
|
||||
if (match.hasMatch()) {
|
||||
QString library = match.captured(1);
|
||||
if (QFileInfo(library).fileName() == QFileInfo(filepath).fileName()) { // It's this.
|
||||
continue;
|
||||
}
|
||||
else if (library.startsWith("@executable_path")) {
|
||||
QString real_path = library;
|
||||
real_path = real_path.replace("@executable_path", bundle_path + "/Contents/MacOS");
|
||||
if (!QFile::exists(real_path)) {
|
||||
qLog(Error) << real_path << "does not exist for" << filepath;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else if (library.startsWith("@rpath")) {
|
||||
QString real_path = library;
|
||||
real_path = real_path.replace("@rpath", bundle_path + "/Contents/Frameworks");
|
||||
if (!QFile::exists(real_path) && !real_path.endsWith("QtSvg")) { // FIXME: Ignore broken svg image plugin.
|
||||
qLog(Error) << real_path << "does not exist for" << filepath;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else if (library.startsWith("@loader_path")) {
|
||||
QString loader_path = QFileInfo(filepath).path();
|
||||
QString real_path = library;
|
||||
real_path = real_path.replace("@loader_path", loader_path);
|
||||
if (!QFile::exists(real_path)) {
|
||||
qLog(Error) << real_path << "does not exist for" << filepath;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else if (library.startsWith("/System/Library/") || library.startsWith("/usr/lib/")) { // System library
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "File" << filepath << "points to" << library;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Could not parse otool output line:" << output_line;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success ? 0 : 1;
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
set(SOURCES main.cpp tagreaderworker.cpp)
|
||||
set(HEADERS tagreaderworker.h)
|
||||
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
link_directories(${GLIB_LIBRARY_DIRS})
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
link_directories(${TAGLIB_LIBRARY_DIRS})
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
link_directories(${TAGPARSER_LIBRARY_DIRS})
|
||||
endif()
|
||||
|
||||
add_executable(strawberry-tagreader ${SOURCES} ${MOC} ${QRC})
|
||||
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${PROTOBUF_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(strawberry-tagreader PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-common
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-tagreader
|
||||
${CMAKE_BINARY_DIR}/ext/libstrawberry-tagreader
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
|
||||
target_link_libraries(strawberry-tagreader PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
libstrawberry-common
|
||||
libstrawberry-tagreader
|
||||
)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE ${TAGLIB_INCLUDE_DIRS})
|
||||
target_link_libraries(strawberry-tagreader PRIVATE ${TAGLIB_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE ${TAGPARSER_INCLUDE_DIRS})
|
||||
target_link_libraries(strawberry-tagreader PRIVATE ${TAGPARSER_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(FREEBSD)
|
||||
target_link_libraries(strawberry-tagreader PRIVATE execinfo)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(strawberry-tagreader PRIVATE /System/Library/Frameworks/Foundation.framework)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
install(TARGETS strawberry-tagreader DESTINATION ${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns)
|
||||
else()
|
||||
install(TARGETS strawberry-tagreader RUNTIME DESTINATION bin)
|
||||
endif()
|
||||
@@ -1,61 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QLocalSocket>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "tagreaderworker.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
QCoreApplication a(argc, argv);
|
||||
QStringList args(a.arguments());
|
||||
|
||||
if (args.count() != 2) {
|
||||
std::cerr << "This program is used internally by Strawberry to parse tags in music files\n"
|
||||
"without exposing the whole application to crashes caused by malformed\n"
|
||||
"files. It is not meant to be run on its own.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
logging::Init();
|
||||
qLog(Info) << "TagReader worker connecting to" << args[1];
|
||||
|
||||
// Connect to the parent process.
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(args[1]);
|
||||
if (!socket.waitForConnected(2000)) {
|
||||
std::cerr << "Failed to connect to the parent process.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
TagReaderWorker worker(&socket);
|
||||
|
||||
return a.exec();
|
||||
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QObject>
|
||||
#include <QIODevice>
|
||||
#include <QByteArray>
|
||||
|
||||
#include "tagreaderworker.h"
|
||||
|
||||
#ifdef HAVE_TAGLIB
|
||||
# include "tagreadertaglib.h"
|
||||
# include "tagreadergme.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_TAGPARSER
|
||||
# include "tagreadertagparser.h"
|
||||
#endif
|
||||
|
||||
using std::make_shared;
|
||||
using std::shared_ptr;
|
||||
|
||||
TagReaderWorker::TagReaderWorker(QIODevice *socket, QObject *parent)
|
||||
: AbstractMessageHandler<spb::tagreader::Message>(socket, parent) {
|
||||
|
||||
#ifdef HAVE_TAGLIB
|
||||
tagreaders_ << make_shared<TagReaderTagLib>();
|
||||
tagreaders_ << make_shared<TagReaderGME>();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_TAGPARSER
|
||||
tagreaders_ << make_shared<TagReaderTagParser>();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
|
||||
|
||||
spb::tagreader::Message reply;
|
||||
|
||||
HandleMessage(message, reply);
|
||||
SendReply(message, &reply);
|
||||
|
||||
}
|
||||
|
||||
void TagReaderWorker::DeviceClosed() {
|
||||
|
||||
AbstractMessageHandler<spb::tagreader::Message>::DeviceClosed();
|
||||
|
||||
QCoreApplication::exit();
|
||||
|
||||
}
|
||||
|
||||
void TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply) {
|
||||
|
||||
for (shared_ptr<TagReaderBase> reader : tagreaders_) {
|
||||
|
||||
if (message.has_is_media_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.is_media_file_request().filename());
|
||||
const bool success = reader->IsMediaFile(filename);
|
||||
reply.mutable_is_media_file_response()->set_success(success);
|
||||
if (success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (message.has_read_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.read_file_request().filename());
|
||||
spb::tagreader::ReadFileResponse *response = reply.mutable_read_file_response();
|
||||
const TagReaderBase::Result result = reader->ReadFile(filename, response->mutable_metadata());
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_write_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.write_file_request().filename());
|
||||
const TagReaderBase::Result result = reader->WriteFile(filename, message.write_file_request());
|
||||
spb::tagreader::WriteFileResponse *response = reply.mutable_write_file_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_load_embedded_art_request()) {
|
||||
const QString filename = QString::fromStdString(message.load_embedded_art_request().filename());
|
||||
QByteArray data;
|
||||
const TagReaderBase::Result result = reader->LoadEmbeddedArt(filename, data);
|
||||
spb::tagreader::LoadEmbeddedArtResponse *response = reply.mutable_load_embedded_art_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
response->set_data(data.toStdString());
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_save_embedded_art_request()) {
|
||||
const QString filename = QString::fromStdString(message.save_embedded_art_request().filename());
|
||||
const TagReaderBase::Result result = reader->SaveEmbeddedArt(filename, message.save_embedded_art_request());
|
||||
spb::tagreader::SaveEmbeddedArtResponse *response = reply.mutable_save_embedded_art_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_save_song_playcount_to_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.save_song_playcount_to_file_request().filename());
|
||||
const TagReaderBase::Result result = reader->SaveSongPlaycountToFile(filename, message.save_song_playcount_to_file_request().playcount());
|
||||
spb::tagreader::SaveSongPlaycountToFileResponse *response = reply.mutable_save_song_playcount_to_file_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_save_song_rating_to_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.save_song_rating_to_file_request().filename());
|
||||
const TagReaderBase::Result result = reader->SaveSongRatingToFile(filename, message.save_song_rating_to_file_request().rating());
|
||||
spb::tagreader::SaveSongRatingToFileResponse *response = reply.mutable_save_song_rating_to_file_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERWORKER_H
|
||||
#define TAGREADERWORKER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
|
||||
#include "core/messagehandler.h"
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
class QIODevice;
|
||||
class TagReaderBase;
|
||||
|
||||
using std::shared_ptr;
|
||||
|
||||
class TagReaderWorker : public AbstractMessageHandler<spb::tagreader::Message> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagReaderWorker(QIODevice *socket, QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void MessageArrived(const spb::tagreader::Message &message) override;
|
||||
void DeviceClosed() override;
|
||||
|
||||
private:
|
||||
void HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply);
|
||||
|
||||
QList<shared_ptr<TagReaderBase>> tagreaders_;
|
||||
};
|
||||
|
||||
#endif // TAGREADERWORKER_H
|
||||
1304
src/CMakeLists.txt
1304
src/CMakeLists.txt
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVector>
|
||||
#include <QList>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QBasicTimer>
|
||||
@@ -50,9 +50,9 @@
|
||||
// Make an INSTRUCTIONS file
|
||||
// can't mod scope in analyze you have to use transform for 2D use setErasePixmap Qt function insetead of m_background
|
||||
|
||||
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scopeSize)
|
||||
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scope_size)
|
||||
: QWidget(parent),
|
||||
fht_(new FHT(scopeSize)),
|
||||
fht_(new FHT(scope_size)),
|
||||
engine_(nullptr),
|
||||
lastscope_(512),
|
||||
new_frame_(false),
|
||||
@@ -67,11 +67,13 @@ AnalyzerBase::~AnalyzerBase() {
|
||||
delete fht_;
|
||||
}
|
||||
|
||||
void AnalyzerBase::showEvent(QShowEvent*) {
|
||||
void AnalyzerBase::showEvent(QShowEvent *e) {
|
||||
Q_UNUSED(e)
|
||||
timer_.start(timeout(), this);
|
||||
}
|
||||
|
||||
void AnalyzerBase::hideEvent(QHideEvent*) {
|
||||
void AnalyzerBase::hideEvent(QHideEvent *e) {
|
||||
Q_UNUSED(e)
|
||||
timer_.stop();
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ void AnalyzerBase::ChangeTimeout(const int timeout) {
|
||||
|
||||
void AnalyzerBase::transform(Scope &scope) {
|
||||
|
||||
QVector<float> aux(fht_->size());
|
||||
QList<float> aux(fht_->size());
|
||||
if (static_cast<quint64>(aux.size()) >= scope.size()) {
|
||||
std::copy(scope.begin(), scope.end(), aux.begin());
|
||||
}
|
||||
@@ -98,7 +100,7 @@ void AnalyzerBase::transform(Scope &scope) {
|
||||
fht_->logSpectrum(scope.data(), aux.data());
|
||||
fht_->scale(scope.data(), 1.0F / 20);
|
||||
|
||||
scope.resize(fht_->size() / 2); // second half of values are rubbish
|
||||
scope.resize(static_cast<size_t>(fht_->size() / 2)); // second half of values are rubbish
|
||||
|
||||
}
|
||||
|
||||
@@ -110,7 +112,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
|
||||
switch (engine_->state()) {
|
||||
case EngineBase::State::Playing:{
|
||||
const EngineBase::Scope &thescope = engine_->scope(timeout_);
|
||||
int i = 0;
|
||||
size_t i = 0;
|
||||
|
||||
// convert to mono here - our built in analyzers need mono, but the engines provide interleaved pcm
|
||||
for (uint x = 0; static_cast<int>(x) < fht_->size(); ++x) {
|
||||
@@ -122,7 +124,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
|
||||
transform(lastscope_);
|
||||
analyze(p, lastscope_, new_frame_);
|
||||
|
||||
lastscope_.resize(fht_->size());
|
||||
lastscope_.resize(static_cast<size_t>(fht_->size()));
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -151,7 +153,7 @@ int AnalyzerBase::resizeExponent(int exp) {
|
||||
|
||||
if (exp != fht_->sizeExp()) {
|
||||
delete fht_;
|
||||
fht_ = new FHT(exp);
|
||||
fht_ = new FHT(static_cast<uint>(exp));
|
||||
}
|
||||
return exp;
|
||||
|
||||
@@ -209,28 +211,28 @@ void AnalyzerBase::demo(QPainter &p) {
|
||||
|
||||
}
|
||||
|
||||
void AnalyzerBase::interpolate(const Scope &inVec, Scope &outVec) {
|
||||
void AnalyzerBase::interpolate(const Scope &in_scope, Scope &out_scope) {
|
||||
|
||||
double pos = 0.0;
|
||||
const double step = static_cast<double>(inVec.size()) / static_cast<double>(outVec.size());
|
||||
const double step = static_cast<double>(in_scope.size()) / static_cast<double>(out_scope.size());
|
||||
|
||||
for (uint i = 0; i < outVec.size(); ++i, pos += step) {
|
||||
for (uint i = 0; i < out_scope.size(); ++i, pos += step) {
|
||||
const double error = pos - std::floor(pos);
|
||||
const uint64_t offset = static_cast<uint64_t>(pos);
|
||||
|
||||
uint64_t indexLeft = offset + 0;
|
||||
|
||||
if (indexLeft >= inVec.size()) {
|
||||
indexLeft = inVec.size() - 1;
|
||||
if (indexLeft >= in_scope.size()) {
|
||||
indexLeft = in_scope.size() - 1;
|
||||
}
|
||||
|
||||
uint64_t indexRight = offset + 1;
|
||||
|
||||
if (indexRight >= inVec.size()) {
|
||||
indexRight = inVec.size() - 1;
|
||||
if (indexRight >= in_scope.size()) {
|
||||
indexRight = in_scope.size() - 1;
|
||||
}
|
||||
|
||||
outVec[i] = inVec[indexLeft] * (1.0F - static_cast<float>(error)) + inVec[indexRight] * static_cast<float>(error);
|
||||
out_scope[i] = in_scope[indexLeft] * (1.0F - static_cast<float>(error)) + in_scope[indexRight] * static_cast<float>(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef ANALYZERBASE_H
|
||||
#define ANALYZERBASE_H
|
||||
@@ -31,14 +31,12 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QBasicTimer>
|
||||
#include <QString>
|
||||
#include <QPainter>
|
||||
|
||||
#include "core/shared_ptr.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "analyzer/fht.h"
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
@@ -63,22 +61,22 @@ class AnalyzerBase : public QWidget {
|
||||
|
||||
protected:
|
||||
using Scope = std::vector<float>;
|
||||
explicit AnalyzerBase(QWidget*, const uint scopeSize = 7);
|
||||
explicit AnalyzerBase(QWidget *parent, const uint scope_size = 7);
|
||||
|
||||
void hideEvent(QHideEvent*) override;
|
||||
void showEvent(QShowEvent*) override;
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void showEvent(QShowEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void timerEvent(QTimerEvent *e) override;
|
||||
|
||||
int resizeExponent(int);
|
||||
int resizeForBands(const int);
|
||||
int resizeExponent(int exp);
|
||||
int resizeForBands(const int bands);
|
||||
virtual void init() {}
|
||||
virtual void transform(Scope&);
|
||||
virtual void analyze(QPainter &p, const Scope&, const bool new_frame) = 0;
|
||||
virtual void transform(Scope &scope);
|
||||
virtual void analyze(QPainter &p, const Scope &s, const bool new_frame) = 0;
|
||||
virtual void demo(QPainter &p);
|
||||
|
||||
void interpolate(const Scope&, Scope&);
|
||||
void initSin(Scope&, const uint = 6000);
|
||||
void interpolate(const Scope &in_scope, Scope &out_scope);
|
||||
void initSin(Scope &v, const uint size = 6000);
|
||||
|
||||
protected:
|
||||
QBasicTimer timer_;
|
||||
@@ -92,4 +90,3 @@ class AnalyzerBase : public QWidget {
|
||||
};
|
||||
|
||||
#endif // ANALYZERBASE_H
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -44,12 +44,13 @@
|
||||
#include "waverubberanalyzer.h"
|
||||
#include "rainbowanalyzer.h"
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/settings.h"
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const char *AnalyzerContainer::kSettingsGroup = "Analyzer";
|
||||
const char *AnalyzerContainer::kSettingsFramerate = "framerate";
|
||||
@@ -60,7 +61,7 @@ constexpr int kLowFramerate = 20;
|
||||
constexpr int kMediumFramerate = 25;
|
||||
constexpr int kHighFramerate = 30;
|
||||
constexpr int kSuperHighFramerate = 60;
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
@@ -111,16 +112,8 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
|
||||
void AnalyzerContainer::mouseReleaseEvent(QMouseEvent *e) {
|
||||
|
||||
if (engine_->type() != EngineBase::Type::GStreamer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e->button() == Qt::RightButton) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
context_menu_->popup(e->globalPosition().toPoint());
|
||||
#else
|
||||
context_menu_->popup(e->globalPos());
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -130,7 +123,7 @@ void AnalyzerContainer::ShowPopupMenu() {
|
||||
}
|
||||
|
||||
void AnalyzerContainer::wheelEvent(QWheelEvent *e) {
|
||||
emit WheelEvent(e->angleDelta().y());
|
||||
Q_EMIT WheelEvent(e->angleDelta().y());
|
||||
}
|
||||
|
||||
void AnalyzerContainer::SetEngine(SharedPtr<EngineBase> engine) {
|
||||
@@ -141,15 +134,17 @@ void AnalyzerContainer::SetEngine(SharedPtr<EngineBase> engine) {
|
||||
}
|
||||
|
||||
void AnalyzerContainer::DisableAnalyzer() {
|
||||
|
||||
delete current_analyzer_;
|
||||
current_analyzer_ = nullptr;
|
||||
|
||||
Save();
|
||||
|
||||
}
|
||||
|
||||
void AnalyzerContainer::ChangeAnalyzer(const int id) {
|
||||
|
||||
QObject *instance = analyzer_types_[id]->newInstance(Q_ARG(QWidget*, this));
|
||||
QObject *instance = analyzer_types_.at(id)->newInstance(Q_ARG(QWidget*, this));
|
||||
|
||||
if (!instance) {
|
||||
qLog(Warning) << "Couldn't initialize a new" << analyzer_types_[id]->className();
|
||||
@@ -187,7 +182,7 @@ void AnalyzerContainer::Load() {
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
QString type = s.value("type", QStringLiteral("BlockAnalyzer")).toString();
|
||||
QString type = s.value("type", u"BlockAnalyzer"_s).toString();
|
||||
current_framerate_ = s.value(kSettingsFramerate, kMediumFramerate).toInt();
|
||||
s.endGroup();
|
||||
|
||||
@@ -200,18 +195,25 @@ void AnalyzerContainer::Load() {
|
||||
for (int i = 0; i < analyzer_types_.count(); ++i) {
|
||||
if (type == QString::fromLatin1(analyzer_types_[i]->className())) {
|
||||
ChangeAnalyzer(i);
|
||||
actions_[i]->setChecked(true);
|
||||
QAction *action = actions_.value(i);
|
||||
action->setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!current_analyzer_) {
|
||||
ChangeAnalyzer(0);
|
||||
QAction *action = actions_.value(0);
|
||||
action->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Framerate
|
||||
QList<QAction*> actions = group_framerate_->actions();
|
||||
const QList<QAction*> actions = group_framerate_->actions();
|
||||
for (int i = 0; i < framerate_list_.count(); ++i) {
|
||||
if (current_framerate_ == framerate_list_[i]) {
|
||||
if (current_framerate_ == framerate_list_.value(i)) {
|
||||
ChangeFramerate(current_framerate_);
|
||||
actions[i]->setChecked(true);
|
||||
QAction *action = actions[i];
|
||||
action->setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef ANALYZERCONTAINER_H
|
||||
#define ANALYZERCONTAINER_H
|
||||
@@ -30,7 +30,7 @@
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
|
||||
#include "core/shared_ptr.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
class QTimer;
|
||||
@@ -50,14 +50,14 @@ class AnalyzerContainer : public QWidget {
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kSettingsFramerate;
|
||||
|
||||
signals:
|
||||
Q_SIGNALS:
|
||||
void WheelEvent(const int delta);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
|
||||
private slots:
|
||||
private Q_SLOTS:
|
||||
void ChangeAnalyzer(const int id);
|
||||
void ChangeFramerate(int new_framerate);
|
||||
void DisableAnalyzer();
|
||||
@@ -101,8 +101,7 @@ void AnalyzerContainer::AddAnalyzerType() {
|
||||
group_->addAction(action);
|
||||
action->setCheckable(true);
|
||||
actions_ << action;
|
||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); } );
|
||||
|
||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); });
|
||||
}
|
||||
|
||||
#endif // ANALYZERCONTAINER_H
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "blockanalyzer.h"
|
||||
|
||||
@@ -36,12 +36,14 @@
|
||||
#include "analyzerbase.h"
|
||||
#include "fht.h"
|
||||
|
||||
const int BlockAnalyzer::kHeight = 2;
|
||||
const int BlockAnalyzer::kWidth = 4;
|
||||
const int BlockAnalyzer::kMinRows = 3; // arbitrary
|
||||
const int BlockAnalyzer::kMinColumns = 32; // arbitrary
|
||||
const int BlockAnalyzer::kMaxColumns = 256; // must be 2**n
|
||||
const int BlockAnalyzer::kFadeSize = 90;
|
||||
namespace {
|
||||
constexpr int kHeight = 2;
|
||||
constexpr int kWidth = 4;
|
||||
constexpr int kMinRows = 3; // arbitrary
|
||||
constexpr int kMinColumns = 32; // arbitrary
|
||||
constexpr int kMaxColumns = 256; // must be 2**n
|
||||
constexpr int kFadeSize = 90;
|
||||
} // namespace
|
||||
|
||||
const char *BlockAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
|
||||
|
||||
@@ -59,11 +61,12 @@ BlockAnalyzer::BlockAnalyzer(QWidget *parent)
|
||||
fade_intensity_(1 << 8, 32),
|
||||
step_(0) {
|
||||
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); //-1 is padding, no drawing takes place there
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); // -1 is padding, no drawing takes place there
|
||||
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
||||
|
||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
||||
|
||||
}
|
||||
|
||||
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
||||
@@ -83,7 +86,7 @@ void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
||||
// this is the y-offset for drawing from the top of the widget
|
||||
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
|
||||
|
||||
scope_.resize(columns_);
|
||||
scope_.resize(static_cast<size_t>(columns_));
|
||||
|
||||
if (rows_ != oldRows) {
|
||||
barpixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
|
||||
@@ -163,35 +166,37 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
|
||||
// Paint the background
|
||||
canvas_painter.drawPixmap(0, 0, background_);
|
||||
|
||||
for (int x = 0, y = 0; x < static_cast<int>(scope_.size()); ++x) {
|
||||
for (qint64 x = 0, y = 0; x < static_cast<qint64>(scope_.size()); ++x) {
|
||||
// determine y
|
||||
for (y = 0; scope_[x] < yscale_[y]; ++y);
|
||||
for (y = 0; scope_[static_cast<quint64>(x)] < yscale_.at(y); ++y);
|
||||
|
||||
// This is opposite to what you'd think, higher than y means the bar is lower than y (physically)
|
||||
if (static_cast<double>(y) > store_[x]) {
|
||||
y = static_cast<int>(store_[x] += step_);
|
||||
if (static_cast<double>(y) > store_.at(x)) {
|
||||
store_[x] += step_;
|
||||
y = static_cast<int>(store_.value(x));
|
||||
}
|
||||
else {
|
||||
store_[x] = y;
|
||||
store_[x] = static_cast<double>(y);
|
||||
}
|
||||
|
||||
// If y is lower than fade_pos_, then the bar has exceeded the height of the fadeout
|
||||
// if the fadeout is quite faded now, then display the new one
|
||||
if (y <= fade_pos_[x] /*|| fade_intensity_[x] < kFadeSize / 3*/) {
|
||||
fade_pos_[x] = y;
|
||||
if (y <= fade_pos_.at(x) /*|| fade_intensity_[x] < kFadeSize / 3*/) {
|
||||
fade_pos_[x] = static_cast<int>(y);
|
||||
fade_intensity_[x] = kFadeSize;
|
||||
}
|
||||
|
||||
if (fade_intensity_[x] > 0) {
|
||||
const int offset = --fade_intensity_[x];
|
||||
const int y2 = y_ + (fade_pos_[x] * (kHeight + 1));
|
||||
canvas_painter.drawPixmap(x * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
|
||||
if (fade_intensity_.at(x) > 0) {
|
||||
--fade_intensity_[x];
|
||||
const int offset = fade_intensity_.value(x);
|
||||
const int y2 = y_ + (fade_pos_.value(x) * (kHeight + 1));
|
||||
canvas_painter.drawPixmap(static_cast<int>(x) * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
|
||||
}
|
||||
|
||||
if (fade_intensity_[x] == 0) fade_pos_[x] = rows_;
|
||||
if (fade_intensity_.at(x) == 0) fade_pos_[x] = rows_;
|
||||
|
||||
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing, rows_ means none are
|
||||
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(), 0, y * (kHeight + 1), bar()->width(), bar()->height());
|
||||
canvas_painter.drawPixmap(static_cast<int>(x) * (kWidth + 1), static_cast<int>(y) * (kHeight + 1) + y_, *bar(), 0, static_cast<int>(y) * (kHeight + 1), bar()->width(), bar()->height());
|
||||
}
|
||||
|
||||
for (int x = 0; x < store_.size(); ++x) {
|
||||
@@ -233,7 +238,7 @@ static inline void adjustToLimits(const int b, int &f, int &amount) {
|
||||
* Clever contrast function
|
||||
*
|
||||
* It will try to adjust the foreground color such that it contrasts well with
|
||||
*the background
|
||||
* the background
|
||||
* It won't modify the hue of fg unless absolutely necessary
|
||||
* @return the adjusted form of fg
|
||||
*/
|
||||
@@ -336,7 +341,9 @@ QColor ensureContrast(const QColor &bg, const QColor &fg, int amount) {
|
||||
|
||||
}
|
||||
|
||||
void BlockAnalyzer::paletteChange(const QPalette&) {
|
||||
void BlockAnalyzer::paletteChange(const QPalette &_palette) {
|
||||
|
||||
Q_UNUSED(_palette)
|
||||
|
||||
const QColor bg = palette().color(QPalette::Window);
|
||||
const QColor fg = ensureContrast(bg, palette().color(QPalette::Highlight));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user