mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-09-27 06:40:51 +02:00
Compare commits
610 Commits
v0.25.2
...
frankenpip
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6c3f31a721 | ||
![]() |
10163e1082 | ||
![]() |
0911d1ce7d | ||
![]() |
df3b56ed63 | ||
![]() |
cf351c28b0 | ||
![]() |
4409a990de | ||
![]() |
16b372dece | ||
![]() |
c02fb89359 | ||
![]() |
fbafdeb2ca | ||
![]() |
781040efaa | ||
![]() |
3f7ef49979 | ||
![]() |
dab0148a78 | ||
![]() |
c0c08a4f63 | ||
![]() |
aaf337421d | ||
![]() |
79a0edacd7 | ||
![]() |
d56eef6ece | ||
![]() |
72f054a4fa | ||
![]() |
172c3c92ac | ||
![]() |
137ef3fee4 | ||
![]() |
e49156fb11 | ||
![]() |
de5d45849f | ||
![]() |
a25034b898 | ||
![]() |
ae9e82b2c1 | ||
![]() |
a70b38a8e5 | ||
![]() |
0cff3a6ecd | ||
![]() |
9b78e49e45 | ||
![]() |
4e55f1bee6 | ||
![]() |
cff3834fde | ||
![]() |
c8b01a06b0 | ||
![]() |
414b1a8344 | ||
![]() |
404d9f3fac | ||
![]() |
55e4014036 | ||
![]() |
1cd5563b27 | ||
![]() |
1abced992b | ||
![]() |
46b9243661 | ||
![]() |
ad72b2cb31 | ||
![]() |
8b79d0ee29 | ||
![]() |
295f719b77 | ||
![]() |
b584353f4d | ||
![]() |
d73314b4dd | ||
![]() |
9f4a33c7a8 | ||
![]() |
3a9540b042 | ||
![]() |
ca855cbca0 | ||
![]() |
6a98b1dac7 | ||
![]() |
7d4a2836fc | ||
![]() |
226b6de34f | ||
![]() |
13585ca0be | ||
![]() |
62ab9bd740 | ||
![]() |
fdf36cbad6 | ||
![]() |
aea2b7c7f3 | ||
![]() |
37d1c784fa | ||
![]() |
cea149f852 | ||
![]() |
a92a28517e | ||
![]() |
800961c3d7 | ||
![]() |
9d8a79b0bd | ||
![]() |
ef56dea817 | ||
![]() |
23b3835af0 | ||
![]() |
412e1d602a | ||
![]() |
802a094154 | ||
![]() |
e6b1341246 | ||
![]() |
36ede243e3 | ||
![]() |
bac9f7eebf | ||
![]() |
8ada566bf1 | ||
![]() |
5bd4ed77df | ||
![]() |
97652ac015 | ||
![]() |
6dd24033a4 | ||
![]() |
4de3ef20be | ||
![]() |
702f74291d | ||
![]() |
d8759993a9 | ||
![]() |
7787eafd3a | ||
![]() |
4f4136c6e9 | ||
![]() |
b399030e19 | ||
![]() |
0991461d04 | ||
![]() |
49bcf2c41b | ||
![]() |
c00c6c460c | ||
![]() |
4c4fe3f511 | ||
![]() |
db485c3d77 | ||
![]() |
6471b64ab6 | ||
![]() |
b9fcf0dff8 | ||
![]() |
3177ca6e8a | ||
![]() |
5017f4f05a | ||
![]() |
035c394cf6 | ||
![]() |
823d4a041f | ||
![]() |
62d4044d6c | ||
![]() |
3785404618 | ||
![]() |
4cac111b66 | ||
![]() |
941b8eb194 | ||
![]() |
b1add13bfd | ||
![]() |
5fffee2c7d | ||
![]() |
f9340ae604 | ||
![]() |
d3a6991fd4 | ||
![]() |
b0bfd4a807 | ||
![]() |
3641698379 | ||
![]() |
2836191fb3 | ||
![]() |
0df6c7fc2c | ||
![]() |
b1ebd3ecd9 | ||
![]() |
4758244cf5 | ||
![]() |
294b9cf347 | ||
![]() |
fad3120b00 | ||
![]() |
6d05af484e | ||
![]() |
38c823a042 | ||
![]() |
e082bca5e0 | ||
![]() |
f9dae9078e | ||
![]() |
e955beeef1 | ||
![]() |
eaac7f3f85 | ||
![]() |
ea414f57d4 | ||
![]() |
f984b26626 | ||
![]() |
edab9a6a1f | ||
![]() |
4740e3be86 | ||
![]() |
e639b02fed | ||
![]() |
ac1ca1412d | ||
![]() |
d131d3399a | ||
![]() |
1009dc4d4e | ||
![]() |
42cb914616 | ||
![]() |
e72da94eb1 | ||
![]() |
c5d94a5b60 | ||
![]() |
02c5f2607a | ||
![]() |
369a46f8fe | ||
![]() |
909d214002 | ||
![]() |
5e7e14ee4d | ||
![]() |
b092fe2c76 | ||
![]() |
b9dd7078ad | ||
![]() |
93310955f2 | ||
![]() |
9c52e039ee | ||
![]() |
be037e0756 | ||
![]() |
5bfb0449cf | ||
![]() |
0ec81c9e52 | ||
![]() |
5841eaa6d7 | ||
![]() |
e92ba8f5d1 | ||
![]() |
1908e18dc4 | ||
![]() |
e30d5e4305 | ||
![]() |
11bb2495ba | ||
![]() |
341cc37ce7 | ||
![]() |
1620668966 | ||
![]() |
56c80ce6dd | ||
![]() |
8ce9a7e43c | ||
![]() |
e05d97732e | ||
![]() |
644a345b55 | ||
![]() |
bda961a04c | ||
![]() |
ba2efded76 | ||
![]() |
b05b98ca61 | ||
![]() |
7a7f81ac7f | ||
![]() |
6e6c171dd7 | ||
![]() |
8a41c8cf66 | ||
![]() |
05271d95a9 | ||
![]() |
9d04a73c85 | ||
![]() |
d336f4cef2 | ||
![]() |
51ee2f8d1e | ||
![]() |
d442b45836 | ||
![]() |
dbcb721dc2 | ||
![]() |
64a8f6575b | ||
![]() |
03a6b5c7b9 | ||
![]() |
56b6241311 | ||
![]() |
947ac2826a | ||
![]() |
0e8303f13a | ||
![]() |
4ec7532126 | ||
![]() |
da83646303 | ||
![]() |
72e9f7f9cf | ||
![]() |
ad6b676c81 | ||
![]() |
0f64158469 | ||
![]() |
acc5be92ac | ||
![]() |
0e0cee1bce | ||
![]() |
6f71c000ad | ||
![]() |
9f766ebf78 | ||
![]() |
5062d38b65 | ||
![]() |
82b492c050 | ||
![]() |
73e3a69aaf | ||
![]() |
348a79f91d | ||
![]() |
5e5e77f746 | ||
![]() |
c4ada7ff6e | ||
![]() |
39d0691c7e | ||
![]() |
71361de8ee | ||
![]() |
8aa2590fd3 | ||
![]() |
e3b7bf467e | ||
![]() |
f74402bc94 | ||
![]() |
4d3b4a7b20 | ||
![]() |
e6302cc868 | ||
![]() |
844b4edf48 | ||
![]() |
92a7f22d3c | ||
![]() |
03167a1e9c | ||
![]() |
1f309854bc | ||
![]() |
2ac0d1f13a | ||
![]() |
4eeea7b787 | ||
![]() |
e64c01d2da | ||
![]() |
0c7a91f852 | ||
![]() |
a2d93b389c | ||
![]() |
c795214abb | ||
![]() |
8583c48264 | ||
![]() |
2a3d133bcf | ||
![]() |
3e3d1fd265 | ||
![]() |
8645618f1a | ||
![]() |
e48ce5a103 | ||
![]() |
46139340fe | ||
![]() |
d479f29e9b | ||
![]() |
1af798b04b | ||
![]() |
7204407690 | ||
![]() |
e37336eef2 | ||
![]() |
879d7a24f0 | ||
![]() |
9e4ac2eacb | ||
![]() |
d9d6fff48f | ||
![]() |
9828586762 | ||
![]() |
8caaa6d297 | ||
![]() |
83ca6b9468 | ||
![]() |
24e65ef018 | ||
![]() |
a69bbab732 | ||
![]() |
a557ac3c7b | ||
![]() |
d61b4b89ea | ||
![]() |
b8daf16b92 | ||
![]() |
caa3812e13 | ||
![]() |
23a087c498 | ||
![]() |
c3c39a7b24 | ||
![]() |
00770fc634 | ||
![]() |
5bf77160f7 | ||
![]() |
d9da84c412 | ||
![]() |
b3a6318672 | ||
![]() |
67b41b970d | ||
![]() |
3738e30949 | ||
![]() |
0ba73b11c1 | ||
![]() |
13baaa31cd | ||
![]() |
f0db2aa43c | ||
![]() |
f704721b59 | ||
![]() |
7abf0f4886 | ||
![]() |
c915b6e68b | ||
![]() |
0b28c688c6 | ||
![]() |
2756ef6d2f | ||
![]() |
7da1d30010 | ||
![]() |
8e192acb63 | ||
![]() |
d8423499dc | ||
![]() |
974167fcb8 | ||
![]() |
6afdbd6fd3 | ||
![]() |
d8668ed226 | ||
![]() |
d75a6eaa41 | ||
![]() |
235fb92638 | ||
![]() |
ea18b4ea1f | ||
![]() |
58f5ec0181 | ||
![]() |
e42c9abdde | ||
![]() |
5e7ad6ffd1 | ||
![]() |
4c8238874e | ||
![]() |
38d4887901 | ||
![]() |
c9051d33c1 | ||
![]() |
3cc0205def | ||
![]() |
90979e2a81 | ||
![]() |
e66e1b542c | ||
![]() |
92e9c3e42e | ||
![]() |
4591c09637 | ||
![]() |
e1ce3fef1b | ||
![]() |
3c0a200f7b | ||
![]() |
bef5907ec3 | ||
![]() |
f0beb662aa | ||
![]() |
92402685f8 | ||
![]() |
3703fed1a5 | ||
![]() |
f4fb960c62 | ||
![]() |
a3bbbf03b4 | ||
![]() |
1d3a69a29f | ||
![]() |
10c57b15da | ||
![]() |
b85f7a6747 | ||
![]() |
3f94e7b638 | ||
![]() |
2af95cc1d4 | ||
![]() |
cefdefdfd2 | ||
![]() |
37f7fa7ef4 | ||
![]() |
e687eb5631 | ||
![]() |
88c3af7647 | ||
![]() |
ddd6c8cbf1 | ||
![]() |
81220f90d6 | ||
![]() |
e0268a91ad | ||
![]() |
29e4135aaa | ||
![]() |
5d9adce40d | ||
![]() |
d3afde8789 | ||
![]() |
d8a5d5545d | ||
![]() |
bed3516687 | ||
![]() |
3a014d8d46 | ||
![]() |
58ae7fbccb | ||
![]() |
b06a9618d4 | ||
![]() |
434c4a5cbc | ||
![]() |
c34d30dc17 | ||
![]() |
0d4c1bee3f | ||
![]() |
34a25d0be3 | ||
![]() |
3134f5e747 | ||
![]() |
1732584e5e | ||
![]() |
f50cafbac1 | ||
![]() |
bc7c3f48ad | ||
![]() |
b760419fd5 | ||
![]() |
5cf3c58d0e | ||
![]() |
206d1b6db4 | ||
![]() |
2e318b8b03 | ||
![]() |
5bdb6f18d6 | ||
![]() |
2e53a99361 | ||
![]() |
bec18e13d3 | ||
![]() |
7edd471ec5 | ||
![]() |
e6a4a3fa4f | ||
![]() |
de2a139340 | ||
![]() |
9d6ac67c46 | ||
![]() |
6f7b905983 | ||
![]() |
bcd4626008 | ||
![]() |
27730a20d6 | ||
![]() |
4aa0190175 | ||
![]() |
6dd62335e9 | ||
![]() |
32d2606a65 | ||
![]() |
2051334bba | ||
![]() |
575e809004 | ||
![]() |
66e8e2a696 | ||
![]() |
55373c95d9 | ||
![]() |
04bdc1cc0b | ||
![]() |
1d8850d1b2 | ||
![]() |
f98548698a | ||
![]() |
4b1824e8c1 | ||
![]() |
17e88f1749 | ||
![]() |
5edafca05a | ||
![]() |
2c4c283099 | ||
![]() |
9fb8125655 | ||
![]() |
aab6580195 | ||
![]() |
30f0db1d28 | ||
![]() |
5a4dae2070 | ||
![]() |
8345f348f6 | ||
![]() |
9220e32463 | ||
![]() |
845e72bf4a | ||
![]() |
49429ff40a | ||
![]() |
3df21ad25e | ||
![]() |
d0f4600be4 | ||
![]() |
0fa2e76c3e | ||
![]() |
9ff1b5230f | ||
![]() |
65eb631711 | ||
![]() |
6c99557553 | ||
![]() |
2b4357fa87 | ||
![]() |
cda4b3faaa | ||
![]() |
5d09a88335 | ||
![]() |
edd4f6b9f3 | ||
![]() |
1e7e2109d2 | ||
![]() |
b31d3831e6 | ||
![]() |
0f81a0504c | ||
![]() |
4a7fda95ae | ||
![]() |
ee3455e1e5 | ||
![]() |
f9fc1cd817 | ||
![]() |
76f1e588f7 | ||
![]() |
f3b458c803 | ||
![]() |
00566ed4d4 | ||
![]() |
e4a07411b8 | ||
![]() |
2c1bb2706f | ||
![]() |
aa84d6fc8f | ||
![]() |
d76e9b0bd8 | ||
![]() |
b4016c91c1 | ||
![]() |
5f32d001cc | ||
![]() |
8c9287d0c8 | ||
![]() |
3f37e27852 | ||
![]() |
f41ab8b086 | ||
![]() |
ad68f784ae | ||
![]() |
4b6392df54 | ||
![]() |
94ea329b50 | ||
![]() |
591ed2e01f | ||
![]() |
78cf9aaa7d | ||
![]() |
f9494a294f | ||
![]() |
0dd4553700 | ||
![]() |
4f7b36cd70 | ||
![]() |
5d350aec87 | ||
![]() |
059db6fb31 | ||
![]() |
4c709b2c4d | ||
![]() |
8f4cd032b7 | ||
![]() |
67629938d6 | ||
![]() |
9aff49bd88 | ||
![]() |
5b999a88f8 | ||
![]() |
482531836f | ||
![]() |
b3c82f54df | ||
![]() |
77fa4bbe2f | ||
![]() |
495c9850b4 | ||
![]() |
c0f8d145f8 | ||
![]() |
80f33daeeb | ||
![]() |
a16dcb63b5 | ||
![]() |
b871b5d2dd | ||
![]() |
e876647af5 | ||
![]() |
8d59812827 | ||
![]() |
e39ac885de | ||
![]() |
e6965622bd | ||
![]() |
0d8d3479e1 | ||
![]() |
35c1dfd145 | ||
![]() |
096115def7 | ||
![]() |
e784af3e2d | ||
![]() |
ce30108efc | ||
![]() |
edbd623e21 | ||
![]() |
7cfd537755 | ||
![]() |
ddd6d03e0b | ||
![]() |
b4a0e08d9d | ||
![]() |
545f9ae5f3 | ||
![]() |
be4a5a5f3e | ||
![]() |
3dc593fe88 | ||
![]() |
e8ed18f1cf | ||
![]() |
bf8890b0df | ||
![]() |
e5fda35c51 | ||
![]() |
84d50da009 | ||
![]() |
2cf7764714 | ||
![]() |
9fab0ec94f | ||
![]() |
6d694518fe | ||
![]() |
5265b767cb | ||
![]() |
d10a93fe4f | ||
![]() |
995986ecc7 | ||
![]() |
6d0bb02544 | ||
![]() |
6f51c47dc9 | ||
![]() |
626daf89c1 | ||
![]() |
b18ccffeb4 | ||
![]() |
2ab2185e0a | ||
![]() |
be47609405 | ||
![]() |
5dee7a5262 | ||
![]() |
bff7ada2d1 | ||
![]() |
ed33d1d4f7 | ||
![]() |
64e64f72f7 | ||
![]() |
d3c783832a | ||
![]() |
d963b69d5c | ||
![]() |
49ce9ba387 | ||
![]() |
d63a6d3f75 | ||
![]() |
3d5a8af52b | ||
![]() |
1cf670dad9 | ||
![]() |
b50e3c07d2 | ||
![]() |
fe7d1692c3 | ||
![]() |
0758cd6980 | ||
![]() |
e80b6b3057 | ||
![]() |
9c86afe40d | ||
![]() |
db4619f5a4 | ||
![]() |
f90d74ca31 | ||
![]() |
609f0a2eee | ||
![]() |
77bbbc88f8 | ||
![]() |
cdb79ef78a | ||
![]() |
1630e309fb | ||
![]() |
2d4f56f57c | ||
![]() |
d622993483 | ||
![]() |
c68a6ee0ed | ||
![]() |
94c1438913 | ||
![]() |
e206a26a85 | ||
![]() |
242e20316b | ||
![]() |
279fd2399d | ||
![]() |
a5fcb41ab0 | ||
![]() |
cb4f656673 | ||
![]() |
b9e5ee6759 | ||
![]() |
1084b7c3ad | ||
![]() |
39c06c5461 | ||
![]() |
b9c7f8769b | ||
![]() |
dc45adf7f2 | ||
![]() |
a69af42f7f | ||
![]() |
1a5dfae7a0 | ||
![]() |
d41b5d80ad | ||
![]() |
f0bcb3ba28 | ||
![]() |
7da35bf71d | ||
![]() |
03c339dd4b | ||
![]() |
11c74bd26b | ||
![]() |
0a292cf893 | ||
![]() |
ac6811867f | ||
![]() |
0c9df501e8 | ||
![]() |
4c4f9b45d9 | ||
![]() |
5a921c9f10 | ||
![]() |
bdc2aa2b39 | ||
![]() |
b508dd69be | ||
![]() |
f8b756c8bc | ||
![]() |
027b829c38 | ||
![]() |
0a2d6d1d62 | ||
![]() |
1b485ddb5a | ||
![]() |
0085ca6416 | ||
![]() |
87dca0f7ec | ||
![]() |
37af2c87e8 | ||
![]() |
bf908f0b7d | ||
![]() |
8d463b9577 | ||
![]() |
4f7d206736 | ||
![]() |
35073c780d | ||
![]() |
0a8f28b1c6 | ||
![]() |
af2375948d | ||
![]() |
df2e0be08d | ||
![]() |
ff1aca272e | ||
![]() |
f2e352832a | ||
![]() |
ad0855ac83 | ||
![]() |
d7ef9b1f0c | ||
![]() |
40a3e1b18a | ||
![]() |
25a73090f5 | ||
![]() |
a239a26b17 | ||
![]() |
06d256294f | ||
![]() |
81ad50e82a | ||
![]() |
23de9bf93e | ||
![]() |
5c46412faa | ||
![]() |
076e9eee01 | ||
![]() |
2103a04092 | ||
![]() |
58517d1d27 | ||
![]() |
aa1847189b | ||
![]() |
5d101e7b88 | ||
![]() |
e2de83188a | ||
![]() |
2a1b506d98 | ||
![]() |
b798ff5c92 | ||
![]() |
673aa0a87b | ||
![]() |
779ea19222 | ||
![]() |
a1f2b7f8e8 | ||
![]() |
fcb855cea9 | ||
![]() |
50fb48f66d | ||
![]() |
0acc3532c9 | ||
![]() |
8bf2d996ea | ||
![]() |
748c2babe9 | ||
![]() |
6859f73c54 | ||
![]() |
b1faed586d | ||
![]() |
6c848b4766 | ||
![]() |
725c18eada | ||
![]() |
992bb5d7be | ||
![]() |
9e353f1cdc | ||
![]() |
8f83e39970 | ||
![]() |
0eae9e7cdc | ||
![]() |
031b893196 | ||
![]() |
64da7a06c0 | ||
![]() |
57eaa1bbe1 | ||
![]() |
109d06b4bb | ||
![]() |
0d9910cbbe | ||
![]() |
8fbc8ffc7c | ||
![]() |
f2ee3859ab | ||
![]() |
89dc44be61 | ||
![]() |
6ab8716e69 | ||
![]() |
5c7c382323 | ||
![]() |
78b4b9441e | ||
![]() |
9e55014a13 | ||
![]() |
6f23b56b06 | ||
![]() |
1519527356 | ||
![]() |
6b3a178f2a | ||
![]() |
604419dd1f | ||
![]() |
c48e702a50 | ||
![]() |
1061bce4f3 | ||
![]() |
013d513450 | ||
![]() |
dca32efadf | ||
![]() |
28d952a643 | ||
![]() |
a2a717bd49 | ||
![]() |
753a92055c | ||
![]() |
371f986773 | ||
![]() |
a1e8b9be4e | ||
![]() |
c076a0f771 | ||
![]() |
dfbd39e898 | ||
![]() |
b5893f3fa3 | ||
![]() |
e3614cb932 | ||
![]() |
193c3e5b3d | ||
![]() |
c03c344f49 | ||
![]() |
25e3031830 | ||
![]() |
b7911a8fd8 | ||
![]() |
88384dc35e | ||
![]() |
39b4ed082c | ||
![]() |
d87aa23ae0 | ||
![]() |
be548dcb52 | ||
![]() |
4357a34339 | ||
![]() |
2c03ba204e | ||
![]() |
2c98d079de | ||
![]() |
16cd47fa2e | ||
![]() |
74a8bfba93 | ||
![]() |
c929f00456 | ||
![]() |
bb062f07f9 | ||
![]() |
c3d1e75a8f | ||
![]() |
506e3724a6 | ||
![]() |
4859ab67d4 | ||
![]() |
6d84d19520 | ||
![]() |
8627efd0a1 | ||
![]() |
6d13cf5e71 | ||
![]() |
7e2ab0d384 | ||
![]() |
19640d5e7c | ||
![]() |
d1a82a85cd | ||
![]() |
b1ab261890 | ||
![]() |
038278283a | ||
![]() |
c74bd11a6f | ||
![]() |
f2c2f1735e | ||
![]() |
4e41e12bd2 | ||
![]() |
6df808f266 | ||
![]() |
2cb973f150 | ||
![]() |
b5463cf5e1 | ||
![]() |
862546205a | ||
![]() |
7c1790bbfd | ||
![]() |
2d16a06bc4 | ||
![]() |
25cf917969 | ||
![]() |
d09c650afd | ||
![]() |
2b833c5250 | ||
![]() |
510db568eb | ||
![]() |
e4003c842b | ||
![]() |
68957d3880 | ||
![]() |
e6747066ae | ||
![]() |
62f0abee47 | ||
![]() |
9118ecd68f | ||
![]() |
15fd47c7f2 | ||
![]() |
ef40ac7bb3 | ||
![]() |
881d04ba1e | ||
![]() |
4af5b5f6f2 | ||
![]() |
90f0809029 | ||
![]() |
db5ed48dbb | ||
![]() |
ba84e7eead | ||
![]() |
e51067177e | ||
![]() |
f3859ed710 | ||
![]() |
0db12e5561 | ||
![]() |
ac5f991c0c | ||
![]() |
4a0ff3f7ef | ||
![]() |
601b1ef742 | ||
![]() |
d957725805 | ||
![]() |
4201723d10 | ||
![]() |
bef79e77aa | ||
![]() |
32f74273f0 | ||
![]() |
c69bcaafbb | ||
![]() |
50d7d1b7b3 | ||
![]() |
c06d61a83c | ||
![]() |
bc4f0c699f | ||
![]() |
8ad7bf60d7 | ||
![]() |
898a936064 | ||
![]() |
4e401bc059 | ||
![]() |
9ecef6f011 | ||
![]() |
ba394a7ab4 | ||
![]() |
d32490a4be | ||
![]() |
6526ff1612 | ||
![]() |
bb5390d63a | ||
![]() |
bd1aae8d66 | ||
![]() |
c24aed054f | ||
![]() |
0aa08a5e40 | ||
![]() |
3c48825699 | ||
![]() |
bfb56b4144 | ||
![]() |
ba8370bcfd | ||
![]() |
813f55152a | ||
![]() |
270a541a7c | ||
![]() |
c34549a47d | ||
![]() |
96d6b309ec |
10
.github/CONTRIBUTING.md
vendored
10
.github/CONTRIBUTING.md
vendored
@@ -42,10 +42,6 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
|
||||
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
||||
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
||||
|
||||
### Kotlin in NewPipe
|
||||
* NewPipe will remain mostly Java for time being
|
||||
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
|
||||
|
||||
### Creating a Pull Request (PR)
|
||||
|
||||
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
@@ -83,6 +79,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
|
||||
|
||||
## Communication
|
||||
|
||||
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
||||
* You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC).
|
||||
|
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
@@ -1,6 +1,3 @@
|
||||
name: Question
|
||||
description: Ask about anything NewPipe-related
|
||||
labels: [question]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
||||
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
||||
required: true
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,9 +3,9 @@ contact_links:
|
||||
- name: ❓ Question
|
||||
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
||||
about: Ask about anything NewPipe-related
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
- name: 💬 IRC
|
||||
url: https://web.libera.chat/#newpipe
|
||||
about: Chat with us via IRC for quick Q/A
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#newpipe:libera.chat
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
|
17
.github/changed-lines-count-labeler.yml
vendored
Normal file
17
.github/changed-lines-count-labeler.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Add 'size/small' label to any changes with less than 50 lines
|
||||
size/small:
|
||||
max: 49
|
||||
|
||||
# Add 'size/medium' label to any changes between 50 and 249 lines
|
||||
size/medium:
|
||||
min: 50
|
||||
max: 249
|
||||
|
||||
# Add 'size/large' label to any changes between 250 and 749 lines
|
||||
size/large:
|
||||
min: 250
|
||||
max: 749
|
||||
|
||||
# Add 'size/giant' label to any changes for more than 749 lines
|
||||
size/giant:
|
||||
min: 750
|
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
- refactor
|
||||
- release**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
@@ -36,8 +37,8 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v2
|
||||
|
||||
- name: create and checkout branch
|
||||
# push events already checked out the branch
|
||||
@@ -46,10 +47,10 @@ jobs:
|
||||
BRANCH: ${{ github.head_ref }}
|
||||
run: git checkout -B "$BRANCH"
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
@@ -57,14 +58,13 @@ jobs:
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
|
||||
test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -80,12 +80,18 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
@@ -98,7 +104,7 @@ jobs:
|
||||
script: ./gradlew connectedCheck --stacktrace
|
||||
|
||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: android-test-report-api${{ matrix.api-level }}
|
||||
@@ -111,19 +117,19 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
|
4
.github/workflows/image-minimizer.js
vendored
4
.github/workflows/image-minimizer.js
vendored
@@ -86,7 +86,7 @@ module.exports = async ({github, context}) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Asnyc replace function from https://stackoverflow.com/a/48032528
|
||||
// Async replace function from https://stackoverflow.com/a/48032528
|
||||
async function replaceAsync(str, regex, asyncFn) {
|
||||
const promises = [];
|
||||
str.replace(regex, (match, ...args) => {
|
||||
@@ -138,7 +138,7 @@ module.exports = async ({github, context}) => {
|
||||
if (shouldModify) {
|
||||
wasMatchModified = true;
|
||||
console.log(`Modifying match '${match}'`);
|
||||
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, (IMG_MAX_HEIGHT_PX * probeAspectRatio).toFixed(0))} />`;
|
||||
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, Math.floor(IMG_MAX_HEIGHT_PX * probeAspectRatio))} />`;
|
||||
}
|
||||
|
||||
console.log(`Match '${match}' is ok/will not be modified`);
|
||||
|
6
.github/workflows/image-minimizer.yml
vendored
6
.github/workflows/image-minimizer.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||
|
||||
- name: Minimize simple images
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
timeout-minutes: 3
|
||||
with:
|
||||
script: |
|
||||
|
18
.github/workflows/pr-labeler.yml
vendored
Normal file
18
.github/workflows/pr-labeler.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: "PR size labeler"
|
||||
on: [pull_request_target]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
changed-lines-count-labeler:
|
||||
runs-on: ubuntu-latest
|
||||
name: Automatically labelling pull requests based on the changed lines count
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Set a label
|
||||
uses: TeamNewPipe/changed-lines-count-labeler@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/changed-lines-count-labeler.yml
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ captures/
|
||||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
.kotlin/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
21
.idea/icon.svg
generated
Normal file
21
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#CD201F;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Alapkör">
|
||||
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
|
||||
</g>
|
||||
<g id="Elemek">
|
||||
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
|
||||
</g>
|
||||
<g id="Fedő">
|
||||
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
|
||||
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
|
||||
</g>
|
||||
<g id="Vonalak">
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 850 B |
253
app/build.gradle
253
app/build.gradle
File diff suppressed because it is too large
Load Diff
48
app/check-dependencies.gradle
Normal file
48
app/check-dependencies.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
tasks.register('checkDependenciesOrder') {
|
||||
group = 'verification'
|
||||
description = 'Checks that each section in libs.versions.toml is sorted alphabetically'
|
||||
|
||||
def tomlFile = file('../gradle/libs.versions.toml')
|
||||
|
||||
doLast {
|
||||
if (!tomlFile.exists()) {
|
||||
throw new GradleException('TOML file not found')
|
||||
}
|
||||
|
||||
def lines = tomlFile.readLines()
|
||||
def nonSortedBlocks = []
|
||||
def currentBlock = []
|
||||
def prevLine = ''
|
||||
def prevIndex = 0
|
||||
|
||||
lines.eachWithIndex { line, lineIndex ->
|
||||
if (line.trim() && !line.startsWith('#')) {
|
||||
if (line.startsWith('[')) {
|
||||
prevLine = ''
|
||||
} else {
|
||||
def currIndex = lineIndex + 1
|
||||
if (prevLine > line) {
|
||||
if (currentBlock && currentBlock[-1] == "${prevIndex}: ${prevLine}") {
|
||||
currentBlock.add("${currIndex}: ${line}")
|
||||
} else {
|
||||
if (!currentBlock.isEmpty()) {
|
||||
nonSortedBlocks.add(currentBlock)
|
||||
currentBlock = []
|
||||
}
|
||||
currentBlock.add("${prevIndex}: ${prevLine}")
|
||||
currentBlock.add("${currIndex}: ${line}")
|
||||
}
|
||||
}
|
||||
prevLine = line
|
||||
prevIndex = lineIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentBlock.isEmpty()) {
|
||||
nonSortedBlocks.add(currentBlock)
|
||||
throw new GradleException("The following lines were not sorted:\n" +
|
||||
nonSortedBlocks.collect { it.join("\n") }.join("\n\n"))
|
||||
}
|
||||
}
|
||||
}
|
10
app/proguard-rules.pro
vendored
10
app/proguard-rules.pro
vendored
@@ -7,20 +7,12 @@
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
|
||||
## Rules for ExoPlayer
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
||||
-dontwarn icepick.**
|
||||
-keep class icepick.** { *; }
|
||||
-keep class **$$Icepick { *; }
|
||||
-keepclasseswithmembernames class * {
|
||||
@icepick.* <fields>;
|
||||
}
|
||||
-keepnames class * { @icepick.State *;}
|
||||
|
||||
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
|
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
File diff suppressed because it is too large
Load Diff
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,14 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -20,13 +24,17 @@ class DatabaseMigrationTest {
|
||||
private const val DEFAULT_SERVICE_ID = 0
|
||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||
private const val DEFAULT_TITLE = "Test Title"
|
||||
private const val DEFAULT_NAME = "Test Name"
|
||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||
private const val DEFAULT_DURATION = 480L
|
||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||
|
||||
private const val DEFAULT_THIRD_SERVICE_ID = 2
|
||||
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
@@ -106,6 +114,20 @@ class DatabaseMigrationTest {
|
||||
Migrations.MIGRATION_6_7
|
||||
)
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_8,
|
||||
true,
|
||||
Migrations.MIGRATION_7_8
|
||||
)
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_9,
|
||||
true,
|
||||
Migrations.MIGRATION_8_9
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
|
||||
@@ -140,6 +162,157 @@ class DatabaseMigrationTest {
|
||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom7to8() {
|
||||
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
|
||||
|
||||
val defaultSearch1 = " abc "
|
||||
val defaultSearch2 = " abc"
|
||||
|
||||
val serviceId = DEFAULT_SERVICE_ID // YouTube
|
||||
// Use id different to YouTube because two searches with the same query
|
||||
// but different service are considered not equal.
|
||||
val otherServiceId = ServiceList.SoundCloud.serviceId
|
||||
|
||||
databaseInV7.run {
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", serviceId)
|
||||
put("search", defaultSearch1)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", serviceId)
|
||||
put("search", defaultSearch2)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", otherServiceId)
|
||||
put("search", defaultSearch1)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", otherServiceId)
|
||||
put("search", defaultSearch2)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
||||
true, Migrations.MIGRATION_7_8
|
||||
)
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
||||
true, Migrations.MIGRATION_8_9
|
||||
)
|
||||
|
||||
val migratedDatabaseV8 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
||||
|
||||
assertEquals(2, listFromDB.size)
|
||||
assertEquals("abc", listFromDB[0].search)
|
||||
assertEquals("abc", listFromDB[1].search)
|
||||
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom8to9() {
|
||||
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
|
||||
|
||||
val localUid1: Long
|
||||
val localUid2: Long
|
||||
val remoteUid1: Long
|
||||
val remoteUid2: Long
|
||||
databaseInV8.run {
|
||||
localUid1 = insert(
|
||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("name", DEFAULT_NAME + "1")
|
||||
put("is_thumbnail_permanent", false)
|
||||
put("thumbnail_stream_id", -1)
|
||||
}
|
||||
)
|
||||
localUid2 = insert(
|
||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("name", DEFAULT_NAME + "2")
|
||||
put("is_thumbnail_permanent", false)
|
||||
put("thumbnail_stream_id", -1)
|
||||
}
|
||||
)
|
||||
delete(
|
||||
"playlists", "uid = ?",
|
||||
Array(1) { localUid1 }
|
||||
)
|
||||
remoteUid1 = insert(
|
||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
}
|
||||
)
|
||||
remoteUid2 = insert(
|
||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
}
|
||||
)
|
||||
delete(
|
||||
"remote_playlists", "uid = ?",
|
||||
Array(1) { remoteUid2 }
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_9,
|
||||
true,
|
||||
Migrations.MIGRATION_8_9
|
||||
)
|
||||
|
||||
val migratedDatabaseV9 = getMigratedDatabase()
|
||||
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||
|
||||
assertEquals(1, localListFromDB.size)
|
||||
assertEquals(localUid2, localListFromDB[0].uid)
|
||||
assertEquals(-1, localListFromDB[0].displayIndex)
|
||||
assertEquals(1, remoteListFromDB.size)
|
||||
assertEquals(remoteUid1, remoteListFromDB[0].uid)
|
||||
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
||||
|
||||
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
||||
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
|
||||
)
|
||||
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
||||
PlaylistRemoteEntity(
|
||||
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
||||
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
||||
)
|
||||
)
|
||||
|
||||
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||
assertEquals(2, localListFromDB.size)
|
||||
assertEquals(localUid3, localListFromDB[1].uid)
|
||||
assertEquals(-1, localListFromDB[1].displayIndex)
|
||||
assertEquals(2, remoteListFromDB.size)
|
||||
assertEquals(remoteUid3, remoteListFromDB[1].uid)
|
||||
assertEquals(-1, remoteListFromDB[1].displayIndex)
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
|
@@ -0,0 +1,130 @@
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.database.feed.dao.FeedDAO
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.streams.toList
|
||||
|
||||
class FeedDAOTest {
|
||||
private lateinit var db: AppDatabase
|
||||
private lateinit var feedDAO: FeedDAO
|
||||
private lateinit var streamDAO: StreamDAO
|
||||
private lateinit var subscriptionDAO: SubscriptionDAO
|
||||
|
||||
private val serviceId = ServiceList.YouTube.serviceId
|
||||
|
||||
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
|
||||
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
|
||||
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
|
||||
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
||||
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
|
||||
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
|
||||
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
||||
|
||||
private val allStreams = listOf(
|
||||
stream1, stream2, stream3, stream4, stream5, stream6, stream7
|
||||
)
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
context, AppDatabase::class.java
|
||||
).build()
|
||||
feedDAO = db.feedDAO()
|
||||
streamDAO = db.streamDAO()
|
||||
subscriptionDAO = db.subscriptionDAO()
|
||||
}
|
||||
|
||||
@After
|
||||
@Throws(IOException::class)
|
||||
fun closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnlinkStreamsOlderThan_KeepOne() {
|
||||
setupUnlinkDelete("2023-08-15T00:00:00Z")
|
||||
val streams = feedDAO.getStreams(
|
||||
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||
)
|
||||
.blockingGet()
|
||||
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
|
||||
assertEqual(streams, allowedStreams)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnlinkStreamsOlderThan_KeepMultiple() {
|
||||
setupUnlinkDelete("2023-08-01T00:00:00Z")
|
||||
val streams = feedDAO.getStreams(
|
||||
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||
)
|
||||
.blockingGet()
|
||||
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
|
||||
assertEqual(streams, allowedStreams)
|
||||
}
|
||||
|
||||
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
||||
assertNotNull(streams)
|
||||
assertEquals(
|
||||
allowedStreams,
|
||||
streams!!
|
||||
.map { it.stream }
|
||||
.sortedBy { it.uid }
|
||||
.toList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupUnlinkDelete(time: String) {
|
||||
clearAndFillTables()
|
||||
Single.fromCallable {
|
||||
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
|
||||
}.blockingSubscribe()
|
||||
Single.fromCallable {
|
||||
streamDAO.deleteOrphans()
|
||||
}.blockingSubscribe()
|
||||
}
|
||||
|
||||
private fun clearAndFillTables() {
|
||||
db.clearAllTables()
|
||||
streamDAO.insertAll(allStreams)
|
||||
subscriptionDAO.insertAll(
|
||||
listOf(
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
|
||||
)
|
||||
)
|
||||
feedDAO.insertAll(
|
||||
listOf(
|
||||
FeedEntity(1, 1),
|
||||
FeedEntity(2, 1),
|
||||
FeedEntity(3, 1),
|
||||
FeedEntity(4, 2),
|
||||
FeedEntity(5, 2),
|
||||
FeedEntity(6, 3),
|
||||
FeedEntity(7, 4),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@@ -10,19 +10,13 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.testUtil.TestDatabase;
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class SubscriptionManagerTest {
|
||||
@@ -58,7 +52,7 @@ public class SubscriptionManagerTest {
|
||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||
|
||||
manager.insertSubscription(subscription, info);
|
||||
manager.insertSubscription(subscription);
|
||||
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
||||
|
||||
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
||||
@@ -76,7 +70,7 @@ public class SubscriptionManagerTest {
|
||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||
subscription.setNotificationMode(0);
|
||||
|
||||
manager.insertSubscription(subscription, info);
|
||||
manager.insertSubscription(subscription);
|
||||
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
||||
.blockingAwait();
|
||||
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
||||
@@ -85,35 +79,4 @@ public class SubscriptionManagerTest {
|
||||
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
||||
assertEquals(1, anotherSubscription.getNotificationMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRememberRecentStreams() throws ExtractionException, IOException {
|
||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/Polyphia");
|
||||
final List<StreamInfoItem> relatedItems = List.of(
|
||||
new StreamInfoItem(0, "a", "b", StreamType.VIDEO_STREAM),
|
||||
new StreamInfoItem(1, "c", "d", StreamType.AUDIO_STREAM),
|
||||
new StreamInfoItem(2, "e", "f", StreamType.AUDIO_LIVE_STREAM),
|
||||
new StreamInfoItem(3, "g", "h", StreamType.LIVE_STREAM));
|
||||
relatedItems.forEach(item -> {
|
||||
// these two fields must be non-null for the insert to succeed
|
||||
item.setUploaderUrl(info.getUrl());
|
||||
item.setUploaderName(info.getName());
|
||||
// the upload date must not be too much in the past for the item to actually be inserted
|
||||
item.setUploadDate(new DateWrapper(OffsetDateTime.now()));
|
||||
});
|
||||
info.setRelatedItems(relatedItems);
|
||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||
|
||||
manager.insertSubscription(subscription, info);
|
||||
final List<StreamEntity> streams = database.streamDAO().getAll().blockingFirst();
|
||||
|
||||
assertEquals(4, streams.size());
|
||||
streams.sort(Comparator.comparing(StreamEntity::getServiceId));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
assertEquals(relatedItems.get(0).getServiceId(), streams.get(0).getServiceId());
|
||||
assertEquals(relatedItems.get(0).getUrl(), streams.get(0).getUrl());
|
||||
assertEquals(relatedItems.get(0).getName(), streams.get(0).getTitle());
|
||||
assertEquals(relatedItems.get(0).getStreamType(), streams.get(0).getStreamType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,15 +12,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.MediaFormat
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||
import org.schabi.newpipe.extractor.stream.Stream
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
|
||||
|
||||
@MediumTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -84,7 +90,7 @@ class StreamItemAdapterTest {
|
||||
@Test
|
||||
fun subtitleStreams_noIcon() {
|
||||
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
(0 until 5).map {
|
||||
SubtitlesStream.Builder()
|
||||
.setContent("https://example.com", true)
|
||||
@@ -105,7 +111,7 @@ class StreamItemAdapterTest {
|
||||
@Test
|
||||
fun audioStreams_noIcon() {
|
||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
(0 until 5).map {
|
||||
AudioStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
@@ -123,12 +129,109 @@ class StreamItemAdapterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retrieveMediaFormatFromFileTypeHeaders() {
|
||||
val streams = getIncompleteAudioStreams(5)
|
||||
val wrapper = StreamInfoWrapper(streams, context)
|
||||
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
||||
StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
|
||||
}
|
||||
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
||||
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)
|
||||
|
||||
helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
|
||||
helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retrieveMediaFormatFromContentDispositionHeader() {
|
||||
val streams = getIncompleteAudioStreams(11)
|
||||
val wrapper = StreamInfoWrapper(streams, context)
|
||||
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
||||
StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
|
||||
}
|
||||
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
||||
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
|
||||
)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
|
||||
)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
|
||||
)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
|
||||
)
|
||||
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
|
||||
5, MediaFormat.OGG
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
|
||||
6, MediaFormat.FLAC
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
|
||||
7, MediaFormat.AIFF
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
|
||||
8, MediaFormat.M4A
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
|
||||
9, MediaFormat.OPUS
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
|
||||
10, MediaFormat.OPUS
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retrieveMediaFormatFromContentTypeHeader() {
|
||||
val streams = getIncompleteAudioStreams(12)
|
||||
val wrapper = StreamInfoWrapper(streams, context)
|
||||
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
||||
StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
|
||||
}
|
||||
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
||||
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
|
||||
helper.assertInvalidResponse(getResponse(mapOf()), 7)
|
||||
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of video streams, in which their video only property mirrors the provided
|
||||
* [videoOnly] vararg.
|
||||
*/
|
||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
videoOnly.map {
|
||||
VideoStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
@@ -161,6 +264,19 @@ class StreamItemAdapterTest {
|
||||
}
|
||||
)
|
||||
|
||||
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
|
||||
val list = ArrayList<AudioStream>(size)
|
||||
for (i in 1..size) {
|
||||
list.add(
|
||||
AudioStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com/$i", true)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
|
||||
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
|
||||
@@ -196,11 +312,56 @@ class StreamItemAdapterTest {
|
||||
streams.forEachIndexed { index, stream ->
|
||||
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||
SecondaryStreamHelper(
|
||||
StreamItemAdapter.StreamSizeWrapper(streams, context),
|
||||
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
||||
it
|
||||
)
|
||||
}
|
||||
put(index, secondaryStreamHelper)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResponse(headers: Map<String, String>): Response {
|
||||
val listHeaders = HashMap<String, List<String>>()
|
||||
headers.forEach { entry ->
|
||||
listHeaders[entry.key] = listOf(entry.value)
|
||||
}
|
||||
return Response(200, null, listHeaders, "", "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for assertion related to extractions of [MediaFormat]s.
|
||||
*/
|
||||
class AssertionHelper<T : Stream>(
|
||||
private val streams: List<T>,
|
||||
private val wrapper: StreamInfoWrapper<T>,
|
||||
private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
|
||||
) {
|
||||
|
||||
/**
|
||||
* Assert that an invalid response does not result in wrongly extracted [MediaFormat].
|
||||
*/
|
||||
fun assertInvalidResponse(
|
||||
response: Response,
|
||||
index: Int
|
||||
) {
|
||||
assertFalse(
|
||||
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
|
||||
)
|
||||
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a valid response results in correctly extracted and handled [MediaFormat].
|
||||
*/
|
||||
fun assertValidResponse(
|
||||
response: Response,
|
||||
index: Int,
|
||||
format: MediaFormat
|
||||
) {
|
||||
assertTrue(
|
||||
"header was not recognized", retrieveMediaFormat(streams[index], response)
|
||||
)
|
||||
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -77,6 +77,11 @@
|
||||
android:exported="false"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsV2Activity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:exported="false"
|
||||
@@ -367,6 +372,7 @@
|
||||
<data android:host="tilvids.com" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="subscribeto.me" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||
|
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.os.BundleCompat;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
@@ -284,7 +285,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
Bundle state = null;
|
||||
if (!mSavedState.isEmpty()) {
|
||||
state = new Bundle();
|
||||
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
||||
state.putParcelableArrayList("states", mSavedState);
|
||||
}
|
||||
for (int i = 0; i < mFragments.size(); i++) {
|
||||
final Fragment f = mFragments.get(i);
|
||||
@@ -311,13 +312,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
if (state != null) {
|
||||
final Bundle bundle = (Bundle) state;
|
||||
bundle.setClassLoader(loader);
|
||||
final Parcelable[] fss = bundle.getParcelableArray("states");
|
||||
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||
Fragment.SavedState.class);
|
||||
mSavedState.clear();
|
||||
mFragments.clear();
|
||||
if (fss != null) {
|
||||
for (final Parcelable parcelable : fss) {
|
||||
mSavedState.add((Fragment.SavedState) parcelable);
|
||||
}
|
||||
if (states != null) {
|
||||
mSavedState.addAll(states);
|
||||
}
|
||||
final Iterable<String> keys = bundle.keySet();
|
||||
for (final String key : keys) {
|
||||
|
@@ -1,255 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe 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.
|
||||
*
|
||||
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
private static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
app = this;
|
||||
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, "This is a phoenix process! "
|
||||
+ "Aborting initialization of App[onCreate]");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
PicassoHelper.setShouldLoadImages(
|
||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
PicassoHelper.terminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||
setCookiesToDownloader(downloader);
|
||||
return downloader;
|
||||
}
|
||||
|
||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext());
|
||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull final Throwable throwable) {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
||||
|
||||
final Throwable actualThrowable;
|
||||
if (throwable instanceof UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper,
|
||||
// get the cause of it to get the "real" exception
|
||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
||||
} else {
|
||||
actualThrowable = throwable;
|
||||
}
|
||||
|
||||
final List<Throwable> errors;
|
||||
if (actualThrowable instanceof CompositeException) {
|
||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||
} else {
|
||||
errors = List.of(actualThrowable);
|
||||
}
|
||||
|
||||
for (final Throwable error : errors) {
|
||||
if (isThrowableIgnored(error)) {
|
||||
return;
|
||||
}
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(actualThrowable);
|
||||
} else {
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||
// Don't crash the application over a simple network problem
|
||||
return ExceptionUtils.hasAssignableCause(throwable,
|
||||
// network api cancellation
|
||||
IOException.class, SocketException.class,
|
||||
// blocking code disposed
|
||||
InterruptedException.class, InterruptedIOException.class);
|
||||
}
|
||||
|
||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||
// Though these exceptions cannot be ignored
|
||||
return ExceptionUtils.hasAssignableCause(throwable,
|
||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||
IllegalStateException.class); // bug in operator
|
||||
}
|
||||
|
||||
private void reportException(@NonNull final Throwable throwable) {
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread.currentThread().getUncaughtExceptionHandler()
|
||||
.uncaughtException(Thread.currentThread(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected void initACRA() {
|
||||
if (ACRA.isACRASenderServiceProcess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
||||
.withBuildConfigClass(BuildConfig.class);
|
||||
ACRA.init(this, acraConfig);
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.app_update_notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
);
|
||||
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
286
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
286
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
File diff suppressed because it is too large
Load Diff
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
}
|
@@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
@@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
@@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
@@ -80,9 +81,29 @@ public abstract class BaseFragment extends Fragment {
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
|
||||
*
|
||||
* <p>
|
||||
* {@link #initListeners()} is called after this method to initialize the corresponding
|
||||
* listeners.
|
||||
* </p>
|
||||
* @param rootView The inflated view for this fragment
|
||||
* (provided by {@link #onViewCreated(View, Bundle)})
|
||||
* @param savedInstanceState The saved state of this fragment
|
||||
* (provided by {@link #onViewCreated(View, Bundle)})
|
||||
*/
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the listeners for this fragment.
|
||||
*
|
||||
* <p>
|
||||
* This method is called after {@link #initViews(View, Bundle)}
|
||||
* in {@link #onViewCreated(View, Bundle)}.
|
||||
* </p>
|
||||
*/
|
||||
protected void initListeners() {
|
||||
}
|
||||
|
||||
@@ -100,9 +121,20 @@ public abstract class BaseFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the root fragment by looping through all of the parent fragments. The root fragment
|
||||
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
|
||||
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
|
||||
* sheet. This function therefore returns the fragment manager of said fragment.
|
||||
*
|
||||
* @return the fragment manager of the root fragment, i.e.
|
||||
* {@link org.schabi.newpipe.fragments.MainFragment}
|
||||
*/
|
||||
protected FragmentManager getFM() {
|
||||
return getParentFragment() == null
|
||||
? getFragmentManager()
|
||||
: getParentFragment().getFragmentManager();
|
||||
Fragment current = this;
|
||||
while (current.getParentFragment() != null) {
|
||||
current = current.getParentFragment();
|
||||
}
|
||||
return current.getFragmentManager();
|
||||
}
|
||||
}
|
||||
|
@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
|
||||
this.mCookies = new HashMap<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OkHttpClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
|
@@ -75,6 +75,7 @@ import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
@@ -82,6 +83,7 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -92,6 +94,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@@ -163,16 +170,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
// if this is enabled by the user.
|
||||
NotificationWorker.initialize(this);
|
||||
}
|
||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||
&& !App.getInstance().isFirstRun()
|
||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final App app = App.getInstance();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
||||
// Start the worker which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||
@@ -219,14 +232,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
int kioskMenuItemId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks));
|
||||
kioskId++;
|
||||
kioskMenuItemId++;
|
||||
}
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
@@ -306,20 +319,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
String serviceName = "";
|
||||
|
||||
int kioskId = 0;
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
if (kioskId == item.getItemId()) {
|
||||
serviceName = ks;
|
||||
final StreamingService currentService = ServiceHelper.getSelectedService(this);
|
||||
int kioskMenuItemId = 0;
|
||||
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
|
||||
if (kioskMenuItemId == item.getItemId()) {
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
|
||||
currentService.getServiceId(), kioskId);
|
||||
break;
|
||||
}
|
||||
kioskId++;
|
||||
kioskMenuItemId++;
|
||||
}
|
||||
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
|
||||
serviceName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -549,32 +558,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||
// handled by it
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final var fragmentManager = getSupportFragmentManager();
|
||||
|
||||
} else {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
if (fragmentManager.getBackStackEntryCount() == 1) {
|
||||
finish();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
@@ -633,10 +637,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
* </pre>
|
||||
*/
|
||||
private void onHomeButtonPressed() {
|
||||
// If search fragment wasn't found in the backstack...
|
||||
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
||||
// ...go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
final var fm = getSupportFragmentManager();
|
||||
|
||||
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
// If search fragment wasn't found in the backstack go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(fm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,8 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -27,7 +29,7 @@ public final class NewPipeDatabase {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||
MIGRATION_5_6, MIGRATION_6_7)
|
||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
268
app/src/main/java/org/schabi/newpipe/NewPlayerComponent.kt
Normal file
268
app/src/main/java/org/schabi/newpipe/NewPlayerComponent.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,7 @@ import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil
|
||||
import java.io.IOException
|
||||
|
||||
class NewVersionWorker(
|
||||
@@ -84,7 +82,7 @@ class NewVersionWorker(
|
||||
@Throws(IOException::class, ReCaptchaException::class)
|
||||
private fun checkNewVersion() {
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk()) {
|
||||
if (!ReleaseVersionUtil.isReleaseApk) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,7 +91,7 @@ class NewVersionWorker(
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||
if (!isLastUpdateCheckExpired(expiry)) {
|
||||
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -108,7 +106,7 @@ class NewVersionWorker(
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||
prefs.edit {
|
||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||
}
|
||||
@@ -120,13 +118,13 @@ class NewVersionWorker(
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
val githubStableObject = JsonParser.`object`()
|
||||
val newpipeVersionInfo = JsonParser.`object`()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable")
|
||||
.getObject("newpipe")
|
||||
|
||||
val versionName = githubStableObject.getString("version")
|
||||
val versionCode = githubStableObject.getInt("version_code")
|
||||
val apkLocationUrl = githubStableObject.getString("apk")
|
||||
val versionName = newpipeVersionInfo.getString("version")
|
||||
val versionCode = newpipeVersionInfo.getInt("version_code")
|
||||
val apkLocationUrl = newpipeVersionInfo.getString("apk")
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||
} catch (e: JsonParserException) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user