mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-06-26 07:03:20 +00:00
Compare commits
4409 Commits
v0.19.6-fd
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4eeea7b787 | ||
![]() |
e64c01d2da | ||
![]() |
0c7a91f852 | ||
![]() |
a2d93b389c | ||
![]() |
c795214abb | ||
![]() |
8583c48264 | ||
![]() |
2a3d133bcf | ||
![]() |
3e3d1fd265 | ||
![]() |
8645618f1a | ||
![]() |
e48ce5a103 | ||
![]() |
46139340fe | ||
![]() |
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 | ||
![]() |
1e8efa7165 | ||
![]() |
d4019f4b54 | ||
![]() |
3f0f66f106 | ||
![]() |
8f644e8aaf | ||
![]() |
27f77518fe | ||
![]() |
b56f3b3324 | ||
![]() |
0195655479 | ||
![]() |
3c91ec33ae | ||
![]() |
6b3f51e5ea | ||
![]() |
d6a1170ddb | ||
![]() |
428a7d418b | ||
![]() |
40d102fcb5 | ||
![]() |
1db73370a7 | ||
![]() |
8b63b437d8 | ||
![]() |
78e577d260 | ||
![]() |
96a7cc2971 | ||
![]() |
9eedbae879 | ||
![]() |
38d3b3c7ef | ||
![]() |
e4d3b74f1b | ||
![]() |
54f3003a6f | ||
![]() |
cbc7b8ce18 | ||
![]() |
ec7d01b794 | ||
![]() |
3edd4c012d | ||
![]() |
3243f97ff2 | ||
![]() |
c658f28b02 | ||
![]() |
5ab3a4a9e0 | ||
![]() |
cb00c57009 | ||
![]() |
cd2884d412 | ||
![]() |
471137093a | ||
![]() |
57064479c8 | ||
![]() |
528bd502b4 | ||
![]() |
90bc1905f5 | ||
![]() |
a01e59e9db | ||
![]() |
3f944c1bb2 | ||
![]() |
43ef852117 | ||
![]() |
bf22515bcd | ||
![]() |
4c17c7b45b | ||
![]() |
ec21200787 | ||
![]() |
25fea73704 | ||
![]() |
2377d85efb | ||
![]() |
ddef550637 | ||
![]() |
2f0ed7f3b7 | ||
![]() |
b6bdd359d6 | ||
![]() |
a4453bc699 | ||
![]() |
3e87c40856 | ||
![]() |
d80e531a2e | ||
![]() |
d25e84a461 | ||
![]() |
05cc520665 | ||
![]() |
eeec6fd002 | ||
![]() |
795bc82c7f | ||
![]() |
7742c40ac0 | ||
![]() |
d9e2ada369 | ||
![]() |
5d6158ea76 | ||
![]() |
00257e969e | ||
![]() |
135f0f7249 | ||
![]() |
fdd8b76add | ||
![]() |
6b7ffbba4c | ||
![]() |
8cfba4003d | ||
![]() |
01b46edf1a | ||
![]() |
f8599d17c2 | ||
![]() |
5c7a9a52f5 | ||
![]() |
c1f0a945c0 | ||
![]() |
db7de05f2b | ||
![]() |
e33bb676f9 | ||
![]() |
30724dbc50 | ||
![]() |
e765343162 | ||
![]() |
62ce0b0408 | ||
![]() |
3bbc606694 | ||
![]() |
56eec9fed1 | ||
![]() |
ea0d798ea0 | ||
![]() |
5716d51112 | ||
![]() |
d845a158f0 | ||
![]() |
1a2fbd8122 | ||
![]() |
8bdeed8f28 | ||
![]() |
3c87462203 | ||
![]() |
3622438a9d | ||
![]() |
1848892ff8 | ||
![]() |
72c6ed2804 | ||
![]() |
42de2c7033 | ||
![]() |
6bcc8691fa | ||
![]() |
6cf13ed8fb | ||
![]() |
ad75db40df | ||
![]() |
4e3bf3c2f9 | ||
![]() |
1925687f18 | ||
![]() |
577301c4eb | ||
![]() |
c87b42de1c | ||
![]() |
c8e8915c2e | ||
![]() |
17cdedfa85 | ||
![]() |
677bb4070f | ||
![]() |
fe82029dc7 | ||
![]() |
0ab9961908 | ||
![]() |
ecbf5d5ead | ||
![]() |
df430badbc | ||
![]() |
8639972a54 | ||
![]() |
41038f452d | ||
![]() |
2f31ea8864 | ||
![]() |
e831059162 | ||
![]() |
e109e8cf1c | ||
![]() |
f1524b6aba | ||
![]() |
51ee6f87e0 | ||
![]() |
0bb3e7cb86 | ||
![]() |
4bf063645a | ||
![]() |
9866eab60f | ||
![]() |
10c42de2f1 | ||
![]() |
e1fd25fb71 | ||
![]() |
2315b082ff | ||
![]() |
023f6166ab | ||
![]() |
d89a3c6c4d | ||
![]() |
fb00ee8cf9 | ||
![]() |
22671ca16c | ||
![]() |
4e837e838d | ||
![]() |
ed1781133c | ||
![]() |
60fc662a26 | ||
![]() |
43b0167a3a | ||
![]() |
8519897089 | ||
![]() |
60a5d02018 | ||
![]() |
c377ffbce8 | ||
![]() |
b567d428ad | ||
![]() |
da30e539df | ||
![]() |
f74d794b2a | ||
![]() |
69ef4a987e | ||
![]() |
78e1e0508e | ||
![]() |
6d98ad7abc | ||
![]() |
70b3ba310a | ||
![]() |
2edc223e77 | ||
![]() |
e18a6b09f8 | ||
![]() |
f8c3ec4be7 | ||
![]() |
ba3afd1e35 | ||
![]() |
20f0011921 | ||
![]() |
acebabd028 | ||
![]() |
6243f34946 | ||
![]() |
787758a436 | ||
![]() |
a02b92fd59 | ||
![]() |
a6ff85a208 | ||
![]() |
41da8fc05f | ||
![]() |
a4a9957a15 | ||
![]() |
29318c64ed | ||
![]() |
74bd28cbd9 | ||
![]() |
365bb2d0e4 | ||
![]() |
c08538d25d | ||
![]() |
140ea8642c | ||
![]() |
445d364193 | ||
![]() |
4bb45c001d | ||
![]() |
7350b1f32e | ||
![]() |
4a33ee6045 | ||
![]() |
704e9bd7b6 | ||
![]() |
d2735607b8 | ||
![]() |
3c72992c39 | ||
![]() |
7689d1d15c | ||
![]() |
65d8589e7a | ||
![]() |
32cec6c9a7 | ||
![]() |
72ca52a29b | ||
![]() |
2ded8c7cc1 | ||
![]() |
759a9080a8 | ||
![]() |
2ba649949f | ||
![]() |
c8d54ec6c7 | ||
![]() |
96e9242431 | ||
![]() |
3c74cb3439 | ||
![]() |
7a8116b2cf | ||
![]() |
d010384c88 | ||
![]() |
07111d86d4 | ||
![]() |
ec974a2b3d | ||
![]() |
02906e8132 | ||
![]() |
6f428d0c6b | ||
![]() |
41da2bfb00 | ||
![]() |
746b1f7eb2 | ||
![]() |
03fd286956 | ||
![]() |
39a5c8bdfb | ||
![]() |
fb1b1c5be1 | ||
![]() |
1a8aa8b17e | ||
![]() |
2317864422 | ||
![]() |
694418d30d | ||
![]() |
ed06f559ae | ||
![]() |
fdd3b03fe5 | ||
![]() |
dbd6e4d11f | ||
![]() |
61a14765f3 | ||
![]() |
9b8ffdd2aa | ||
![]() |
ef0a4cf8b2 | ||
![]() |
7aed2eed8a | ||
![]() |
87a88e4df7 | ||
![]() |
366c39d4c6 | ||
![]() |
77649d388c | ||
![]() |
dba53d23aa | ||
![]() |
208887d538 | ||
![]() |
0cd1a86aa5 | ||
![]() |
de7872d8f2 | ||
![]() |
bb1f5d8f38 | ||
![]() |
7c39421297 | ||
![]() |
d06cc862c8 | ||
![]() |
c5cf2f4514 | ||
![]() |
3f8e44dc66 | ||
![]() |
d33229a3b8 | ||
![]() |
bb57f9cc9d | ||
![]() |
23a20712da | ||
![]() |
43f46e29ad | ||
![]() |
7617f8cdc7 | ||
![]() |
2e3490bce2 | ||
![]() |
1dd0930b83 | ||
![]() |
265de55a07 | ||
![]() |
d8ed2c8503 | ||
![]() |
73aebc1110 | ||
![]() |
3cb76e4c34 | ||
![]() |
65680b2ccf | ||
![]() |
c8ffe65acf | ||
![]() |
a4767fc48a | ||
![]() |
42d861688e | ||
![]() |
2ee4c6e289 | ||
![]() |
097c2368f4 | ||
![]() |
80e0c6ab0e | ||
![]() |
9067c770a7 | ||
![]() |
f1a071b668 | ||
![]() |
8e888ebdf7 | ||
![]() |
612122997b | ||
![]() |
4b050c0dd8 | ||
![]() |
be4f3d9d62 | ||
![]() |
24ff6a4313 | ||
![]() |
c2968a3ff2 | ||
![]() |
671dd4afd3 | ||
![]() |
600ebdae18 | ||
![]() |
5560cea470 | ||
![]() |
39c500f33c | ||
![]() |
624ad6a47c | ||
![]() |
68ea99d6e6 | ||
![]() |
bc29f40d69 | ||
![]() |
42fb13f17a | ||
![]() |
d5b54c85ed | ||
![]() |
f0307b1b48 | ||
![]() |
75292e099c | ||
![]() |
e0cb2892b8 | ||
![]() |
831f36e18e | ||
![]() |
d2f8f31d1f | ||
![]() |
8d43499e5b | ||
![]() |
63375627e9 | ||
![]() |
4903786b14 | ||
![]() |
4cc653fdf1 | ||
![]() |
4c5c2a3d79 | ||
![]() |
26b29ca78d | ||
![]() |
c85af7861a | ||
![]() |
dc1ecc19ed | ||
![]() |
e947e86eae | ||
![]() |
5d3955854e | ||
![]() |
3ff4b713e8 | ||
![]() |
68097568d5 | ||
![]() |
cd8d57040c | ||
![]() |
812efca08e | ||
![]() |
1db1a00581 | ||
![]() |
e0ba872b66 | ||
![]() |
9c82441c19 | ||
![]() |
38db0cc713 | ||
![]() |
ee217eb9b7 | ||
![]() |
3d36eb5baf | ||
![]() |
d2d324f2dd | ||
![]() |
2b37721a6e | ||
![]() |
353db0bc6c | ||
![]() |
d1aed94d27 | ||
![]() |
281cdf65da | ||
![]() |
5af5c90492 | ||
![]() |
ca421c28a1 | ||
![]() |
711345eff7 | ||
![]() |
102975aeb3 | ||
![]() |
cd12503f99 | ||
![]() |
1e724eba6c | ||
![]() |
c70ce791db | ||
![]() |
0821f6463a | ||
![]() |
444ac5fe95 | ||
![]() |
b9228df32c | ||
![]() |
b6bf0ffc40 | ||
![]() |
34e6e70be9 | ||
![]() |
5b3f8a3d30 | ||
![]() |
fceec71ad3 | ||
![]() |
a69f74f51b | ||
![]() |
e26c038565 | ||
![]() |
52e39c3402 | ||
![]() |
f2af168986 | ||
![]() |
6e1ffb4e52 | ||
![]() |
f88c1e1e8b | ||
![]() |
ddda80a577 | ||
![]() |
d758e50634 | ||
![]() |
a6021730cd | ||
![]() |
e9fcad4787 | ||
![]() |
640d4b0280 | ||
![]() |
b9378a7c1f | ||
![]() |
abb6b4282d | ||
![]() |
9ecd5dff09 | ||
![]() |
aa41fec466 | ||
![]() |
e4641cd427 | ||
![]() |
dba24ec1f9 | ||
![]() |
abe6dfb99c | ||
![]() |
d08d7cf31f | ||
![]() |
6e73c489de | ||
![]() |
489df0ed7d | ||
![]() |
7924bb5b6b | ||
![]() |
c47d1af5e3 | ||
![]() |
51af961e0d | ||
![]() |
86997794ab | ||
![]() |
2db29187f4 | ||
![]() |
22c201be39 | ||
![]() |
cdd5e89b86 | ||
![]() |
764b6aa2b1 | ||
![]() |
f766ef2033 | ||
![]() |
ef4a6238c8 | ||
![]() |
b3554a6a49 | ||
![]() |
5fb7b3266b | ||
![]() |
8b6e110635 | ||
![]() |
f5a1f915be | ||
![]() |
ac15339911 | ||
![]() |
fdfeac081a | ||
![]() |
31396a632f | ||
![]() |
223150aa42 | ||
![]() |
135fc08212 | ||
![]() |
5e3caf68a5 | ||
![]() |
262b3a2945 | ||
![]() |
e44d09208c | ||
![]() |
0546c9b9fc | ||
![]() |
38c4a1ed85 | ||
![]() |
fd8e92cf77 | ||
![]() |
062570cc47 | ||
![]() |
9514316be3 | ||
![]() |
a15a5adacc | ||
![]() |
b6e6d39985 | ||
![]() |
48ae830262 | ||
![]() |
03f5dd71a5 | ||
![]() |
2afbe58722 | ||
![]() |
0a64eac778 | ||
![]() |
ad605e2c5a | ||
![]() |
eed44b3231 | ||
![]() |
944e295ae7 | ||
![]() |
28109fef38 | ||
![]() |
40442f3f82 | ||
![]() |
61da167b4f | ||
![]() |
c744f6756b | ||
![]() |
de7057ac3a | ||
![]() |
585bfff11d | ||
![]() |
0f9c20c986 | ||
![]() |
f860392ae9 | ||
![]() |
391830558e | ||
![]() |
c1f37d8591 | ||
![]() |
b175774ad8 | ||
![]() |
73e32889b6 | ||
![]() |
400ee808e0 | ||
![]() |
87976693f8 | ||
![]() |
9c7ed80662 | ||
![]() |
eb3363d4dd | ||
![]() |
edff696ecc | ||
![]() |
9c19e9813a | ||
![]() |
2679a4bf1e | ||
![]() |
e8216b2e80 | ||
![]() |
e3062d7c66 | ||
![]() |
fd55d85bbf | ||
![]() |
f10d591462 | ||
![]() |
3e15c77a05 | ||
![]() |
1bb166a9e8 | ||
![]() |
8fa949537b | ||
![]() |
7454b31788 | ||
![]() |
b6488fe342 | ||
![]() |
b1d9080a0f | ||
![]() |
50269d0f5e | ||
![]() |
f17155bb3f | ||
![]() |
7988fe0c5a | ||
![]() |
f4a5b3bcbf | ||
![]() |
cd0e585586 | ||
![]() |
464247784d | ||
![]() |
56800c24b9 | ||
![]() |
6af2242d5d | ||
![]() |
d21fac658b | ||
![]() |
27f6c3b634 | ||
![]() |
b3bfec9505 | ||
![]() |
367ece8ffa | ||
![]() |
661cd4c182 | ||
![]() |
be856f71c8 | ||
![]() |
97978033dd | ||
![]() |
413a1b504a | ||
![]() |
8078620977 | ||
![]() |
69e8e4d63e | ||
![]() |
fb1360b72a | ||
![]() |
231e677b16 | ||
![]() |
fcac53cdc0 | ||
![]() |
b07f1a77aa | ||
![]() |
c13b858f02 | ||
![]() |
5d9bf8055e | ||
![]() |
dfc46c3b6c | ||
![]() |
d255d3e376 | ||
![]() |
eea4f0f41c | ||
![]() |
12796920a3 | ||
![]() |
dfd6534a1c | ||
![]() |
fedc26e3cb | ||
![]() |
1ac62541a8 | ||
![]() |
5942add141 | ||
![]() |
9eb72d5a86 | ||
![]() |
26579cc170 | ||
![]() |
d70b768031 | ||
![]() |
0c47fc7017 | ||
![]() |
c537776826 | ||
![]() |
7c5b4510af | ||
![]() |
bf1ebf8733 | ||
![]() |
8edfafcf09 | ||
![]() |
10a5741f36 | ||
![]() |
c7d392e77e | ||
![]() |
161007fe92 | ||
![]() |
5fc85fa2e0 | ||
![]() |
4a27d371e0 | ||
![]() |
a4c9e0a35e | ||
![]() |
a6f57a8665 | ||
![]() |
0df696739f | ||
![]() |
86ee94eb04 | ||
![]() |
0923594e51 | ||
![]() |
3bb51875bc | ||
![]() |
40225443ed | ||
![]() |
10977eaefa | ||
![]() |
3103fd7302 | ||
![]() |
281ac13eed | ||
![]() |
e5f30a07bf | ||
![]() |
9c4d5526f4 | ||
![]() |
77737a5687 | ||
![]() |
869d46f15c | ||
![]() |
1afb9cdba9 | ||
![]() |
730664eefb | ||
![]() |
6b210e1542 | ||
![]() |
f1b15a95a4 | ||
![]() |
1d53389ca9 | ||
![]() |
8fc5fa979d | ||
![]() |
074a8ff46a | ||
![]() |
a2f2d562f6 | ||
![]() |
bd6b3c53c5 | ||
![]() |
8282b8a6c0 | ||
![]() |
72a250b610 | ||
![]() |
b0516fbf1d | ||
![]() |
05903502c5 | ||
![]() |
2bf58abb89 | ||
![]() |
9d01d88eed | ||
![]() |
f07886fc5e | ||
![]() |
2984649106 | ||
![]() |
60671c99ed | ||
![]() |
bce77aaec7 | ||
![]() |
f2e3020f9d | ||
![]() |
e9ef9451e5 | ||
![]() |
7c1d06e023 | ||
![]() |
6b89b44dcd | ||
![]() |
225f69b75b | ||
![]() |
44bc6bf069 | ||
![]() |
e5af1c93ae | ||
![]() |
d6617007d4 | ||
![]() |
8db90ba449 | ||
![]() |
16b0df69b1 | ||
![]() |
048b0972de | ||
![]() |
a7989795e8 | ||
![]() |
a40f035810 | ||
![]() |
aad5e26f31 | ||
![]() |
627c6e29a2 | ||
![]() |
95c32d6f4a | ||
![]() |
747df59741 | ||
![]() |
a4e883c119 | ||
![]() |
289f9105d9 | ||
![]() |
5804483c89 | ||
![]() |
16732905bf | ||
![]() |
ef1e7e5b52 | ||
![]() |
abf1cc536d | ||
![]() |
c38f150562 | ||
![]() |
d2b6bda7a2 | ||
![]() |
9e5c68c575 | ||
![]() |
88eed6cc23 | ||
![]() |
a1773d166f | ||
![]() |
5e2ef7ff0d | ||
![]() |
cfda073aa5 | ||
![]() |
ff774a1870 | ||
![]() |
feb03f7e30 | ||
![]() |
95a65d5704 | ||
![]() |
5c1af6d296 | ||
![]() |
6d812b86aa | ||
![]() |
7b7ab3f419 | ||
![]() |
ef35b36eba | ||
![]() |
bb83d2b489 | ||
![]() |
3dc1adb69e | ||
![]() |
a95a5ed13e | ||
![]() |
da61c9f915 | ||
![]() |
9472c36cbd | ||
![]() |
49c12a31e9 | ||
![]() |
fc061599f8 | ||
![]() |
b066457ccf | ||
![]() |
2c5c7dfe3a | ||
![]() |
4573407fc7 | ||
![]() |
9912c11043 | ||
![]() |
231c5e515f | ||
![]() |
e9870d9e1d | ||
![]() |
c274ee9873 | ||
![]() |
c8caf48cda | ||
![]() |
1de662f779 | ||
![]() |
e4f97465a4 | ||
![]() |
84887395f8 | ||
![]() |
e333197ed5 | ||
![]() |
bf766f1670 | ||
![]() |
51bdc30ed0 | ||
![]() |
4b892e2b30 | ||
![]() |
43b2176956 | ||
![]() |
00283fac30 | ||
![]() |
78f6a86645 | ||
![]() |
9d2ab61993 | ||
![]() |
8fdd828de4 | ||
![]() |
25795c3a96 | ||
![]() |
7f3da04fee | ||
![]() |
7864521cb4 | ||
![]() |
31b83ba47a | ||
![]() |
9524c6245d | ||
![]() |
57d2fe113a | ||
![]() |
2f6cb87bba | ||
![]() |
3cef7f3201 | ||
![]() |
2225933946 | ||
![]() |
47259ef152 | ||
![]() |
b2eb631a97 | ||
![]() |
9e0f37a2de | ||
![]() |
f712ea34e0 | ||
![]() |
a44b7c9c9e | ||
![]() |
4b32890b5f | ||
![]() |
a41aa01461 | ||
![]() |
2ed6819e2c | ||
![]() |
ea875c59af | ||
![]() |
a22162ffac | ||
![]() |
83d16dc656 | ||
![]() |
8ceefee1e3 | ||
![]() |
8f157be7e0 | ||
![]() |
38579e9a29 | ||
![]() |
30a91f59ae | ||
![]() |
0e169951f7 | ||
![]() |
8b9db369f6 | ||
![]() |
f7e10eb094 | ||
![]() |
0d73d193ad | ||
![]() |
40815086ad | ||
![]() |
16860603fd | ||
![]() |
c607089cbb | ||
![]() |
28464344c1 | ||
![]() |
ed68e3bd46 | ||
![]() |
082d7a3f18 | ||
![]() |
6eddaa0d38 | ||
![]() |
1aa1a0287e | ||
![]() |
3bfcb16f9a | ||
![]() |
f37d869ea2 | ||
![]() |
78547b4fa4 | ||
![]() |
29e56b9f2d | ||
![]() |
83357ca67e | ||
![]() |
8482bf9fed | ||
![]() |
2a98cca801 | ||
![]() |
6277d4981c | ||
![]() |
02deaa0f1a | ||
![]() |
4a278ef102 | ||
![]() |
7ab8f9f112 | ||
![]() |
7fca0e0786 | ||
![]() |
0b0dfd0a37 | ||
![]() |
dd07bd91a4 | ||
![]() |
ed4eb124e4 | ||
![]() |
4070007c93 | ||
![]() |
5b213a19e4 | ||
![]() |
34d81d3bf2 | ||
![]() |
8bc8355b68 | ||
![]() |
ab99c14fd2 | ||
![]() |
1047158a66 | ||
![]() |
0c63950429 | ||
![]() |
aa9cd8c88f | ||
![]() |
3110b08988 | ||
![]() |
fe227d5b94 | ||
![]() |
489f052ef9 | ||
![]() |
8313f6bb51 | ||
![]() |
bf55ed262f | ||
![]() |
f26bf33ead | ||
![]() |
ca29f6cc1f | ||
![]() |
cb80891a5f | ||
![]() |
9db0133a5b | ||
![]() |
28b34f3796 | ||
![]() |
1f57c87859 | ||
![]() |
fbf5549182 | ||
![]() |
051c572e7f | ||
![]() |
464a646671 | ||
![]() |
ed87465565 | ||
![]() |
f9109ebc81 | ||
![]() |
4a7af6f9ac | ||
![]() |
7fbef35daa | ||
![]() |
e6391a860a | ||
![]() |
ebce4c5b7e | ||
![]() |
e7e61a0c4c | ||
![]() |
131f78c0c2 | ||
![]() |
67b5de38b1 | ||
![]() |
f9994abb94 | ||
![]() |
ca0f56eea8 | ||
![]() |
500acce178 | ||
![]() |
6805c75c9c | ||
![]() |
75917c7f61 | ||
![]() |
59d1ded94e | ||
![]() |
973a966011 | ||
![]() |
510efaae97 | ||
![]() |
11bd2369e5 | ||
![]() |
f80d1dc48d | ||
![]() |
8bff445ec3 | ||
![]() |
d73ca41cfe | ||
![]() |
f3a9b81b67 | ||
![]() |
3cc43e9fb9 | ||
![]() |
bc33322d4b | ||
![]() |
c054ea0737 | ||
![]() |
ce6f3ca5df | ||
![]() |
52dbfdee00 | ||
![]() |
1e964a36a9 | ||
![]() |
679e81e091 | ||
![]() |
2e3e4f5a84 | ||
![]() |
208dde631f | ||
![]() |
4227866fcf | ||
![]() |
335e682299 | ||
![]() |
5c0ed22b09 | ||
![]() |
e1b8a3fbdf | ||
![]() |
1a432f2ee3 | ||
![]() |
db45042a56 | ||
![]() |
a50b9bd6ff | ||
![]() |
2089f3e54c | ||
![]() |
5e0788b99c | ||
![]() |
67669c286b | ||
![]() |
408a71cfdc | ||
![]() |
6399e39507 | ||
![]() |
f9443f7421 | ||
![]() |
4f6b5b3b89 | ||
![]() |
b9b09d325a | ||
![]() |
50f3131f1a | ||
![]() |
697b8411df | ||
![]() |
fcaebc838e | ||
![]() |
cde32a8aed | ||
![]() |
ec3efea05a | ||
![]() |
571bf397c5 | ||
![]() |
737a331c85 | ||
![]() |
2de33d8d07 | ||
![]() |
7f21f6e80e | ||
![]() |
0b11afaf2f | ||
![]() |
e136a6f915 | ||
![]() |
74921d3afa | ||
![]() |
edd2b110b0 | ||
![]() |
80fb21e031 | ||
![]() |
ebd06bdd24 | ||
![]() |
6f86e21605 | ||
![]() |
816154c7cb | ||
![]() |
d9230c0103 | ||
![]() |
5c7dfd1d69 | ||
![]() |
7aacaf8c38 | ||
![]() |
ee6a279596 | ||
![]() |
a9af1dfdd2 | ||
![]() |
fc46233baf | ||
![]() |
2eec2e9128 | ||
![]() |
8024b437e9 | ||
![]() |
d1f3f15478 | ||
![]() |
059cfcbad2 | ||
![]() |
1a8f396e77 | ||
![]() |
5640365fbd | ||
![]() |
4b7de86a92 | ||
![]() |
24ec642181 | ||
![]() |
8dce66d76f | ||
![]() |
22d75f3ecb | ||
![]() |
7972678fe6 | ||
![]() |
ffc1d9a212 | ||
![]() |
7f018b90db | ||
![]() |
8a774dc90d | ||
![]() |
368c6c0ccb | ||
![]() |
5c4874b90f | ||
![]() |
3420faab08 | ||
![]() |
a548b34811 | ||
![]() |
56cbf3736b | ||
![]() |
ad30eb809c | ||
![]() |
ee368452ae | ||
![]() |
a9095ca2ad | ||
![]() |
013522c376 | ||
![]() |
947242d913 | ||
![]() |
8a896114c1 | ||
![]() |
47f58040d1 | ||
![]() |
35a118a2a7 | ||
![]() |
582032f372 | ||
![]() |
311d392386 | ||
![]() |
404c13d4c1 | ||
![]() |
5c68c8ece8 | ||
![]() |
4d7a6fb6de | ||
![]() |
630558ed4f | ||
![]() |
69942003f7 | ||
![]() |
af9c2bd59d | ||
![]() |
81c4b822e0 | ||
![]() |
81fb44c45c | ||
![]() |
d66997c2ed | ||
![]() |
d7a654fc27 | ||
![]() |
229422bfa9 | ||
![]() |
baabba1dea | ||
![]() |
8f5d564f84 | ||
![]() |
dcb332e08d | ||
![]() |
51e72d1a05 | ||
![]() |
8f37015dbb | ||
![]() |
74df7fcd66 | ||
![]() |
bfaf074f4e | ||
![]() |
3281ed2ef1 | ||
![]() |
b2c2570a85 | ||
![]() |
abf185c691 | ||
![]() |
f4fe5fcb16 | ||
![]() |
37275e8fe3 | ||
![]() |
f1dab11f1f | ||
![]() |
6d1c61407d | ||
![]() |
8b400b48f7 | ||
![]() |
b845645b80 | ||
![]() |
cacce6d2d0 | ||
![]() |
373ee53143 | ||
![]() |
344c33d9a1 | ||
![]() |
c5b970cca3 | ||
![]() |
15947161e6 | ||
![]() |
394eb92e71 | ||
![]() |
d62cdc659f | ||
![]() |
a6cc13845a | ||
![]() |
55a995c4cd | ||
![]() |
ca26fcb0eb | ||
![]() |
4eddd2c3d1 | ||
![]() |
c53143ef4f | ||
![]() |
e772244440 | ||
![]() |
ae369ec9ba | ||
![]() |
e8669d4ab5 | ||
![]() |
cd14096dbe | ||
![]() |
d9ff114e1a | ||
![]() |
a1c6f0073e | ||
![]() |
f1de353b74 | ||
![]() |
5da8d5fc73 | ||
![]() |
3ba04f179f | ||
![]() |
3890d0abdb | ||
![]() |
8b209df253 | ||
![]() |
25a43b57b2 | ||
![]() |
b7a44560f5 | ||
![]() |
0e8cc72b13 | ||
![]() |
33e20766c9 | ||
![]() |
9f993e0c49 | ||
![]() |
6ea85e6380 | ||
![]() |
4d58026d06 | ||
![]() |
7b9b9218dc | ||
![]() |
dff1adb1ad | ||
![]() |
35eeccd45a | ||
![]() |
429f2536af | ||
![]() |
7b41acb781 | ||
![]() |
cc7a8fb1a6 | ||
![]() |
c1e78cf55b | ||
![]() |
4536e8b55b | ||
![]() |
70e3c9805a | ||
![]() |
8187a3bc04 | ||
![]() |
4443c908cb | ||
![]() |
c03eac1dc9 | ||
![]() |
61c1da144e | ||
![]() |
3692858a3d | ||
![]() |
9c51fc3ade | ||
![]() |
1cf746f721 | ||
![]() |
4979f84e41 | ||
![]() |
a19073ec01 | ||
![]() |
1b39b5376f | ||
![]() |
6559416bd8 | ||
![]() |
fa25ecf521 | ||
![]() |
6fb0256997 | ||
![]() |
8c26403e91 | ||
![]() |
90a89f8ca5 | ||
![]() |
0bba1d95de | ||
![]() |
b3f99645a3 | ||
![]() |
76ced59b62 | ||
![]() |
bc3731265e | ||
![]() |
189c92affa | ||
![]() |
4ec9cbe379 | ||
![]() |
9648525ac1 | ||
![]() |
b125780991 | ||
![]() |
99104fc11d | ||
![]() |
7cb137ae8d | ||
![]() |
e55e79bcca | ||
![]() |
0b644fd794 | ||
![]() |
d5599ebfa3 | ||
![]() |
f7d8781bac | ||
![]() |
6f7298b9db | ||
![]() |
d0b6d95f1b | ||
![]() |
93b913e14d | ||
![]() |
b96c8a0c2f | ||
![]() |
a392a06cc0 | ||
![]() |
d9af788514 | ||
![]() |
a4724fec4a | ||
![]() |
0e5580390f | ||
![]() |
acc34cb618 | ||
![]() |
d033a6e40d | ||
![]() |
4fd8294b09 | ||
![]() |
8d26d9da46 | ||
![]() |
d81607c9d5 | ||
![]() |
5ac71e0579 | ||
![]() |
d04ecbcb0a | ||
![]() |
e4987d9a59 | ||
![]() |
155c6e94a3 | ||
![]() |
4e285a4e70 | ||
![]() |
9c00e681bb | ||
![]() |
81369d7e04 | ||
![]() |
160891592b | ||
![]() |
70b20f90cd | ||
![]() |
47a2adca96 | ||
![]() |
a1f1acfbf9 | ||
![]() |
00b9c082a3 | ||
![]() |
45d2492bcb | ||
![]() |
085d1e0d38 | ||
![]() |
1404581e9b | ||
![]() |
d5985be94a | ||
![]() |
4ee1cd5826 | ||
![]() |
dc7fce86a5 | ||
![]() |
f22417e7e7 | ||
![]() |
10c9661369 | ||
![]() |
8ad7bf60d7 | ||
![]() |
898a936064 | ||
![]() |
4e401bc059 | ||
![]() |
9ecef6f011 | ||
![]() |
ad97b3d995 | ||
![]() |
04e8e03d8f | ||
![]() |
bd19013771 | ||
![]() |
3901ffca17 | ||
![]() |
cbd3308da6 | ||
![]() |
0ad6b3b88e | ||
![]() |
4e87f5aabc | ||
![]() |
2019af831a | ||
![]() |
1e076ea63d | ||
![]() |
4863084fa2 | ||
![]() |
7ba79171c7 | ||
![]() |
e3c2aea3cc | ||
![]() |
21c9530e8b | ||
![]() |
036196a487 | ||
![]() |
73855cacb7 | ||
![]() |
8dad6d7e1c | ||
![]() |
e5ffa2aa09 | ||
![]() |
8445c381c5 | ||
![]() |
fa46b7bf85 | ||
![]() |
7ce2250d85 | ||
![]() |
ef20d9b91a | ||
![]() |
fbee310261 | ||
![]() |
7d6bf4b0ca | ||
![]() |
210834fbe9 | ||
![]() |
24cf19710f | ||
![]() |
a59660f421 | ||
![]() |
be5af0b777 | ||
![]() |
75e5fe7d27 | ||
![]() |
2985258074 | ||
![]() |
911ac65d1e | ||
![]() |
d2967f514b | ||
![]() |
a68c6a2cfc | ||
![]() |
733f6aae85 | ||
![]() |
1daece3bee | ||
![]() |
adddd48c1d | ||
![]() |
8c870cd3ca | ||
![]() |
bd5eda92a7 | ||
![]() |
cf09cef6d8 | ||
![]() |
b3f9f8275d | ||
![]() |
9597d474d0 | ||
![]() |
e6f2e9791c | ||
![]() |
31b1370270 | ||
![]() |
064a4ce798 | ||
![]() |
ac5843edb0 | ||
![]() |
a1f64e4774 | ||
![]() |
21d2ae709f | ||
![]() |
c5e509f069 | ||
![]() |
761c0ff9ac | ||
![]() |
ce8289e753 | ||
![]() |
2dd4f8b04a | ||
![]() |
b4615f7655 | ||
![]() |
ba394a7ab4 | ||
![]() |
d32490a4be | ||
![]() |
fcaa787060 | ||
![]() |
23c1fc3544 | ||
![]() |
a4037a8268 | ||
![]() |
61ee1c61df | ||
![]() |
69f95f4148 | ||
![]() |
212a413e93 | ||
![]() |
de4b5a8f0f | ||
![]() |
1228ce277f | ||
![]() |
bd6fdd625a | ||
![]() |
7de17ad949 | ||
![]() |
7ab11a8379 | ||
![]() |
70e0085596 | ||
![]() |
f9ccc19df5 | ||
![]() |
5c69568c7f | ||
![]() |
1d69bd48be | ||
![]() |
5b435c586e | ||
![]() |
71e46d1eca | ||
![]() |
238aff7c31 | ||
![]() |
a1435bd566 | ||
![]() |
59d8c570b7 | ||
![]() |
8f34f69397 | ||
![]() |
47af21d248 | ||
![]() |
c2a3c1cb8f | ||
![]() |
1e2d76a686 | ||
![]() |
34468c16ad | ||
![]() |
b84c6b4b32 | ||
![]() |
8395cf8d5a | ||
![]() |
c2bf7f09ce | ||
![]() |
c2762d3b5e | ||
![]() |
01d996a5c0 | ||
![]() |
50739277c4 | ||
![]() |
0fef4e6e2e | ||
![]() |
218012558a | ||
![]() |
e40e86500b | ||
![]() |
6f0942ac6e | ||
![]() |
a67927c29c | ||
![]() |
7e50eed95e | ||
![]() |
173b6c3f00 | ||
![]() |
7646c683b5 | ||
![]() |
047fe21c14 | ||
![]() |
b59a601b52 | ||
![]() |
ecb8ef6bb1 | ||
![]() |
cd2eab6ba2 | ||
![]() |
6a4d8329c3 | ||
![]() |
b8dbb3f073 | ||
![]() |
9a5decdb28 | ||
![]() |
31e762d921 | ||
![]() |
bb495f567c | ||
![]() |
aa1db617d5 | ||
![]() |
9b3e43ffc1 | ||
![]() |
d5a0f8f23c | ||
![]() |
ec5cfe0019 | ||
![]() |
fd5626e9e2 | ||
![]() |
53bf3420e7 | ||
![]() |
127a27315e | ||
![]() |
671441bdf8 | ||
![]() |
5c6e2ed071 | ||
![]() |
254b276a54 | ||
![]() |
31df4e42d7 | ||
![]() |
2b8eb7ed66 | ||
![]() |
29fc0eff38 | ||
![]() |
4917da2d2e | ||
![]() |
8ea98b64aa | ||
![]() |
6526ff1612 | ||
![]() |
4904b48f5c | ||
![]() |
bb5390d63a | ||
![]() |
05d5ef602c | ||
![]() |
a311519314 | ||
![]() |
1dc146322c | ||
![]() |
0f551baf37 | ||
![]() |
b9190eddfe | ||
![]() |
44dada9e60 | ||
![]() |
1b8c517e3e | ||
![]() |
20602889be | ||
![]() |
4b06536582 | ||
![]() |
621b38c98b | ||
![]() |
321cf8bf7d | ||
![]() |
762cdc812c | ||
![]() |
dae5aa38a8 | ||
![]() |
7d42e50f5b | ||
![]() |
a4c083e7f9 | ||
![]() |
e4f202834c | ||
![]() |
6e0c380409 | ||
![]() |
4cdf6eda2c | ||
![]() |
652d50173e | ||
![]() |
fa58a81852 | ||
![]() |
f2fc2cc24a | ||
![]() |
0a2fc08706 | ||
![]() |
6e81f2430b | ||
![]() |
509036f162 | ||
![]() |
bd1aae8d66 | ||
![]() |
c24aed054f | ||
![]() |
4040cb36bb | ||
![]() |
53a659c0cf | ||
![]() |
0cf412efb3 | ||
![]() |
2c7977d3e8 | ||
![]() |
85b5cb55de | ||
![]() |
6ed69d8ed8 | ||
![]() |
0a92ac97d4 | ||
![]() |
0aa08a5e40 | ||
![]() |
955748f00f | ||
![]() |
3c48825699 | ||
![]() |
bc53bc7cfd | ||
![]() |
5ce5f84f4a | ||
![]() |
ffa7efedd6 | ||
![]() |
5e6752db14 | ||
![]() |
248ca5ee12 | ||
![]() |
7fb2973431 | ||
![]() |
3a419126f3 | ||
![]() |
ef5c71374b | ||
![]() |
5ccf2d7bcc | ||
![]() |
c85936bb11 | ||
![]() |
bfb56b4144 | ||
![]() |
3fb5073feb | ||
![]() |
75df1fa3ac | ||
![]() |
ba8370bcfd | ||
![]() |
813f55152a | ||
![]() |
270a541a7c | ||
![]() |
c34549a47d | ||
![]() |
96d6b309ec | ||
![]() |
931906c9f3 | ||
![]() |
b6368b1296 | ||
![]() |
cb1fa8b5ae | ||
![]() |
601bc96734 | ||
![]() |
8441aff066 | ||
![]() |
9818f179c4 | ||
![]() |
91e1d35a10 | ||
![]() |
74c9a3dc50 | ||
![]() |
55fc3fc177 | ||
![]() |
724eac9168 | ||
![]() |
a528cee5f4 | ||
![]() |
e16917f63a | ||
![]() |
984d19a9a5 | ||
![]() |
229481c89c | ||
![]() |
f9af698521 | ||
![]() |
db96d5246f | ||
![]() |
2e771cd65a | ||
![]() |
638f227b51 | ||
![]() |
0e73eb568e | ||
![]() |
3261855b8f | ||
![]() |
3bbabb8416 | ||
![]() |
629b685f5a | ||
![]() |
6b1a6d264b | ||
![]() |
79540a8b9c | ||
![]() |
99d62381b9 | ||
![]() |
860d28e16c | ||
![]() |
ac00c8f6ae | ||
![]() |
b5fa93eda0 | ||
![]() |
a8573f268b | ||
![]() |
fa141e394b | ||
![]() |
e72bb87cc1 | ||
![]() |
32b294f1f3 | ||
![]() |
70d4369d81 | ||
![]() |
75f601c154 | ||
![]() |
335cc234c8 | ||
![]() |
5343781e14 | ||
![]() |
a00bc95acc | ||
![]() |
d289dc8a53 | ||
![]() |
93deaa5687 | ||
![]() |
102c05e927 | ||
![]() |
cf598dc3cb | ||
![]() |
1ecb0ca081 | ||
![]() |
5459a55406 | ||
![]() |
fa1c11f5f9 | ||
![]() |
2623f0e360 | ||
![]() |
b81eb35f3d | ||
![]() |
66fffce87c | ||
![]() |
6e8c9f92cb | ||
![]() |
fc61aae20a | ||
![]() |
3d9d25df52 | ||
![]() |
5f3db017af | ||
![]() |
69646e5b5d | ||
![]() |
4e459b3383 | ||
![]() |
8c5e8bdf78 | ||
![]() |
6bc750cab7 | ||
![]() |
70d9a77e9b | ||
![]() |
6d2b5d976d | ||
![]() |
57231382a6 | ||
![]() |
1f6fc0630d | ||
![]() |
e5ee405971 | ||
![]() |
5522a7a2e5 | ||
![]() |
cbc718437b | ||
![]() |
8ca701b882 | ||
![]() |
f5e253456c | ||
![]() |
8cc29241e2 | ||
![]() |
4ea0d05c17 | ||
![]() |
7898c33819 | ||
![]() |
d0ba87f7ee | ||
![]() |
5ee961d3eb | ||
![]() |
ac10e15c15 | ||
![]() |
71159cf0c8 | ||
![]() |
b2f22ac584 | ||
![]() |
8c662c9d7b | ||
![]() |
8cd3083e52 | ||
![]() |
06227d4514 | ||
![]() |
f5ce228059 | ||
![]() |
53f8415e9b | ||
![]() |
710964b47d | ||
![]() |
4dafe424cf | ||
![]() |
bc4a0a575c | ||
![]() |
cf213affa2 | ||
![]() |
e29aaaf162 | ||
![]() |
979a320347 | ||
![]() |
20bddd8e47 | ||
![]() |
86f335b01f | ||
![]() |
102204e293 | ||
![]() |
67651354d5 | ||
![]() |
cefb52471f | ||
![]() |
ee5e0e13b7 | ||
![]() |
30a8f25d52 | ||
![]() |
d348c2099e | ||
![]() |
6a400dda7b | ||
![]() |
080c4ba680 | ||
![]() |
37aca3f1c7 | ||
![]() |
0158f1363b | ||
![]() |
f47f2d13fa | ||
![]() |
6fe6f4b3e0 | ||
![]() |
00e4631b3b | ||
![]() |
2e7503ff78 | ||
![]() |
02fa5aa0fa | ||
![]() |
9b4a67276a | ||
![]() |
b607a09125 | ||
![]() |
af89f05011 | ||
![]() |
fed5161fc6 | ||
![]() |
b8b97fa6d4 | ||
![]() |
71f141f3f8 | ||
![]() |
81fef1be19 | ||
![]() |
0f175de599 | ||
![]() |
1602befc51 | ||
![]() |
162a838afc | ||
![]() |
05a5e4372a | ||
![]() |
e588abd4e7 | ||
![]() |
f85b206bdf | ||
![]() |
5b3bbfce10 | ||
![]() |
2a9733fbaf | ||
![]() |
96185faca6 | ||
![]() |
af20b2ce0d | ||
![]() |
919b92a0b5 | ||
![]() |
3b0153ca7a | ||
![]() |
5f16e4ef87 | ||
![]() |
483dc06ecb | ||
![]() |
b8e389c6e8 | ||
![]() |
cde4ee91f8 | ||
![]() |
ab45efceab | ||
![]() |
cbdcf5905f | ||
![]() |
62c0e6605c | ||
![]() |
f1c6988552 | ||
![]() |
e1197f7253 | ||
![]() |
146062d921 | ||
![]() |
96c4201929 | ||
![]() |
a0bbcd2fee | ||
![]() |
627b4c8b14 | ||
![]() |
fd6c352881 | ||
![]() |
3f7ba2e3d1 | ||
![]() |
7c180727b9 | ||
![]() |
19d4e2224b | ||
![]() |
678edb1846 | ||
![]() |
ae2ba5771f | ||
![]() |
cd9dd2e679 | ||
![]() |
47f9ed08e9 | ||
![]() |
ee3c06394d | ||
![]() |
ffba1d5037 | ||
![]() |
ccc3d38c45 | ||
![]() |
37517c7dd1 | ||
![]() |
a4dee77728 | ||
![]() |
a95318a4f9 | ||
![]() |
46fad32837 | ||
![]() |
5be40f62f3 | ||
![]() |
fb75519ff8 | ||
![]() |
5fea12d8eb | ||
![]() |
8291098b6d | ||
![]() |
1a000fecd5 | ||
![]() |
a0dc66abe7 | ||
![]() |
3d47d73ba9 | ||
![]() |
443ebc46d6 | ||
![]() |
99379ede8a | ||
![]() |
4871095a3e | ||
![]() |
21dc988e45 | ||
![]() |
01e0dd50ad | ||
![]() |
d3bc184971 | ||
![]() |
c42f29446d | ||
![]() |
1030e09fc1 | ||
![]() |
96b930cd07 | ||
![]() |
de08edb831 | ||
![]() |
ee477b25e5 | ||
![]() |
277f21d5b2 | ||
![]() |
a7d5d9a1d6 | ||
![]() |
fd0d76e866 | ||
![]() |
646d8f431c | ||
![]() |
ef0d562702 | ||
![]() |
962fe9c36d | ||
![]() |
50e2385e82 | ||
![]() |
1cd3ef5dba | ||
![]() |
80157fc1be | ||
![]() |
c5fc37150d | ||
![]() |
8932adbf88 | ||
![]() |
d27d36b76a | ||
![]() |
ba804c7d4a | ||
![]() |
3db37166b4 | ||
![]() |
bf02a569ee | ||
![]() |
015982bed4 | ||
![]() |
a1c5c94753 | ||
![]() |
7a356412d5 | ||
![]() |
1ea716a31f | ||
![]() |
bb27bf9d34 | ||
![]() |
a489f40b76 | ||
![]() |
8ed87e7fbb | ||
![]() |
cc96ac173c | ||
![]() |
79f8270c35 | ||
![]() |
336f9f3813 | ||
![]() |
835c5e9d43 | ||
![]() |
4789cf6c31 | ||
![]() |
5f1f52b6ce | ||
![]() |
62abfa96b8 | ||
![]() |
a8a96b7631 | ||
![]() |
af80d96b9e | ||
![]() |
3c23fb0b13 | ||
![]() |
bba0ea1255 | ||
![]() |
750490cd2f | ||
![]() |
ff8e44e4f3 | ||
![]() |
579b8611be | ||
![]() |
da12b92d75 | ||
![]() |
a3f99bd781 | ||
![]() |
7ae908a466 | ||
![]() |
00767f4bf3 | ||
![]() |
54f0b3d8b3 | ||
![]() |
08eb70833d | ||
![]() |
42aafd3a2d | ||
![]() |
7f846429cf | ||
![]() |
2acaefdb2a | ||
![]() |
9c2cdd2513 | ||
![]() |
01683aa816 | ||
![]() |
85f701b94e | ||
![]() |
ff7cfe4715 | ||
![]() |
d3cd3d62b4 | ||
![]() |
91c67b085b | ||
![]() |
cd8c7ec3c0 | ||
![]() |
2c51a7970d | ||
![]() |
fb362022f7 | ||
![]() |
2814ae6d3c | ||
![]() |
ed2967ec7d | ||
![]() |
616fb47983 | ||
![]() |
7225199deb | ||
![]() |
c08a4e851b | ||
![]() |
9f8e8c0856 | ||
![]() |
e2a7b9ac56 | ||
![]() |
5e593f687d | ||
![]() |
3223ec04e3 | ||
![]() |
f388a1af67 | ||
![]() |
c1fe5c8d07 | ||
![]() |
608e73e2f2 | ||
![]() |
2e538b8959 | ||
![]() |
1278fc27ae | ||
![]() |
be95d7fe0f | ||
![]() |
9397ff8dd0 | ||
![]() |
906ee75278 | ||
![]() |
377914f1d8 | ||
![]() |
4049abf2c0 | ||
![]() |
47798febed | ||
![]() |
5bf439ad9e | ||
![]() |
3b1b23ba2a | ||
![]() |
9274e6417a | ||
![]() |
67b2503062 | ||
![]() |
dce6565af4 | ||
![]() |
8b3aec5edb | ||
![]() |
b0e4f947ea | ||
![]() |
3a9cdb28ab | ||
![]() |
79060f0bfe | ||
![]() |
91bcd8766a | ||
![]() |
4e633504a8 | ||
![]() |
144a10f7a6 | ||
![]() |
72a2644f25 | ||
![]() |
e865c4350e | ||
![]() |
52cc4a0a05 | ||
![]() |
e103e4817c | ||
![]() |
d0637a8832 | ||
![]() |
94f774b82d | ||
![]() |
651b79d3ed | ||
![]() |
9e5b9ca326 | ||
![]() |
dfa606ef49 | ||
![]() |
2886bc3b01 | ||
![]() |
71c5aaa11e | ||
![]() |
466db83375 | ||
![]() |
17c0fffd73 | ||
![]() |
8a069b497f | ||
![]() |
af79479716 | ||
![]() |
8cfe8c17e3 | ||
![]() |
5108d75682 | ||
![]() |
ac53196dcc | ||
![]() |
1e652b159e | ||
![]() |
ea07d7751b | ||
![]() |
82de35d724 | ||
![]() |
f55e8ea3aa | ||
![]() |
7067ebdd12 | ||
![]() |
03bb2123f2 | ||
![]() |
e2f449f0c8 | ||
![]() |
b16e972710 | ||
![]() |
37cd71328c | ||
![]() |
9b2c86a37b | ||
![]() |
ce4dd33eab | ||
![]() |
8bbc3e531c | ||
![]() |
c5a06243a6 | ||
![]() |
bebd2b449c | ||
![]() |
658168eb8d | ||
![]() |
6b23df0659 | ||
![]() |
d59314801c | ||
![]() |
0f45c69388 | ||
![]() |
52542e04e8 | ||
![]() |
7fc0a3841a | ||
![]() |
22db4175f3 | ||
![]() |
8fc935b09d | ||
![]() |
07fb319e88 | ||
![]() |
12a78a826d | ||
![]() |
4a061f20ed | ||
![]() |
f3be89b503 | ||
![]() |
12acaf29dd | ||
![]() |
683d9816cb | ||
![]() |
8802582997 | ||
![]() |
983c98d262 | ||
![]() |
c38389672a | ||
![]() |
93148400a2 | ||
![]() |
d5cfcb28fc | ||
![]() |
40ea51e622 | ||
![]() |
194e43f5cb | ||
![]() |
08c928e1d0 | ||
![]() |
69dacb34b9 | ||
![]() |
60c3a2dc9c | ||
![]() |
b8e5e036b2 | ||
![]() |
2f87305f2d | ||
![]() |
15dc99f110 | ||
![]() |
2d907706ea | ||
![]() |
f5dbb07893 | ||
![]() |
a437672dc1 | ||
![]() |
388a4860b5 | ||
![]() |
4b72ee53b0 | ||
![]() |
d77c23ed34 | ||
![]() |
31635c122e | ||
![]() |
afef793fbb | ||
![]() |
3bc2ec90ef | ||
![]() |
a3e68c93f8 | ||
![]() |
15e6f1cb3b | ||
![]() |
89c540c520 | ||
![]() |
6632720bc3 | ||
![]() |
b5662c2d07 | ||
![]() |
0f74c2463e | ||
![]() |
fdfdf94cb9 | ||
![]() |
8595078053 | ||
![]() |
80be089ca9 | ||
![]() |
96ab2f855e | ||
![]() |
4206ae84c1 | ||
![]() |
2f21523da9 | ||
![]() |
6c1222ea32 | ||
![]() |
ba50de236c | ||
![]() |
bef8882a7c | ||
![]() |
0d8b7e23e7 | ||
![]() |
864c19e7dc | ||
![]() |
4b0ed9de5d | ||
![]() |
d18a34b766 | ||
![]() |
0cf24c5d36 | ||
![]() |
fa293e3415 | ||
![]() |
1531a5112c | ||
![]() |
e127db6fa6 | ||
![]() |
49b1649348 | ||
![]() |
9f503917c2 | ||
![]() |
54ef604569 | ||
![]() |
30ce906f72 | ||
![]() |
1c20eabb48 | ||
![]() |
f8c52c4dac | ||
![]() |
345ba74d58 | ||
![]() |
d2aaf152a0 | ||
![]() |
7bf1f3dba6 | ||
![]() |
452fe3a8e2 | ||
![]() |
c25e523df6 | ||
![]() |
65bb1dcdbf | ||
![]() |
fe42206e94 | ||
![]() |
dac47d9f52 | ||
![]() |
83a3d11f38 | ||
![]() |
03d5372525 | ||
![]() |
a454a41b51 | ||
![]() |
95631dba46 | ||
![]() |
ee827407aa | ||
![]() |
3aebfa22e9 | ||
![]() |
72eb3b4415 | ||
![]() |
3a40759cd2 | ||
![]() |
6f44ced7b6 | ||
![]() |
81843ddb6e | ||
![]() |
23d14ab443 | ||
![]() |
3d3d94655b | ||
![]() |
a6515d5450 | ||
![]() |
2d0da2c7a4 | ||
![]() |
f05affa984 | ||
![]() |
cd265fc31f | ||
![]() |
3c21be8fa5 | ||
![]() |
f681b0bb5a | ||
![]() |
d7fbddf6f8 | ||
![]() |
993c34911a | ||
![]() |
4a7cfd1a6c | ||
![]() |
402990dd9d | ||
![]() |
41faf70da1 | ||
![]() |
15e3b6301c | ||
![]() |
5b9c28b93b | ||
![]() |
6672169707 | ||
![]() |
9ff1baefde | ||
![]() |
552734faa5 | ||
![]() |
7268e04361 | ||
![]() |
45d8fef00c | ||
![]() |
0f83497284 | ||
![]() |
1475ff805f | ||
![]() |
7907182e7e | ||
![]() |
0397a3120f | ||
![]() |
cc34734131 | ||
![]() |
6dcde96f85 | ||
![]() |
0f457127df | ||
![]() |
064242d962 | ||
![]() |
ddcbe27fd3 | ||
![]() |
ccbc3af964 | ||
![]() |
cd95ec4e12 | ||
![]() |
fcd2d63df4 | ||
![]() |
e68d49e7df | ||
![]() |
ee19ea66b3 | ||
![]() |
6b490ee547 | ||
![]() |
e127697fff | ||
![]() |
558c9147a2 | ||
![]() |
4147c7c1d1 | ||
![]() |
45ef9b0278 | ||
![]() |
fc0e709817 | ||
![]() |
b67bf16d4f | ||
![]() |
fb3be544ce | ||
![]() |
53f5741317 | ||
![]() |
5134080f87 | ||
![]() |
07015973d2 | ||
![]() |
215880207e | ||
![]() |
41c4ab5739 | ||
![]() |
ff8868f6a3 | ||
![]() |
8c6e37d1d1 | ||
![]() |
c90237c14c | ||
![]() |
989bcbf895 | ||
![]() |
3e44856d01 | ||
![]() |
19dd9d266a | ||
![]() |
05370dbb94 | ||
![]() |
f3edc69897 | ||
![]() |
f6cad2d9cf | ||
![]() |
bd1c0033eb | ||
![]() |
dc67628ba5 | ||
![]() |
37b8a9375f | ||
![]() |
d71af9a625 | ||
![]() |
a163d5461d | ||
![]() |
5514616372 | ||
![]() |
a274baf5cd | ||
![]() |
96eb1425f8 | ||
![]() |
361760be0a | ||
![]() |
eea2768633 | ||
![]() |
d3562c70f5 | ||
![]() |
e06342eacf | ||
![]() |
e8d909553d | ||
![]() |
b21d231e3a | ||
![]() |
4058277b7a | ||
![]() |
dd9772cde2 | ||
![]() |
a924f819a9 | ||
![]() |
156bbad5b5 | ||
![]() |
01f3ed0e5e | ||
![]() |
2963cd5c6e | ||
![]() |
7d6688f497 | ||
![]() |
b056faa97f | ||
![]() |
3ff00ff50e | ||
![]() |
baee915db5 | ||
![]() |
4e6dcc693b | ||
![]() |
3750561b4d | ||
![]() |
6b026557d4 | ||
![]() |
1ee137bbda | ||
![]() |
19fd7bc37e | ||
![]() |
c92a90749e | ||
![]() |
779d3dce6f | ||
![]() |
e806f8c4e6 | ||
![]() |
8a5e2ffa57 | ||
![]() |
ad405d9e0b | ||
![]() |
b9ee14ac30 | ||
![]() |
bb49b1cfb1 | ||
![]() |
3ade2bb6ec | ||
![]() |
4fc9443b4f | ||
![]() |
581ede022e | ||
![]() |
f86fc03c46 | ||
![]() |
75db002369 | ||
![]() |
dbfa4e554b | ||
![]() |
84d87a2e60 | ||
![]() |
9e3577e77b | ||
![]() |
41a0dc1abd | ||
![]() |
950956ebf2 | ||
![]() |
c000c1d455 | ||
![]() |
c8e2ab4c83 | ||
![]() |
397f93b079 | ||
![]() |
09d137f740 | ||
![]() |
81f740d409 | ||
![]() |
1d2642f1e3 | ||
![]() |
7cd3603bbb | ||
![]() |
ec7de2a6dc | ||
![]() |
3d1a3606c9 | ||
![]() |
6472e9b6b6 | ||
![]() |
2c88e9d068 | ||
![]() |
4825a0a35f | ||
![]() |
7adebbe989 | ||
![]() |
fd1155928e | ||
![]() |
122b0b0de4 | ||
![]() |
744cfe5672 | ||
![]() |
17724a901c | ||
![]() |
7dc85af5fb | ||
![]() |
a8fe2d7e83 | ||
![]() |
c7daf32904 | ||
![]() |
b2323859e5 | ||
![]() |
4c8dca5300 | ||
![]() |
68e7fcf8ee | ||
![]() |
f78983b16b | ||
![]() |
ef91214085 | ||
![]() |
dc09a4621b | ||
![]() |
2f99a217c3 | ||
![]() |
6992b2c308 | ||
![]() |
0d51eefbb9 | ||
![]() |
aa28a85747 | ||
![]() |
f18ee8e83d | ||
![]() |
fb58967766 | ||
![]() |
c3f1478fde | ||
![]() |
e5c00a7ef4 | ||
![]() |
769791af7a | ||
![]() |
e632fab4d0 | ||
![]() |
91611fcae4 | ||
![]() |
6cd25d7e55 | ||
![]() |
c9488eb042 | ||
![]() |
8ce996e065 | ||
![]() |
892a1df280 | ||
![]() |
c8516a04dc | ||
![]() |
02d1b98b1c | ||
![]() |
44fa98497f | ||
![]() |
d8236bbedd | ||
![]() |
1de21fb0c2 | ||
![]() |
13cac07b8d | ||
![]() |
bd9dcfb28a | ||
![]() |
d5199eac3e | ||
![]() |
7638d229c0 | ||
![]() |
a641c5bb58 | ||
![]() |
1e0c9f46ad | ||
![]() |
4eb02f584e | ||
![]() |
700c1b4b25 | ||
![]() |
4b4337e078 | ||
![]() |
38ce800685 | ||
![]() |
2310e8c1d6 | ||
![]() |
1b2b3a4f88 | ||
![]() |
d11129a76b | ||
![]() |
02789122a0 | ||
![]() |
676bc02d52 | ||
![]() |
8b807b0706 | ||
![]() |
72dfe974ab | ||
![]() |
316db0e4c6 | ||
![]() |
010c607e40 | ||
![]() |
3e099fb2a3 | ||
![]() |
9c9730b152 | ||
![]() |
9e44053e22 | ||
![]() |
dee32c3dc5 | ||
![]() |
344fbff59a | ||
![]() |
e39a816bdc | ||
![]() |
605b8fac5e | ||
![]() |
dfba10f8ae | ||
![]() |
48a1ab64b0 | ||
![]() |
dd2cde3c1a | ||
![]() |
1b9c2b37c5 | ||
![]() |
eae1f8b597 | ||
![]() |
18ce86c2ed | ||
![]() |
d5f25e05d9 | ||
![]() |
53303ac5d3 | ||
![]() |
90cc8e2144 | ||
![]() |
adf9badbf6 | ||
![]() |
c35fe4f3f1 | ||
![]() |
63291f8101 | ||
![]() |
62efb588ef | ||
![]() |
cfd5d7ae35 | ||
![]() |
203ca9afc6 | ||
![]() |
a23f941ac8 | ||
![]() |
b0a10f0542 | ||
![]() |
478ad42977 | ||
![]() |
0764983ac6 | ||
![]() |
2b2f1ee8f5 | ||
![]() |
28f167fd99 | ||
![]() |
272be36dd9 | ||
![]() |
7b4e5dd107 | ||
![]() |
1289b1a283 | ||
![]() |
f933db8117 | ||
![]() |
cddb9bccb9 | ||
![]() |
b5ad24eb47 | ||
![]() |
ad8f791f71 | ||
![]() |
2e862b4ccc | ||
![]() |
ecac897e7b | ||
![]() |
702adb53a7 | ||
![]() |
2934841152 | ||
![]() |
4ea962f523 | ||
![]() |
5ae72d1ed2 | ||
![]() |
bc68836c8d | ||
![]() |
f0112a2de2 | ||
![]() |
94219b78e7 | ||
![]() |
0f4b6d7d9f | ||
![]() |
58418bcf46 | ||
![]() |
e4cd52060c | ||
![]() |
4f8552835e | ||
![]() |
707f2835a8 | ||
![]() |
acaf92d671 | ||
![]() |
1130aba7ca | ||
![]() |
c673cb6157 | ||
![]() |
c0f7b123a3 | ||
![]() |
34ab93c9bd | ||
![]() |
bc2f0f9f3e | ||
![]() |
e9e2afa61a | ||
![]() |
403154b2e1 | ||
![]() |
e5fd24b0d1 | ||
![]() |
8dc34274a1 | ||
![]() |
467bd21de2 | ||
![]() |
5c9705d94e | ||
![]() |
85fb5827aa | ||
![]() |
0bcc9bd3ba | ||
![]() |
25e120bec1 | ||
![]() |
2d2b96420f | ||
![]() |
77aaa15082 | ||
![]() |
7067deb328 | ||
![]() |
f6efd302dc | ||
![]() |
61972141ae | ||
![]() |
af936bc646 | ||
![]() |
d66f933c69 | ||
![]() |
cf81c37683 | ||
![]() |
d2306b0fd7 | ||
![]() |
80bf47493e | ||
![]() |
94dfabf3dc | ||
![]() |
5522dc10b8 | ||
![]() |
0ae04b8ead | ||
![]() |
44cad27d0a | ||
![]() |
5d59025b3c | ||
![]() |
768bb0bbcd | ||
![]() |
ac071b383f | ||
![]() |
e0b1a6b88b | ||
![]() |
ed86b1c572 | ||
![]() |
b6c2bade73 | ||
![]() |
b6b19b474e | ||
![]() |
231b7492fb | ||
![]() |
7d4c7718aa | ||
![]() |
b4950fcb2e | ||
![]() |
b79ea7b51b | ||
![]() |
28c72e7f63 | ||
![]() |
5fcc3b4dab | ||
![]() |
51837ce36f | ||
![]() |
ddaafb68c8 | ||
![]() |
a744775fe7 | ||
![]() |
50b85a7734 | ||
![]() |
aab09c0c65 | ||
![]() |
3ded6feddb | ||
![]() |
c8802fe5d0 | ||
![]() |
411b3129f9 | ||
![]() |
a55acd38df | ||
![]() |
e7773d8807 | ||
![]() |
793ff1a728 | ||
![]() |
7edef8d5a2 | ||
![]() |
4f7cdcce55 | ||
![]() |
03d2ca9f9f | ||
![]() |
2271ea4281 | ||
![]() |
64a7978c7f | ||
![]() |
7c6140b331 | ||
![]() |
16d4a034e2 | ||
![]() |
afc8db8f81 | ||
![]() |
4af49ee5a6 | ||
![]() |
d7b29aae5c | ||
![]() |
9f7a8407ca | ||
![]() |
7eb13a9b93 | ||
![]() |
7c9896beaf | ||
![]() |
54d3bff26d | ||
![]() |
55c51ad49d | ||
![]() |
a2050a5211 | ||
![]() |
048743c062 | ||
![]() |
e9bd2934c3 | ||
![]() |
50634eb2b3 | ||
![]() |
cea14c9d0d | ||
![]() |
08489b81fb | ||
![]() |
a2ff770afc | ||
![]() |
e0ba9b3902 | ||
![]() |
f11b5ae7a1 | ||
![]() |
7baeb6eca7 | ||
![]() |
658d988254 | ||
![]() |
9d7e9289bb | ||
![]() |
4e8519a1b9 | ||
![]() |
12aac09c7b | ||
![]() |
d7d87691cb | ||
![]() |
731640997e | ||
![]() |
64d7432852 | ||
![]() |
e6fffc0d5b | ||
![]() |
1c9f68bcae | ||
![]() |
4fde62ff89 | ||
![]() |
4c5fc7fa7c | ||
![]() |
b633108a4c | ||
![]() |
ceb55d0ede | ||
![]() |
87c958b2e7 | ||
![]() |
d844e0aba6 | ||
![]() |
3d42da5ff5 | ||
![]() |
1b869199f4 | ||
![]() |
f3cd2f6c9d | ||
![]() |
2e3e7f9bf2 | ||
![]() |
92327dd9e3 | ||
![]() |
d40b432f46 | ||
![]() |
5b3137093f | ||
![]() |
4fc9f2e5fd | ||
![]() |
ce592f4baf | ||
![]() |
2b3edcf2d1 | ||
![]() |
f165f97bd9 | ||
![]() |
4ec572372e | ||
![]() |
a953aab9b4 | ||
![]() |
672eb34049 | ||
![]() |
a0b042091b | ||
![]() |
b753705a84 | ||
![]() |
f48ff610a3 | ||
![]() |
93aed9f34c | ||
![]() |
3cf94382e6 | ||
![]() |
f52cb3bbe0 | ||
![]() |
d45182cb5c | ||
![]() |
22847c6c92 | ||
![]() |
a70c51b71c | ||
![]() |
02d417476e | ||
![]() |
bc3139e5f9 | ||
![]() |
c1f7b2653c | ||
![]() |
72dbb9441e | ||
![]() |
bbc13756f3 | ||
![]() |
ba0876b43b | ||
![]() |
c0d41661e8 | ||
![]() |
b2e2551e33 | ||
![]() |
ac371e6fb4 | ||
![]() |
108af48b76 | ||
![]() |
a225ac5deb | ||
![]() |
920695f90a | ||
![]() |
49fc57eee9 | ||
![]() |
b61d44aaa6 | ||
![]() |
f36fd2f7b2 | ||
![]() |
fb0473da39 | ||
![]() |
7e26748dc4 | ||
![]() |
ba6fdecbae | ||
![]() |
f791e83380 | ||
![]() |
dd7f914b8d | ||
![]() |
7667b2ce59 | ||
![]() |
62d36126ea | ||
![]() |
8272b2508b | ||
![]() |
70354eb73e | ||
![]() |
63083ac0c3 | ||
![]() |
9346f9b0f3 | ||
![]() |
605e5d265c | ||
![]() |
25456b15e7 | ||
![]() |
ebbe7ef944 | ||
![]() |
60a272e70a | ||
![]() |
672fcb9ce3 | ||
![]() |
870d50ebcd | ||
![]() |
b62b3e91a0 | ||
![]() |
b022d90303 | ||
![]() |
02af529551 | ||
![]() |
dd9cc619ed | ||
![]() |
75c9e959de | ||
![]() |
fb8afec1bf | ||
![]() |
a2887034a6 | ||
![]() |
7eb5aa1bc5 | ||
![]() |
08ebd7d39a | ||
![]() |
9ea263f72e | ||
![]() |
e4a2d2f3c1 | ||
![]() |
9d249904bd | ||
![]() |
111dc4963d | ||
![]() |
5a6d0455ec | ||
![]() |
a5b9fe4c35 | ||
![]() |
c95aec9da6 | ||
![]() |
e0c674bc9e | ||
![]() |
da9bd1d420 | ||
![]() |
892b4a15f6 | ||
![]() |
fda0a550fd | ||
![]() |
638825cdff | ||
![]() |
6a1d81fcf3 | ||
![]() |
8afd44a72f | ||
![]() |
22c5135740 | ||
![]() |
4d51ebc37a | ||
![]() |
433c6dc33b | ||
![]() |
ed4fdadd4d | ||
![]() |
298e96b821 | ||
![]() |
9006667b4d | ||
![]() |
abbf71982d | ||
![]() |
57110717d3 | ||
![]() |
c3b5444281 | ||
![]() |
7a542975ca | ||
![]() |
490aff5846 | ||
![]() |
1dfc036ead | ||
![]() |
360d6b998c | ||
![]() |
be7307cf39 | ||
![]() |
12096ab050 | ||
![]() |
225f23ce02 | ||
![]() |
9c15ee7285 | ||
![]() |
8dd617fc6b | ||
![]() |
ae8e72f34b | ||
![]() |
fc52a6e871 | ||
![]() |
722b47b86f | ||
![]() |
3a09039b93 | ||
![]() |
669a35bc78 | ||
![]() |
81fa0c1558 | ||
![]() |
ed408b2094 | ||
![]() |
3bc661f583 | ||
![]() |
cf9b482be2 | ||
![]() |
1d935b46f9 | ||
![]() |
520ac2e935 | ||
![]() |
c6316abbce | ||
![]() |
2dfe837c35 | ||
![]() |
3c2ea7697c | ||
![]() |
faa7a91764 | ||
![]() |
f629a4d206 | ||
![]() |
4b7c37e919 | ||
![]() |
a4c9732916 | ||
![]() |
f8f2dfce4b | ||
![]() |
5284072b8d | ||
![]() |
e603dddc54 | ||
![]() |
15691ba41a | ||
![]() |
a555aab3e7 | ||
![]() |
88f1c3a808 | ||
![]() |
0e6668636d | ||
![]() |
d0f4d8b132 | ||
![]() |
cfdcb92fa3 | ||
![]() |
039bd5d413 | ||
![]() |
5ffba55b4a | ||
![]() |
57ca281c80 | ||
![]() |
46f74b908a | ||
![]() |
703f1550d8 | ||
![]() |
8bfd380b89 | ||
![]() |
43e91ae4ae | ||
![]() |
023a2c1d9c | ||
![]() |
d931d058d9 | ||
![]() |
a825253b7f | ||
![]() |
d9086300f3 | ||
![]() |
f18a7c91ca | ||
![]() |
556aad0114 | ||
![]() |
05f6ea6401 | ||
![]() |
43d0543b9f | ||
![]() |
e95637f7b7 | ||
![]() |
4cd7c42b9e | ||
![]() |
0787d62254 | ||
![]() |
b061423847 | ||
![]() |
dbd90299bd | ||
![]() |
1faf1b261c | ||
![]() |
c6ead351c0 | ||
![]() |
bbcfdf2969 | ||
![]() |
36e72d5a41 | ||
![]() |
f8297a8a9b | ||
![]() |
a4503eb609 | ||
![]() |
a1cb3e59d6 | ||
![]() |
ef94458249 | ||
![]() |
1b05c404d5 | ||
![]() |
5de455bb86 | ||
![]() |
acdfee5c25 | ||
![]() |
a6d6ed6474 | ||
![]() |
87e7d95966 | ||
![]() |
d37ee1e0dc | ||
![]() |
1d33e7ab49 | ||
![]() |
2027b743b4 | ||
![]() |
7e27e73532 | ||
![]() |
3705a1adad | ||
![]() |
793b88a7d4 | ||
![]() |
2928df0cc9 | ||
![]() |
4f5e772157 | ||
![]() |
f7a0b9951e | ||
![]() |
44128f9145 | ||
![]() |
6eaff5ca6a | ||
![]() |
c0664c1cb6 | ||
![]() |
e229e5355d | ||
![]() |
52189fc5df | ||
![]() |
314964c5f9 | ||
![]() |
fcef783bbb | ||
![]() |
9c5ac069d7 | ||
![]() |
160f9df64e | ||
![]() |
bdbb9bead2 | ||
![]() |
e4dfce9ee2 | ||
![]() |
6fbb601802 | ||
![]() |
94b4c76749 | ||
![]() |
8715e7dd98 | ||
![]() |
ccc2d892c1 | ||
![]() |
d1ce8e7baa | ||
![]() |
82fbbbecac | ||
![]() |
bf029ddd9f | ||
![]() |
af5f0c042a | ||
![]() |
4e15f0ddac | ||
![]() |
b566355c4f | ||
![]() |
5c31dff72d | ||
![]() |
d69672e113 | ||
![]() |
a209e87c69 | ||
![]() |
71610a365f | ||
![]() |
44860f2ea7 | ||
![]() |
967bdf8f08 | ||
![]() |
02aa6fcab0 | ||
![]() |
712985ced1 | ||
![]() |
0683dafa55 | ||
![]() |
6f1958d398 | ||
![]() |
85fbd2560d | ||
![]() |
65f2730261 | ||
![]() |
21bcadeecb | ||
![]() |
bd0427c79f | ||
![]() |
241054fd26 | ||
![]() |
d8888e3495 | ||
![]() |
137d9e6d6e | ||
![]() |
d0cbd1e663 | ||
![]() |
da51e1ed72 | ||
![]() |
76803bfcb1 | ||
![]() |
c248741c00 | ||
![]() |
759a078ce0 | ||
![]() |
a536311d56 | ||
![]() |
9dd2a82b7d | ||
![]() |
4d50a66e40 | ||
![]() |
e6c56cacc6 | ||
![]() |
c3b9465aa3 | ||
![]() |
5f3b8bea52 | ||
![]() |
0e4c8ea8af | ||
![]() |
f9ab23bb4a | ||
![]() |
9f8b2264a2 | ||
![]() |
52cc3f10c1 | ||
![]() |
1d61bb58f5 | ||
![]() |
a3440cc8ef | ||
![]() |
51c60e5261 | ||
![]() |
c3349e18a5 | ||
![]() |
12e46e0a36 | ||
![]() |
f8caed139a | ||
![]() |
a2297fb5b8 | ||
![]() |
26c39381a8 | ||
![]() |
a4742ad9e9 | ||
![]() |
23a6973291 | ||
![]() |
340a84e583 | ||
![]() |
4291877830 | ||
![]() |
8f6d608a43 | ||
![]() |
45dd98e639 | ||
![]() |
2ac265a6f5 | ||
![]() |
e100806fd9 | ||
![]() |
c7f75bf7d1 | ||
![]() |
4bf5ddbfe9 | ||
![]() |
32dffb577c | ||
![]() |
a9623f8e6a | ||
![]() |
bc74bb6bf6 | ||
![]() |
d32450255c | ||
![]() |
896aec5295 | ||
![]() |
d42a534fc3 | ||
![]() |
398007ca90 | ||
![]() |
551e8df8b8 | ||
![]() |
dc0a28b93d | ||
![]() |
644396149b | ||
![]() |
a25bb2618a | ||
![]() |
0e12cdea7c | ||
![]() |
903296014a | ||
![]() |
cd713db029 | ||
![]() |
bdd16e06e0 | ||
![]() |
4c632810ec | ||
![]() |
f451bdbfa4 | ||
![]() |
bfac73b992 | ||
![]() |
2b41f710a8 | ||
![]() |
5924edb289 | ||
![]() |
5ceec31adf | ||
![]() |
e2791cdf0f | ||
![]() |
50f3b08c59 | ||
![]() |
2aebf6ceaf | ||
![]() |
7ceea2cd8d | ||
![]() |
0cb801179c | ||
![]() |
1822d21676 | ||
![]() |
7fd2ebc252 | ||
![]() |
f709ac16f8 | ||
![]() |
74173317de | ||
![]() |
3874e16187 | ||
![]() |
39722a5563 | ||
![]() |
1f9ad12593 | ||
![]() |
52c136439e | ||
![]() |
cd86ed3877 | ||
![]() |
1d85661ab9 | ||
![]() |
736cefed5a | ||
![]() |
fa8630ddae | ||
![]() |
4a2bd7bd7b | ||
![]() |
a9e21a35ea | ||
![]() |
fd4e1b8d2c | ||
![]() |
420f0505ae | ||
![]() |
b58f7856a1 | ||
![]() |
44a6429267 | ||
![]() |
472bde9eea | ||
![]() |
c422f65935 | ||
![]() |
f5962375f8 | ||
![]() |
4e33f2dcb6 | ||
![]() |
dce874bbc7 | ||
![]() |
7d69dfa62a | ||
![]() |
a56f17cc3b | ||
![]() |
7be7a32d70 | ||
![]() |
a7dd3af4e5 | ||
![]() |
63fdc100d6 | ||
![]() |
9e2ece78dd | ||
![]() |
cebcaf4d6a | ||
![]() |
4a242e43a7 | ||
![]() |
d8f442cc89 | ||
![]() |
f6923e073e | ||
![]() |
f02c6be10d | ||
![]() |
5ba3ef0a25 | ||
![]() |
9458b9f37d | ||
![]() |
ca282f2be8 | ||
![]() |
0cde08c46e | ||
![]() |
bec8512c7b | ||
![]() |
46e7da4e21 | ||
![]() |
c7b8bd3436 | ||
![]() |
1721817fdb | ||
![]() |
d57bfde604 | ||
![]() |
3167ab3ba0 | ||
![]() |
8f559965f6 | ||
![]() |
35e005caaa | ||
![]() |
6c25ce56a3 | ||
![]() |
baa12c7069 | ||
![]() |
e2b044d2ee | ||
![]() |
621af8d812 | ||
![]() |
efd038a536 | ||
![]() |
0b2629e910 | ||
![]() |
a9b5ef3bd3 | ||
![]() |
2a24532e1d | ||
![]() |
88c4195260 | ||
![]() |
c5f2eb1dd8 | ||
![]() |
384d964827 | ||
![]() |
253526e565 | ||
![]() |
2e2dbaf77f | ||
![]() |
43133df2ad | ||
![]() |
eef568b24c | ||
![]() |
e7d5011f42 | ||
![]() |
36c198fc33 | ||
![]() |
75a8edf20f | ||
![]() |
81107df53f | ||
![]() |
a932bc2503 | ||
![]() |
f4e2eca256 | ||
![]() |
08d5dfa49c | ||
![]() |
e7f339a946 | ||
![]() |
d3375a921d | ||
![]() |
a2eb810df0 | ||
![]() |
6e576a165c | ||
![]() |
dfa941a9e7 | ||
![]() |
1584028995 | ||
![]() |
14dab85ff0 | ||
![]() |
403e336a64 | ||
![]() |
2aa5f68b7b | ||
![]() |
56ea526cce | ||
![]() |
96f5cd9f17 | ||
![]() |
64efb89cce | ||
![]() |
4d5b68792b | ||
![]() |
85d813a94b | ||
![]() |
e9b008ee84 | ||
![]() |
b795c5f017 | ||
![]() |
1e4686463b | ||
![]() |
4e9631a8d8 | ||
![]() |
3a83062670 | ||
![]() |
79102a20d2 | ||
![]() |
2e053ea25a | ||
![]() |
fd3d46c813 | ||
![]() |
ab838fd84f | ||
![]() |
9ca2691a2c | ||
![]() |
7c3f5a62c5 | ||
![]() |
6711dae4e0 | ||
![]() |
a73a4afcad | ||
![]() |
4ea2d8e7ba | ||
![]() |
bb386fea16 | ||
![]() |
82cdb0fdb3 | ||
![]() |
a94dacf03c | ||
![]() |
de312eb768 | ||
![]() |
29aa1de4e3 | ||
![]() |
09435a1b63 | ||
![]() |
85e864a01e | ||
![]() |
573839c0ff | ||
![]() |
9c636f5ee2 | ||
![]() |
f78d2a5ed8 | ||
![]() |
48c2c156cb | ||
![]() |
435813355f | ||
![]() |
e30a552b6c | ||
![]() |
22a4a4b2df | ||
![]() |
5ac418aa61 | ||
![]() |
d8a0a74d47 | ||
![]() |
3931c0d200 | ||
![]() |
e26607fbd1 | ||
![]() |
a63683e6b8 | ||
![]() |
83b198f6fe | ||
![]() |
23f6e1084b | ||
![]() |
99335bab7a | ||
![]() |
33fbc889fb | ||
![]() |
201e5ee09d | ||
![]() |
c398308872 | ||
![]() |
090c063644 | ||
![]() |
ec40c8ed1e | ||
![]() |
78a99526a9 | ||
![]() |
25914b0263 | ||
![]() |
aaa3e20c5a | ||
![]() |
0da8e28651 | ||
![]() |
d7dcfa5729 | ||
![]() |
65824ff64d | ||
![]() |
63cad7ebb0 | ||
![]() |
b996fa7eef | ||
![]() |
5ebf3726ed | ||
![]() |
484c852efd | ||
![]() |
25cf8dc20a | ||
![]() |
cb1a138140 | ||
![]() |
384ca66205 | ||
![]() |
46bfec66cb | ||
![]() |
afe06b379f | ||
![]() |
a9e85abd7f | ||
![]() |
08d4651ef0 | ||
![]() |
62b4f333bb | ||
![]() |
02b0909829 | ||
![]() |
ae39b31c68 | ||
![]() |
e5a1438673 | ||
![]() |
72d305b283 | ||
![]() |
785c0376f8 | ||
![]() |
0bdf8de38e | ||
![]() |
6c575511be | ||
![]() |
9767e98e50 | ||
![]() |
79deff3261 | ||
![]() |
0782410a14 | ||
![]() |
f5d015e8f9 | ||
![]() |
74ad488f4a | ||
![]() |
0db3406ad8 | ||
![]() |
6e377dd3c5 | ||
![]() |
be676ad93c | ||
![]() |
f00cffd17e | ||
![]() |
0803d9f2b5 | ||
![]() |
841fb4cfc5 | ||
![]() |
8b3e32b6eb | ||
![]() |
90de75968d | ||
![]() |
2de9d7b4a7 | ||
![]() |
a9ab2f54ea | ||
![]() |
a1432e939f | ||
![]() |
cae160b5be | ||
![]() |
aa4e5da146 | ||
![]() |
1061fca6a3 | ||
![]() |
e4885e3c52 | ||
![]() |
a98c0bdec7 | ||
![]() |
d6e0bd8c26 | ||
![]() |
e01ef42d31 | ||
![]() |
92910eb227 | ||
![]() |
cdfe686322 | ||
![]() |
553943ab93 | ||
![]() |
32df4d39a4 | ||
![]() |
1281ea858c | ||
![]() |
30a303f873 | ||
![]() |
fdb6679d2d | ||
![]() |
7145b117cc | ||
![]() |
4698d07323 | ||
![]() |
2142f05a88 | ||
![]() |
40a2df847b | ||
![]() |
6063ff063b | ||
![]() |
547a1a9970 | ||
![]() |
8c52a812d9 | ||
![]() |
4eef498d24 | ||
![]() |
32b0bdb98c | ||
![]() |
edfe0f9c30 | ||
![]() |
eef418a757 | ||
![]() |
218f25c171 | ||
![]() |
f02df6d80c | ||
![]() |
da4d379b22 | ||
![]() |
f13f4cc5d2 | ||
![]() |
a79badd783 | ||
![]() |
2702700d10 | ||
![]() |
267686fd37 | ||
![]() |
e5df2f65b8 | ||
![]() |
d6decc05d7 | ||
![]() |
d85afd6435 | ||
![]() |
2fb86364ab | ||
![]() |
c972940338 | ||
![]() |
6abdd2a6d8 | ||
![]() |
9e9d1a04e4 | ||
![]() |
ae9349e36c | ||
![]() |
4031777606 | ||
![]() |
9591f14551 | ||
![]() |
06d10cf9aa | ||
![]() |
0113ad5e14 | ||
![]() |
e58feadba9 | ||
![]() |
360f5ac6f7 | ||
![]() |
e846f69e38 | ||
![]() |
fa1d7ffac3 | ||
![]() |
272d589518 | ||
![]() |
6ab4787e97 | ||
![]() |
060f09ff55 | ||
![]() |
f47ae3668f | ||
![]() |
56cd84c1fe | ||
![]() |
a2eead521f | ||
![]() |
a2fd5ae20c | ||
![]() |
543440e38d | ||
![]() |
0b64382ef6 | ||
![]() |
bede758507 | ||
![]() |
5532666ad5 | ||
![]() |
63cff25616 | ||
![]() |
5e2735aaa2 | ||
![]() |
6fc0d8fce4 | ||
![]() |
e0c1ca1209 | ||
![]() |
3dc4ed1764 | ||
![]() |
f63a4ee2ae | ||
![]() |
c96bdfcb32 | ||
![]() |
2a99e0e435 | ||
![]() |
5ffc667bea | ||
![]() |
21b8df0375 | ||
![]() |
b78ac7d2e9 | ||
![]() |
114dc8ffa0 | ||
![]() |
eea43d5a73 | ||
![]() |
bcb1cf6603 | ||
![]() |
6a0c5a874c | ||
![]() |
1e8b3826dc | ||
![]() |
7efe62ee80 | ||
![]() |
febb21a01d | ||
![]() |
cb4e6159c4 | ||
![]() |
1164ea52f9 | ||
![]() |
0f75024e03 | ||
![]() |
1e09a1768e | ||
![]() |
7c78d963d9 | ||
![]() |
b57ecae565 | ||
![]() |
89317d4abc | ||
![]() |
c5dd3dc7a9 | ||
![]() |
ccc46971b4 | ||
![]() |
6ad4b425e4 | ||
![]() |
761e01c3b9 | ||
![]() |
70b9330b61 | ||
![]() |
f1e8667945 | ||
![]() |
509f501696 | ||
![]() |
3fe0368486 | ||
![]() |
8f027e274e | ||
![]() |
3b0045917c | ||
![]() |
a102fc9cad | ||
![]() |
f6bca68da2 | ||
![]() |
d921e2e61b | ||
![]() |
0f7ed0ec70 | ||
![]() |
49b12ea4f8 | ||
![]() |
69fc466323 | ||
![]() |
81d00f2e97 | ||
![]() |
ded6540422 | ||
![]() |
583a028529 | ||
![]() |
f1bb56e2fb | ||
![]() |
f583dd47ac | ||
![]() |
7e3b3453c0 | ||
![]() |
abc354f516 | ||
![]() |
79efffe12f | ||
![]() |
25130db371 | ||
![]() |
932eb94f9d | ||
![]() |
9bf4eff173 | ||
![]() |
9fc3ddeab7 | ||
![]() |
98fdbec442 | ||
![]() |
332b90d6c1 | ||
![]() |
db2e03eb14 | ||
![]() |
8ed8b94ec7 | ||
![]() |
63c9308f59 | ||
![]() |
1306a777fc | ||
![]() |
f739ed7581 | ||
![]() |
b4d6015464 | ||
![]() |
b9aaafdb30 | ||
![]() |
71aa6c6e92 | ||
![]() |
f98d2631e5 | ||
![]() |
9e94c81ef2 | ||
![]() |
d025ef11f8 | ||
![]() |
fe7536e374 | ||
![]() |
14256137e8 | ||
![]() |
bc3e43ac58 | ||
![]() |
d0d5373be9 | ||
![]() |
997267bad1 | ||
![]() |
ef6d0cc4b1 | ||
![]() |
ffad244e1e | ||
![]() |
fdee7c3d06 | ||
![]() |
142cde975f | ||
![]() |
004907d306 | ||
![]() |
05eb0d0fbe | ||
![]() |
f13a1b04e6 | ||
![]() |
fd4408e572 | ||
![]() |
a84ab7413c | ||
![]() |
62b593da08 | ||
![]() |
0eb69b6659 | ||
![]() |
67b83388b1 | ||
![]() |
ecc998aea8 | ||
![]() |
6956d16f0e | ||
![]() |
f1bc4f5c20 | ||
![]() |
f134e2d02a | ||
![]() |
6ec72ef945 | ||
![]() |
e8d518cd6c | ||
![]() |
b564433ff6 | ||
![]() |
79f7dcd1a3 | ||
![]() |
23ee9b7867 | ||
![]() |
afbf36900f | ||
![]() |
2829851e49 | ||
![]() |
2b8fda3511 | ||
![]() |
d31959990e | ||
![]() |
26c535db84 | ||
![]() |
ea1b910d7e | ||
![]() |
8f4c6fb6ac | ||
![]() |
9b1861417c | ||
![]() |
448989f32f | ||
![]() |
2fc26bc154 | ||
![]() |
1812249d37 | ||
![]() |
14bbaccb9f | ||
![]() |
d2b03afcf4 | ||
![]() |
1cac3895dc | ||
![]() |
01aab25889 | ||
![]() |
96d731dfc7 | ||
![]() |
8080c32b1f | ||
![]() |
4b27aec196 | ||
![]() |
38fb510375 | ||
![]() |
6422e31b10 | ||
![]() |
c0f47195a2 | ||
![]() |
40f66977c7 | ||
![]() |
e518c0dc14 | ||
![]() |
2e161a1f45 | ||
![]() |
5ab6e84044 | ||
![]() |
e1a6347c4e | ||
![]() |
bf8e8798d9 | ||
![]() |
08949ee347 | ||
![]() |
92a67bb8cb | ||
![]() |
363bbf5fd3 | ||
![]() |
77f6940336 | ||
![]() |
e8eeac6735 | ||
![]() |
775fbc9a75 | ||
![]() |
8d0f2d371d | ||
![]() |
8efe2859b8 | ||
![]() |
441c68ead2 | ||
![]() |
882b235a78 | ||
![]() |
4cd1f201f5 | ||
![]() |
013c59f904 | ||
![]() |
57474e2dab | ||
![]() |
139ced885d | ||
![]() |
10b1da135e | ||
![]() |
f0bb2e8687 | ||
![]() |
4643ccef6f | ||
![]() |
753ca7cb53 | ||
![]() |
87d2f33e55 | ||
![]() |
fc7944d287 | ||
![]() |
376e5c1546 | ||
![]() |
e8ad947d37 | ||
![]() |
067528211f | ||
![]() |
92ab9cae27 | ||
![]() |
fa2b11b768 | ||
![]() |
82f43ac6a6 | ||
![]() |
c7660b8c2d | ||
![]() |
847831c195 | ||
![]() |
e0b246431f | ||
![]() |
3b1c4b043d | ||
![]() |
e8b8391868 | ||
![]() |
cd0a87785e | ||
![]() |
c808beec30 | ||
![]() |
2d4a3c2554 | ||
![]() |
b2b9938484 | ||
![]() |
eb1cefe2fa | ||
![]() |
5eb5dbddde | ||
![]() |
bfe3eff5ff | ||
![]() |
e7936e6c9a | ||
![]() |
514f92e6f2 | ||
![]() |
68fd7a031f | ||
![]() |
95f61542b5 | ||
![]() |
9fc6f19702 | ||
![]() |
4038617d59 | ||
![]() |
98ccd577d6 | ||
![]() |
1d43a2362c | ||
![]() |
0ff675171b | ||
![]() |
59594c6637 | ||
![]() |
9595733563 | ||
![]() |
5eb1d49857 | ||
![]() |
fa1fdbf73e | ||
![]() |
52e52b3ca1 | ||
![]() |
5b4fbe32b1 | ||
![]() |
31ea44ccf1 | ||
![]() |
7fdb6e1425 | ||
![]() |
621f049a5c | ||
![]() |
d26ca194b3 | ||
![]() |
a012e26d63 | ||
![]() |
f80b1fb2fe | ||
![]() |
38ed07caa7 | ||
![]() |
72ee4be495 | ||
![]() |
c85b97a484 | ||
![]() |
c7510c628f | ||
![]() |
3ca1e550fe | ||
![]() |
01e8944077 | ||
![]() |
d6ab3298a3 | ||
![]() |
97b28bba4d | ||
![]() |
7f6674a0e6 | ||
![]() |
2c1df5f875 | ||
![]() |
e7ae215ab0 | ||
![]() |
4a9c790652 | ||
![]() |
91ca680911 | ||
![]() |
01376aba86 | ||
![]() |
d56ffa3531 | ||
![]() |
5a5a24bf1a | ||
![]() |
3d2c65b398 | ||
![]() |
bacb35fb1c | ||
![]() |
0a2ed805a2 | ||
![]() |
e70c153cd3 | ||
![]() |
b54c2b7f57 | ||
![]() |
3fe80ec5ac | ||
![]() |
e52048c69e | ||
![]() |
ceb930aed6 | ||
![]() |
e775037366 | ||
![]() |
4357e02c58 | ||
![]() |
67c0ceedc9 | ||
![]() |
0039312a64 | ||
![]() |
57f1152751 | ||
![]() |
bfb9be1225 | ||
![]() |
8837b54aab | ||
![]() |
8ab5a4d394 | ||
![]() |
29bcf94d50 | ||
![]() |
dd68bf8eeb | ||
![]() |
fc4dd4524a | ||
![]() |
c12ac64678 | ||
![]() |
264044272a | ||
![]() |
c74162c586 | ||
![]() |
fa6ff4e5eb | ||
![]() |
01bbc50c68 | ||
![]() |
e5457e5029 | ||
![]() |
b025bdf0c7 | ||
![]() |
600e156c4c | ||
![]() |
13ba708adc | ||
![]() |
edf8bf2c9d | ||
![]() |
c9e0bf4f02 | ||
![]() |
8f9eaa22e6 | ||
![]() |
3fcd580491 | ||
![]() |
cf3cc2e984 | ||
![]() |
76322d8089 | ||
![]() |
9e29d8d692 | ||
![]() |
5d5f8b4d51 | ||
![]() |
4d74be881d | ||
![]() |
425a312151 | ||
![]() |
ea294e8e5d | ||
![]() |
e75d0de135 | ||
![]() |
81cacbd917 | ||
![]() |
c0c78ae9bb | ||
![]() |
eb572e8d8f | ||
![]() |
781c499806 | ||
![]() |
a3d74ea444 | ||
![]() |
86a19aa037 | ||
![]() |
e484339cca | ||
![]() |
6b5a1d0202 | ||
![]() |
24247fd6a6 | ||
![]() |
2af20d5c40 | ||
![]() |
dfb983c3cf | ||
![]() |
e1a6b69f9a | ||
![]() |
8df935f5fe | ||
![]() |
04c5acd1d7 | ||
![]() |
c9766d25ef | ||
![]() |
aaea661b70 | ||
![]() |
7061859112 | ||
![]() |
66c24af3d2 | ||
![]() |
083c315fd6 | ||
![]() |
29b44a181b | ||
![]() |
4fdc5ea646 | ||
![]() |
e17bfa029c | ||
![]() |
279e4c2fa8 | ||
![]() |
856a39855e | ||
![]() |
4a9d21062a | ||
![]() |
ad8f3aa6c9 | ||
![]() |
43f85408be | ||
![]() |
5739caaa5a | ||
![]() |
73cfa5499d | ||
![]() |
8f0323fb8d | ||
![]() |
83d16932a4 | ||
![]() |
84e3f6ca18 | ||
![]() |
5d6a568308 | ||
![]() |
3e8cba745a | ||
![]() |
be4d12789d | ||
![]() |
00fbfb5a56 | ||
![]() |
27d0f7f277 | ||
![]() |
fb1aab2a49 | ||
![]() |
cc72fa4793 | ||
![]() |
e9c60eff85 | ||
![]() |
5b7c87ee79 | ||
![]() |
c8b4685fc9 | ||
![]() |
561d5675f7 | ||
![]() |
c906cb57ee | ||
![]() |
84de865daf | ||
![]() |
24a264d78c | ||
![]() |
62c3c7ac21 | ||
![]() |
a7d6ad5162 | ||
![]() |
0dbb212d13 | ||
![]() |
8002cc2771 | ||
![]() |
8d64eac853 | ||
![]() |
80d1c5b9f5 | ||
![]() |
7175f27da8 | ||
![]() |
d6f9aace8c | ||
![]() |
aeccb5b472 | ||
![]() |
09a7b7718a | ||
![]() |
ef2c76efaf | ||
![]() |
69793049c3 | ||
![]() |
67942a906a | ||
![]() |
6a9cae3de8 | ||
![]() |
0afdac5683 | ||
![]() |
609d09a8e2 | ||
![]() |
01e8654fbd | ||
![]() |
f477ab84d5 | ||
![]() |
9f59d4baa3 | ||
![]() |
2e9a1d958c | ||
![]() |
e4f2c58933 | ||
![]() |
a9e8b3e06b | ||
![]() |
daa5b7827a | ||
![]() |
dd00152485 | ||
![]() |
df52a6ea6b | ||
![]() |
7d5197e6fd | ||
![]() |
81e08d0cc4 | ||
![]() |
32a159d48f | ||
![]() |
44f3a7484d | ||
![]() |
38eb8e40ea | ||
![]() |
fd14c8cdce | ||
![]() |
eb6968fb3f | ||
![]() |
216da63276 | ||
![]() |
08d8f2564a | ||
![]() |
1d51002173 | ||
![]() |
610d0b272e | ||
![]() |
b3e2418b93 | ||
![]() |
464d0e50b0 | ||
![]() |
7411c54f9e | ||
![]() |
7c74deb700 | ||
![]() |
e63165e80f | ||
![]() |
e2bc9dfacd | ||
![]() |
48789dbab7 | ||
![]() |
196f0f0475 | ||
![]() |
085b59f2e1 | ||
![]() |
2fdc2664ff | ||
![]() |
a33a5c5527 | ||
![]() |
a7c0f37904 | ||
![]() |
4889ab3462 | ||
![]() |
3923deeaad | ||
![]() |
80d6fff0ca | ||
![]() |
fe43b4da39 | ||
![]() |
67afd05e22 | ||
![]() |
0fcaf20221 | ||
![]() |
0277b94b37 | ||
![]() |
6a9d5fd4cc | ||
![]() |
a83106f717 | ||
![]() |
4c2a6e346d | ||
![]() |
cae63a7ada | ||
![]() |
c7efa8c4f1 | ||
![]() |
bf6645e829 | ||
![]() |
ea1b42510c | ||
![]() |
72818ffa42 | ||
![]() |
985308bf0c | ||
![]() |
86381696f4 | ||
![]() |
08b960cc6e | ||
![]() |
731c65cd59 | ||
![]() |
a85e8a29ff | ||
![]() |
22b2f52f8c | ||
![]() |
a713ce2126 | ||
![]() |
4fac3cf304 | ||
![]() |
74e20a8c52 | ||
![]() |
8cf4ba25f5 | ||
![]() |
feb65cf8f3 | ||
![]() |
93592d23f4 | ||
![]() |
338a4837bc | ||
![]() |
8e19fe535c | ||
![]() |
2aeccc0c5c | ||
![]() |
8db1234a59 | ||
![]() |
80fb351ad3 | ||
![]() |
523f85d4d1 | ||
![]() |
bfff500915 | ||
![]() |
3e83bb0d95 | ||
![]() |
bdaee25e61 | ||
![]() |
71d3227791 | ||
![]() |
a28aa6a8c4 | ||
![]() |
292e103073 | ||
![]() |
39a3f03e79 | ||
![]() |
404a6c12a6 | ||
![]() |
8271409afe | ||
![]() |
985f659026 | ||
![]() |
7c36cbffd0 | ||
![]() |
285ea4e3fd | ||
![]() |
8ce18647f1 | ||
![]() |
aee0478235 | ||
![]() |
c3cf1d81c2 | ||
![]() |
c2b6cec37d | ||
![]() |
b265cabc22 | ||
![]() |
463dd8ea74 | ||
![]() |
0263125e11 | ||
![]() |
1fc8e4c148 | ||
![]() |
c43bca6007 | ||
![]() |
4c31636d19 | ||
![]() |
3a61ab59f2 | ||
![]() |
1db3c57ef0 | ||
![]() |
eeaf3496d5 | ||
![]() |
70f421b787 | ||
![]() |
86fa629591 | ||
![]() |
553b80164b | ||
![]() |
8518933ca8 | ||
![]() |
ea53b7d4ad | ||
![]() |
37a96d063f | ||
![]() |
b360920472 | ||
![]() |
9e1744f904 | ||
![]() |
85a468bda9 | ||
![]() |
7955ef8105 | ||
![]() |
80cd41893b | ||
![]() |
9b09f2ad71 | ||
![]() |
c45d9559c4 | ||
![]() |
f0d978b4c6 | ||
![]() |
8734f4bbe3 | ||
![]() |
9f03280075 | ||
![]() |
427ac4ef35 | ||
![]() |
d9c4495e8e | ||
![]() |
d09070b61d | ||
![]() |
d6855a6b50 | ||
![]() |
9be970a4c4 | ||
![]() |
b236bb407b | ||
![]() |
8978187c64 | ||
![]() |
eba0b07782 | ||
![]() |
1f77e00df4 | ||
![]() |
41c70cc85d | ||
![]() |
5bc0a8fba1 | ||
![]() |
687020e595 | ||
![]() |
8c75b96c38 | ||
![]() |
6f7a01bc53 | ||
![]() |
5383a0af0b | ||
![]() |
3e50466024 | ||
![]() |
45b703daf6 | ||
![]() |
f1e1f6424a | ||
![]() |
460f031cef | ||
![]() |
25aaf4e48b | ||
![]() |
a26baa3061 | ||
![]() |
138513d790 | ||
![]() |
1e5dc01825 | ||
![]() |
8c4b1b967d | ||
![]() |
7faa107547 | ||
![]() |
85ccc2384f | ||
![]() |
469a5b1974 | ||
![]() |
80161c36c6 | ||
![]() |
aea912f499 | ||
![]() |
156d7139fa | ||
![]() |
0ad3d0247d | ||
![]() |
092f9170cc | ||
![]() |
b820e9a888 | ||
![]() |
b9cd55188e | ||
![]() |
ebd45dfae3 | ||
![]() |
40195b2d98 | ||
![]() |
0b0305eaed | ||
![]() |
950997ea66 | ||
![]() |
be4beb41b6 | ||
![]() |
c55f87c962 | ||
![]() |
bdc85b435c | ||
![]() |
522d6d8b01 | ||
![]() |
e98838ad7e | ||
![]() |
3829565ea0 | ||
![]() |
c16a8dacd0 | ||
![]() |
02db971b7c | ||
![]() |
0d522aae6c | ||
![]() |
fdb0f01b38 | ||
![]() |
376cba696e | ||
![]() |
cade272501 | ||
![]() |
4f828fbe00 | ||
![]() |
3d348c63d9 | ||
![]() |
fe10c19956 | ||
![]() |
9ada979484 | ||
![]() |
2926cb7682 | ||
![]() |
8c15cc1c17 | ||
![]() |
bea4fb6ae6 | ||
![]() |
cafc64534b | ||
![]() |
327fc742d3 | ||
![]() |
0c5df29417 | ||
![]() |
cce896e900 | ||
![]() |
50c0f9e622 | ||
![]() |
10ec67854e | ||
![]() |
a3c4a10721 | ||
![]() |
e327f7ba2c | ||
![]() |
9a65f02d5b | ||
![]() |
d1fc9c5880 | ||
![]() |
3c9ae68314 | ||
![]() |
5e7c2c11f6 | ||
![]() |
053b6ab8c6 | ||
![]() |
5814743d59 | ||
![]() |
fa7613b8d1 | ||
![]() |
d3d05d613d | ||
![]() |
23b5cd5b72 | ||
![]() |
d4a33603ab | ||
![]() |
39724de6e6 | ||
![]() |
156adaa1a0 | ||
![]() |
3868243c2a | ||
![]() |
243f539439 | ||
![]() |
71d92c8d1b | ||
![]() |
e840d42fb9 | ||
![]() |
750c4ffbd3 | ||
![]() |
d043a4f410 | ||
![]() |
4c3ba0fe3d | ||
![]() |
a314f55a17 | ||
![]() |
78a9811fe3 | ||
![]() |
6277639ded | ||
![]() |
d1c807487a | ||
![]() |
fe92abde0e | ||
![]() |
098c954ef1 | ||
![]() |
01396923f1 | ||
![]() |
e0de66b1be | ||
![]() |
77675b361f | ||
![]() |
e2dd058430 | ||
![]() |
a188125982 | ||
![]() |
9e5f079cf2 | ||
![]() |
51a948bfcf | ||
![]() |
9d27d49c1f | ||
![]() |
761f6568fa | ||
![]() |
ee94b296ae | ||
![]() |
b387946d34 | ||
![]() |
46afe5153f | ||
![]() |
68be87724a | ||
![]() |
8c9f2af855 | ||
![]() |
594f0b10ba | ||
![]() |
79e98db3bd | ||
![]() |
a57fd69fb4 | ||
![]() |
b73eb9438d | ||
![]() |
920e560b4b | ||
![]() |
0d33f8b460 | ||
![]() |
5b58850c31 | ||
![]() |
17746f35f9 | ||
![]() |
ca0b211854 | ||
![]() |
54cb26ff03 | ||
![]() |
a7ff73dbfd | ||
![]() |
815dd0f706 | ||
![]() |
7455dc93ac | ||
![]() |
337662bd40 | ||
![]() |
91305771bc | ||
![]() |
98ed80d305 | ||
![]() |
5313e1861a | ||
![]() |
d8665366ef | ||
![]() |
c216f29fb0 | ||
![]() |
302fde6004 | ||
![]() |
14ddf37988 | ||
![]() |
87568b6590 | ||
![]() |
37aa41afae | ||
![]() |
41968918bb | ||
![]() |
8fd48a88be | ||
![]() |
10c35f354e | ||
![]() |
9ee7740fcc | ||
![]() |
94b086de20 | ||
![]() |
c90696e67e | ||
![]() |
8378789f6a | ||
![]() |
059bb7622d | ||
![]() |
cece83328a | ||
![]() |
4a12b0ab2d | ||
![]() |
f6e2dd1480 | ||
![]() |
f04b5fd42f | ||
![]() |
5994cd8ea2 | ||
![]() |
83f33a7d1b | ||
![]() |
f80e1bd214 | ||
![]() |
97672f06de | ||
![]() |
6039484a02 | ||
![]() |
7682ebd245 | ||
![]() |
7c581ec108 | ||
![]() |
910d22daa6 | ||
![]() |
979102a2d9 | ||
![]() |
98be89a20a | ||
![]() |
0264383ad2 | ||
![]() |
e2ea217bc5 | ||
![]() |
fa75c79d34 | ||
![]() |
0c86a4e608 | ||
![]() |
031585be3f | ||
![]() |
92a87a5ed2 | ||
![]() |
4c26e597e4 | ||
![]() |
5108bf1742 | ||
![]() |
6215faa06c | ||
![]() |
fee1fed0a1 | ||
![]() |
50dcf308a2 | ||
![]() |
486e720e00 | ||
![]() |
a6c09e2dac | ||
![]() |
43e4dc8170 | ||
![]() |
5c4d72ec42 | ||
![]() |
114806db55 | ||
![]() |
0ff7170ab1 | ||
![]() |
6b2f084cda | ||
![]() |
907106156f | ||
![]() |
50a026183d | ||
![]() |
716d795970 | ||
![]() |
fcfdcd1025 | ||
![]() |
af119db1d7 | ||
![]() |
122e80fae9 | ||
![]() |
8fceffd6fd | ||
![]() |
f778c48923 | ||
![]() |
19cd3a17df | ||
![]() |
ea91a62c89 | ||
![]() |
cef791ba1b | ||
![]() |
f78a7fa630 | ||
![]() |
ac59382b84 | ||
![]() |
68175c1cf0 | ||
![]() |
aeca8dc5b2 | ||
![]() |
e75ef086af | ||
![]() |
14a2171035 | ||
![]() |
0cdd866393 | ||
![]() |
24c1cfbf72 | ||
![]() |
31899d2ab9 | ||
![]() |
16c44f3a30 | ||
![]() |
1b4bde4e78 | ||
![]() |
ff9ae57f39 | ||
![]() |
71add5a7c2 | ||
![]() |
ce2719d77e | ||
![]() |
8193a0df63 | ||
![]() |
48a5107296 | ||
![]() |
ebd589c9cb | ||
![]() |
1f15368b7b | ||
![]() |
8fe1a76ec6 | ||
![]() |
83faf119a9 | ||
![]() |
0a05534c84 | ||
![]() |
137fbb34d9 | ||
![]() |
d45ce19b04 | ||
![]() |
7153506ddb | ||
![]() |
0483d3ff32 | ||
![]() |
7e784ce9a7 | ||
![]() |
8343d9cc18 | ||
![]() |
db0ecd92ca | ||
![]() |
b5140cfecd | ||
![]() |
36aea35a92 | ||
![]() |
1984436b41 | ||
![]() |
8ba2f5f964 | ||
![]() |
c923d35a1f | ||
![]() |
c550779472 | ||
![]() |
90d3c9ced0 | ||
![]() |
43cbc09f1f | ||
![]() |
feea084c60 | ||
![]() |
d403a83a24 | ||
![]() |
35fc27cfb0 | ||
![]() |
81742565a4 | ||
![]() |
923d0b7c80 | ||
![]() |
d416465371 | ||
![]() |
2586c543d3 | ||
![]() |
c32bc26328 | ||
![]() |
4074c71b6a | ||
![]() |
a7493d1039 | ||
![]() |
d7cab6a8d8 | ||
![]() |
eddc12693a | ||
![]() |
ced3898499 | ||
![]() |
318a5df109 | ||
![]() |
b2e9981313 | ||
![]() |
5f092e37f9 | ||
![]() |
81bbef04dc | ||
![]() |
74f43639ad | ||
![]() |
fc342bd458 | ||
![]() |
adfbf5b49f | ||
![]() |
2cb7bb84f7 | ||
![]() |
84d1792e7f | ||
![]() |
531859ac60 | ||
![]() |
b5bf0d7e1d | ||
![]() |
bf071d65d7 | ||
![]() |
e4aa7a90c7 | ||
![]() |
6d4e3c5633 | ||
![]() |
19f9b4f502 | ||
![]() |
2b8837609b | ||
![]() |
f3dbb19364 | ||
![]() |
0a831ec84e | ||
![]() |
0c656abb8e | ||
![]() |
7a7a90bf79 | ||
![]() |
908dff3931 | ||
![]() |
a786cff036 | ||
![]() |
28802805f8 | ||
![]() |
f59099395f | ||
![]() |
0fe3fe7594 | ||
![]() |
467dacd35a | ||
![]() |
173150591d | ||
![]() |
e4d94b1a4e | ||
![]() |
75e34a5a8e | ||
![]() |
d6121c8e21 | ||
![]() |
b4d77df1be | ||
![]() |
e6021465f6 | ||
![]() |
22ec70e94d | ||
![]() |
a1a70a94a8 | ||
![]() |
a65ed7e914 | ||
![]() |
4545b8e92d | ||
![]() |
ba0c0fb109 | ||
![]() |
18d530021c | ||
![]() |
31bb70e333 | ||
![]() |
a919a039e5 | ||
![]() |
aacb1f46a8 | ||
![]() |
96862cbcb3 | ||
![]() |
10f79e1307 | ||
![]() |
e0ee3dce40 | ||
![]() |
13e7d2e7ac | ||
![]() |
a7723373a0 | ||
![]() |
7e469ead45 | ||
![]() |
5397a4e410 | ||
![]() |
99b59f0126 | ||
![]() |
d46c7eb8fe | ||
![]() |
e4a1fc9d95 | ||
![]() |
276f50a944 | ||
![]() |
40fcd93312 | ||
![]() |
807e4d4af9 | ||
![]() |
480348f11a | ||
![]() |
30613b7064 | ||
![]() |
79189dcc83 | ||
![]() |
c2210330b6 | ||
![]() |
917f459569 | ||
![]() |
0ced9ba799 | ||
![]() |
5e95277d7c | ||
![]() |
efb417dba7 | ||
![]() |
5c2d4c4d9d | ||
![]() |
e8bd9920fd | ||
![]() |
69ed531a5c | ||
![]() |
b967d7c148 | ||
![]() |
90150c42ed | ||
![]() |
8e2fd9ccce | ||
![]() |
567ffad41d | ||
![]() |
3c306a0971 | ||
![]() |
c0d6c8aeb3 | ||
![]() |
b27b49e4f3 | ||
![]() |
7ed0dbcf1a | ||
![]() |
8a23de6b20 | ||
![]() |
6cc3089204 | ||
![]() |
093e95c078 | ||
![]() |
7c8ac04e35 | ||
![]() |
dc88f8b172 | ||
![]() |
a00ac6b9ca | ||
![]() |
c94f0ded27 | ||
![]() |
b553aa2159 | ||
![]() |
a7bd2666f0 | ||
![]() |
fe2fc60581 | ||
![]() |
ce59c05d5b | ||
![]() |
a4858bc702 | ||
![]() |
a2bb58a991 | ||
![]() |
f7b41227d2 | ||
![]() |
5b1a6831d5 | ||
![]() |
42b1bbe414 | ||
![]() |
ac86fe80c8 | ||
![]() |
db9f20a22f | ||
![]() |
b30e025bda | ||
![]() |
5f3eb4871a | ||
![]() |
9a223532c5 | ||
![]() |
cf67b592da | ||
![]() |
e867bfbc82 | ||
![]() |
f341f43427 | ||
![]() |
9a671851df | ||
![]() |
4b92f78cc8 | ||
![]() |
c585982557 | ||
![]() |
6bf22e7ad0 | ||
![]() |
2f8dccf7f6 | ||
![]() |
027768d97d | ||
![]() |
085f63b8c5 | ||
![]() |
6f7c337e00 | ||
![]() |
16a968f3bb | ||
![]() |
d7e0167fed | ||
![]() |
41c4f515cf | ||
![]() |
d9a8218372 | ||
![]() |
dd9bd4da8b | ||
![]() |
cf98500b7f | ||
![]() |
2ce8facc05 | ||
![]() |
d1b117d07c | ||
![]() |
c0377c7ebf | ||
![]() |
a2490a5730 | ||
![]() |
177334ba62 | ||
![]() |
f7f00293cc | ||
![]() |
7bce588767 | ||
![]() |
4bb67c634f | ||
![]() |
3653afbcc4 | ||
![]() |
1f4a4ea09f | ||
![]() |
3d38add4b4 | ||
![]() |
124b7eefb5 | ||
![]() |
b52924048c | ||
![]() |
93393f5dff | ||
![]() |
275a75ebaa | ||
![]() |
3e4a7a19cc | ||
![]() |
734af457f3 | ||
![]() |
55bdb1f47a | ||
![]() |
adff0d199d | ||
![]() |
f95b3262a0 | ||
![]() |
794a14e76c | ||
![]() |
ba857b5ef7 | ||
![]() |
2aed04a8c2 | ||
![]() |
5f9e6b51da | ||
![]() |
e7b5c99ed6 | ||
![]() |
9c0b3d35be | ||
![]() |
a54bc96eab | ||
![]() |
a2a8e4b965 | ||
![]() |
81ad2c61d9 | ||
![]() |
32616493b3 | ||
![]() |
05183ffd0f | ||
![]() |
e72ddc9439 | ||
![]() |
32e3caecac | ||
![]() |
df43389183 | ||
![]() |
19b77809ec | ||
![]() |
be05b827f3 | ||
![]() |
5dfc6f822d | ||
![]() |
c3e004da03 | ||
![]() |
8bae73b6ea | ||
![]() |
d1e19d3b63 | ||
![]() |
ffca897ddf | ||
![]() |
4277b6e262 | ||
![]() |
506c4ce701 | ||
![]() |
d251e58984 | ||
![]() |
4a1213c081 | ||
![]() |
8b7609255c | ||
![]() |
ef78fe0653 | ||
![]() |
70b3ccb422 | ||
![]() |
81d6b367fe | ||
![]() |
0a78ae60be | ||
![]() |
a61830a860 | ||
![]() |
86bae9ddc9 | ||
![]() |
033780862a | ||
![]() |
6094d8a74e | ||
![]() |
356ca3d177 | ||
![]() |
d69806faa9 | ||
![]() |
ab67635dcb | ||
![]() |
cee3d49458 | ||
![]() |
5b53a7aef7 | ||
![]() |
9b29665cc0 | ||
![]() |
f447c87b45 | ||
![]() |
e3eea45d86 | ||
![]() |
f61a06ce0a | ||
![]() |
539842aa99 | ||
![]() |
5925f1d2aa | ||
![]() |
61eb150825 | ||
![]() |
cf95de4d27 | ||
![]() |
fdad7ec1ba | ||
![]() |
850efb4237 | ||
![]() |
853cb3887f | ||
![]() |
412f2c1664 | ||
![]() |
2810a69bd4 | ||
![]() |
5347f95f50 | ||
![]() |
6b469f0621 | ||
![]() |
0021562c93 | ||
![]() |
f2bd2b0a59 | ||
![]() |
647eb8bbf5 | ||
![]() |
816d13ae3f | ||
![]() |
578fea4a9c | ||
![]() |
1a660d9a4a | ||
![]() |
227ac6d9e3 | ||
![]() |
bb57407733 | ||
![]() |
13ddcce0a2 | ||
![]() |
53767a78d1 | ||
![]() |
5600e8a2ad | ||
![]() |
c6ed52c592 | ||
![]() |
3ad14e4adf | ||
![]() |
8a22bdea5d | ||
![]() |
6135a3c3e2 | ||
![]() |
1e3c979303 | ||
![]() |
d0228406b6 | ||
![]() |
507a2237b7 | ||
![]() |
c15c597d99 | ||
![]() |
7c26cd3270 | ||
![]() |
938af73059 | ||
![]() |
1c047366d2 | ||
![]() |
cb20f0cbb0 | ||
![]() |
468251c84e | ||
![]() |
ca86ae0c9a | ||
![]() |
59221b0b4e | ||
![]() |
d3e0640400 | ||
![]() |
bcb72321f5 | ||
![]() |
4060af715d | ||
![]() |
2ec0237e83 | ||
![]() |
c5593880f2 | ||
![]() |
3673cbce4f | ||
![]() |
1f6f7be4b2 | ||
![]() |
580cce3506 | ||
![]() |
36ba546fc6 | ||
![]() |
7f37799cbe | ||
![]() |
5570eeeff9 | ||
![]() |
2b186ce6e0 | ||
![]() |
72938fed69 | ||
![]() |
d54c806e03 | ||
![]() |
7eb3551485 | ||
![]() |
57abe27895 | ||
![]() |
a628a36082 | ||
![]() |
0d3e04ff25 | ||
![]() |
0c78a3f7b0 | ||
![]() |
fb1f574c26 | ||
![]() |
7f15c18fca | ||
![]() |
e274650956 | ||
![]() |
2a1db4a338 | ||
![]() |
0b6ea9ec61 | ||
![]() |
257a826d45 | ||
![]() |
e9f48f5134 | ||
![]() |
fcf04624d4 | ||
![]() |
16218d6dc5 | ||
![]() |
071f33e3cd | ||
![]() |
6d15389da8 | ||
![]() |
2e8530ec00 | ||
![]() |
4edd1c5497 | ||
![]() |
8e693b8b42 | ||
![]() |
29376066e8 | ||
![]() |
a44f3071bf | ||
![]() |
b66047e084 | ||
![]() |
f0ca916432 | ||
![]() |
c88b4032ef | ||
![]() |
6f5e99be6f | ||
![]() |
fd4c37e9b3 | ||
![]() |
7a8dab2d58 | ||
![]() |
6f3dfad550 | ||
![]() |
18fb0a13d7 | ||
![]() |
e88f9ae03b | ||
![]() |
30c010ad3f | ||
![]() |
e84e70bdc6 | ||
![]() |
1e6b6165ae | ||
![]() |
c6d149d091 | ||
![]() |
d55d8d78de | ||
![]() |
eff59f7b5e | ||
![]() |
a7a5437245 | ||
![]() |
6671b9e55b | ||
![]() |
2dde1cc589 | ||
![]() |
17866c29ae | ||
![]() |
8dc4e6dc2a | ||
![]() |
1197f44262 | ||
![]() |
a86ed1f801 | ||
![]() |
e98d3423e4 | ||
![]() |
95333d37c8 | ||
![]() |
8bcf0c6498 | ||
![]() |
340b92e32b | ||
![]() |
6e68ab19f9 | ||
![]() |
15fed32d92 | ||
![]() |
897c754dd4 | ||
![]() |
ec1e746a22 | ||
![]() |
001f078ba9 | ||
![]() |
c0ff1e86b9 | ||
![]() |
b5321152fd | ||
![]() |
66d15ea635 | ||
![]() |
72177033d2 | ||
![]() |
06b7072240 | ||
![]() |
4700f35739 | ||
![]() |
bbfa280e86 | ||
![]() |
2669ba944d | ||
![]() |
c8788dbfbe | ||
![]() |
5e7c3b53f8 | ||
![]() |
99348c2300 | ||
![]() |
ab2b9797fd | ||
![]() |
637653ea11 | ||
![]() |
04cb6ba3d0 | ||
![]() |
eb1cddd85a | ||
![]() |
723b230093 | ||
![]() |
7185fae491 | ||
![]() |
0274cd6beb | ||
![]() |
ad2ea0b807 | ||
![]() |
c24999075d | ||
![]() |
773bde14ab | ||
![]() |
00b08318a5 | ||
![]() |
39e5d8ccc2 | ||
![]() |
e25622df4b | ||
![]() |
ea5939c1b7 | ||
![]() |
4734d04d4f | ||
![]() |
493e47f7e6 | ||
![]() |
ef2b32eb05 | ||
![]() |
1da91d44e1 | ||
![]() |
ebd7ab3e46 | ||
![]() |
99bddfdf0a | ||
![]() |
144f48c9a6 | ||
![]() |
8d0d2ba07b | ||
![]() |
a6ad334dc0 | ||
![]() |
468ca30070 | ||
![]() |
7b2d2d9338 | ||
![]() |
0e08819cf3 | ||
![]() |
1d3f7b49dc | ||
![]() |
3566ec7012 | ||
![]() |
f37a36efa4 | ||
![]() |
2ea069cd8c | ||
![]() |
08e111f6dc | ||
![]() |
6339881684 | ||
![]() |
e222538575 | ||
![]() |
c4a67ce420 | ||
![]() |
77e348ba62 | ||
![]() |
bde39d8c37 | ||
![]() |
ebb906c273 | ||
![]() |
46b91bf8b0 | ||
![]() |
7a432b38e9 | ||
![]() |
9b6a201bbb | ||
![]() |
a79d7c8417 | ||
![]() |
7a6e0d651f | ||
![]() |
7476498823 | ||
![]() |
4c7b5d44a0 | ||
![]() |
620bb54881 | ||
![]() |
73be747cbe | ||
![]() |
4d874451c9 | ||
![]() |
3245d620c3 | ||
![]() |
a59f80589a | ||
![]() |
6b269c7559 | ||
![]() |
53cadeab61 | ||
![]() |
e90d388fdb | ||
![]() |
219f059834 | ||
![]() |
dc2dac66a3 | ||
![]() |
e04ee666b8 | ||
![]() |
7d27003bb2 | ||
![]() |
b866c9dd08 | ||
![]() |
d17236fe45 | ||
![]() |
d2580ec87c | ||
![]() |
7c10f414dc | ||
![]() |
2a4717cb7f | ||
![]() |
be9cb8a4da | ||
![]() |
e887363910 | ||
![]() |
a274159726 | ||
![]() |
83384e0de4 | ||
![]() |
748904b8ad | ||
![]() |
3e72df8b1e | ||
![]() |
2921563e9c | ||
![]() |
01c1346696 | ||
![]() |
796e0456ef | ||
![]() |
eeb68497fe | ||
![]() |
7eadb6acad | ||
![]() |
9d588aa7e7 | ||
![]() |
204b5f7f09 | ||
![]() |
e0f53b63ce | ||
![]() |
83f4dbe40e | ||
![]() |
8d8ba68838 | ||
![]() |
3f25940dec | ||
![]() |
745773b207 | ||
![]() |
35f5575595 | ||
![]() |
e4746f8b32 | ||
![]() |
7e0552efde | ||
![]() |
6075b98634 | ||
![]() |
ebe9f518d0 | ||
![]() |
37ceddd11b | ||
![]() |
d1d8b911b9 | ||
![]() |
796e656328 | ||
![]() |
8b869915e7 | ||
![]() |
9b05243d61 | ||
![]() |
81c24510a8 | ||
![]() |
9e7fb4d21a | ||
![]() |
7805f8a9b1 | ||
![]() |
ae7f04578d | ||
![]() |
ce814cffd1 | ||
![]() |
703b310ef0 | ||
![]() |
da6c4ad36a | ||
![]() |
a8c849d38a | ||
![]() |
d8d5e04a51 | ||
![]() |
fa348cb98f | ||
![]() |
f4ec2d8107 | ||
![]() |
de39d828de | ||
![]() |
25d3d0d0ba | ||
![]() |
6f3b1000a7 | ||
![]() |
1ebb8d8d14 | ||
![]() |
4e4acdaecc | ||
![]() |
f7fb03bf56 | ||
![]() |
429aafc7ba | ||
![]() |
acdfede2a8 | ||
![]() |
b822c5a039 | ||
![]() |
8366c4c165 | ||
![]() |
4c7260b043 | ||
![]() |
c878f7dc25 | ||
![]() |
aca21f6ef2 | ||
![]() |
10c582bafb | ||
![]() |
7b3bd26631 | ||
![]() |
731f88da84 | ||
![]() |
b7fb9a65b6 | ||
![]() |
843c24b17a | ||
![]() |
18dbbfc95a | ||
![]() |
5b6e187b49 | ||
![]() |
9025a9b88c | ||
![]() |
07b2891671 | ||
![]() |
c4a739bef6 | ||
![]() |
1008c74cd7 | ||
![]() |
60dc9d27bc | ||
![]() |
9eb0f48a7a | ||
![]() |
7b1fccdd06 | ||
![]() |
64ae07b03b | ||
![]() |
6ecbbd1f79 | ||
![]() |
a1fb268764 | ||
![]() |
868661edf0 | ||
![]() |
9899e63d53 | ||
![]() |
0b37b8b059 | ||
![]() |
1b34ca822f | ||
![]() |
a4e3a874ad | ||
![]() |
8ec3df552a | ||
![]() |
b4b1c9256b | ||
![]() |
acee20d897 | ||
![]() |
6ea3ebb72d | ||
![]() |
55f23e9304 | ||
![]() |
ad223a04f8 | ||
![]() |
0b150ea475 | ||
![]() |
167e9fbc6d | ||
![]() |
77ea160cd9 | ||
![]() |
f9204450f1 | ||
![]() |
21ef76816f | ||
![]() |
f166cfbac8 | ||
![]() |
e5f64710f4 | ||
![]() |
32a5062081 | ||
![]() |
e6bc29281e | ||
![]() |
617ee0afc0 | ||
![]() |
1b47a1a994 | ||
![]() |
5a87cfc25d | ||
![]() |
00a178f7d3 | ||
![]() |
2a2c82e73b | ||
![]() |
bb882ada2c | ||
![]() |
1d42e45d78 | ||
![]() |
15c4a5c9ea | ||
![]() |
f4435f9031 | ||
![]() |
5a423c89a3 | ||
![]() |
8b02154f5a | ||
![]() |
97c454ea77 | ||
![]() |
f07a6d03b5 | ||
![]() |
1f18fb5446 | ||
![]() |
92ee5b66ab | ||
![]() |
fcc92c3e27 | ||
![]() |
3e91b5a793 | ||
![]() |
42e5cc3bef | ||
![]() |
16c61a1919 | ||
![]() |
c2b4b0490b | ||
![]() |
a310a06e3c | ||
![]() |
9228511527 | ||
![]() |
bbc4174501 | ||
![]() |
5f3196b74c | ||
![]() |
725bd8029f | ||
![]() |
479ab5df0e | ||
![]() |
18c45ad30b | ||
![]() |
6f32f098eb | ||
![]() |
e8289d3912 | ||
![]() |
472d9322ce | ||
![]() |
0233ffafb6 | ||
![]() |
468ee4756f | ||
![]() |
abf9365bbe | ||
![]() |
1e0789162f | ||
![]() |
31f407f4e8 | ||
![]() |
77330ffc50 | ||
![]() |
6f132f3fed | ||
![]() |
c193b4f07c | ||
![]() |
c745b845c5 | ||
![]() |
3b69e0dd25 | ||
![]() |
8ec55ef394 | ||
![]() |
ef5084036c | ||
![]() |
7dd317e530 | ||
![]() |
e5db3ed9b7 | ||
![]() |
3a00dc5b5f | ||
![]() |
2d848020fc | ||
![]() |
70123d19fe | ||
![]() |
ea3770260a | ||
![]() |
eea1a80de6 | ||
![]() |
4889775ae6 | ||
![]() |
355effd93d | ||
![]() |
f1583b6e0c | ||
![]() |
347566c311 | ||
![]() |
1f73572dd3 | ||
![]() |
2bfb83c4cd | ||
![]() |
ceed1c4962 | ||
![]() |
2b5b9d3599 | ||
![]() |
96e3709b7b | ||
![]() |
239fc2f6f8 | ||
![]() |
4c77e5cdd2 | ||
![]() |
974f8f692c | ||
![]() |
e97d0b9a69 | ||
![]() |
b0b0a75c87 | ||
![]() |
abcacf8c74 | ||
![]() |
290428b981 | ||
![]() |
37d1541d6b | ||
![]() |
1500ce7490 | ||
![]() |
a48529872d | ||
![]() |
31cffa68c5 | ||
![]() |
6909d1e527 | ||
![]() |
972235bfba | ||
![]() |
6db560fd2c | ||
![]() |
1a64d8aec9 | ||
![]() |
1e1fb32558 | ||
![]() |
008eb5ba4a | ||
![]() |
f46e0acc89 | ||
![]() |
b615ef5810 | ||
![]() |
d773279de8 | ||
![]() |
56d721651a | ||
![]() |
ee17abff92 | ||
![]() |
952bb1a2eb | ||
![]() |
c287813e00 | ||
![]() |
eab4fd80d7 | ||
![]() |
5c8f8869d4 | ||
![]() |
4954dfe107 | ||
![]() |
4eb8094fb8 | ||
![]() |
1dd2423a0b | ||
![]() |
6fcf989c62 | ||
![]() |
67d1a4f643 | ||
![]() |
3df9433baf | ||
![]() |
b12d568147 | ||
![]() |
8691e035a0 | ||
![]() |
2683043762 | ||
![]() |
be5aa59f61 | ||
![]() |
9398dfb7cf | ||
![]() |
ff6d2b30e4 | ||
![]() |
29bb999a32 | ||
![]() |
9320507e26 | ||
![]() |
181fc4fa0a | ||
![]() |
4f3dd4b662 | ||
![]() |
7b09de99ea | ||
![]() |
f9f0da18e1 | ||
![]() |
ba0fdb9478 | ||
![]() |
8c684bca22 | ||
![]() |
1266a75549 | ||
![]() |
3871d5aed7 | ||
![]() |
25d555126d | ||
![]() |
f529d15d7a | ||
![]() |
9eda30fc71 | ||
![]() |
dfd6424d9c | ||
![]() |
fdc961f2de | ||
![]() |
a6d4000d24 | ||
![]() |
e1024e59c3 | ||
![]() |
990164802d | ||
![]() |
34f18fbdb3 | ||
![]() |
17e24bb038 | ||
![]() |
df7e2b7734 | ||
![]() |
1ddef06bd2 | ||
![]() |
18bd910bf0 | ||
![]() |
0db44f6e33 | ||
![]() |
7aac3d38f0 | ||
![]() |
e406b6f780 | ||
![]() |
2dad9666a9 | ||
![]() |
063abf1688 | ||
![]() |
a86f8e9a22 | ||
![]() |
0ce6d4fe92 | ||
![]() |
886f6c721c | ||
![]() |
9d7d089279 | ||
![]() |
0bd624dfa9 | ||
![]() |
8c29760d93 | ||
![]() |
9b893d841d | ||
![]() |
e8c0163153 | ||
![]() |
256568d966 | ||
![]() |
34bed47a52 | ||
![]() |
9b0996fade | ||
![]() |
7c1028df5d | ||
![]() |
eaea60e0cb | ||
![]() |
efab05dcfc | ||
![]() |
d79f77f7e0 | ||
![]() |
f0d459d490 | ||
![]() |
e269c073ac | ||
![]() |
85d5609144 | ||
![]() |
671c593db1 | ||
![]() |
8b14c7a2cb | ||
![]() |
23862419eb | ||
![]() |
43d54db4dd | ||
![]() |
b8b0060440 | ||
![]() |
64d79ceb30 | ||
![]() |
25f7b44d48 | ||
![]() |
ada7e628da | ||
![]() |
76f2338c3d | ||
![]() |
a0ed8036c0 | ||
![]() |
863ce65b10 | ||
![]() |
85190b16cb | ||
![]() |
abc6fd8b2a | ||
![]() |
b6f603154e | ||
![]() |
90cb9d3de1 | ||
![]() |
d47c9a2e29 | ||
![]() |
b7aea96ca0 | ||
![]() |
a5879a4407 | ||
![]() |
0452c69771 | ||
![]() |
4eb9dff45e | ||
![]() |
b7c1e88b59 | ||
![]() |
23814330d9 | ||
![]() |
8bc75aacea | ||
![]() |
17576d223a | ||
![]() |
90e8f0ca63 | ||
![]() |
adc4a811b7 | ||
![]() |
7c80233f26 | ||
![]() |
f05ae2de35 | ||
![]() |
cf9da556a8 | ||
![]() |
32a142bf79 | ||
![]() |
2680d41a3d | ||
![]() |
1550fc4398 | ||
![]() |
f2a85f3b7e | ||
![]() |
49c2b4c196 | ||
![]() |
a8281e174e | ||
![]() |
d7f5c8bd55 | ||
![]() |
b3337df88b | ||
![]() |
d760616e55 | ||
![]() |
914a4d32b4 | ||
![]() |
148f53e21e | ||
![]() |
5e7fa0f964 | ||
![]() |
0973ceb9d2 | ||
![]() |
5214bfe8cb | ||
![]() |
187aaafddc | ||
![]() |
208cb405ca | ||
![]() |
9b9d267cd4 | ||
![]() |
6f90a27f9f | ||
![]() |
4e1dddc06d | ||
![]() |
1f504d6f23 | ||
![]() |
2db1fd813f | ||
![]() |
f39383a3d8 | ||
![]() |
b593bdbf1b | ||
![]() |
4de93ba3c0 | ||
![]() |
2eb7a91987 | ||
![]() |
94e4264c2a | ||
![]() |
e54d28f157 | ||
![]() |
6111c8bde0 | ||
![]() |
3bacdfd4fc | ||
![]() |
65db645ff9 | ||
![]() |
de2e2c45a5 | ||
![]() |
9b655e18e3 | ||
![]() |
fb4b9b5f76 | ||
![]() |
820b39840a | ||
![]() |
4b1052eb70 | ||
![]() |
c0eb3972a7 | ||
![]() |
66ba8d56b7 | ||
![]() |
a73baf32f1 | ||
![]() |
333cf0a2f0 | ||
![]() |
8347d8700a | ||
![]() |
09af0e2448 | ||
![]() |
001914764a | ||
![]() |
6938dd6267 | ||
![]() |
941028ba6f | ||
![]() |
4ca7ed9f8c | ||
![]() |
03d99887c5 | ||
![]() |
293e2ff5e3 | ||
![]() |
55d242fa08 | ||
![]() |
7e9fba2d96 | ||
![]() |
175652f23b | ||
![]() |
3329e0c4a1 | ||
![]() |
3c5ed2c885 | ||
![]() |
c4af93c363 | ||
![]() |
e2685c4503 | ||
![]() |
48b1d3fff8 | ||
![]() |
6c4920949d | ||
![]() |
69447b75af | ||
![]() |
e1104570a9 | ||
![]() |
2c23678fb9 | ||
![]() |
613070d39f | ||
![]() |
6deae64f45 | ||
![]() |
aced2b124c | ||
![]() |
e6b08de2e8 | ||
![]() |
d2d02d0749 | ||
![]() |
28d27801b2 | ||
![]() |
be340dd275 | ||
![]() |
58090fb3de | ||
![]() |
ae33c6cf18 | ||
![]() |
f8cd6afbf8 | ||
![]() |
6fce06906d | ||
![]() |
84694a8bbd | ||
![]() |
1639e68424 | ||
![]() |
ff48fe8b49 | ||
![]() |
efe06267ec | ||
![]() |
53647ea5a8 | ||
![]() |
7742de5af4 | ||
![]() |
724a260f71 | ||
![]() |
cf75e40332 | ||
![]() |
3c67df263c | ||
![]() |
c4ae72c3c1 | ||
![]() |
f6925fc5b8 | ||
![]() |
18be9655d6 | ||
![]() |
6235b6123e | ||
![]() |
b3555385e6 | ||
![]() |
c9fbdb322b | ||
![]() |
94bac7d8db | ||
![]() |
f38119be96 | ||
![]() |
bc1d2ba839 | ||
![]() |
18f5b70b1f | ||
![]() |
7df9b07305 | ||
![]() |
cf01c1fd1f | ||
![]() |
2ce6fe420b | ||
![]() |
f55381d689 | ||
![]() |
c4084c4f97 | ||
![]() |
58b720b004 | ||
![]() |
f6d0c1f05e | ||
![]() |
f4620be859 | ||
![]() |
e2b3a98690 | ||
![]() |
4cd391d5ef | ||
![]() |
01c37c34dd | ||
![]() |
f4827cde0e | ||
![]() |
3e722295b0 | ||
![]() |
0d2eab3ad4 | ||
![]() |
9e1bc631cf | ||
![]() |
ca9fbe2f11 | ||
![]() |
2e28fad102 | ||
![]() |
69760200dd | ||
![]() |
f945ee1288 | ||
![]() |
5e3486c481 | ||
![]() |
c15a943cf4 | ||
![]() |
b8cb29c66c | ||
![]() |
003badcb5a | ||
![]() |
5b2493fa68 | ||
![]() |
bc342b9b33 | ||
![]() |
314615bfef | ||
![]() |
44e82217c1 | ||
![]() |
cbf364f24f | ||
![]() |
44dfcb927b | ||
![]() |
12f615c6da | ||
![]() |
ed6fc4d848 | ||
![]() |
cd515993f5 | ||
![]() |
a918eaac3f | ||
![]() |
9f63e2d39a | ||
![]() |
36248ff046 | ||
![]() |
a88f5113e0 | ||
![]() |
06fb89fae2 | ||
![]() |
733531356f | ||
![]() |
3f8fb30066 | ||
![]() |
1a4a2d2b30 | ||
![]() |
eb6d3b3f8d | ||
![]() |
e8e3363d06 | ||
![]() |
dd55ad61f4 | ||
![]() |
2464bfd70b | ||
![]() |
09bc36bb13 | ||
![]() |
39da89b556 | ||
![]() |
c23de4b3b0 | ||
![]() |
7c79d7f5d7 | ||
![]() |
710507da51 | ||
![]() |
10e95bf1b1 | ||
![]() |
66a893c84e | ||
![]() |
dd6392e380 | ||
![]() |
ea0a0c7c5a | ||
![]() |
b8f7ba62c7 | ||
![]() |
25b318ba00 | ||
![]() |
9add51b59d | ||
![]() |
535a0504d8 | ||
![]() |
365c49d6d2 | ||
![]() |
b70bea48f2 | ||
![]() |
996f8644c4 | ||
![]() |
b3882ec6e3 | ||
![]() |
f87d447397 | ||
![]() |
9d8570d0d2 | ||
![]() |
796755dad8 | ||
![]() |
9387753995 | ||
![]() |
618d36dc07 | ||
![]() |
f11b0be483 | ||
![]() |
1b8b15b136 | ||
![]() |
bb63673cce | ||
![]() |
6770ad68d5 | ||
![]() |
bfe90c58d1 | ||
![]() |
f1cbeb3c29 | ||
![]() |
554ab4ea16 | ||
![]() |
a2becac2e6 | ||
![]() |
67a651f5e9 | ||
![]() |
ac8efe19d8 | ||
![]() |
ffd65d5afa | ||
![]() |
e86677178f | ||
![]() |
3c49a3341a | ||
![]() |
3433b2a73e | ||
![]() |
730988e7b7 | ||
![]() |
2a3b89e596 | ||
![]() |
f1a31bf58c | ||
![]() |
f1b62a9056 | ||
![]() |
dd943d24c8 | ||
![]() |
801320a3f3 | ||
![]() |
222ed2debd | ||
![]() |
7aab782c5f | ||
![]() |
3836f2f353 | ||
![]() |
5bfaa9a5db | ||
![]() |
95581771d6 | ||
![]() |
db5e3f2479 | ||
![]() |
b5a9631bcc | ||
![]() |
4a2d62ece0 | ||
![]() |
c3836decee | ||
![]() |
f7a030c895 | ||
![]() |
f171a692d3 | ||
![]() |
df5e73192b | ||
![]() |
fc1447d614 | ||
![]() |
77fd206b06 | ||
![]() |
23bdc03490 | ||
![]() |
9fe4de5709 | ||
![]() |
54fd601809 | ||
![]() |
63d54e6570 | ||
![]() |
71d027a966 | ||
![]() |
8b63aa2fe6 | ||
![]() |
5a35842c28 | ||
![]() |
d6a1ae3b3a | ||
![]() |
be5f4cb562 | ||
![]() |
d8b5464833 | ||
![]() |
5e7bbcd3bc | ||
![]() |
5383e53c4d | ||
![]() |
5b6fc713d6 | ||
![]() |
272be025e1 | ||
![]() |
e4ab250729 | ||
![]() |
4dcca9d5af | ||
![]() |
1988a08631 | ||
![]() |
34de0e569f | ||
![]() |
343d0fa09d | ||
![]() |
8eb6686103 | ||
![]() |
9e9687b5b8 | ||
![]() |
ef888d1afe | ||
![]() |
0e70e1a37a | ||
![]() |
06aaceb673 | ||
![]() |
703a4b7858 | ||
![]() |
32ba2ba83d | ||
![]() |
272b03ed92 | ||
![]() |
ecf19214ee | ||
![]() |
f3eb0c497f | ||
![]() |
c1f29a7565 | ||
![]() |
fb745b9108 | ||
![]() |
9410bf40d3 | ||
![]() |
c7a695cb04 | ||
![]() |
b991d5cab6 | ||
![]() |
42fd318321 | ||
![]() |
903aeec383 | ||
![]() |
8768fe4dcf | ||
![]() |
d8ba2ceed4 | ||
![]() |
ef8a1bcf47 | ||
![]() |
2b1469e02e | ||
![]() |
83ea91586b | ||
![]() |
dbb86d25e1 | ||
![]() |
794c74e514 | ||
![]() |
fbcdaa77e3 | ||
![]() |
dbdc04c45e | ||
![]() |
a4bb22280f | ||
![]() |
c0e1bbbfb6 | ||
![]() |
196b9dc771 | ||
![]() |
09578b4e46 | ||
![]() |
4d88dadf8c | ||
![]() |
d4fda5847d | ||
![]() |
b1ea7d6cbc | ||
![]() |
4e7632949d | ||
![]() |
26a8bd147b | ||
![]() |
dd726fac02 | ||
![]() |
3a3ecc7775 | ||
![]() |
6665d630ec | ||
![]() |
7706d7471a | ||
![]() |
ed87d6b268 | ||
![]() |
ed51c8b318 | ||
![]() |
6ffbb7b1ed | ||
![]() |
06764db118 | ||
![]() |
4864fa3f2d | ||
![]() |
2d25b6a1f4 | ||
![]() |
be76b3d105 | ||
![]() |
81cbeb4b24 | ||
![]() |
f0b658ba14 | ||
![]() |
295836fc7e | ||
![]() |
54e9858148 | ||
![]() |
b68f015825 | ||
![]() |
87ae26ede3 | ||
![]() |
49615f81b4 | ||
![]() |
87ce5140fa | ||
![]() |
3ba9fb375c | ||
![]() |
f4bd20361a | ||
![]() |
54f8a17aac | ||
![]() |
7a1e5026c4 | ||
![]() |
323161c6de | ||
![]() |
1ac4890893 | ||
![]() |
e9c88fecc5 | ||
![]() |
33deaaefac | ||
![]() |
bb6438ebe4 | ||
![]() |
d42af74afa | ||
![]() |
ea1f2f4ad4 | ||
![]() |
95b45651bb | ||
![]() |
0d5730d33e | ||
![]() |
0625a35ddf | ||
![]() |
2d3271ee13 | ||
![]() |
6da2e80027 | ||
![]() |
439edbf85c | ||
![]() |
e0237a0b86 | ||
![]() |
e1845ba603 | ||
![]() |
1cf757d401 | ||
![]() |
14985b1727 | ||
![]() |
6b2788be57 | ||
![]() |
ac888f4cb2 | ||
![]() |
df06cfc4c5 | ||
![]() |
dcba3a681c | ||
![]() |
7fd49c22a8 | ||
![]() |
314287a6d9 | ||
![]() |
f5e7b8f229 | ||
![]() |
de84db070e | ||
![]() |
c1d5a5cd98 | ||
![]() |
160a04c3c7 | ||
![]() |
23bfc30c57 | ||
![]() |
d9329bffd1 | ||
![]() |
bafc1df988 | ||
![]() |
30b8835919 | ||
![]() |
44b19e75f6 | ||
![]() |
2a558ad11d | ||
![]() |
123d8972e1 | ||
![]() |
e550a8ea27 | ||
![]() |
8b29460fed | ||
![]() |
e380d63c57 | ||
![]() |
89b4f2c4d4 | ||
![]() |
6c2f63f738 | ||
![]() |
77c612f0f5 | ||
![]() |
2d06c01192 | ||
![]() |
d13c19f05f | ||
![]() |
3d2ba05c77 | ||
![]() |
6898b9d9a4 | ||
![]() |
a65aaa6b83 | ||
![]() |
b3136c20c4 | ||
![]() |
0ae3dfd9cc | ||
![]() |
0370fa6c00 | ||
![]() |
7ab323b00c | ||
![]() |
541eb70b9c | ||
![]() |
e53e5ca20e | ||
![]() |
609bf64856 | ||
![]() |
a9fafe91a5 | ||
![]() |
0466b320dd | ||
![]() |
5b74d22d0a | ||
![]() |
4152c7f956 | ||
![]() |
d5f603303d | ||
![]() |
fc9c073a60 | ||
![]() |
9a0c2c40bd | ||
![]() |
d0fc9fda71 | ||
![]() |
df9823988e | ||
![]() |
eeb09c074c | ||
![]() |
af0928e2bd | ||
![]() |
d9cf4de3f7 | ||
![]() |
2d6dd4b3be | ||
![]() |
160312393a | ||
![]() |
e3ff9f9c86 | ||
![]() |
95570d796d | ||
![]() |
cd0d58a915 | ||
![]() |
2f1007c725 | ||
![]() |
b53d5d8c00 | ||
![]() |
3c4a4e5384 | ||
![]() |
0e5f85db95 | ||
![]() |
ad3364671d | ||
![]() |
3add24b8aa | ||
![]() |
e0f02d4080 | ||
![]() |
00c4c10472 | ||
![]() |
a2b8cc9dc2 | ||
![]() |
3465002cbb | ||
![]() |
631cb73305 | ||
![]() |
b3b6384bef | ||
![]() |
941ca575fb | ||
![]() |
2d65c3595d | ||
![]() |
b3812d913a | ||
![]() |
adcc420c81 | ||
![]() |
b97ad99bb4 | ||
![]() |
e93a2850d6 | ||
![]() |
411d0691fa | ||
![]() |
c843e77183 | ||
![]() |
093d6e5336 | ||
![]() |
8a3c752d42 | ||
![]() |
b4e073cde7 | ||
![]() |
814efbf8df | ||
![]() |
11e048abb1 | ||
![]() |
34e7855af6 | ||
![]() |
de54dc27ad | ||
![]() |
790133978d | ||
![]() |
b914d67d9d | ||
![]() |
518eb97e3a | ||
![]() |
f41549ccf1 | ||
![]() |
b69e477ecd | ||
![]() |
0062ff9cfa | ||
![]() |
f8de72f59f | ||
![]() |
7317737e90 | ||
![]() |
5b8eda4805 | ||
![]() |
886a949a00 | ||
![]() |
92e13dafe5 | ||
![]() |
c9be812330 | ||
![]() |
59e7ebabfa | ||
![]() |
1afc48fce3 | ||
![]() |
a1e4ef9e8e | ||
![]() |
5ada0ae2c7 | ||
![]() |
a5312c1341 | ||
![]() |
150e156d26 | ||
![]() |
6d38615ea8 | ||
![]() |
011cc7d337 | ||
![]() |
b747d09836 | ||
![]() |
4b7311bafd | ||
![]() |
eeba9c0a5f | ||
![]() |
11d9a037f7 | ||
![]() |
883e4fcd7c | ||
![]() |
2017e6a3e3 | ||
![]() |
bccfe500b3 | ||
![]() |
5846fbabce | ||
![]() |
52e89c1d1c | ||
![]() |
1605e50cef | ||
![]() |
2215ce58a4 | ||
![]() |
a13e6b69e3 | ||
![]() |
1d6370e11c | ||
![]() |
bd34c7ede3 | ||
![]() |
bc8954fbba | ||
![]() |
9cf0bc6c82 | ||
![]() |
71b32fe641 | ||
![]() |
530f745e44 | ||
![]() |
a5a2313851 | ||
![]() |
4e98c2e7f6 | ||
![]() |
14486782dc | ||
![]() |
31814b70da | ||
![]() |
408e819d32 | ||
![]() |
622676f9bc | ||
![]() |
5b631e0387 | ||
![]() |
06d54ef77e | ||
![]() |
6c5ef567ed | ||
![]() |
f86b40302d | ||
![]() |
273c287fbf | ||
![]() |
6cb16be5df | ||
![]() |
a4feb3fc09 | ||
![]() |
ba6c7de35a | ||
![]() |
79e2bb382f | ||
![]() |
0090256ded | ||
![]() |
00ce077758 | ||
![]() |
a801d0994f | ||
![]() |
628575dc5f | ||
![]() |
0a22f21410 | ||
![]() |
97ff9e9c5b | ||
![]() |
8b3a09306b | ||
![]() |
7766fd13fd | ||
![]() |
c79997ebe3 | ||
![]() |
0fd1e2fcd9 | ||
![]() |
99442b6e04 | ||
![]() |
b8a35e9e4a | ||
![]() |
e833d415e3 | ||
![]() |
8030312924 | ||
![]() |
a84b54f940 | ||
![]() |
c66c81294e | ||
![]() |
bfdc215c65 | ||
![]() |
e10c7beedb | ||
![]() |
18c3286364 | ||
![]() |
8d2ec30818 | ||
![]() |
e5ffddfc6b | ||
![]() |
59fc1e4b5f | ||
![]() |
7ead581953 | ||
![]() |
b860980df4 | ||
![]() |
97a366d62e | ||
![]() |
9ad68097d0 | ||
![]() |
331999fb95 | ||
![]() |
6519d7051d | ||
![]() |
552d585fca | ||
![]() |
24c24d6c72 | ||
![]() |
b7f50c3e12 | ||
![]() |
aed1687a45 | ||
![]() |
daa427dc15 | ||
![]() |
e9d4303fdb | ||
![]() |
5485e994ee | ||
![]() |
87228673b4 | ||
![]() |
d306513319 | ||
![]() |
e08480f345 | ||
![]() |
13c9096417 | ||
![]() |
d3d65c8e3a | ||
![]() |
12ac5ef781 | ||
![]() |
adef9a8acf | ||
![]() |
5ef407d15f | ||
![]() |
970b636eb4 | ||
![]() |
fcf9131aae | ||
![]() |
7fd27fac45 | ||
![]() |
6e17af91fb | ||
![]() |
68555573ad | ||
![]() |
fb9905a89e | ||
![]() |
1e7504dc5a | ||
![]() |
d7af019511 | ||
![]() |
04bb070afa | ||
![]() |
e693d80857 | ||
![]() |
45ae05f1b5 | ||
![]() |
05a83beb44 | ||
![]() |
8a1a42e83b | ||
![]() |
1b3f3cedb3 | ||
![]() |
f815ae5973 | ||
![]() |
fb7035bf22 | ||
![]() |
02bcbc3221 | ||
![]() |
f0a51d4ab4 | ||
![]() |
24fe8fe9a0 | ||
![]() |
244e95d959 | ||
![]() |
ad72c64e32 | ||
![]() |
767ac6a51b | ||
![]() |
3a6f87659a | ||
![]() |
b7ac16c7d9 | ||
![]() |
f9f84cbd89 | ||
![]() |
701c87eefa | ||
![]() |
5379cf0544 | ||
![]() |
6b5c37f17f | ||
![]() |
50b2fad180 | ||
![]() |
b9cb65b24e | ||
![]() |
d7574973e9 | ||
![]() |
ff48c93d59 | ||
![]() |
d2e6700dd1 | ||
![]() |
05b8c3f35f | ||
![]() |
70b643e7ba | ||
![]() |
dce973a519 | ||
![]() |
eb2f75579a | ||
![]() |
773316ce4f | ||
![]() |
5fd7ae33b4 | ||
![]() |
13a065f2dc | ||
![]() |
1a8ff81087 | ||
![]() |
65637fce40 | ||
![]() |
b11fa7a28e | ||
![]() |
45408caf33 | ||
![]() |
963ee4dbab | ||
![]() |
3b46d5a440 | ||
![]() |
212fddd8e1 | ||
![]() |
433485470e | ||
![]() |
e160a1f794 | ||
![]() |
7911b7e637 | ||
![]() |
7fc5a77e7e | ||
![]() |
f0fb55640e | ||
![]() |
d5685f2255 | ||
![]() |
a732233db6 | ||
![]() |
a5918c29ee | ||
![]() |
3b719803bb | ||
![]() |
d77463c9f1 | ||
![]() |
2d8fd9bedf | ||
![]() |
b17a667a9d | ||
![]() |
b7287a070b | ||
![]() |
fbb5c8cdd6 | ||
![]() |
baaf2815e4 | ||
![]() |
d8b5549fd9 | ||
![]() |
6de03f2bf0 | ||
![]() |
caf7c55069 | ||
![]() |
7d499ffba1 | ||
![]() |
4abf6b2f5c | ||
![]() |
a842b06301 | ||
![]() |
0bca4925d7 | ||
![]() |
ae3953cbec | ||
![]() |
a99667c54c | ||
![]() |
465963a8c2 | ||
![]() |
b9b4762faf | ||
![]() |
a56b128a4b | ||
![]() |
d43cc089fd | ||
![]() |
771513d287 | ||
![]() |
c387678217 | ||
![]() |
847368718b | ||
![]() |
458b3daac3 | ||
![]() |
3ea5278b12 | ||
![]() |
20b9748a8c | ||
![]() |
79487adbec | ||
![]() |
89f3fca6b1 | ||
![]() |
f290b2bf5a | ||
![]() |
04e7d13043 | ||
![]() |
e41218c46b | ||
![]() |
8562fbdbbe | ||
![]() |
c841d7a32b | ||
![]() |
8827ae4d2c | ||
![]() |
2e1029e157 | ||
![]() |
a7dc0c2d55 | ||
![]() |
7f1749d853 | ||
![]() |
b4a34d58db | ||
![]() |
4a50fcab2c | ||
![]() |
c9ef089199 | ||
![]() |
94ecf9a081 | ||
![]() |
67aaa9a655 | ||
![]() |
823f5640f7 | ||
![]() |
45d1c63895 | ||
![]() |
d3b6781bb8 | ||
![]() |
21d1f69d6d | ||
![]() |
1b9f5989ef | ||
![]() |
348e46ff3b | ||
![]() |
7918e3a1aa | ||
![]() |
d6d8c7830c | ||
![]() |
f53a0f0d07 | ||
![]() |
17edd1c3d4 | ||
![]() |
b0685c153a | ||
![]() |
a5ca20ee4c | ||
![]() |
03685db2fc | ||
![]() |
68ed738dcd | ||
![]() |
5293d17e32 | ||
![]() |
f2e4b69466 | ||
![]() |
ec8b00042b | ||
![]() |
08db1d59e5 | ||
![]() |
7c79d421e8 | ||
![]() |
7385aa09a8 | ||
![]() |
185a5fad88 | ||
![]() |
a1e2477d14 | ||
![]() |
a1200a5fff | ||
![]() |
91a0257c8f | ||
![]() |
53ffc82fe2 | ||
![]() |
801267df18 | ||
![]() |
6e73e0b395 | ||
![]() |
7aa8a5c368 | ||
![]() |
3ecbbea7cb | ||
![]() |
77cd3182f1 | ||
![]() |
c7ccf9bab8 | ||
![]() |
06e70abb86 | ||
![]() |
88c86e88b0 | ||
![]() |
2d6cf48532 | ||
![]() |
19e152a54b | ||
![]() |
2898bead66 | ||
![]() |
381c329441 | ||
![]() |
a3de3705f7 | ||
![]() |
dc3dc6b77f | ||
![]() |
e028a63f30 | ||
![]() |
d196f8b4b2 | ||
![]() |
4274827dbe | ||
![]() |
7a30f4a7d2 | ||
![]() |
d0c03a0211 | ||
![]() |
787b136d13 | ||
![]() |
08412d6108 | ||
![]() |
d8f7db4715 | ||
![]() |
bff238774e | ||
![]() |
d2aaa6f691 | ||
![]() |
b2164ce5fc | ||
![]() |
5cc60ed760 | ||
![]() |
a7fbe05a73 | ||
![]() |
c796e2ae3c | ||
![]() |
398cbe9284 | ||
![]() |
d87e488c23 | ||
![]() |
5c2ff9b777 | ||
![]() |
6d7e37610c | ||
![]() |
a47e6dd8c5 | ||
![]() |
f334a2740f | ||
![]() |
26e487c01a | ||
![]() |
cc438fdb7b | ||
![]() |
92ff98d99a | ||
![]() |
d1609cba90 | ||
![]() |
0c394b123c | ||
![]() |
421b8214cb | ||
![]() |
6fc91312d2 | ||
![]() |
22bb129bd9 | ||
![]() |
4c57893312 | ||
![]() |
a2d5314cf7 | ||
![]() |
e063967734 | ||
![]() |
4519dd010d | ||
![]() |
bc2dc8d933 | ||
![]() |
fc9b63298c | ||
![]() |
c45514b989 |
108
.github/CONTRIBUTING.md
vendored
108
.github/CONTRIBUTING.md
vendored
|
@ -1,64 +1,84 @@
|
||||||
|
### Please do **not** open pull requests for *new features* now, as we are planning to rewrite large chunks of the code. Only bugfix PRs will be accepted. More details will be announced soon!
|
||||||
|
|
||||||
NewPipe contribution guidelines
|
NewPipe contribution guidelines
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
|
||||||
|
|
||||||
## Crash reporting
|
## Crash reporting
|
||||||
|
|
||||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to
|
Report crashes through the **automated crash report system** of NewPipe.
|
||||||
send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even
|
This way all the data needed for debugging is included in your bugreport for GitHub.
|
||||||
add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
||||||
|
|
||||||
## Issue reporting/feature requests
|
## Issue reporting/feature requests
|
||||||
|
|
||||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
* **Already reported**? Browse the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) to make sure your issue/feature hasn't been reported/requested.
|
||||||
hasn't been reported/requested before.
|
* **Already fixed**? Check whether your issue/feature is already fixed/implemented.
|
||||||
* Check whether your issue/feature is already fixed/implemented.
|
* **Still relevant**? Check if the issue still exists in the latest release/beta version.
|
||||||
* Check if the issue still exists in the latest release/beta version.
|
* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! See [Code contribution](#code-contribution) for more info.
|
||||||
* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
|
* **Is it in English**? Issues in other languages will be ignored unless someone translates them.
|
||||||
* We use English for development. Issues in other languages will be closed and ignored.
|
* **Is it one issue**? Multiple issues require multiple reports, that can be linked to track their statuses.
|
||||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
* **The template**: Fill it out, everyone wins. Your issue has a chance of getting fixed.
|
||||||
* Follow the template! Issues or feature requests not matching the template might be closed.
|
|
||||||
|
|
||||||
## Bug Fixing
|
|
||||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
|
|
||||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a> to let us know that you intend to help. We'll send you further instructions. You may, on request,
|
|
||||||
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
|
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
|
||||||
with your GitHub account.
|
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||||
* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
* NewPipe uses the [PrettyTime](https://github.com/ocpsoft/prettytime) library to display localized versions of dates and times. It needs to be translated, too. Read [these instructions to add a new language](https://www.ocpsoft.org/prettytime/#section-14) and [this issue](https://github.com/TeamNewPipe/NewPipe/issues/9134) for more info.
|
||||||
|
|
||||||
## Code contribution
|
## Code contribution
|
||||||
|
|
||||||
* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
|
### Guidelines
|
||||||
* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
|
|
||||||
libraries.
|
* Stick to NewPipe's *style conventions* of [checkStyle](https://github.com/checkstyle/checkstyle) and [ktlint](https://github.com/pinterest/ktlint). They run each time you build the project.
|
||||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
||||||
* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
|
* In particular **do not bring non-free software** (e.g. binary blobs) into the project. Make sure you do not introduce any closed-source library from Google.
|
||||||
may then send your changes as a pull request (PR) on GitHub.
|
|
||||||
* When submitting changes, you confirm that your code is licensed under the terms of the
|
### Before starting development
|
||||||
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
|
||||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
|
* If you want to help out with an existing bug report or feature request, **leave a comment** on that issue saying you want to try your hand at it.
|
||||||
description. Untested code will **not** be merged!
|
* If there is no existing issue for what you want to work on, **open a new one** describing the changes you are planning to introduce. This gives the team and the community a chance to give **feedback** before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||||
|
* Please show **intention to maintain your features** and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
* Please **test** (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||||
|
* Respond if someone requests changes or otherwise raises issues about your PRs.
|
||||||
* Try to figure out yourself why builds on our CI fail.
|
* Try to figure out yourself why builds on our CI fail.
|
||||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
|
* Make sure your PR is **up-to-date** with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must *rebase* your branch on the `dev` branch manually and resolve the conflicts on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). Doing this makes the maintainers' job way easier.
|
||||||
but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
|
|
||||||
maintainers' jobs way easier.
|
## IDE setup & building the app
|
||||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
|
|
||||||
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
|
### Basic setup
|
||||||
about submission, or clearly state that in the description of your PR.
|
|
||||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
NewPipe is developed using [Android Studio](https://developer.android.com/studio/). Learn more about how to install it and how it works in the [official documentation](https://developer.android.com/studio/intro). In particular, make sure you have accepted Android Studio's SDK licences. Once Android Studio is ready, setting up the NewPipe project is fairly simple:
|
||||||
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
- Clone the NewPipe repository with `git clone https://github.com/TeamNewPipe/NewPipe.git` (or use the link from your own fork, if you want to open a PR).
|
||||||
independent solutions.
|
- Open the folder you just cloned with Android Studio.
|
||||||
|
- Build and run it just like you would do with any other app, with the green triangle in the top bar.
|
||||||
|
|
||||||
|
You may find [SonarLint](https://www.sonarlint.org/intellij)'s **inspections** useful in helping you to write good code and prevent bugs.
|
||||||
|
|
||||||
|
### checkStyle setup
|
||||||
|
|
||||||
|
The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that Java code abides by the project style. It runs automatically each time you build the project. If you want to view errors directly in the editor, instead of having to skim through the build output, you can install an Android Studio plugin:
|
||||||
|
- Go to `File -> Settings -> Plugins`, search for `checkstyle` and install `CheckStyle-IDEA`.
|
||||||
|
- Go to `File -> Settings -> Tools -> Checkstyle`.
|
||||||
|
- Add NewPipe's configuration file by clicking the `+` in the right toolbar of the "Configuration File" list.
|
||||||
|
- Under the "Use a local Checkstyle file" bullet, click on `Browse` and, enter `checkstyle` folder under the project's root path and pick the file named `checkstyle.xml`.
|
||||||
|
- Enable "Store relative to project location" so that moving the directory around does not create issues.
|
||||||
|
- Insert a description in the top bar, then click `Next` and then `Finish`.
|
||||||
|
- Activate the configuration file you just added by enabling the checkbox on the left.
|
||||||
|
- Click `Ok` and you are done.
|
||||||
|
|
||||||
|
### ktlint setup
|
||||||
|
|
||||||
|
The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as checkStyle for Kotlin files. Installing the related plugin is as simple as going to `File -> Settings -> Plugins`, searching for `ktlint` and installing `Ktlint (unofficial)`.
|
||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
|
* 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)!
|
||||||
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=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.
|
||||||
* If you want to get in touch with the core team or one of our other contributors you can send an email to
|
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
||||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a>. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
|
||||||
tracker described above!
|
|
||||||
* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!
|
|
||||||
|
|
34
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
Normal file
34
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this form! :hugs:
|
||||||
|
|
||||||
|
Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe).
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: "Checklist"
|
||||||
|
options:
|
||||||
|
- label: "I made sure that there are *no existing issues or discussions* - [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
|
||||||
|
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed."
|
||||||
|
required: true
|
||||||
|
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
||||||
|
required: true
|
||||||
|
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-is-the-question
|
||||||
|
attributes:
|
||||||
|
label: What is/are your question(s)?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-information
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
||||||
liberapay: TeamNewPipe
|
liberapay: TeamNewPipe
|
||||||
|
custom: 'https://newpipe.net/donate/'
|
||||||
|
|
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,46 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a bug report to help us improve
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. If this is your first bug report, read the following information before proceeding:
|
|
||||||
|
|
||||||
Please note, we only support the latest version of NewPipe. In order to check your app version, open the left drawer and click on "About". If you don't have the latest version, upgrade to it and reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is where you can get it.
|
|
||||||
|
|
||||||
P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md
|
|
||||||
|
|
||||||
To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
|
|
||||||
|
|
||||||
### Version
|
|
||||||
<!-- Which version are you using? Hopefully the latest! We just told you that above! -->
|
|
||||||
-
|
|
||||||
|
|
||||||
### Steps to reproduce the bug
|
|
||||||
<!--
|
|
||||||
1. Go to '...'
|
|
||||||
2. Press on '....'
|
|
||||||
3. Swipe down to '....'
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
<!-- Tell us what you expect to happen. -->
|
|
||||||
|
|
||||||
### Actual behaviour
|
|
||||||
<!-- Tell us what happens instead. -->
|
|
||||||
|
|
||||||
### Screenshots/Screen recordings
|
|
||||||
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), copy it to the clipboard (there is a share button for this), head over to our bug report to markdown converter at https://teamnewpipe.github.io/CrashReportToMarkdown/ and paste it. Copy the converted text (it is MUCH easier to read this way) from there and paste it here: -->
|
|
||||||
|
|
||||||
<!-- That's right, here! -->
|
|
115
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
115
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
name: Bug report
|
||||||
|
description: Create a bug report to help us improve
|
||||||
|
labels: [bug, needs triage]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for helping to make NewPipe better by reporting a bug. :hugs:
|
||||||
|
|
||||||
|
Please fill in as much information as possible about your bug so that we don't have to play "information ping-pong" and can help you immediately.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: "Checklist"
|
||||||
|
options:
|
||||||
|
- 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
|
||||||
|
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
|
||||||
|
required: true
|
||||||
|
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
||||||
|
required: true
|
||||||
|
- label: "This issue contains only one bug."
|
||||||
|
required: true
|
||||||
|
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: Affected version
|
||||||
|
description: "In which NewPipe version did you encounter the bug?"
|
||||||
|
placeholder: "x.xx.x - Can be seen in the app from the 'About' section in the sidebar"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce the bug
|
||||||
|
description: |
|
||||||
|
What did you do for the bug to show up?
|
||||||
|
|
||||||
|
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Press on '....'
|
||||||
|
3. Swipe down to '....'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: |
|
||||||
|
Tell us what you expect to happen.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: |
|
||||||
|
Tell us what happens with the steps given above.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screen-media
|
||||||
|
attributes:
|
||||||
|
label: Screenshots/Screen recordings
|
||||||
|
description: |
|
||||||
|
A picture or video is worth a thousand words.
|
||||||
|
|
||||||
|
If applicable, add screenshots or a screen recording to help explain your problem.
|
||||||
|
GitHub supports uploading them directly in the text box.
|
||||||
|
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
|
||||||
|
|
||||||
|
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
|
||||||
|
Instead, follow the instructions in the "Logs" section below.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: |
|
||||||
|
If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device-os-info
|
||||||
|
attributes:
|
||||||
|
label: Affected Android/Custom ROM version
|
||||||
|
description: |
|
||||||
|
With what operating system (+ version) did you encounter the bug?
|
||||||
|
placeholder: "Example: Android 12 / LineageOS 18.1"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device-model-info
|
||||||
|
attributes:
|
||||||
|
label: Affected device model
|
||||||
|
description: |
|
||||||
|
On what device did you encounter the bug?
|
||||||
|
placeholder: "Example: Huawei P20 lite (ANE-LX1) / Samsung Galaxy S20"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-information
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |
|
||||||
|
Any other information you'd like to include, for instance that
|
||||||
|
* the affected device is foldable or a TV
|
||||||
|
* you have disabled all animations on your device
|
||||||
|
* your cat disabled your network connection
|
||||||
|
* ...
|
||||||
|
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: ❓ Question
|
||||||
|
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
||||||
|
about: Ask about anything NewPipe-related
|
||||||
|
- 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
|
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,39 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
<!-- Hey. Our contribution guidelines (https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) might be an appropriate
|
|
||||||
document to read before you fill out the request :) -->
|
|
||||||
|
|
||||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
|
|
||||||
|
|
||||||
#### Describe the feature you want
|
|
||||||
<!-- A clear and concise description of what you want to happen. PLEASE MAKE SURE it is one feature ONLY. You should open separate issues for separate feature requests, because those issues will be used to track their status.
|
|
||||||
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
|
||||||
|
|
||||||
Optionally, also describe alternatives you've considered.
|
|
||||||
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
|
||||||
|
|
||||||
<!-- Write below this -->
|
|
||||||
|
|
||||||
#### Is your feature request related to a problem? Please describe it
|
|
||||||
<!-- A clear and concise description of what the problem is. Maybe the developers could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
|
||||||
Example: *I want to do X, but there is no way to do it.* -->
|
|
||||||
|
|
||||||
<!-- Write below this -->
|
|
||||||
|
|
||||||
#### Additional context
|
|
||||||
<!-- Add any other context, like screenshots, about the feature request here.
|
|
||||||
Example: *Here's a photo of my cat!* -->
|
|
||||||
|
|
||||||
<!-- Write below this -->
|
|
||||||
|
|
||||||
#### How will you/everyone benefit from this feature?
|
|
||||||
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
|
||||||
The better this paragraph is, the more likely a developer will think about working on it.
|
|
||||||
Example: *This feature will help us colonize the galaxy! -->
|
|
||||||
|
|
||||||
<!-- Write below this -->
|
|
52
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
52
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: [feature request, needs triage]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
|
||||||
|
|
||||||
|
Your ideas are highly welcome! The app is made for you, the users, after all.
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: "Checklist"
|
||||||
|
options:
|
||||||
|
- 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
|
||||||
|
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
|
||||||
|
required: true
|
||||||
|
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
|
||||||
|
required: true
|
||||||
|
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
|
||||||
|
required: true
|
||||||
|
- label: "This issue contains only one feature request."
|
||||||
|
required: true
|
||||||
|
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
description: |
|
||||||
|
Explain how you want the app's look or behavior to change to suit your needs.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: why-is-the-feature-requested
|
||||||
|
attributes:
|
||||||
|
label: Why do you want this feature?
|
||||||
|
description: |
|
||||||
|
Describe any problem or limitation you come across while using the app which would be solved by this feature.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-information
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.
|
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,28 +1,34 @@
|
||||||
<!-- Hey there. Thank you so much for improving NewPipe. Please take a moment to fill out the following suggestion on how to structure this PR description. Having roughly the same layout helps everyone considerably :)-->
|
<!-- Hey there. Thank you so much for improving NewPipe, and filling out the details. Having roughly the same layout helps everyone considerably :)-->
|
||||||
|
|
||||||
#### What is it?
|
#### What is it?
|
||||||
- [ ] Bug fix (user facing)
|
- [ ] Bugfix (user facing)
|
||||||
- [ ] Feature (user facing)
|
- [ ] Feature (user facing)
|
||||||
- [ ] Code base improvement (dev facing)
|
- [ ] Codebase improvement (dev facing)
|
||||||
- [ ] Meta improvement to the project (dev facing)
|
- [ ] Meta improvement to the project (dev facing)
|
||||||
|
|
||||||
#### Description of the changes in your PR
|
#### Description of the changes in your PR
|
||||||
<!-- While bullet points are the norm in this section, feel free to write a text instead if you can't fit it in a list -->
|
<!-- While bullet points are the norm in this section, feel free to write free-form text instead of a list -->
|
||||||
- record videos
|
- record videos
|
||||||
- create clones
|
- create clones
|
||||||
- take over the world
|
- take over the world
|
||||||
|
|
||||||
|
#### Before/After Screenshots/Screen Record
|
||||||
|
<!-- If your PR changes the app's UI in any way, please include screenshots or a video showing exactly what changed, so that developers and users can pinpoint it easily. Delete this if it doesn't apply to your PR.-->
|
||||||
|
- Before:
|
||||||
|
- After:
|
||||||
|
|
||||||
#### Fixes the following issue(s)
|
#### Fixes the following issue(s)
|
||||||
<!-- Also add reddit or other links which are relevant to your change. -->
|
<!-- Prefix issues with "Fixes" so that GitHub closes them when the PR is merged (note that each "Fixes #" should be in its own item). Also add any other relevant links. -->
|
||||||
-
|
- Fixes #
|
||||||
|
|
||||||
#### Relies on the following changes
|
#### Relies on the following changes
|
||||||
<!-- Delete this if it doesn't apply to you. -->
|
<!-- Delete this if it doesn't apply to your PR. -->
|
||||||
-
|
-
|
||||||
|
|
||||||
#### Testing apk
|
#### APK testing
|
||||||
<!-- Ensure that you have your changes on a new branch which has a meaningful name. This name will be used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe. Do NOT name your branches like "patch-0" and "feature-1". For example, if your PR implements a bug fix for comments, an appropriate branch name would be "commentfix". -->
|
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
|
||||||
debug.zip
|
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
|
||||||
|
The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR. You can find more info and a video demonstration [on this wiki page](https://github.com/TeamNewPipe/NewPipe/wiki/Download-APK-for-PR).
|
||||||
|
|
||||||
#### Agreement
|
#### Due diligence
|
||||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).
|
||||||
|
|
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
|
141
.github/workflows/ci.yml
vendored
Normal file
141
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
- release**
|
||||||
|
paths-ignore:
|
||||||
|
- 'README.md'
|
||||||
|
- 'doc/**'
|
||||||
|
- 'fastlane/**'
|
||||||
|
- 'assets/**'
|
||||||
|
- '.github/**/*.md'
|
||||||
|
- '.github/FUNDING.yml'
|
||||||
|
- '.github/ISSUE_TEMPLATE/**'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- 'README.md'
|
||||||
|
- 'doc/**'
|
||||||
|
- 'fastlane/**'
|
||||||
|
- 'assets/**'
|
||||||
|
- '.github/**/*.md'
|
||||||
|
- '.github/FUNDING.yml'
|
||||||
|
- '.github/ISSUE_TEMPLATE/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test-jvm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
|
- name: create and checkout branch
|
||||||
|
# push events already checked out the branch
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
|
- name: set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: "temurin"
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: Build debug APK and run jvm tests
|
||||||
|
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app
|
||||||
|
path: app/build/outputs/apk/debug/*.apk
|
||||||
|
|
||||||
|
test-android:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- api-level: 21
|
||||||
|
target: default
|
||||||
|
arch: x86
|
||||||
|
- api-level: 33
|
||||||
|
target: google_apis # emulator API 33 only exists with Google APIs
|
||||||
|
arch: x86_64
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: "temurin"
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: Run android tests
|
||||||
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
with:
|
||||||
|
api-level: ${{ matrix.api-level }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
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@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: android-test-report-api${{ matrix.api-level }}
|
||||||
|
path: app/build/reports/androidTests/connected/**
|
||||||
|
|
||||||
|
sonar:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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@v4
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: "temurin"
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: Cache SonarCloud packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.sonar/cache
|
||||||
|
key: ${{ runner.os }}-sonar
|
||||||
|
restore-keys: ${{ runner.os }}-sonar
|
||||||
|
|
||||||
|
- name: Build and analyze
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
run: ./gradlew build sonar --info
|
147
.github/workflows/image-minimizer.js
vendored
Normal file
147
.github/workflows/image-minimizer.js
vendored
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
* Script for minimizing big images (jpg,gif,png) when they are uploaded to GitHub and not edited otherwise
|
||||||
|
*/
|
||||||
|
module.exports = async ({github, context}) => {
|
||||||
|
const IGNORE_KEY = '<!-- IGNORE IMAGE MINIFY -->';
|
||||||
|
const IGNORE_ALT_NAME_END = 'ignoreImageMinify';
|
||||||
|
// Targeted maximum height
|
||||||
|
const IMG_MAX_HEIGHT_PX = 600;
|
||||||
|
// maximum width of GitHub issues/comments
|
||||||
|
const IMG_MAX_WIDTH_PX = 800;
|
||||||
|
// all images that have a lower aspect ratio (-> have a smaller width) than this will be minimized
|
||||||
|
const MIN_ASPECT_RATIO = IMG_MAX_WIDTH_PX / IMG_MAX_HEIGHT_PX
|
||||||
|
|
||||||
|
// Get the body of the image
|
||||||
|
let initialBody = null;
|
||||||
|
if (context.eventName == 'issue_comment') {
|
||||||
|
initialBody = context.payload.comment.body;
|
||||||
|
} else if (context.eventName == 'issues') {
|
||||||
|
initialBody = context.payload.issue.body;
|
||||||
|
} else if (context.eventName == 'pull_request') {
|
||||||
|
initialBody = context.payload.pull_request.body;
|
||||||
|
} else {
|
||||||
|
console.log('Aborting: No body found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Found body: \n${initialBody}\n`);
|
||||||
|
|
||||||
|
// Check if we should ignore the currently processing element
|
||||||
|
if (initialBody.includes(IGNORE_KEY)) {
|
||||||
|
console.log('Ignoring: Body contains IGNORE_KEY');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>)
|
||||||
|
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||||
|
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
||||||
|
|
||||||
|
// Check if we found something
|
||||||
|
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
||||||
|
|| REGEX_ASSETS_IMAGE_LOCKUP.test(initialBody);
|
||||||
|
if (!foundSimpleImages) {
|
||||||
|
console.log('Found no simple images to process');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Found at least one simple image to process');
|
||||||
|
|
||||||
|
// Require the probe lib for getting the image dimensions
|
||||||
|
const probe = require('probe-image-size');
|
||||||
|
|
||||||
|
var wasMatchModified = false;
|
||||||
|
|
||||||
|
// Try to find and replace the images with minimized ones
|
||||||
|
let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync);
|
||||||
|
newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOCKUP, minimizeAsync);
|
||||||
|
|
||||||
|
if (!wasMatchModified) {
|
||||||
|
console.log('Nothing was modified. Skipping update');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the corresponding element
|
||||||
|
if (context.eventName == 'issue_comment') {
|
||||||
|
console.log('Updating comment with id', context.payload.comment.id);
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: context.payload.comment.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: newBody
|
||||||
|
})
|
||||||
|
} else if (context.eventName == 'issues') {
|
||||||
|
console.log('Updating issue', context.payload.issue.number);
|
||||||
|
await github.rest.issues.update({
|
||||||
|
issue_number: context.payload.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: newBody
|
||||||
|
});
|
||||||
|
} else if (context.eventName == 'pull_request') {
|
||||||
|
console.log('Updating pull request', context.payload.pull_request.number);
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
pull_number: context.payload.pull_request.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: newBody
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async replace function from https://stackoverflow.com/a/48032528
|
||||||
|
async function replaceAsync(str, regex, asyncFn) {
|
||||||
|
const promises = [];
|
||||||
|
str.replace(regex, (match, ...args) => {
|
||||||
|
const promise = asyncFn(match, ...args);
|
||||||
|
promises.push(promise);
|
||||||
|
});
|
||||||
|
const data = await Promise.all(promises);
|
||||||
|
return str.replace(regex, () => data.shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function minimizeAsync(match, g1, g2) {
|
||||||
|
console.log(`Found match '${match}'`);
|
||||||
|
|
||||||
|
if (g1.endsWith(IGNORE_ALT_NAME_END)) {
|
||||||
|
console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
let probeAspectRatio = 0;
|
||||||
|
let shouldModify = false;
|
||||||
|
try {
|
||||||
|
console.log(`Probing ${g2}`);
|
||||||
|
let probeResult = await probe(g2);
|
||||||
|
if (probeResult == null) {
|
||||||
|
throw 'No probeResult';
|
||||||
|
}
|
||||||
|
if (probeResult.hUnits != 'px') {
|
||||||
|
throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`;
|
||||||
|
}
|
||||||
|
if (probeResult.height <= 0) {
|
||||||
|
throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`;
|
||||||
|
}
|
||||||
|
if (probeResult.wUnits != 'px') {
|
||||||
|
throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`;
|
||||||
|
}
|
||||||
|
if (probeResult.width <= 0) {
|
||||||
|
throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`;
|
||||||
|
}
|
||||||
|
console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`);
|
||||||
|
|
||||||
|
probeAspectRatio = probeResult.width / probeResult.height;
|
||||||
|
shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && probeAspectRatio < MIN_ASPECT_RATIO;
|
||||||
|
} catch(e) {
|
||||||
|
console.log('Probing failed:', e);
|
||||||
|
// Immediately abort
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldModify) {
|
||||||
|
wasMatchModified = true;
|
||||||
|
console.log(`Modifying match '${match}'`);
|
||||||
|
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`);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
35
.github/workflows/image-minimizer.yml
vendored
Normal file
35
.github/workflows/image-minimizer.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
name: Image Minimizer
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
issues:
|
||||||
|
types: [opened, edited]
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
try-minimize:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: Install probe-image-size
|
||||||
|
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||||
|
|
||||||
|
- name: Minimize simple images
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
timeout-minutes: 3
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const script = require('.github/workflows/image-minimizer.js');
|
||||||
|
await script({github, context});
|
24
.github/workflows/no-response.yml
vendored
Normal file
24
.github/workflows/no-response.yml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
name: No Response
|
||||||
|
|
||||||
|
# Both `issue_comment` and `scheduled` event types are required for this Action
|
||||||
|
# to work properly.
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
schedule:
|
||||||
|
# Run daily at midnight.
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
noResponse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: lee-dohm/no-response@v0.5.0
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
daysUntilClose: 14
|
||||||
|
responseRequiredLabel: waiting for author
|
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
|
16
.gitignore
vendored
16
.gitignore
vendored
|
@ -1,15 +1,15 @@
|
||||||
.gitignore
|
.gradle/
|
||||||
.gradle
|
local.properties
|
||||||
/local.properties
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
build/
|
||||||
/captures
|
captures/
|
||||||
/app/app.iml
|
.idea/
|
||||||
/.idea
|
*.iml
|
||||||
/*.iml
|
|
||||||
*~
|
*~
|
||||||
.weblate
|
.weblate
|
||||||
*.class
|
*.class
|
||||||
|
app/debug/
|
||||||
|
app/release/
|
||||||
|
|
||||||
# vscode / eclipse files
|
# vscode / eclipse files
|
||||||
*.classpath
|
*.classpath
|
||||||
|
|
0
.gitmodules
vendored
0
.gitmodules
vendored
18
.travis.yml
18
.travis.yml
|
@ -1,18 +0,0 @@
|
||||||
language: android
|
|
||||||
jdk:
|
|
||||||
- oraclejdk8
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
# The BuildTools version used by NewPipe
|
|
||||||
- tools
|
|
||||||
- build-tools-29.0.3
|
|
||||||
|
|
||||||
# The SDK version used to compile NewPipe
|
|
||||||
- android-29
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- yes | sdkmanager "platforms;android-29"
|
|
||||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
|
||||||
|
|
||||||
licenses:
|
|
||||||
- '.+'
|
|
8
LICENSE
8
LICENSE
|
@ -1,7 +1,7 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|
194
README.md
194
README.md
|
@ -1,139 +1,139 @@
|
||||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
<h3 align="center">We are planning to <i>rewrite</i> large chunks of the codebase, to bring about <a href="https://github.com/TeamNewPipe/NewPipe/discussions/10118">a new, modern and stable NewPipe</a>!</h3>
|
||||||
|
<h4 align="center">Please do <b>not</b> open pull requests for <i>new features</i> now, only bugfix PRs will be accepted.</h4>
|
||||||
|
|
||||||
|
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||||
<h2 align="center"><b>NewPipe</b></h2>
|
<h2 align="center"><b>NewPipe</b></h2>
|
||||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
<h4 align="center">A libre lightweight streaming front-end for Android.</h4>
|
||||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
|
||||||
|
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||||
|
|
||||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
> [!warning]
|
||||||
|
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||||
|
>
|
||||||
|
> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/00.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/00.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/01.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/02.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/03.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/04.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/05.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/06.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/07.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/08.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
<br/><br/>
|
||||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/09.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/09.png)
|
||||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/10.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/10.png)
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
NewPipe does not use any Google framework libraries, nor the YouTube API. Websites are only parsed to fetch required info, so this app can be used on devices without Google services installed. Also, you don't need a YouTube account to use NewPipe, which is copylefted libre software.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Search videos
|
|
||||||
* Display general info about videos
|
|
||||||
* Watch YouTube videos
|
|
||||||
* Listen to YouTube videos
|
|
||||||
* Popup mode (floating player)
|
|
||||||
* Select streaming player to watch video with
|
|
||||||
* Download videos
|
|
||||||
* Download audio only
|
|
||||||
* Open a video in Kodi
|
|
||||||
* Show next/related videos
|
|
||||||
* Search YouTube in a specific language
|
|
||||||
* Watch/Block age restricted material
|
|
||||||
* Display general info about channels
|
|
||||||
* Search channels
|
|
||||||
* Watch videos from a channel
|
|
||||||
* Orbot/Tor support (not yet directly)
|
|
||||||
* 1080p/2K/4K support
|
|
||||||
* View history
|
|
||||||
* Subscribe to channels
|
|
||||||
* Search history
|
|
||||||
* Search/watch playlists
|
|
||||||
* Watch as enqueued playlists
|
|
||||||
* Enqueue videos
|
|
||||||
* Local playlists
|
|
||||||
* Subtitles
|
|
||||||
* Livestream support
|
|
||||||
* Show comments
|
|
||||||
|
|
||||||
### Coming Features
|
|
||||||
|
|
||||||
* Cast to UPnP and Cast
|
|
||||||
* … and many more
|
|
||||||
|
|
||||||
### Supported Services
|
### Supported Services
|
||||||
|
|
||||||
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
|
NewPipe currently supports these services:
|
||||||
|
|
||||||
* YouTube
|
<!-- We link to the service websites separately to avoid people accidentally opening a website they didn't want to. -->
|
||||||
* SoundCloud \[beta\]
|
* YouTube ([website](https://www.youtube.com/)) and YouTube Music ([website](https://music.youtube.com/)) ([wiki](https://en.wikipedia.org/wiki/YouTube))
|
||||||
* media.ccc.de \[beta\]
|
* PeerTube ([website](https://joinpeertube.org/)) and all its instances (open the website to know what that means!) ([wiki](https://en.wikipedia.org/wiki/PeerTube))
|
||||||
* PeerTube instances \[beta\]
|
* Bandcamp ([website](https://bandcamp.com/)) ([wiki](https://en.wikipedia.org/wiki/Bandcamp))
|
||||||
|
* SoundCloud ([website](https://soundcloud.com/)) ([wiki](https://en.wikipedia.org/wiki/SoundCloud))
|
||||||
|
* media.ccc.de ([website](https://media.ccc.de/)) ([wiki](https://en.wikipedia.org/wiki/Chaos_Computer_Club))
|
||||||
|
|
||||||
## Updates
|
As you can see, NewPipe supports multiple video and audio services. Though it started off with YouTube, other people have added more services over the years, making NewPipe more and more versatile!
|
||||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
|
||||||
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
|
||||||
* Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
|
||||||
* Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
|
||||||
|
|
||||||
When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid.
|
Partially due to circumstance, and partially due to its popularity, YouTube is the best supported out of these services. If you use or are familiar with any of these other services, please help us improve support for them! We're looking for maintainers for SoundCloud and PeerTube.
|
||||||
|
|
||||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
If you intend to add a new service, please get in touch with us first! Our [docs](https://teamnewpipe.github.io/documentation/) provide more information on how a new service can be added to the app and to the [NewPipe Extractor](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
NewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.
|
||||||
|
|
||||||
|
Also, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Watch videos at resolutions up to 4K
|
||||||
|
* Listen to audio in the background, only loading the audio stream to save data
|
||||||
|
* Popup mode (floating player, aka Picture-in-Picture)
|
||||||
|
* Watch live streams
|
||||||
|
* Show/hide subtitles/closed captions
|
||||||
|
* Search videos and audios (on YouTube, you can specify the content language as well)
|
||||||
|
* Enqueue videos (and optionally save them as local playlists)
|
||||||
|
* Show/hide general information about videos (such as description and tags)
|
||||||
|
* Show/hide next/related videos
|
||||||
|
* Show/hide comments
|
||||||
|
* Search videos, audios, channels, playlists and albums
|
||||||
|
* Browse videos and audios within a channel
|
||||||
|
* Subscribe to channels (yes, without logging into any account!)
|
||||||
|
* Get notifications about new videos from channels you're subscribed to
|
||||||
|
* Create and edit channel groups (for easier browsing and management)
|
||||||
|
* Browse video feeds generated from your channel groups
|
||||||
|
* View and search your watch history
|
||||||
|
* Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing)
|
||||||
|
* Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service)
|
||||||
|
* Download videos/audios/subtitles (closed captions)
|
||||||
|
* Open in Kodi
|
||||||
|
* Watch/Block age-restricted material
|
||||||
|
|
||||||
|
<!-- Hidden span to keep old links compatible. You should remove this span if you're translating the README into another language.-->
|
||||||
|
<span id="updates"></span>
|
||||||
|
|
||||||
|
## Installation and updates
|
||||||
|
You can install NewPipe using one of the following methods:
|
||||||
|
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||||
|
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||||
|
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
|
||||||
|
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||||
|
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
|
||||||
|
|
||||||
|
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
||||||
|
|
||||||
|
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
||||||
|
1. Back up your data via Settings > Backup and Restore > Export Database so you keep your history, subscriptions, and playlists
|
||||||
2. Uninstall NewPipe
|
2. Uninstall NewPipe
|
||||||
3. Download the APK from the new source and install it
|
3. Download the APK from the new source and install it
|
||||||
4. Import the data from step 1 via "Settings>Content>Import Database"
|
4. Import the data from step 1 via Settings > Backup and Restore > Import Database
|
||||||
|
|
||||||
|
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||||
The more is done the better it gets!
|
|
||||||
|
|
||||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
|
If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as it is both open-source and non-profit. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
|
||||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
|
||||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
|
||||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Privacy Policy
|
## Privacy Policy
|
||||||
|
|
||||||
The NewPipe project aims to provide a private, anonymous experience for using media web services.
|
The NewPipe project aims to provide a private, anonymous experience for using web-based media services. Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or leave a comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
|
||||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.schabi.org/legal/privacy/).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
NewPipe is Free Software: You can use, study share and improve it at your
|
NewPipe is Free Software: You can use, study, share, and improve it at will. Specifically you can redistribute and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
|
||||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
|
||||||
published by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
3
app/.gitignore
vendored
3
app/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
.gitignore
|
|
||||||
/build
|
|
||||||
*.iml
|
|
300
app/build.gradle
300
app/build.gradle
|
@ -1,23 +1,29 @@
|
||||||
apply plugin: 'com.android.application'
|
import com.android.tools.profgen.ArtProfileKt
|
||||||
apply plugin: 'kotlin-android'
|
import com.android.tools.profgen.ArtProfileSerializer
|
||||||
apply plugin: 'kotlin-android-extensions'
|
import com.android.tools.profgen.DexFile
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'checkstyle'
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "kotlin-kapt"
|
||||||
|
id "kotlin-parcelize"
|
||||||
|
id "checkstyle"
|
||||||
|
id "org.sonarqube" version "4.0.0.2929"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdk 34
|
||||||
buildToolsVersion '29.0.3'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.schabi.newpipe"
|
applicationId "org.schabi.newpipe"
|
||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdkVersion 19
|
minSdk 21
|
||||||
targetSdkVersion 29
|
targetSdk 33
|
||||||
versionCode 951
|
versionCode 997
|
||||||
versionName "0.19.6"
|
versionName "0.27.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
annotationProcessorOptions {
|
annotationProcessorOptions {
|
||||||
|
@ -28,12 +34,11 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
multiDexEnabled true
|
|
||||||
debuggable true
|
debuggable true
|
||||||
|
|
||||||
// suffix the app id and the app name with git branch name
|
// suffix the app id and the app name with git branch name
|
||||||
def workingBranch = getGitWorkingBranch()
|
def workingBranch = getGitWorkingBranch()
|
||||||
def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase()
|
def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "")
|
||||||
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
|
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
|
||||||
// default values when branch name could not be determined or is master or dev
|
// default values when branch name could not be determined or is master or dev
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
|
@ -45,10 +50,12 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the release build type at the end of the list to override 'archivesBaseName' of
|
|
||||||
// debug build. This seems to be a Gradle bug, therefore
|
|
||||||
// TODO: update Gradle version
|
|
||||||
release {
|
release {
|
||||||
|
if (System.properties.containsKey('packageSuffix')) {
|
||||||
|
applicationIdSuffix System.getProperty('packageSuffix')
|
||||||
|
resValue "string", "app_name", "NewPipe " + System.getProperty('packageSuffix')
|
||||||
|
archivesBaseName = 'NewPipe_' + System.getProperty('packageSuffix')
|
||||||
|
}
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources false // disabled to fix F-Droid's reproducible build
|
shrinkResources false // disabled to fix F-Droid's reproducible build
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
@ -56,39 +63,63 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lint {
|
||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||||
// but continue the build even when errors are found:
|
// but continue the build even when errors are found:
|
||||||
abortOnError false
|
abortOnError false
|
||||||
|
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
|
||||||
|
// 5.0, avoid using them in switch case statements"), which affects only library projects
|
||||||
|
disable 'NonConstantResourceId'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
// Flag to enable support for the new language APIs
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
coreLibraryDesugaringEnabled true
|
||||||
|
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
encoding 'utf-8'
|
encoding 'utf-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required and used only by groupie
|
kotlinOptions {
|
||||||
androidExtensions {
|
jvmTarget = JavaVersion.VERSION_17
|
||||||
experimental = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
// remove two files which belong to jsoup
|
||||||
|
// no idea how they ended up in the META-INF dir...
|
||||||
|
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
||||||
|
// 'COPYRIGHT' belongs to RxJava...
|
||||||
|
'META-INF/COPYRIGHT']
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
checkstyleVersion = '10.12.1'
|
||||||
|
|
||||||
|
androidxLifecycleVersion = '2.6.2'
|
||||||
|
androidxRoomVersion = '2.6.1'
|
||||||
|
androidxWorkVersion = '2.8.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
checkstyleVersion = '8.32'
|
exoPlayerVersion = '2.18.7'
|
||||||
stethoVersion = '1.5.1'
|
googleAutoServiceVersion = '1.1.1'
|
||||||
leakCanaryVersion = '2.2'
|
groupieVersion = '2.10.1'
|
||||||
exoPlayerVersion = '2.11.6'
|
markwonVersion = '4.6.2'
|
||||||
androidxLifecycleVersion = '2.2.0'
|
|
||||||
androidxRoomVersion = '2.2.5'
|
leakCanaryVersion = '2.12'
|
||||||
groupieVersion = '2.8.0'
|
stethoVersion = '1.6.0'
|
||||||
markwonVersion = '4.3.1'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
@ -97,13 +128,13 @@ configurations {
|
||||||
}
|
}
|
||||||
|
|
||||||
checkstyle {
|
checkstyle {
|
||||||
configFile rootProject.file('checkstyle.xml')
|
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||||
ignoreFailures false
|
ignoreFailures false
|
||||||
showViolations true
|
showViolations true
|
||||||
toolVersion = checkstyleVersion
|
toolVersion = checkstyleVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
task runCheckstyle(type: Checkstyle) {
|
tasks.register('runCheckstyle', Checkstyle) {
|
||||||
source 'src'
|
source 'src'
|
||||||
include '**/*.java'
|
include '**/*.java'
|
||||||
exclude '**/gen/**'
|
exclude '**/gen/**'
|
||||||
|
@ -116,97 +147,160 @@ task runCheckstyle(type: Checkstyle) {
|
||||||
showViolations true
|
showViolations true
|
||||||
|
|
||||||
reports {
|
reports {
|
||||||
xml.enabled true
|
xml.getRequired().set(true)
|
||||||
html.enabled true
|
html.getRequired().set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task runKtlint(type: JavaExec) {
|
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||||
main = "com.pinterest.ktlint.Main"
|
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||||
|
|
||||||
|
tasks.register('runKtlint', JavaExec) {
|
||||||
|
inputs.files(inputFiles)
|
||||||
|
outputs.dir(outputDir)
|
||||||
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
classpath = configurations.ktlint
|
classpath = configurations.ktlint
|
||||||
args "src/**/*.kt"
|
args "src/**/*.kt"
|
||||||
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
task formatKtlint(type: JavaExec) {
|
tasks.register('formatKtlint', JavaExec) {
|
||||||
main = "com.pinterest.ktlint.Main"
|
inputs.files(inputFiles)
|
||||||
|
outputs.dir(outputDir)
|
||||||
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
classpath = configurations.ktlint
|
classpath = configurations.ktlint
|
||||||
args "-F", "src/**/*.kt"
|
args "-F", "src/**/*.kt"
|
||||||
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
|
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||||
|
preDebugBuild.dependsOn formatKtlint
|
||||||
|
}
|
||||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
sonar {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
properties {
|
||||||
|
property "sonar.projectKey", "TeamNewPipe_NewPipe"
|
||||||
|
property "sonar.organization", "teamnewpipe"
|
||||||
|
property "sonar.host.url", "https://sonarcloud.io"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
/** Desugaring **/
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
||||||
|
|
||||||
|
/** NewPipe libraries **/
|
||||||
|
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||||
|
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
|
||||||
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.0'
|
||||||
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
|
/** Checkstyle **/
|
||||||
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
|
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||||
|
|
||||||
|
/** Kotlin **/
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||||
|
|
||||||
|
/** AndroidX **/
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
|
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||||
|
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||||
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
|
||||||
|
/** Third-party libraries **/
|
||||||
|
// Instance state boilerplate elimination
|
||||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||||
|
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
// HTML parser
|
||||||
ktlint "com.pinterest:ktlint:0.35.0"
|
implementation "org.jsoup:jsoup:1.17.2"
|
||||||
|
|
||||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
// HTTP client
|
||||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
|
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
// Media player
|
||||||
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
||||||
debugImplementation "androidx.multidex:multidex:2.0.1"
|
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
||||||
testImplementation 'junit:junit:4.13'
|
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
||||||
testImplementation 'org.mockito:mockito-core:3.3.3'
|
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.1"
|
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", {
|
|
||||||
exclude module: 'support-annotations'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:a70cb0283ffc3bba2709815673a5a7940aab0a3a'
|
|
||||||
|
|
||||||
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
|
||||||
implementation "org.jsoup:jsoup:1.13.1"
|
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:3.12.11"
|
|
||||||
|
|
||||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.1.0"
|
// Metadata generator for service descriptors
|
||||||
|
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||||
|
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
// Manager for complex RecyclerView layouts
|
||||||
implementation "androidx.preference:preference:1.1.1"
|
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
|
||||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
|
||||||
|
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
// Image loading
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:${androidxLifecycleVersion}"
|
implementation "com.squareup.picasso:picasso:2.8"
|
||||||
|
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
|
||||||
implementation "androidx.room:room-rxjava2:${androidxRoomVersion}"
|
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
|
||||||
|
|
||||||
implementation "com.xwray:groupie:${groupieVersion}"
|
|
||||||
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
|
||||||
|
|
||||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
|
||||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
|
||||||
|
|
||||||
|
// Markdown library for Android
|
||||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
// Crash reporting
|
||||||
|
implementation "ch.acra:acra-core:5.11.3"
|
||||||
|
|
||||||
implementation "ch.acra:acra-core:5.5.0"
|
// Properly restarting
|
||||||
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
||||||
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||||
|
// RxJava binding APIs for Android UI widgets
|
||||||
|
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||||
|
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.5.Final"
|
// Date and time formatting
|
||||||
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
||||||
|
|
||||||
|
/** Debugging **/
|
||||||
|
// Memory leak detection
|
||||||
|
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||||
|
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||||
|
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
|
||||||
|
// Debug bridge for Android
|
||||||
|
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||||
|
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||||
|
|
||||||
|
/** Testing **/
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||||
|
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
|
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||||
|
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||||
|
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
@ -224,3 +318,25 @@ static String getGitWorkingBranch() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fix reproducible builds
|
||||||
|
project.afterEvaluate {
|
||||||
|
tasks.compileReleaseArtProfile.doLast {
|
||||||
|
outputs.files.each { file ->
|
||||||
|
if (file.toString().endsWith(".profm")) {
|
||||||
|
println("Sorting ${file} ...")
|
||||||
|
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
|
||||||
|
def profile = ArtProfileKt.ArtProfile(file)
|
||||||
|
def keys = new ArrayList(profile.profileData.keySet())
|
||||||
|
def sortedData = new LinkedHashMap()
|
||||||
|
Collections.sort keys, new DexFile.Companion()
|
||||||
|
keys.each { key -> sortedData[key] = profile.profileData[key] }
|
||||||
|
new FileOutputStream(file).with {
|
||||||
|
write(version.magicBytes$profgen)
|
||||||
|
write(version.versionBytes$profgen)
|
||||||
|
version.write$profgen(it, sortedData, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
app/proguard-rules.pro
vendored
44
app/proguard-rules.pro
vendored
|
@ -1,36 +1,18 @@
|
||||||
# Add project specific ProGuard rules here.
|
# https://developer.android.com/build/shrink-code
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /home/the-scrabi/bin/Android/Sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
|
## Helps debug release versions
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
|
## Rules for NewPipeExtractor
|
||||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
-keep class org.ocpsoft.prettytime.i18n.** { *; }
|
|
||||||
|
|
||||||
-keep class org.mozilla.javascript.** { *; }
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
|
||||||
-keep class org.mozilla.classfile.ClassFileWriter
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
|
||||||
|
## Rules for ExoPlayer
|
||||||
-keep class com.google.android.exoplayer2.** { *; }
|
-keep class com.google.android.exoplayer2.** { *; }
|
||||||
|
|
||||||
-dontwarn org.mozilla.javascript.tools.**
|
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
||||||
-dontwarn android.arch.util.paging.CountedDataSource
|
|
||||||
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
|
|
||||||
|
|
||||||
|
|
||||||
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
|
|
||||||
-dontwarn icepick.**
|
-dontwarn icepick.**
|
||||||
-keep class icepick.** { *; }
|
-keep class icepick.** { *; }
|
||||||
-keep class **$$Icepick { *; }
|
-keep class **$$Icepick { *; }
|
||||||
|
@ -39,15 +21,17 @@
|
||||||
}
|
}
|
||||||
-keepnames class * { @icepick.State *;}
|
-keepnames class * { @icepick.State *;}
|
||||||
|
|
||||||
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
-dontwarn javax.annotation.**
|
|
||||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
## See https://github.com/TeamNewPipe/NewPipe/pull/1441
|
||||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
|
||||||
-keepclassmembers class * implements java.io.Serializable {
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
static final long serialVersionUID;
|
static final long serialVersionUID;
|
||||||
!static !transient <fields>;
|
!static !transient <fields>;
|
||||||
private void writeObject(java.io.ObjectOutputStream);
|
private void writeObject(java.io.ObjectOutputStream);
|
||||||
private void readObject(java.io.ObjectInputStream);
|
private void readObject(java.io.ObjectInputStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
||||||
|
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
||||||
|
|
19
app/sampledata/channels.json
Normal file
19
app/sampledata/channels.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "BBC",
|
||||||
|
"additional": "12K subscribers•233 videos",
|
||||||
|
"description": "The BBC is the world’s leading public service broadcaster. We’re impartial and independent, and every day we create distinctive, world-class programmes and content which inform, educate and entertain millions of people in the UK and around the world. SUBSCRIBE to our YouTube channel to get the best of BBC entertainment and comedy programmes, stories from science and nature documentaries, and much more! https://bit.ly/2IXqEIn Get ALL your fresh TV, and sofa-hugging box sets on iPlayer https://bbc.in/2J18jYJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Linus Tech Tips",
|
||||||
|
"additional": "1M subscribers•233 videos",
|
||||||
|
"description": "Looking for a Tech YouTuber?\n\nLinus Tech Tips is a passionate team of \"professionally curious\" experts in consumer technology and video production which aims to inform and educate people of all ages through our entertaining videos. We create product reviews, step-by-step computer build guides, and a variety of other tech-focused content.\n\nSchedule:\nNew videos every Saturday to Thursday @ 10:00am Pacific\nLive WAN Show podcasts every Friday @ ~5:00pm Pacific"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Marques Brownlee",
|
||||||
|
"additional": "13 subscribers•12K videos",
|
||||||
|
"description": "MKBHD: Quality Tech Videos | YouTuber | Geek | Consumer Electronics | Tech Head | Internet Personality!\n\nbusiness@MKBHD.com\n\nNYC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
|
@ -0,0 +1,713 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "d8070091972a7011bce18aed62f80b90",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd8070091972a7011bce18aed62f80b90')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
719
app/schemas/org.schabi.newpipe.database.AppDatabase/5.json
Normal file
719
app/schemas/org.schabi.newpipe.database.AppDatabase/5.json
Normal file
|
@ -0,0 +1,719 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "096731b513bb71dd44517639f4a2c1e3",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '096731b513bb71dd44517639f4a2c1e3')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
737
app/schemas/org.schabi.newpipe.database.AppDatabase/6.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/6.json
Normal file
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 6,
|
||||||
|
"identityHash": "4084aa342aef315dc7b558770a7755a9",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4084aa342aef315dc7b558770a7755a9')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
737
app/schemas/org.schabi.newpipe.database.AppDatabase/7.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/7.json
Normal file
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 7,
|
||||||
|
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
|
@ -0,0 +1,730 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 9,
|
||||||
|
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,119 +0,0 @@
|
||||||
package org.schabi.newpipe.database
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
||||||
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.assertNull
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class AppDatabaseTest {
|
|
||||||
companion object {
|
|
||||||
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 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_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
|
||||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateDatabaseFrom2to3() {
|
|
||||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
|
||||||
|
|
||||||
databaseInV2.run {
|
|
||||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
|
||||||
// put("uid", null)
|
|
||||||
put("service_id", DEFAULT_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_URL)
|
|
||||||
put("title", DEFAULT_TITLE)
|
|
||||||
put("stream_type", DEFAULT_TYPE.name)
|
|
||||||
put("duration", DEFAULT_DURATION)
|
|
||||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
|
||||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
|
||||||
})
|
|
||||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
|
||||||
// put("uid", null)
|
|
||||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_SECOND_URL)
|
|
||||||
// put("title", null)
|
|
||||||
// put("stream_type", null)
|
|
||||||
// put("duration", null)
|
|
||||||
// put("uploader", null)
|
|
||||||
// put("thumbnail_url", null)
|
|
||||||
})
|
|
||||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
|
||||||
// put("uid", null)
|
|
||||||
put("service_id", DEFAULT_SERVICE_ID)
|
|
||||||
// put("url", null)
|
|
||||||
// put("title", null)
|
|
||||||
// put("stream_type", null)
|
|
||||||
// put("duration", null)
|
|
||||||
// put("uploader", null)
|
|
||||||
// put("thumbnail_url", null)
|
|
||||||
})
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
|
||||||
true, Migrations.MIGRATION_2_3)
|
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
|
||||||
|
|
||||||
// Only expect 2, the one with the null url will be ignored
|
|
||||||
assertEquals(2, listFromDB.size)
|
|
||||||
|
|
||||||
val streamFromMigratedDatabase = listFromDB[0]
|
|
||||||
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
|
||||||
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
|
||||||
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
|
||||||
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
|
||||||
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
|
||||||
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
|
||||||
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
|
||||||
assertNull(streamFromMigratedDatabase.viewCount)
|
|
||||||
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
|
||||||
assertNull(streamFromMigratedDatabase.uploadDate)
|
|
||||||
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
|
||||||
|
|
||||||
val secondStreamFromMigratedDatabase = listFromDB[1]
|
|
||||||
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
|
||||||
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
|
||||||
assertEquals("", secondStreamFromMigratedDatabase.title)
|
|
||||||
// Should fallback to VIDEO_STREAM
|
|
||||||
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
|
||||||
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
|
||||||
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
|
||||||
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
|
||||||
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
|
|
||||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
|
|
||||||
.build()
|
|
||||||
testHelper.closeWhenFinished(database)
|
|
||||||
return database
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,326 @@
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.testing.MigrationTestHelper
|
||||||
|
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)
|
||||||
|
class DatabaseMigrationTest {
|
||||||
|
companion object {
|
||||||
|
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 = 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
|
||||||
|
val testHelper = MigrationTestHelper(
|
||||||
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
|
AppDatabase::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom2to3() {
|
||||||
|
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||||
|
|
||||||
|
databaseInV2.run {
|
||||||
|
insert(
|
||||||
|
"streams",
|
||||||
|
SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_URL)
|
||||||
|
put("title", DEFAULT_TITLE)
|
||||||
|
put("stream_type", DEFAULT_TYPE.name)
|
||||||
|
put("duration", DEFAULT_DURATION)
|
||||||
|
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||||
|
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"streams",
|
||||||
|
SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_SECOND_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"streams",
|
||||||
|
SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_3,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_2_3
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_4,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_3_4
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_5,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_4_5
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_6,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_5_6
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_7,
|
||||||
|
true,
|
||||||
|
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()
|
||||||
|
|
||||||
|
// Only expect 2, the one with the null url will be ignored
|
||||||
|
assertEquals(2, listFromDB.size)
|
||||||
|
|
||||||
|
val streamFromMigratedDatabase = listFromDB[0]
|
||||||
|
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||||
|
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||||
|
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||||
|
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
||||||
|
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
||||||
|
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
||||||
|
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
||||||
|
assertNull(streamFromMigratedDatabase.viewCount)
|
||||||
|
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||||
|
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||||
|
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
|
|
||||||
|
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||||
|
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||||
|
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||||
|
// Should fallback to VIDEO_STREAM
|
||||||
|
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||||
|
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||||
|
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(),
|
||||||
|
AppDatabase::class.java,
|
||||||
|
AppDatabase.DATABASE_NAME
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
testHelper.closeWhenFinished(database)
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.schabi.newpipe.error;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented tests for {@link ErrorInfo}.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
@LargeTest
|
||||||
|
public class ErrorInfoTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void errorInfoTestParcelable() {
|
||||||
|
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
|
||||||
|
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
|
||||||
|
// Obtain a Parcel object and write the parcelable object to it:
|
||||||
|
final Parcel parcel = Parcel.obtain();
|
||||||
|
info.writeToParcel(parcel, 0);
|
||||||
|
parcel.setDataPosition(0);
|
||||||
|
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||||
|
|
||||||
|
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
|
||||||
|
.contains(ErrorInfoTest.class.getSimpleName()));
|
||||||
|
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||||
|
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
|
||||||
|
infoFromParcel.getServiceName());
|
||||||
|
assertEquals("request", infoFromParcel.getRequest());
|
||||||
|
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
|
||||||
|
|
||||||
|
parcel.recycle();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package org.schabi.newpipe.local.history
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||||
|
import org.schabi.newpipe.testUtil.TestDatabase
|
||||||
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
class HistoryRecordManagerTest {
|
||||||
|
|
||||||
|
private lateinit var manager: HistoryRecordManager
|
||||||
|
private lateinit var database: AppDatabase
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val trampolineScheduler = TrampolineSchedulerRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||||
|
manager = HistoryRecordManager(ApplicationProvider.getApplicationContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSearched() {
|
||||||
|
manager.onSearched(0, "Hello").test().await().assertValue(1)
|
||||||
|
|
||||||
|
// For some reason the Flowable returned by getAll() never completes, so we can't assert
|
||||||
|
// that the number of Lists it returns is exactly 1, we can only check if the first List is
|
||||||
|
// correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
|
||||||
|
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||||
|
assertThat(entities).hasSize(1)
|
||||||
|
assertThat(entities[0].id).isEqualTo(1)
|
||||||
|
assertThat(entities[0].serviceId).isEqualTo(0)
|
||||||
|
assertThat(entities[0].search).isEqualTo("Hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteSearchHistory() {
|
||||||
|
val entries = listOf(
|
||||||
|
SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// make sure all 4 were inserted
|
||||||
|
database.searchHistoryDAO().insertAll(entries)
|
||||||
|
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||||
|
|
||||||
|
// try to delete only "A" entries, "B" entries should be untouched
|
||||||
|
manager.deleteSearchHistory("A").test().await().assertValue(2)
|
||||||
|
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||||
|
assertThat(entities).hasSize(2)
|
||||||
|
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||||
|
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||||
|
|
||||||
|
// assert that nothing happens if we delete a search query that does exist in the db
|
||||||
|
manager.deleteSearchHistory("A").test().await().assertValue(0)
|
||||||
|
val entities2 = database.searchHistoryDAO().all.blockingFirst()
|
||||||
|
assertThat(entities2).hasSize(2)
|
||||||
|
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||||
|
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||||
|
|
||||||
|
// delete all remaining entries
|
||||||
|
manager.deleteSearchHistory("B").test().await().assertValue(2)
|
||||||
|
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteCompleteSearchHistory() {
|
||||||
|
val entries = listOf(
|
||||||
|
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// make sure all 3 were inserted
|
||||||
|
database.searchHistoryDAO().insertAll(entries)
|
||||||
|
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||||
|
|
||||||
|
// should remove everything
|
||||||
|
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
||||||
|
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
||||||
|
|
||||||
|
// shuffle to make sure the order of items returned by queries depends only on
|
||||||
|
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
|
||||||
|
// verify that the `ORDER BY` clause does its job
|
||||||
|
database.searchHistoryDAO().insertAll(relatedSearches.shuffled())
|
||||||
|
|
||||||
|
// make sure all entries were inserted
|
||||||
|
assertEquals(
|
||||||
|
relatedSearches.size,
|
||||||
|
database.searchHistoryDAO().all.blockingFirst().size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRelatedSearches_emptyQuery() {
|
||||||
|
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||||
|
|
||||||
|
// make sure correct number of searches is returned and in correct order
|
||||||
|
val searches = manager.getRelatedSearches("", 6, 4).blockingFirst()
|
||||||
|
assertThat(searches).containsExactly(
|
||||||
|
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||||
|
RELATED_SEARCHES_ENTRIES[4].search, // B
|
||||||
|
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||||
|
RELATED_SEARCHES_ENTRIES[2].search, // BA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
||||||
|
insertShuffledRelatedSearches(
|
||||||
|
listOf(
|
||||||
|
SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
|
||||||
|
assertThat(searches).containsExactly("AA", "A", "BA")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRelatedSearched_nonEmptyQuery() {
|
||||||
|
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||||
|
|
||||||
|
// make sure correct number of searches is returned and in correct order
|
||||||
|
val searches = manager.getRelatedSearches("A", 3, 5).blockingFirst()
|
||||||
|
assertThat(searches).containsExactly(
|
||||||
|
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||||
|
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||||
|
RELATED_SEARCHES_ENTRIES[1].search, // BA
|
||||||
|
)
|
||||||
|
|
||||||
|
// also make sure that the string comparison is case insensitive
|
||||||
|
val searches2 = manager.getRelatedSearches("a", 3, 5).blockingFirst()
|
||||||
|
assertThat(searches).isEqualTo(searches2)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
|
||||||
|
|
||||||
|
private val RELATED_SEARCHES_ENTRIES = listOf(
|
||||||
|
SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
|
||||||
|
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package org.schabi.newpipe.local.playlist
|
||||||
|
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.testUtil.TestDatabase
|
||||||
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||||
|
|
||||||
|
class LocalPlaylistManagerTest {
|
||||||
|
|
||||||
|
private lateinit var manager: LocalPlaylistManager
|
||||||
|
private lateinit var database: AppDatabase
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val trampolineScheduler = TrampolineSchedulerRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||||
|
manager = LocalPlaylistManager(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createPlaylist() {
|
||||||
|
val NEWPIPE_URL = "https://newpipe.net/"
|
||||||
|
val stream = StreamEntity(
|
||||||
|
serviceId = 1, url = NEWPIPE_URL, title = "title",
|
||||||
|
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||||
|
uploaderUrl = NEWPIPE_URL
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = manager.createPlaylist("name", listOf(stream))
|
||||||
|
|
||||||
|
// This should not behave like this.
|
||||||
|
// Currently list of all stream ids is returned instead of playlist id
|
||||||
|
result.test().await().assertValue(listOf(1L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createPlaylist_emptyPlaylistMustReturnEmpty() {
|
||||||
|
val result = manager.createPlaylist("name", emptyList())
|
||||||
|
|
||||||
|
// This should not behave like this.
|
||||||
|
// It should throw an error because currently the result is null
|
||||||
|
result.test().await().assertComplete()
|
||||||
|
manager.playlists.test().awaitCount(1).assertValue(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||||
|
val stream = StreamEntity(
|
||||||
|
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||||
|
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
|
)
|
||||||
|
database.streamDAO().insert(stream)
|
||||||
|
val upserted = StreamEntity(
|
||||||
|
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
||||||
|
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||||
|
|
||||||
|
result.test().await().assertComplete()
|
||||||
|
database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package org.schabi.newpipe.local.subscription;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
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.subscription.SubscriptionEntity;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.testUtil.TestDatabase;
|
||||||
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SubscriptionManagerTest {
|
||||||
|
private AppDatabase database;
|
||||||
|
private SubscriptionManager manager;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TrampolineSchedulerRule trampolineScheduler = new TrampolineSchedulerRule();
|
||||||
|
|
||||||
|
|
||||||
|
private SubscriptionEntity getAssertOneSubscriptionEntity() {
|
||||||
|
final List<SubscriptionEntity> entities = manager
|
||||||
|
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
|
||||||
|
.blockingFirst();
|
||||||
|
assertEquals(1, entities.size());
|
||||||
|
return entities.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
database = TestDatabase.Companion.createReplacingNewPipeDatabase();
|
||||||
|
manager = new SubscriptionManager(ApplicationProvider.getApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void cleanUp() {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInsert() throws ExtractionException, IOException {
|
||||||
|
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
||||||
|
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||||
|
|
||||||
|
manager.insertSubscription(subscription);
|
||||||
|
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
||||||
|
|
||||||
|
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
||||||
|
assertEquals(subscription.getServiceId(), readSubscription.getServiceId());
|
||||||
|
assertEquals(subscription.getUrl(), readSubscription.getUrl());
|
||||||
|
assertEquals(subscription.getName(), readSubscription.getName());
|
||||||
|
assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl());
|
||||||
|
assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount());
|
||||||
|
assertEquals(subscription.getDescription(), readSubscription.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateNotificationMode() throws ExtractionException, IOException {
|
||||||
|
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium");
|
||||||
|
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||||
|
subscription.setNotificationMode(0);
|
||||||
|
|
||||||
|
manager.insertSubscription(subscription);
|
||||||
|
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
||||||
|
.blockingAwait();
|
||||||
|
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
||||||
|
|
||||||
|
assertEquals(0, subscription.getNotificationMode());
|
||||||
|
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
||||||
|
assertEquals(1, anotherSubscription.getNotificationMode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
package org.schabi.newpipe.report;
|
|
||||||
|
|
||||||
import android.os.Parcel;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import androidx.test.filters.LargeTest;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented tests for {@link ErrorInfo}.
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@LargeTest
|
|
||||||
public class ErrorInfoTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void errorInfoTestParcelable() {
|
|
||||||
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
|
|
||||||
R.string.general_error);
|
|
||||||
// Obtain a Parcel object and write the parcelable object to it:
|
|
||||||
Parcel parcel = Parcel.obtain();
|
|
||||||
info.writeToParcel(parcel, 0);
|
|
||||||
parcel.setDataPosition(0);
|
|
||||||
ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
|
||||||
|
|
||||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction);
|
|
||||||
assertEquals("youtube", infoFromParcel.serviceName);
|
|
||||||
assertEquals("request", infoFromParcel.request);
|
|
||||||
assertEquals(R.string.general_error, infoFromParcel.message);
|
|
||||||
|
|
||||||
parcel.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.schabi.newpipe.testUtil
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
|
||||||
|
class TestDatabase {
|
||||||
|
companion object {
|
||||||
|
fun createReplacingNewPipeDatabase(): AppDatabase {
|
||||||
|
val database = Room.inMemoryDatabaseBuilder(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
AppDatabase::class.java
|
||||||
|
)
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val databaseField = NewPipeDatabase::class.java.getDeclaredField("databaseInstance")
|
||||||
|
databaseField.isAccessible = true
|
||||||
|
databaseField.set(NewPipeDatabase::class, database)
|
||||||
|
|
||||||
|
assertSame(
|
||||||
|
"Mocking database failed!",
|
||||||
|
database,
|
||||||
|
NewPipeDatabase.getInstance(ApplicationProvider.getApplicationContext())
|
||||||
|
)
|
||||||
|
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.schabi.newpipe.testUtil
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import org.junit.runners.model.Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always run on [Schedulers.trampoline].
|
||||||
|
* This executes the task in the current thread in FIFO manner.
|
||||||
|
* This ensures that tasks are run quickly inside the tests
|
||||||
|
* and not scheduled away to another thread for later execution
|
||||||
|
*/
|
||||||
|
class TrampolineSchedulerRule : TestRule {
|
||||||
|
|
||||||
|
private val scheduler = Schedulers.trampoline()
|
||||||
|
|
||||||
|
override fun apply(base: Statement, description: Description): Statement =
|
||||||
|
object : Statement() {
|
||||||
|
override fun evaluate() {
|
||||||
|
try {
|
||||||
|
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
|
||||||
|
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||||
|
|
||||||
|
base.evaluate()
|
||||||
|
} finally {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,367 @@
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.INVISIBLE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
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)
|
||||||
|
class StreamItemAdapterTest {
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var spinner: Spinner
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
UiThreadStatement.runOnUiThread {
|
||||||
|
spinner = Spinner(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun videoStreams_noSecondaryStream() {
|
||||||
|
val adapter = StreamItemAdapter<VideoStream, AudioStream>(
|
||||||
|
getVideoStreams(true, true, true, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner.adapter = adapter
|
||||||
|
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 1, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun videoStreams_hasSecondaryStream() {
|
||||||
|
val adapter = StreamItemAdapter(
|
||||||
|
getVideoStreams(false, true, false, true),
|
||||||
|
getAudioStreams(false, true, false, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner.adapter = adapter
|
||||||
|
assertIconVisibility(spinner, 0, GONE, GONE)
|
||||||
|
assertIconVisibility(spinner, 1, GONE, GONE)
|
||||||
|
assertIconVisibility(spinner, 2, GONE, GONE)
|
||||||
|
assertIconVisibility(spinner, 3, GONE, GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun videoStreams_Mixed() {
|
||||||
|
val adapter = StreamItemAdapter(
|
||||||
|
getVideoStreams(true, true, true, true, true, false, true, true),
|
||||||
|
getAudioStreams(false, true, false, false, false, true, true, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner.adapter = adapter
|
||||||
|
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 1, GONE, INVISIBLE)
|
||||||
|
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 4, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 5, GONE, INVISIBLE)
|
||||||
|
assertIconVisibility(spinner, 6, GONE, INVISIBLE)
|
||||||
|
assertIconVisibility(spinner, 7, GONE, INVISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subtitleStreams_noIcon() {
|
||||||
|
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
||||||
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
|
(0 until 5).map {
|
||||||
|
SubtitlesStream.Builder()
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.SRT)
|
||||||
|
.setLanguageCode("pt-BR")
|
||||||
|
.setAutoGenerated(false)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
spinner.adapter = adapter
|
||||||
|
for (i in 0 until spinner.count) {
|
||||||
|
assertIconVisibility(spinner, i, GONE, GONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun audioStreams_noIcon() {
|
||||||
|
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||||
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
|
(0 until 5).map {
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com/$it", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
spinner.adapter = adapter
|
||||||
|
for (i in 0 until spinner.count) {
|
||||||
|
assertIconVisibility(spinner, i, GONE, GONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.StreamInfoWrapper(
|
||||||
|
videoOnly.map {
|
||||||
|
VideoStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.MPEG_4)
|
||||||
|
.setResolution("720p")
|
||||||
|
.setIsVideoOnly(it)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a list of audio streams, containing valid and null elements mirroring the provided
|
||||||
|
* [shouldBeValid] vararg.
|
||||||
|
*/
|
||||||
|
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
||||||
|
getSecondaryStreamsFromList(
|
||||||
|
shouldBeValid.map {
|
||||||
|
if (it) {
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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).
|
||||||
|
*/
|
||||||
|
private fun assertIconVisibility(
|
||||||
|
spinner: Spinner,
|
||||||
|
position: Int,
|
||||||
|
normalVisibility: Int,
|
||||||
|
dropDownVisibility: Int
|
||||||
|
) {
|
||||||
|
spinner.setSelection(position)
|
||||||
|
spinner.adapter.getView(position, null, spinner).run {
|
||||||
|
Assert.assertEquals(
|
||||||
|
"normal visibility (pos=[$position]) is not correct",
|
||||||
|
findViewById<View>(R.id.wo_sound_icon).visibility,
|
||||||
|
normalVisibility,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
spinner.adapter.getDropDownView(position, null, spinner).run {
|
||||||
|
Assert.assertEquals(
|
||||||
|
"drop down visibility (pos=[$position]) is not correct",
|
||||||
|
findViewById<View>(R.id.wo_sound_icon).visibility,
|
||||||
|
dropDownVisibility
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that builds a secondary stream list.
|
||||||
|
*/
|
||||||
|
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) =
|
||||||
|
SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
|
||||||
|
streams.forEachIndexed { index, stream ->
|
||||||
|
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||||
|
SecondaryStreamHelper(
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="org.schabi.newpipe">
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".DebugApp"
|
android:name=".DebugApp"
|
||||||
|
|
|
@ -1,35 +1,33 @@
|
||||||
package org.schabi.newpipe
|
package org.schabi.newpipe
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.multidex.MultiDex
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.facebook.stetho.Stetho
|
import com.facebook.stetho.Stetho
|
||||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||||
import leakcanary.AppWatcher
|
|
||||||
import leakcanary.LeakCanary
|
import leakcanary.LeakCanary
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
|
||||||
class DebugApp : App() {
|
class DebugApp : App() {
|
||||||
override fun attachBaseContext(base: Context) {
|
|
||||||
super.attachBaseContext(base)
|
|
||||||
MultiDex.install(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
initStetho()
|
initStetho()
|
||||||
|
|
||||||
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
|
LeakCanary.config = LeakCanary.config.copy(
|
||||||
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
|
dumpHeap = PreferenceManager
|
||||||
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager
|
.getDefaultSharedPreferences(this).getBoolean(
|
||||||
.getDefaultSharedPreferences(this).getBoolean(getString(
|
getString(
|
||||||
R.string.allow_heap_dumping_key), false))
|
R.string.allow_heap_dumping_key
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDownloader(): Downloader {
|
override fun getDownloader(): Downloader {
|
||||||
val downloader = DownloaderImpl.init(OkHttpClient.Builder()
|
val downloader = DownloaderImpl.init(
|
||||||
.addNetworkInterceptor(StethoInterceptor()))
|
OkHttpClient.Builder()
|
||||||
|
.addNetworkInterceptor(StethoInterceptor())
|
||||||
|
)
|
||||||
setCookiesToDownloader(downloader)
|
setCookiesToDownloader(downloader)
|
||||||
return downloader
|
return downloader
|
||||||
}
|
}
|
||||||
|
@ -43,7 +41,8 @@ class DebugApp : App() {
|
||||||
|
|
||||||
// Enable command line interface
|
// Enable command line interface
|
||||||
initializerBuilder.enableDumpapp(
|
initializerBuilder.enableDumpapp(
|
||||||
Stetho.defaultDumperPluginsProvider(applicationContext))
|
Stetho.defaultDumperPluginsProvider(applicationContext)
|
||||||
|
)
|
||||||
|
|
||||||
// Use the InitializerBuilder to generate an Initializer
|
// Use the InitializerBuilder to generate an Initializer
|
||||||
val initializer = initializerBuilder.build()
|
val initializer = initializerBuilder.build()
|
||||||
|
@ -54,6 +53,6 @@ class DebugApp : App() {
|
||||||
|
|
||||||
override fun isDisposedRxExceptionsReported(): Boolean {
|
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import leakcanary.LeakCanary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||||
|
* This class is loaded via reflection by
|
||||||
|
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused") // Class is used but loaded via reflection
|
||||||
|
public class DebugSettingsBVDLeakCanary
|
||||||
|
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent getNewLeakDisplayActivityIntent() {
|
||||||
|
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.schabi.newpipe">
|
android:installLocation="auto">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
@ -9,10 +9,22 @@
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
|
<!-- We need to be able to open links in the browser on API 30+ -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
@ -21,11 +33,12 @@
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:logo="@mipmap/ic_launcher"
|
android:logo="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/OpeningTheme"
|
android:theme="@style/OpeningTheme"
|
||||||
tools:ignore="AllowBackup">
|
tools:ignore="AllowBackup">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -36,47 +49,37 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
<receiver
|
||||||
|
android:name="androidx.media.session.MediaButtonReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".player.BackgroundPlayer"
|
android:name=".player.PlayerService"
|
||||||
android:exported="false">
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".player.BackgroundPlayerActivity"
|
android:name=".player.PlayQueueActivity"
|
||||||
android:label="@string/title_activity_background_player"
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_play_queue"
|
||||||
android:launchMode="singleTask" />
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".player.PopupVideoPlayerActivity"
|
|
||||||
android:label="@string/title_activity_popup_player"
|
|
||||||
android:launchMode="singleTask" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".player.PopupVideoPlayer"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".player.MainVideoPlayer"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@style/VideoPlayerTheme" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/title_activity_about" />
|
android:label="@string/title_activity_about" />
|
||||||
|
|
||||||
<service android:name=".local.subscription.services.SubscriptionsImportService" />
|
<service android:name=".local.subscription.services.SubscriptionsImportService" />
|
||||||
|
@ -85,6 +88,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PanicResponderActivity"
|
android:name=".PanicResponderActivity"
|
||||||
|
android:exported="true"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -97,13 +101,18 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ExitActivity"
|
android:name=".ExitActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/general_error"
|
android:label="@string/general_error"
|
||||||
android:theme="@android:style/Theme.NoDisplay" />
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
<activity android:name=".report.ErrorActivity" />
|
|
||||||
|
<activity
|
||||||
|
android:name=".error.ErrorActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- giga get related -->
|
<!-- giga get related -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".download.DownloadActivity"
|
android:name=".download.DownloadActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask" />
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
|
@ -111,6 +120,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".util.FilePickerActivityHelper"
|
android:name=".util.FilePickerActivityHelper"
|
||||||
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerThemeDark">
|
android:theme="@style/FilePickerThemeDark">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -120,7 +130,8 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ReCaptchaActivity"
|
android:name=".error.ReCaptchaActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/recaptcha" />
|
android:label="@string/recaptcha" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
@ -136,6 +147,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".RouterActivity"
|
android:name=".RouterActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
android:label="@string/preferred_open_action_share_menu_title"
|
android:label="@string/preferred_open_action_share_menu_title"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/RouterActivityThemeDark">
|
android:theme="@style/RouterActivityThemeDark">
|
||||||
|
@ -160,10 +172,13 @@
|
||||||
<data android:pathPrefix="/embed/" />
|
<data android:pathPrefix="/embed/" />
|
||||||
<data android:pathPrefix="/watch" />
|
<data android:pathPrefix="/watch" />
|
||||||
<data android:pathPrefix="/attribution_link" />
|
<data android:pathPrefix="/attribution_link" />
|
||||||
|
<data android:pathPrefix="/shorts/" />
|
||||||
|
<data android:pathPrefix="/live/" />
|
||||||
<!-- channel prefix -->
|
<!-- channel prefix -->
|
||||||
<data android:pathPrefix="/channel/" />
|
<data android:pathPrefix="/channel/" />
|
||||||
<data android:pathPrefix="/user/" />
|
<data android:pathPrefix="/user/" />
|
||||||
<data android:pathPrefix="/c/" />
|
<data android:pathPrefix="/c/" />
|
||||||
|
<data android:pathPrefix="/@" />
|
||||||
<!-- playlist prefix -->
|
<!-- playlist prefix -->
|
||||||
<data android:pathPrefix="/playlist" />
|
<data android:pathPrefix="/playlist" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -238,23 +253,49 @@
|
||||||
|
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
<data android:host="tubus.eduvid.org" />
|
||||||
<data android:host="invidio.us" />
|
<data android:host="invidio.us" />
|
||||||
<data android:host="dev.invidio.us" />
|
<data android:host="dev.invidio.us" />
|
||||||
<data android:host="www.invidio.us" />
|
<data android:host="www.invidio.us" />
|
||||||
|
<data android:host="redirect.invidious.io" />
|
||||||
<data android:host="invidious.snopyta.org" />
|
<data android:host="invidious.snopyta.org" />
|
||||||
<data android:host="fi.invidious.snopyta.org" />
|
|
||||||
<data android:host="yewtu.be" />
|
<data android:host="yewtu.be" />
|
||||||
<data android:host="invidious.ggc-project.de" />
|
<data android:host="tube.connect.cafe" />
|
||||||
<data android:host="yt.maisputain.ovh" />
|
<data android:host="invidious.kavin.rocks" />
|
||||||
<data android:host="invidious.13ad.de" />
|
<data android:host="invidious-us.kavin.rocks" />
|
||||||
<data android:host="invidious.toot.koeln" />
|
<data android:host="piped.kavin.rocks" />
|
||||||
|
<data android:host="invidious.site" />
|
||||||
|
<data android:host="vid.mint.lgbt" />
|
||||||
|
<data android:host="invidiou.site" />
|
||||||
<data android:host="invidious.fdn.fr" />
|
<data android:host="invidious.fdn.fr" />
|
||||||
<data android:host="watch.nettohikari.com" />
|
<data android:host="invidious.048596.xyz" />
|
||||||
<data android:host="invidious.snwmds.net" />
|
<data android:host="invidious.zee.li" />
|
||||||
<data android:host="invidious.snwmds.org" />
|
<data android:host="vid.puffyan.us" />
|
||||||
<data android:host="invidious.snwmds.com" />
|
<data android:host="ytprivate.com" />
|
||||||
<data android:host="invidious.sunsetravens.com" />
|
<data android:host="invidious.namazso.eu" />
|
||||||
<data android:host="invidious.gachirangers.com" />
|
<data android:host="invidious.silkky.cloud" />
|
||||||
|
<data android:host="invidious.exonip.de" />
|
||||||
|
<data android:host="inv.riverside.rocks" />
|
||||||
|
<data android:host="invidious.blamefran.net" />
|
||||||
|
<data android:host="invidious.moomoo.me" />
|
||||||
|
<data android:host="ytb.trom.tf" />
|
||||||
|
<data android:host="yt.cyberhost.uk" />
|
||||||
|
<data android:host="y.com.cm" />
|
||||||
|
<data android:pathPrefix="/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- y2u.be filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="y2u.be" />
|
||||||
<data android:pathPrefix="/" />
|
<data android:pathPrefix="/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
@ -282,7 +323,7 @@
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- MediaCCC filter -->
|
<!-- media.ccc.de filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
@ -313,26 +354,75 @@
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<data android:host="eduvid.org" />
|
||||||
<data android:host="framatube.org" />
|
<data android:host="framatube.org" />
|
||||||
<data android:host="media.assassinate-you.net" />
|
<data android:host="media.assassinate-you.net" />
|
||||||
|
<data android:host="media.fsfe.org" />
|
||||||
<data android:host="peertube.co.uk" />
|
<data android:host="peertube.co.uk" />
|
||||||
<data android:host="peertube.cpy.re" />
|
<data android:host="peertube.cpy.re" />
|
||||||
<data android:host="peertube.mastodon.host" />
|
|
||||||
<data android:host="peertube.fr" />
|
<data android:host="peertube.fr" />
|
||||||
<data android:host="peertube.live" />
|
<data android:host="peertube.mastodon.host" />
|
||||||
<data android:host="peertube.video" />
|
<data android:host="peertube.stream" />
|
||||||
<data android:host="tube.privacytools.io" />
|
|
||||||
<data android:host="video.ploud.fr" />
|
|
||||||
<data android:host="video.lqdn.fr" />
|
|
||||||
<data android:host="skeptikon.fr" />
|
<data android:host="skeptikon.fr" />
|
||||||
|
<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="/videos/" /> <!-- it contains playlists -->
|
||||||
|
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||||
|
<data android:pathPrefix="/w/p/" /> <!-- short playlist URLs -->
|
||||||
<data android:pathPrefix="/accounts/" />
|
<data android:pathPrefix="/accounts/" />
|
||||||
|
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||||
<data android:pathPrefix="/video-channels/" />
|
<data android:pathPrefix="/video-channels/" />
|
||||||
|
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="*.bandcamp.com" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Bandcamp filter for radio -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:sspPattern="bandcamp.com/?show=*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<service
|
||||||
android:name=".RouterActivity$FetcherService"
|
android:name=".RouterActivity$FetcherService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||||
|
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.keepalive.density"
|
||||||
|
android:value="true" />
|
||||||
|
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||||
|
android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
245
app/src/main/assets/epl1.html
Normal file
245
app/src/main/assets/epl1.html
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- saved from url=(0050)https://www.eclipse.org/org/documents/epl-v10.html -->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
|
||||||
|
<title>Eclipse Public License - Version 1.0</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body cz-shortcut-listen="true" lang="EN-US">
|
||||||
|
|
||||||
|
<h2>Eclipse Public License - v 1.0</h2>
|
||||||
|
|
||||||
|
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||||
|
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
|
||||||
|
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
|
||||||
|
AGREEMENT.</p>
|
||||||
|
|
||||||
|
<p><b>1. DEFINITIONS</b></p>
|
||||||
|
|
||||||
|
<p>"Contribution" means:</p>
|
||||||
|
|
||||||
|
<p class="list">a) in the case of the initial Contributor, the initial
|
||||||
|
code and documentation distributed under this Agreement, and</p>
|
||||||
|
<p class="list">b) in the case of each subsequent Contributor:</p>
|
||||||
|
<p class="list">i) changes to the Program, and</p>
|
||||||
|
<p class="list">ii) additions to the Program;</p>
|
||||||
|
<p class="list">where such changes and/or additions to the Program
|
||||||
|
originate from and are distributed by that particular Contributor. A
|
||||||
|
Contribution 'originates' from a Contributor if it was added to the
|
||||||
|
Program by such Contributor itself or anyone acting on such
|
||||||
|
Contributor's behalf. Contributions do not include additions to the
|
||||||
|
Program which: (i) are separate modules of software distributed in
|
||||||
|
conjunction with the Program under their own license agreement, and (ii)
|
||||||
|
are not derivative works of the Program.</p>
|
||||||
|
|
||||||
|
<p>"Contributor" means any person or entity that distributes
|
||||||
|
the Program.</p>
|
||||||
|
|
||||||
|
<p>"Licensed Patents" mean patent claims licensable by a
|
||||||
|
Contributor which are necessarily infringed by the use or sale of its
|
||||||
|
Contribution alone or when combined with the Program.</p>
|
||||||
|
|
||||||
|
<p>"Program" means the Contributions distributed in accordance
|
||||||
|
with this Agreement.</p>
|
||||||
|
|
||||||
|
<p>"Recipient" means anyone who receives the Program under
|
||||||
|
this Agreement, including all Contributors.</p>
|
||||||
|
|
||||||
|
<p><b>2. GRANT OF RIGHTS</b></p>
|
||||||
|
|
||||||
|
<p class="list">a) Subject to the terms of this Agreement, each
|
||||||
|
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||||
|
royalty-free copyright license to reproduce, prepare derivative works
|
||||||
|
of, publicly display, publicly perform, distribute and sublicense the
|
||||||
|
Contribution of such Contributor, if any, and such derivative works, in
|
||||||
|
source code and object code form.</p>
|
||||||
|
|
||||||
|
<p class="list">b) Subject to the terms of this Agreement, each
|
||||||
|
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||||
|
royalty-free patent license under Licensed Patents to make, use, sell,
|
||||||
|
offer to sell, import and otherwise transfer the Contribution of such
|
||||||
|
Contributor, if any, in source code and object code form. This patent
|
||||||
|
license shall apply to the combination of the Contribution and the
|
||||||
|
Program if, at the time the Contribution is added by the Contributor,
|
||||||
|
such addition of the Contribution causes such combination to be covered
|
||||||
|
by the Licensed Patents. The patent license shall not apply to any other
|
||||||
|
combinations which include the Contribution. No hardware per se is
|
||||||
|
licensed hereunder.</p>
|
||||||
|
|
||||||
|
<p class="list">c) Recipient understands that although each Contributor
|
||||||
|
grants the licenses to its Contributions set forth herein, no assurances
|
||||||
|
are provided by any Contributor that the Program does not infringe the
|
||||||
|
patent or other intellectual property rights of any other entity. Each
|
||||||
|
Contributor disclaims any liability to Recipient for claims brought by
|
||||||
|
any other entity based on infringement of intellectual property rights
|
||||||
|
or otherwise. As a condition to exercising the rights and licenses
|
||||||
|
granted hereunder, each Recipient hereby assumes sole responsibility to
|
||||||
|
secure any other intellectual property rights needed, if any. For
|
||||||
|
example, if a third party patent license is required to allow Recipient
|
||||||
|
to distribute the Program, it is Recipient's responsibility to acquire
|
||||||
|
that license before distributing the Program.</p>
|
||||||
|
|
||||||
|
<p class="list">d) Each Contributor represents that to its knowledge it
|
||||||
|
has sufficient copyright rights in its Contribution, if any, to grant
|
||||||
|
the copyright license set forth in this Agreement.</p>
|
||||||
|
|
||||||
|
<p><b>3. REQUIREMENTS</b></p>
|
||||||
|
|
||||||
|
<p>A Contributor may choose to distribute the Program in object code
|
||||||
|
form under its own license agreement, provided that:</p>
|
||||||
|
|
||||||
|
<p class="list">a) it complies with the terms and conditions of this
|
||||||
|
Agreement; and</p>
|
||||||
|
|
||||||
|
<p class="list">b) its license agreement:</p>
|
||||||
|
|
||||||
|
<p class="list">i) effectively disclaims on behalf of all Contributors
|
||||||
|
all warranties and conditions, express and implied, including warranties
|
||||||
|
or conditions of title and non-infringement, and implied warranties or
|
||||||
|
conditions of merchantability and fitness for a particular purpose;</p>
|
||||||
|
|
||||||
|
<p class="list">ii) effectively excludes on behalf of all Contributors
|
||||||
|
all liability for damages, including direct, indirect, special,
|
||||||
|
incidental and consequential damages, such as lost profits;</p>
|
||||||
|
|
||||||
|
<p class="list">iii) states that any provisions which differ from this
|
||||||
|
Agreement are offered by that Contributor alone and not by any other
|
||||||
|
party; and</p>
|
||||||
|
|
||||||
|
<p class="list">iv) states that source code for the Program is available
|
||||||
|
from such Contributor, and informs licensees how to obtain it in a
|
||||||
|
reasonable manner on or through a medium customarily used for software
|
||||||
|
exchange.</p>
|
||||||
|
|
||||||
|
<p>When the Program is made available in source code form:</p>
|
||||||
|
|
||||||
|
<p class="list">a) it must be made available under this Agreement; and</p>
|
||||||
|
|
||||||
|
<p class="list">b) a copy of this Agreement must be included with each
|
||||||
|
copy of the Program.</p>
|
||||||
|
|
||||||
|
<p>Contributors may not remove or alter any copyright notices contained
|
||||||
|
within the Program.</p>
|
||||||
|
|
||||||
|
<p>Each Contributor must identify itself as the originator of its
|
||||||
|
Contribution, if any, in a manner that reasonably allows subsequent
|
||||||
|
Recipients to identify the originator of the Contribution.</p>
|
||||||
|
|
||||||
|
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
|
||||||
|
|
||||||
|
<p>Commercial distributors of software may accept certain
|
||||||
|
responsibilities with respect to end users, business partners and the
|
||||||
|
like. While this license is intended to facilitate the commercial use of
|
||||||
|
the Program, the Contributor who includes the Program in a commercial
|
||||||
|
product offering should do so in a manner which does not create
|
||||||
|
potential liability for other Contributors. Therefore, if a Contributor
|
||||||
|
includes the Program in a commercial product offering, such Contributor
|
||||||
|
("Commercial Contributor") hereby agrees to defend and
|
||||||
|
indemnify every other Contributor ("Indemnified Contributor")
|
||||||
|
against any losses, damages and costs (collectively "Losses")
|
||||||
|
arising from claims, lawsuits and other legal actions brought by a third
|
||||||
|
party against the Indemnified Contributor to the extent caused by the
|
||||||
|
acts or omissions of such Commercial Contributor in connection with its
|
||||||
|
distribution of the Program in a commercial product offering. The
|
||||||
|
obligations in this section do not apply to any claims or Losses
|
||||||
|
relating to any actual or alleged intellectual property infringement. In
|
||||||
|
order to qualify, an Indemnified Contributor must: a) promptly notify
|
||||||
|
the Commercial Contributor in writing of such claim, and b) allow the
|
||||||
|
Commercial Contributor to control, and cooperate with the Commercial
|
||||||
|
Contributor in, the defense and any related settlement negotiations. The
|
||||||
|
Indemnified Contributor may participate in any such claim at its own
|
||||||
|
expense.</p>
|
||||||
|
|
||||||
|
<p>For example, a Contributor might include the Program in a commercial
|
||||||
|
product offering, Product X. That Contributor is then a Commercial
|
||||||
|
Contributor. If that Commercial Contributor then makes performance
|
||||||
|
claims, or offers warranties related to Product X, those performance
|
||||||
|
claims and warranties are such Commercial Contributor's responsibility
|
||||||
|
alone. Under this section, the Commercial Contributor would have to
|
||||||
|
defend claims against the other Contributors related to those
|
||||||
|
performance claims and warranties, and if a court requires any other
|
||||||
|
Contributor to pay any damages as a result, the Commercial Contributor
|
||||||
|
must pay those damages.</p>
|
||||||
|
|
||||||
|
<p><b>5. NO WARRANTY</b></p>
|
||||||
|
|
||||||
|
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
|
||||||
|
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||||
|
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
|
||||||
|
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
|
||||||
|
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
|
||||||
|
responsible for determining the appropriateness of using and
|
||||||
|
distributing the Program and assumes all risks associated with its
|
||||||
|
exercise of rights under this Agreement , including but not limited to
|
||||||
|
the risks and costs of program errors, compliance with applicable laws,
|
||||||
|
damage to or loss of data, programs or equipment, and unavailability or
|
||||||
|
interruption of operations.</p>
|
||||||
|
|
||||||
|
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
|
||||||
|
|
||||||
|
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
|
||||||
|
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
|
||||||
|
WITHOUT LIMITATION LOST PROFITS), 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 OR
|
||||||
|
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
|
||||||
|
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
|
||||||
|
|
||||||
|
<p><b>7. GENERAL</b></p>
|
||||||
|
|
||||||
|
<p>If any provision of this Agreement is invalid or unenforceable under
|
||||||
|
applicable law, it shall not affect the validity or enforceability of
|
||||||
|
the remainder of the terms of this Agreement, and without further action
|
||||||
|
by the parties hereto, such provision shall be reformed to the minimum
|
||||||
|
extent necessary to make such provision valid and enforceable.</p>
|
||||||
|
|
||||||
|
<p>If Recipient institutes patent litigation against any entity
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||||
|
Program itself (excluding combinations of the Program with other
|
||||||
|
software or hardware) infringes such Recipient's patent(s), then such
|
||||||
|
Recipient's rights granted under Section 2(b) shall terminate as of the
|
||||||
|
date such litigation is filed.</p>
|
||||||
|
|
||||||
|
<p>All Recipient's rights under this Agreement shall terminate if it
|
||||||
|
fails to comply with any of the material terms or conditions of this
|
||||||
|
Agreement and does not cure such failure in a reasonable period of time
|
||||||
|
after becoming aware of such noncompliance. If all Recipient's rights
|
||||||
|
under this Agreement terminate, Recipient agrees to cease use and
|
||||||
|
distribution of the Program as soon as reasonably practicable. However,
|
||||||
|
Recipient's obligations under this Agreement and any licenses granted by
|
||||||
|
Recipient relating to the Program shall continue and survive.</p>
|
||||||
|
|
||||||
|
<p>Everyone is permitted to copy and distribute copies of this
|
||||||
|
Agreement, but in order to avoid inconsistency the Agreement is
|
||||||
|
copyrighted and may only be modified in the following manner. The
|
||||||
|
Agreement Steward reserves the right to publish new versions (including
|
||||||
|
revisions) of this Agreement from time to time. No one other than the
|
||||||
|
Agreement Steward has the right to modify this Agreement. The Eclipse
|
||||||
|
Foundation is the initial Agreement Steward. The Eclipse Foundation may
|
||||||
|
assign the responsibility to serve as the Agreement Steward to a
|
||||||
|
suitable separate entity. Each new version of the Agreement will be
|
||||||
|
given a distinguishing version number. The Program (including
|
||||||
|
Contributions) may always be distributed subject to the version of the
|
||||||
|
Agreement under which it was received. In addition, after a new version
|
||||||
|
of the Agreement is published, Contributor may elect to distribute the
|
||||||
|
Program (including its Contributions) under the new version. Except as
|
||||||
|
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
|
||||||
|
rights or licenses to the intellectual property of any Contributor under
|
||||||
|
this Agreement, whether expressly, by implication, estoppel or
|
||||||
|
otherwise. All rights in the Program not expressly granted under this
|
||||||
|
Agreement are reserved.</p>
|
||||||
|
|
||||||
|
<p>This Agreement is governed by the laws of the State of New York and
|
||||||
|
the intellectual property laws of the United States of America. No party
|
||||||
|
to this Agreement will bring a legal action under this Agreement more
|
||||||
|
than one year after the cause of action arose. Each party waives its
|
||||||
|
rights to a jury trial in any resulting litigation.</p>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,400 +0,0 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<title>GNU General Public License v2.0 - GNU Project - Free Software Foundation (FSF)</title>
|
|
||||||
<link rel="alternate" type="application/rdf+xml"
|
|
||||||
href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.rdf" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h3><a id="SEC1">GNU GENERAL PUBLIC LICENSE</a></h3>
|
|
||||||
<p>
|
|
||||||
Version 2, June 1991
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.<br/>
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA<br/>
|
|
||||||
<br/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3 id="preamble"><a id="SEC2">Preamble</a></h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
this service if you wish), that you receive source code or can get it
|
|
||||||
if you want it, that you can change the software or use pieces of it
|
|
||||||
in new free programs; and that you know you can do these things.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<h3 id="terms"><a id="SEC3">TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION</a></h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p id="section0">
|
|
||||||
<strong>0.</strong>
|
|
||||||
This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section1">
|
|
||||||
<strong>1.</strong>
|
|
||||||
You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section2">
|
|
||||||
<strong>2.</strong>
|
|
||||||
You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt></dt>
|
|
||||||
<dd>
|
|
||||||
<strong>a)</strong>
|
|
||||||
You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
</dd>
|
|
||||||
<dt></dt>
|
|
||||||
<dd>
|
|
||||||
<strong>b)</strong>
|
|
||||||
You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
</dd>
|
|
||||||
<dt></dt>
|
|
||||||
<dd>
|
|
||||||
<strong>c)</strong>
|
|
||||||
If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section3">
|
|
||||||
<strong>3.</strong>
|
|
||||||
You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- we use this doubled UL to get the sub-sections indented, -->
|
|
||||||
<!-- while making the bullets as unobvious as possible. -->
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt></dt>
|
|
||||||
<dd>
|
|
||||||
<strong>a)</strong>
|
|
||||||
Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
</dd>
|
|
||||||
<dt></dt>
|
|
||||||
<dd>
|
|
||||||
<strong>b)</strong>
|
|
||||||
Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
</dd>
|
|
||||||
<dt></dt>
|
|
||||||
<dd>
|
|
||||||
<strong>c)</strong>
|
|
||||||
Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major softwareComponents (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section4">
|
|
||||||
<strong>4.</strong>
|
|
||||||
You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section5">
|
|
||||||
<strong>5.</strong>
|
|
||||||
You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section6">
|
|
||||||
<strong>6.</strong>
|
|
||||||
Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section7">
|
|
||||||
<strong>7.</strong>
|
|
||||||
If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section8">
|
|
||||||
<strong>8.</strong>
|
|
||||||
If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section9">
|
|
||||||
<strong>9.</strong>
|
|
||||||
The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section10">
|
|
||||||
<strong>10.</strong>
|
|
||||||
If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section11"><strong>NO WARRANTY</strong></p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>11.</strong>
|
|
||||||
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p id="section12">
|
|
||||||
<strong>12.</strong>
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
</p>
|
|
||||||
</body></html>
|
|
|
@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
|
@ -51,8 +52,12 @@ import java.util.ArrayList;
|
||||||
* <li>{@link #saveState()}</li>
|
* <li>{@link #saveState()}</li>
|
||||||
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
|
*
|
||||||
|
* @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||||
|
* {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
||||||
private static final String TAG = "FragmentStatePagerAdapt";
|
private static final String TAG = "FragmentStatePagerAdapt";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
|
@ -86,9 +91,10 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
private final int mBehavior;
|
private final int mBehavior;
|
||||||
private FragmentTransaction mCurTransaction = null;
|
private FragmentTransaction mCurTransaction = null;
|
||||||
|
|
||||||
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
|
||||||
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
private final ArrayList<Fragment> mFragments = new ArrayList<>();
|
||||||
private Fragment mCurrentPrimaryItem = null;
|
private Fragment mCurrentPrimaryItem = null;
|
||||||
|
private boolean mExecutingFinishUpdate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||||
|
@ -150,7 +156,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
// from its saved state, where the fragment manager has already
|
// from its saved state, where the fragment manager has already
|
||||||
// taken care of restoring the fragments we previously had instantiated.
|
// taken care of restoring the fragments we previously had instantiated.
|
||||||
if (mFragments.size() > position) {
|
if (mFragments.size() > position) {
|
||||||
Fragment f = mFragments.get(position);
|
final Fragment f = mFragments.get(position);
|
||||||
if (f != null) {
|
if (f != null) {
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
@ -160,12 +166,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
mCurTransaction = mFragmentManager.beginTransaction();
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
Fragment fragment = getItem(position);
|
final Fragment fragment = getItem(position);
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||||
}
|
}
|
||||||
if (mSavedState.size() > position) {
|
if (mSavedState.size() > position) {
|
||||||
Fragment.SavedState fss = mSavedState.get(position);
|
final Fragment.SavedState fss = mSavedState.get(position);
|
||||||
if (fss != null) {
|
if (fss != null) {
|
||||||
fragment.setInitialSavedState(fss);
|
fragment.setInitialSavedState(fss);
|
||||||
}
|
}
|
||||||
|
@ -191,7 +197,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
@Override
|
@Override
|
||||||
public void destroyItem(@NonNull final ViewGroup container, final int position,
|
public void destroyItem(@NonNull final ViewGroup container, final int position,
|
||||||
@NonNull final Object object) {
|
@NonNull final Object object) {
|
||||||
Fragment fragment = (Fragment) object;
|
final Fragment fragment = (Fragment) object;
|
||||||
|
|
||||||
if (mCurTransaction == null) {
|
if (mCurTransaction == null) {
|
||||||
mCurTransaction = mFragmentManager.beginTransaction();
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
@ -208,7 +214,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
mFragments.set(position, null);
|
mFragments.set(position, null);
|
||||||
|
|
||||||
mCurTransaction.remove(fragment);
|
mCurTransaction.remove(fragment);
|
||||||
if (fragment == mCurrentPrimaryItem) {
|
if (fragment.equals(mCurrentPrimaryItem)) {
|
||||||
mCurrentPrimaryItem = null;
|
mCurrentPrimaryItem = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,7 +223,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
||||||
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||||
@NonNull final Object object) {
|
@NonNull final Object object) {
|
||||||
Fragment fragment = (Fragment) object;
|
final Fragment fragment = (Fragment) object;
|
||||||
if (fragment != mCurrentPrimaryItem) {
|
if (fragment != mCurrentPrimaryItem) {
|
||||||
if (mCurrentPrimaryItem != null) {
|
if (mCurrentPrimaryItem != null) {
|
||||||
mCurrentPrimaryItem.setMenuVisibility(false);
|
mCurrentPrimaryItem.setMenuVisibility(false);
|
||||||
|
@ -247,7 +253,19 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
@Override
|
@Override
|
||||||
public void finishUpdate(@NonNull final ViewGroup container) {
|
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||||
if (mCurTransaction != null) {
|
if (mCurTransaction != null) {
|
||||||
mCurTransaction.commitNowAllowingStateLoss();
|
// We drop any transactions that attempt to be committed
|
||||||
|
// from a re-entrant call to finishUpdate(). We need to
|
||||||
|
// do this as a workaround for Robolectric running measure/layout
|
||||||
|
// calls inline rather than allowing them to be posted
|
||||||
|
// as they would on a real device.
|
||||||
|
if (!mExecutingFinishUpdate) {
|
||||||
|
try {
|
||||||
|
mExecutingFinishUpdate = true;
|
||||||
|
mCurTransaction.commitNowAllowingStateLoss();
|
||||||
|
} finally {
|
||||||
|
mExecutingFinishUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
mCurTransaction = null;
|
mCurTransaction = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,19 +283,17 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
@Nullable
|
@Nullable
|
||||||
public Parcelable saveState() {
|
public Parcelable saveState() {
|
||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (mSavedState.size() > 0) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
state.putParcelableArrayList("states", mSavedState);
|
||||||
mSavedState.toArray(fss);
|
|
||||||
state.putParcelableArray("states", fss);
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
if (f != null && f.isAdded()) {
|
if (f != null && f.isAdded()) {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
}
|
}
|
||||||
String key = "f" + i;
|
final String key = "f" + i;
|
||||||
mFragmentManager.putFragment(state, key, f);
|
mFragmentManager.putFragment(state, key, f);
|
||||||
|
|
||||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
@ -294,21 +310,20 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
@Override
|
@Override
|
||||||
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
|
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
Bundle bundle = (Bundle) state;
|
final Bundle bundle = (Bundle) state;
|
||||||
bundle.setClassLoader(loader);
|
bundle.setClassLoader(loader);
|
||||||
Parcelable[] fss = bundle.getParcelableArray("states");
|
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||||
|
Fragment.SavedState.class);
|
||||||
mSavedState.clear();
|
mSavedState.clear();
|
||||||
mFragments.clear();
|
mFragments.clear();
|
||||||
if (fss != null) {
|
if (states != null) {
|
||||||
for (int i = 0; i < fss.length; i++) {
|
mSavedState.addAll(states);
|
||||||
mSavedState.add((Fragment.SavedState) fss[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Iterable<String> keys = bundle.keySet();
|
final Iterable<String> keys = bundle.keySet();
|
||||||
for (String key: keys) {
|
for (final String key : keys) {
|
||||||
if (key.startsWith("f")) {
|
if (key.startsWith("f")) {
|
||||||
int index = Integer.parseInt(key.substring(1));
|
final int index = Integer.parseInt(key.substring(1));
|
||||||
Fragment f = mFragmentManager.getFragment(bundle, key);
|
final Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||||
if (f != null) {
|
if (f != null) {
|
||||||
while (mFragments.size() <= index) {
|
while (mFragments.size() <= index) {
|
||||||
mFragments.add(null);
|
mFragments.add(null);
|
||||||
|
|
|
@ -4,13 +4,17 @@ import android.content.Context;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.OverScroller;
|
import android.widget.OverScroller;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
// See https://stackoverflow.com/questions/56849221#57997489
|
// See https://stackoverflow.com/questions/56849221#57997489
|
||||||
public final class FlingBehavior extends AppBarLayout.Behavior {
|
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
|
@ -20,23 +24,28 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean allowScroll = true;
|
||||||
|
private final Rect globalRect = new Rect();
|
||||||
|
private final List<Integer> skipInterceptionOfElements = List.of(
|
||||||
|
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||||
|
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onRequestChildRectangleOnScreen(
|
public boolean onRequestChildRectangleOnScreen(
|
||||||
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
|
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
|
||||||
@NonNull final Rect rectangle, final boolean immediate) {
|
@NonNull final Rect rectangle, final boolean immediate) {
|
||||||
|
|
||||||
focusScrollRect.set(rectangle);
|
focusScrollRect.set(rectangle);
|
||||||
|
|
||||||
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
|
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
|
||||||
|
|
||||||
int height = coordinatorLayout.getHeight();
|
final int height = coordinatorLayout.getHeight();
|
||||||
|
|
||||||
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
||||||
// the child is too big to fit inside ourselves completely, ignore request
|
// the child is too big to fit inside ourselves completely, ignore request
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int dy;
|
final int dy;
|
||||||
|
|
||||||
if (focusScrollRect.bottom > height) {
|
if (focusScrollRect.bottom > height) {
|
||||||
dy = focusScrollRect.top;
|
dy = focusScrollRect.top;
|
||||||
|
@ -48,13 +57,26 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
final int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||||
|
|
||||||
return consumed == dy;
|
return consumed == dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
|
@Override
|
||||||
final MotionEvent ev) {
|
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||||
|
@NonNull final AppBarLayout child,
|
||||||
|
@NonNull final MotionEvent ev) {
|
||||||
|
for (final int element : skipInterceptionOfElements) {
|
||||||
|
final View view = child.findViewById(element);
|
||||||
|
if (view != null) {
|
||||||
|
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||||
|
if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
|
||||||
|
allowScroll = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allowScroll = true;
|
||||||
switch (ev.getActionMasked()) {
|
switch (ev.getActionMasked()) {
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
// remove reference to old nested scrolling child
|
// remove reference to old nested scrolling child
|
||||||
|
@ -68,17 +90,37 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
return super.onInterceptTouchEvent(parent, child, ev);
|
return super.onInterceptTouchEvent(parent, child, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent,
|
||||||
|
@NonNull final AppBarLayout child,
|
||||||
|
@NonNull final View directTargetChild,
|
||||||
|
final View target,
|
||||||
|
final int nestedScrollAxes,
|
||||||
|
final int type) {
|
||||||
|
return allowScroll && super.onStartNestedScroll(
|
||||||
|
parent, child, directTargetChild, target, nestedScrollAxes, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout,
|
||||||
|
@NonNull final AppBarLayout child,
|
||||||
|
@NonNull final View target, final float velocityX,
|
||||||
|
final float velocityY, final boolean consumed) {
|
||||||
|
return allowScroll && super.onNestedFling(
|
||||||
|
coordinatorLayout, child, target, velocityX, velocityY, consumed);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private OverScroller getScrollerField() {
|
private OverScroller getScrollerField() {
|
||||||
try {
|
try {
|
||||||
Class<?> headerBehaviorType = this.getClass()
|
final Class<?> headerBehaviorType = this.getClass()
|
||||||
.getSuperclass().getSuperclass().getSuperclass();
|
.getSuperclass().getSuperclass().getSuperclass();
|
||||||
if (headerBehaviorType != null) {
|
if (headerBehaviorType != null) {
|
||||||
Field field = headerBehaviorType.getDeclaredField("scroller");
|
final Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return ((OverScroller) field.get(this));
|
return ((OverScroller) field.get(this));
|
||||||
}
|
}
|
||||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||||
// ?
|
// ?
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -87,34 +129,35 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
@Nullable
|
@Nullable
|
||||||
private Field getLastNestedScrollingChildRefField() {
|
private Field getLastNestedScrollingChildRefField() {
|
||||||
try {
|
try {
|
||||||
Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||||
if (headerBehaviorType != null) {
|
if (headerBehaviorType != null) {
|
||||||
Field field = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
final Field field =
|
||||||
|
headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
} catch (NoSuchFieldException e) {
|
} catch (final NoSuchFieldException e) {
|
||||||
// ?
|
// ?
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetNestedScrollingChild() {
|
private void resetNestedScrollingChild() {
|
||||||
Field field = getLastNestedScrollingChildRefField();
|
final Field field = getLastNestedScrollingChildRefField();
|
||||||
if (field != null) {
|
if (field != null) {
|
||||||
try {
|
try {
|
||||||
Object value = field.get(this);
|
final Object value = field.get(this);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
field.set(this, null);
|
field.set(this, null);
|
||||||
}
|
}
|
||||||
} catch (IllegalAccessException e) {
|
} catch (final IllegalAccessException e) {
|
||||||
// ?
|
// ?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopAppBarLayoutFling() {
|
private void stopAppBarLayoutFling() {
|
||||||
OverScroller scroller = getScrollerField();
|
final OverScroller scroller = getScrollerField();
|
||||||
if (scroller != null) {
|
if (scroller != null) {
|
||||||
scroller.forceFinished(true);
|
scroller.forceFinished(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You 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.
|
||||||
|
*/
|
||||||
|
package org.apache.commons.text.similarity;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||||
|
* as Sublime Text, TextMate, Atom and others.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||||
|
* A higher score indicates a higher similarity.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This code has been adapted from Apache Commons Lang 3.3.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*
|
||||||
|
* Note: This class was forked from
|
||||||
|
* <a href="https://git.io/JyYJg">
|
||||||
|
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class FuzzyScore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locale used to change the case of text.
|
||||||
|
*/
|
||||||
|
private final Locale locale;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns a {@link Locale}-specific {@link FuzzyScore}.
|
||||||
|
*
|
||||||
|
* @param locale The string matching logic is case insensitive.
|
||||||
|
A {@link Locale} is necessary to normalize both Strings to lower case.
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
* This is thrown if the {@link Locale} parameter is {@code null}.
|
||||||
|
*/
|
||||||
|
public FuzzyScore(final Locale locale) {
|
||||||
|
if (locale == null) {
|
||||||
|
throw new IllegalArgumentException("Locale must not be null");
|
||||||
|
}
|
||||||
|
this.locale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the Fuzzy Score which indicates the similarity score between two
|
||||||
|
* Strings.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* score.fuzzyScore(null, null) = IllegalArgumentException
|
||||||
|
* score.fuzzyScore("not null", null) = IllegalArgumentException
|
||||||
|
* score.fuzzyScore(null, "not null") = IllegalArgumentException
|
||||||
|
* score.fuzzyScore("", "") = 0
|
||||||
|
* score.fuzzyScore("Workshop", "b") = 0
|
||||||
|
* score.fuzzyScore("Room", "o") = 1
|
||||||
|
* score.fuzzyScore("Workshop", "w") = 1
|
||||||
|
* score.fuzzyScore("Workshop", "ws") = 2
|
||||||
|
* score.fuzzyScore("Workshop", "wo") = 4
|
||||||
|
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param term a full term that should be matched against, must not be null
|
||||||
|
* @param query the query that will be matched against a term, must not be
|
||||||
|
* null
|
||||||
|
* @return result score
|
||||||
|
* @throws IllegalArgumentException if the term or query is {@code null}
|
||||||
|
*/
|
||||||
|
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
|
||||||
|
if (term == null || query == null) {
|
||||||
|
throw new IllegalArgumentException("CharSequences must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||||
|
// case right from the start. Turning characters to lower case
|
||||||
|
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||||
|
// as it does not accept a locale.
|
||||||
|
final String termLowerCase = term.toString().toLowerCase(locale);
|
||||||
|
final String queryLowerCase = query.toString().toLowerCase(locale);
|
||||||
|
|
||||||
|
// the resulting score
|
||||||
|
int score = 0;
|
||||||
|
|
||||||
|
// the position in the term which will be scanned next for potential
|
||||||
|
// query character matches
|
||||||
|
int termIndex = 0;
|
||||||
|
|
||||||
|
// index of the previously matched character in the term
|
||||||
|
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
||||||
|
final char queryChar = queryLowerCase.charAt(queryIndex);
|
||||||
|
|
||||||
|
boolean termCharacterMatchFound = false;
|
||||||
|
for (; termIndex < termLowerCase.length()
|
||||||
|
&& !termCharacterMatchFound; termIndex++) {
|
||||||
|
final char termChar = termLowerCase.charAt(termIndex);
|
||||||
|
|
||||||
|
if (queryChar == termChar) {
|
||||||
|
// simple character matches result in one point
|
||||||
|
score++;
|
||||||
|
|
||||||
|
// subsequent character matches further improve
|
||||||
|
// the score.
|
||||||
|
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousMatchingCharacterIndex = termIndex;
|
||||||
|
|
||||||
|
// we can leave the nested loop. Every character in the
|
||||||
|
// query can match at most one character in the term.
|
||||||
|
termCharacterMatchFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the locale.
|
||||||
|
*
|
||||||
|
* @return The locale
|
||||||
|
*/
|
||||||
|
public Locale getLocale() {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 24.12.15.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
|
||||||
* ActivityCommunicator.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Singleton:
|
|
||||||
* Used to send data between certain Activity/Services within the same process.
|
|
||||||
* This can be considered as an ugly hack inside the Android universe.
|
|
||||||
**/
|
|
||||||
public class ActivityCommunicator {
|
|
||||||
|
|
||||||
private static ActivityCommunicator activityCommunicator;
|
|
||||||
private volatile Class returnActivity;
|
|
||||||
|
|
||||||
public static ActivityCommunicator getCommunicator() {
|
|
||||||
if (activityCommunicator == null) {
|
|
||||||
activityCommunicator = new ActivityCommunicator();
|
|
||||||
}
|
|
||||||
return activityCommunicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Class getReturnActivity() {
|
|
||||||
return returnActivity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReturnActivity(final Class returnActivity) {
|
|
||||||
this.returnActivity = returnActivity;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +1,43 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.NotificationChannelCompat;
|
||||||
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
import org.acra.ACRA;
|
||||||
import org.acra.config.ACRAConfigurationException;
|
|
||||||
import org.acra.config.CoreConfiguration;
|
|
||||||
import org.acra.config.CoreConfigurationBuilder;
|
import org.acra.config.CoreConfigurationBuilder;
|
||||||
import org.acra.sender.ReportSenderFactory;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
|
||||||
import org.schabi.newpipe.util.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import io.reactivex.annotations.NonNull;
|
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||||
import io.reactivex.exceptions.CompositeException;
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||||
import io.reactivex.exceptions.MissingBackpressureException;
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||||
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||||
import io.reactivex.exceptions.UndeliverableException;
|
import io.reactivex.rxjava3.functions.Consumer;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
import io.reactivex.plugins.RxJavaPlugins;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
@ -64,12 +58,13 @@ import io.reactivex.plugins.RxJavaPlugins;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
protected static final String TAG = App.class.toString();
|
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
@SuppressWarnings("unchecked")
|
private static final String TAG = App.class.toString();
|
||||||
private static final Class<? extends ReportSenderFactory>[]
|
|
||||||
REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class};
|
private boolean isFirstRun = false;
|
||||||
private static App app;
|
private static App app;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public static App getApp() {
|
public static App getApp() {
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
@ -77,7 +72,6 @@ public class App extends Application {
|
||||||
@Override
|
@Override
|
||||||
protected void attachBaseContext(final Context base) {
|
protected void attachBaseContext(final Context base) {
|
||||||
super.attachBaseContext(base);
|
super.attachBaseContext(base);
|
||||||
|
|
||||||
initACRA();
|
initACRA();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,30 +81,51 @@ public class App extends Application {
|
||||||
|
|
||||||
app = this;
|
app = this;
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
SettingsActivity.initSettings(this);
|
Log.i(TAG, "This is a phoenix process! "
|
||||||
|
+ "Aborting initialization of App[onCreate]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.getInt(getString(R.string.last_used_preferences_version), -1);
|
||||||
|
isFirstRun = lastUsedPrefVersion == -1;
|
||||||
|
|
||||||
|
// Initialize settings first because other initializations can use its values
|
||||||
|
NewPipeSettings.initSettings(this);
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
NewPipe.init(getDownloader(),
|
||||||
Localization.getPreferredLocalization(this),
|
Localization.getPreferredLocalization(this),
|
||||||
Localization.getPreferredContentCountry(this));
|
Localization.getPreferredContentCountry(this));
|
||||||
Localization.init(getApplicationContext());
|
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||||
|
|
||||||
StateSaver.init(this);
|
StateSaver.init(this);
|
||||||
initNotificationChannel();
|
initNotificationChannels();
|
||||||
|
|
||||||
ServiceHelper.initServices(this);
|
ServiceHelper.initServices(this);
|
||||||
|
|
||||||
// Initialize image loader
|
// Initialize image loader
|
||||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
PicassoHelper.init(this);
|
||||||
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||||
|
prefs.getString(getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default))));
|
||||||
|
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||||
|
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
configureRxJavaErrorHandler();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for new version
|
@Override
|
||||||
new CheckForNewAppVersionTask().execute();
|
public void onTerminate() {
|
||||||
|
super.onTerminate();
|
||||||
|
PicassoHelper.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
protected Downloader getDownloader() {
|
||||||
DownloaderImpl downloader = DownloaderImpl.init(null);
|
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||||
setCookiesToDownloader(downloader);
|
setCookiesToDownloader(downloader);
|
||||||
return downloader;
|
return downloader;
|
||||||
}
|
}
|
||||||
|
@ -119,7 +134,7 @@ public class App extends Application {
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||||
getApplicationContext());
|
getApplicationContext());
|
||||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
||||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +150,7 @@ public class App extends Application {
|
||||||
if (throwable instanceof UndeliverableException) {
|
if (throwable instanceof UndeliverableException) {
|
||||||
// As UndeliverableException is a wrapper,
|
// As UndeliverableException is a wrapper,
|
||||||
// get the cause of it to get the "real" exception
|
// get the cause of it to get the "real" exception
|
||||||
actualThrowable = throwable.getCause();
|
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
||||||
} else {
|
} else {
|
||||||
actualThrowable = throwable;
|
actualThrowable = throwable;
|
||||||
}
|
}
|
||||||
|
@ -144,7 +159,7 @@ public class App extends Application {
|
||||||
if (actualThrowable instanceof CompositeException) {
|
if (actualThrowable instanceof CompositeException) {
|
||||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||||
} else {
|
} else {
|
||||||
errors = Collections.singletonList(actualThrowable);
|
errors = List.of(actualThrowable);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
for (final Throwable error : errors) {
|
||||||
|
@ -191,79 +206,64 @@ public class App extends Application {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
/**
|
||||||
final int diskCacheSizeMb) {
|
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||||
return new ImageLoaderConfiguration.Builder(this)
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
*/
|
||||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
protected void initACRA() {
|
||||||
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
if (ACRA.isACRASenderServiceProcess()) {
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initACRA() {
|
|
||||||
try {
|
|
||||||
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
|
||||||
.setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES)
|
|
||||||
.setBuildConfigClass(BuildConfig.class)
|
|
||||||
.build();
|
|
||||||
ACRA.init(this, acraConfig);
|
|
||||||
} catch (ACRAConfigurationException ace) {
|
|
||||||
ace.printStackTrace();
|
|
||||||
ErrorActivity.reportError(this,
|
|
||||||
ace,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
|
||||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initNotificationChannel() {
|
|
||||||
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String id = getString(R.string.notification_channel_id);
|
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
||||||
final CharSequence name = getString(R.string.notification_channel_name);
|
.withBuildConfigClass(BuildConfig.class);
|
||||||
final String description = getString(R.string.notification_channel_description);
|
ACRA.init(this, acraConfig);
|
||||||
|
|
||||||
// Keep this below DEFAULT to avoid making noise on every notification update
|
|
||||||
final int importance = NotificationManager.IMPORTANCE_LOW;
|
|
||||||
|
|
||||||
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
|
|
||||||
mChannel.setDescription(description);
|
|
||||||
|
|
||||||
NotificationManager mNotificationManager =
|
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
mNotificationManager.createNotificationChannel(mChannel);
|
|
||||||
|
|
||||||
setUpUpdateNotificationChannel(importance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void initNotificationChannels() {
|
||||||
* Set up notification channel for app update.
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
*
|
// the main and update channels
|
||||||
* @param importance
|
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
||||||
*/
|
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
private void setUpUpdateNotificationChannel(final int importance) {
|
.setName(getString(R.string.notification_channel_name))
|
||||||
final String appUpdateId
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
= getString(R.string.app_update_notification_channel_id);
|
.build(),
|
||||||
final CharSequence appUpdateName
|
new NotificationChannelCompat
|
||||||
= getString(R.string.app_update_notification_channel_name);
|
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||||
final String appUpdateDescription
|
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
= getString(R.string.app_update_notification_channel_description);
|
.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()
|
||||||
|
);
|
||||||
|
|
||||||
NotificationChannel appUpdateChannel
|
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||||
= new NotificationChannel(appUpdateId, appUpdateName, importance);
|
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||||
appUpdateChannel.setDescription(appUpdateDescription);
|
|
||||||
|
|
||||||
NotificationManager appUpdateNotificationManager
|
|
||||||
= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isFirstRun() {
|
||||||
|
return isFirstRun;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,21 +10,16 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import leakcanary.AppWatcher;
|
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment {
|
public abstract class BaseFragment extends Fragment {
|
||||||
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
|
||||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
protected AppCompatActivity activity;
|
protected AppCompatActivity activity;
|
||||||
//These values are used for controlling fragments when they are part of the frontpage
|
//These values are used for controlling fragments when they are part of the frontpage
|
||||||
@State
|
@State
|
||||||
protected boolean useAsFrontPage = false;
|
protected boolean useAsFrontPage = false;
|
||||||
private boolean mIsVisibleToUser = false;
|
|
||||||
|
|
||||||
public void useAsFrontPage(final boolean value) {
|
public void useAsFrontPage(final boolean value) {
|
||||||
useAsFrontPage = value;
|
useAsFrontPage = value;
|
||||||
|
@ -35,7 +30,7 @@ public abstract class BaseFragment extends Fragment {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
activity = (AppCompatActivity) context;
|
activity = (AppCompatActivity) context;
|
||||||
}
|
}
|
||||||
|
@ -61,7 +56,7 @@ public abstract class BaseFragment extends Fragment {
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onViewCreated() called with: "
|
Log.d(TAG, "onViewCreated() called with: "
|
||||||
|
@ -73,7 +68,7 @@ public abstract class BaseFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Icepick.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
@ -81,26 +76,33 @@ public abstract class BaseFragment extends Fragment {
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
|
||||||
mIsVisibleToUser = isVisibleToUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Init
|
// 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) {
|
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() {
|
protected void initListeners() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,16 +114,26 @@ public abstract class BaseFragment extends Fragment {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||||
}
|
}
|
||||||
if ((!useAsFrontPage || mIsVisibleToUser)
|
if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
|
||||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
|
||||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||||
activity.getSupportActionBar().setTitle(title);
|
activity.getSupportActionBar().setTitle(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
protected FragmentManager getFM() {
|
||||||
return getParentFragment() == null
|
Fragment current = this;
|
||||||
? getFragmentManager()
|
while (current.getParentFragment() != null) {
|
||||||
: getParentFragment().getFragmentManager();
|
current = current.getParentFragment();
|
||||||
|
}
|
||||||
|
return current.getFragmentManager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,221 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.Signature;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
|
||||||
import com.grack.nanojson.JsonParser;
|
|
||||||
import com.grack.nanojson.JsonParserException;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
|
||||||
import org.schabi.newpipe.report.UserAction;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
|
||||||
* If there is a newer version we show a notification, informing the user. On tapping
|
|
||||||
* the notification, the user will be directed to the download link.
|
|
||||||
*/
|
|
||||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final Application APP = App.getApp();
|
|
||||||
private static final String GITHUB_APK_SHA1
|
|
||||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
|
||||||
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
|
||||||
*
|
|
||||||
* @return String with the apk's SHA1 fingeprint in hexadecimal
|
|
||||||
*/
|
|
||||||
private static String getCertificateSHA1Fingerprint() {
|
|
||||||
final PackageManager pm = APP.getPackageManager();
|
|
||||||
final String packageName = APP.getPackageName();
|
|
||||||
final int flags = PackageManager.GET_SIGNATURES;
|
|
||||||
PackageInfo packageInfo = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
packageInfo = pm.getPackageInfo(packageName, flags);
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
ErrorActivity.reportError(APP, e, null, null,
|
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
|
||||||
"Could not find package info", R.string.app_ui_crash));
|
|
||||||
}
|
|
||||||
|
|
||||||
final Signature[] signatures = packageInfo.signatures;
|
|
||||||
final byte[] cert = signatures[0].toByteArray();
|
|
||||||
final InputStream input = new ByteArrayInputStream(cert);
|
|
||||||
|
|
||||||
X509Certificate c = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
|
||||||
c = (X509Certificate) cf.generateCertificate(input);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
ErrorActivity.reportError(APP, e, null, null,
|
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
|
||||||
"Certificate error", R.string.app_ui_crash));
|
|
||||||
}
|
|
||||||
|
|
||||||
String hexString = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
final byte[] publicKey = md.digest(c.getEncoded());
|
|
||||||
hexString = byte2HexFormatted(publicKey);
|
|
||||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
|
||||||
ErrorActivity.reportError(APP, e, null, null,
|
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
|
||||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
|
||||||
}
|
|
||||||
|
|
||||||
return hexString;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String byte2HexFormatted(final byte[] arr) {
|
|
||||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
|
||||||
|
|
||||||
for (int i = 0; i < arr.length; i++) {
|
|
||||||
String h = Integer.toHexString(arr[i]);
|
|
||||||
final int l = h.length();
|
|
||||||
if (l == 1) {
|
|
||||||
h = "0" + h;
|
|
||||||
}
|
|
||||||
if (l > 2) {
|
|
||||||
h = h.substring(l - 2, l);
|
|
||||||
}
|
|
||||||
str.append(h.toUpperCase());
|
|
||||||
if (i < (arr.length - 1)) {
|
|
||||||
str.append(':');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isGithubApk() {
|
|
||||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP);
|
|
||||||
|
|
||||||
// Check if user has enabled/disabled update checking
|
|
||||||
// and if the current apk is a github one or not.
|
|
||||||
if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) {
|
|
||||||
this.cancel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(final Void... voids) {
|
|
||||||
if (isCancelled() || !isConnected()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a network request to get latest NewPipe data.
|
|
||||||
try {
|
|
||||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
|
||||||
} catch (IOException | ReCaptchaException e) {
|
|
||||||
// connectivity problems, do not alarm user and fail silently
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.w(TAG, Log.getStackTraceString(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(final String response) {
|
|
||||||
// Parse the json from the response.
|
|
||||||
if (response != null) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
final JsonObject githubStableObject = JsonParser.object().from(response)
|
|
||||||
.getObject("flavors").getObject("github").getObject("stable");
|
|
||||||
|
|
||||||
final String versionName = githubStableObject.getString("version");
|
|
||||||
final int versionCode = githubStableObject.getInt("version_code");
|
|
||||||
final String apkLocationUrl = githubStableObject.getString("apk");
|
|
||||||
|
|
||||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
|
||||||
|
|
||||||
} catch (JsonParserException e) {
|
|
||||||
// connectivity problems, do not alarm user and fail silently
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.w(TAG, Log.getStackTraceString(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to compare the current and latest available app version.
|
|
||||||
* If a newer version is available, we show the update notification.
|
|
||||||
*
|
|
||||||
* @param versionName Name of new version
|
|
||||||
* @param apkLocationUrl Url with the new apk
|
|
||||||
* @param versionCode Code of new version
|
|
||||||
*/
|
|
||||||
private void compareAppVersionAndShowNotification(final String versionName,
|
|
||||||
final String apkLocationUrl,
|
|
||||||
final int versionCode) {
|
|
||||||
int notificationId = 2000;
|
|
||||||
|
|
||||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
|
||||||
|
|
||||||
// A pending intent to open the apk location url in the browser.
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
|
||||||
final PendingIntent pendingIntent
|
|
||||||
= PendingIntent.getActivity(APP, 0, intent, 0);
|
|
||||||
|
|
||||||
final NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
|
||||||
.Builder(APP, APP.getString(R.string.app_update_notification_channel_id))
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentTitle(APP.getString(R.string.app_update_notification_content_title))
|
|
||||||
.setContentText(APP.getString(R.string.app_update_notification_content_text)
|
|
||||||
+ " " + versionName);
|
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager
|
|
||||||
= NotificationManagerCompat.from(APP);
|
|
||||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConnected() {
|
|
||||||
final ConnectivityManager cm =
|
|
||||||
(ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
return cm.getActiveNetworkInfo() != null
|
|
||||||
&& cm.getActiveNetworkInfo().isConnected();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +1,45 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.extractor.downloader.Request;
|
import org.schabi.newpipe.extractor.downloader.Request;
|
||||||
import org.schabi.newpipe.extractor.downloader.Response;
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.util.CookieUtils;
|
|
||||||
import org.schabi.newpipe.util.InfoCache;
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
|
|
||||||
import okhttp3.CipherSuite;
|
|
||||||
import okhttp3.ConnectionSpec;
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
public final class DownloaderImpl extends Downloader {
|
public final class DownloaderImpl extends Downloader {
|
||||||
public static final String USER_AGENT
|
public static final String USER_AGENT =
|
||||||
= "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
|
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||||
= "youtube_restricted_mode_key";
|
"youtube_restricted_mode_key";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||||
|
|
||||||
private static DownloaderImpl instance;
|
private static DownloaderImpl instance;
|
||||||
private Map<String, String> mCookies;
|
private final Map<String, String> mCookies;
|
||||||
private OkHttpClient client;
|
private final OkHttpClient client;
|
||||||
|
|
||||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
|
||||||
enableModernTLS(builder);
|
|
||||||
}
|
|
||||||
this.client = builder
|
this.client = builder
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||||
|
@ -81,69 +64,16 @@ public final class DownloaderImpl extends Downloader {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
|
|
||||||
* from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
|
|
||||||
* <p>
|
|
||||||
* If there is an error, the function will safely fall back to doing nothing
|
|
||||||
* and printing the error to the console.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
|
||||||
*/
|
|
||||||
private static void enableModernTLS(final OkHttpClient.Builder builder) {
|
|
||||||
try {
|
|
||||||
// get the default TrustManager
|
|
||||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
|
||||||
TrustManagerFactory.getDefaultAlgorithm());
|
|
||||||
trustManagerFactory.init((KeyStore) null);
|
|
||||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
|
||||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
|
||||||
throw new IllegalStateException("Unexpected default trust managers:"
|
|
||||||
+ Arrays.toString(trustManagers));
|
|
||||||
}
|
|
||||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
|
||||||
|
|
||||||
// insert our own TLSSocketFactory
|
|
||||||
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
|
||||||
|
|
||||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
|
||||||
|
|
||||||
// This will try to enable all modern CipherSuites(+2 more)
|
|
||||||
// that are supported on the device.
|
|
||||||
// Necessary because some servers (e.g. Framatube.org)
|
|
||||||
// don't support the old cipher suites.
|
|
||||||
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
|
||||||
List<CipherSuite> cipherSuites = new ArrayList<>();
|
|
||||||
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
|
|
||||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
|
||||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
|
||||||
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
|
||||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
|
||||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCookies(final String url) {
|
public String getCookies(final String url) {
|
||||||
List<String> resultCookies = new ArrayList<>();
|
final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
|
||||||
if (url.contains(YOUTUBE_DOMAIN)) {
|
? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
|
||||||
String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
|
||||||
if (youtubeCookie != null) {
|
|
||||||
resultCookies.add(youtubeCookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||||
String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
|
||||||
if (recaptchaCookie != null) {
|
.filter(Objects::nonNull)
|
||||||
resultCookies.add(recaptchaCookie);
|
.flatMap(cookies -> Arrays.stream(cookies.split("; *")))
|
||||||
}
|
.distinct()
|
||||||
return CookieUtils.concatCookies(resultCookies);
|
.collect(Collectors.joining("; "));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCookie(final String key) {
|
public String getCookie(final String key) {
|
||||||
|
@ -159,9 +89,9 @@ public final class DownloaderImpl extends Downloader {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateYoutubeRestrictedModeCookies(final Context context) {
|
public void updateYoutubeRestrictedModeCookies(final Context context) {
|
||||||
String restrictedModeEnabledKey =
|
final String restrictedModeEnabledKey =
|
||||||
context.getString(R.string.youtube_restricted_mode_enabled);
|
context.getString(R.string.youtube_restricted_mode_enabled);
|
||||||
boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
final boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getBoolean(restrictedModeEnabledKey, false);
|
.getBoolean(restrictedModeEnabledKey, false);
|
||||||
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
|
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
|
||||||
}
|
}
|
||||||
|
@ -186,43 +116,13 @@ public final class DownloaderImpl extends Downloader {
|
||||||
try {
|
try {
|
||||||
final Response response = head(url);
|
final Response response = head(url);
|
||||||
return Long.parseLong(response.getHeader("Content-Length"));
|
return Long.parseLong(response.getHeader("Content-Length"));
|
||||||
} catch (NumberFormatException e) {
|
} catch (final NumberFormatException e) {
|
||||||
throw new IOException("Invalid content length", e);
|
throw new IOException("Invalid content length", e);
|
||||||
} catch (ReCaptchaException e) {
|
} catch (final ReCaptchaException e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream stream(final String siteUrl) throws IOException {
|
|
||||||
try {
|
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
|
||||||
.method("GET", null).url(siteUrl)
|
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
|
||||||
|
|
||||||
String cookies = getCookies(siteUrl);
|
|
||||||
if (!cookies.isEmpty()) {
|
|
||||||
requestBuilder.addHeader("Cookie", cookies);
|
|
||||||
}
|
|
||||||
|
|
||||||
final okhttp3.Request request = requestBuilder.build();
|
|
||||||
final okhttp3.Response response = client.newCall(request).execute();
|
|
||||||
final ResponseBody body = response.body();
|
|
||||||
|
|
||||||
if (response.code() == 429) {
|
|
||||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body == null) {
|
|
||||||
response.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return body.byteStream();
|
|
||||||
} catch (ReCaptchaException e) {
|
|
||||||
throw new IOException(e.getMessage(), e.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response execute(@NonNull final Request request)
|
public Response execute(@NonNull final Request request)
|
||||||
throws IOException, ReCaptchaException {
|
throws IOException, ReCaptchaException {
|
||||||
|
@ -233,25 +133,25 @@ public final class DownloaderImpl extends Downloader {
|
||||||
|
|
||||||
RequestBody requestBody = null;
|
RequestBody requestBody = null;
|
||||||
if (dataToSend != null) {
|
if (dataToSend != null) {
|
||||||
requestBody = RequestBody.create(null, dataToSend);
|
requestBody = RequestBody.create(dataToSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody).url(url)
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
String cookies = getCookies(url);
|
final String cookies = getCookies(url);
|
||||||
if (!cookies.isEmpty()) {
|
if (!cookies.isEmpty()) {
|
||||||
requestBuilder.addHeader("Cookie", cookies);
|
requestBuilder.addHeader("Cookie", cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||||
final String headerName = pair.getKey();
|
final String headerName = pair.getKey();
|
||||||
final List<String> headerValueList = pair.getValue();
|
final List<String> headerValueList = pair.getValue();
|
||||||
|
|
||||||
if (headerValueList.size() > 1) {
|
if (headerValueList.size() > 1) {
|
||||||
requestBuilder.removeHeader(headerName);
|
requestBuilder.removeHeader(headerName);
|
||||||
for (String headerValue : headerValueList) {
|
for (final String headerValue : headerValueList) {
|
||||||
requestBuilder.addHeader(headerName, headerValue);
|
requestBuilder.addHeader(headerName, headerValue);
|
||||||
}
|
}
|
||||||
} else if (headerValueList.size() == 1) {
|
} else if (headerValueList.size() == 1) {
|
||||||
|
|
|
@ -3,9 +3,10 @@ package org.schabi.newpipe;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
* ExitActivity.java is part of NewPipe.
|
* ExitActivity.java is part of NewPipe.
|
||||||
|
@ -27,7 +28,7 @@ import android.os.Bundle;
|
||||||
public class ExitActivity extends Activity {
|
public class ExitActivity extends Activity {
|
||||||
|
|
||||||
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
||||||
Intent intent = new Intent(activity, ExitActivity.class);
|
final Intent intent = new Intent(activity, ExitActivity.class);
|
||||||
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
@ -42,12 +43,8 @@ public class ExitActivity extends Activity {
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
finishAndRemoveTask();
|
||||||
finishAndRemoveTask();
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.exit(0);
|
NavigationHelper.restartApp(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class ImageDownloader extends BaseImageDownloader {
|
|
||||||
private final Resources resources;
|
|
||||||
private final SharedPreferences preferences;
|
|
||||||
private final String downloadThumbnailKey;
|
|
||||||
|
|
||||||
public ImageDownloader(final Context context) {
|
|
||||||
super(context);
|
|
||||||
this.resources = context.getResources();
|
|
||||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDownloadingThumbnail() {
|
|
||||||
return preferences.getBoolean(downloadThumbnailKey, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ResourceType")
|
|
||||||
@Override
|
|
||||||
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
|
||||||
if (isDownloadingThumbnail()) {
|
|
||||||
return super.getStream(imageUri, extra);
|
|
||||||
} else {
|
|
||||||
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
|
||||||
throws IOException {
|
|
||||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
|
||||||
return downloader.stream(imageUri);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,15 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||||
|
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.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
@ -8,10 +18,6 @@ import androidx.room.Room;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
|
||||||
|
|
||||||
public final class NewPipeDatabase {
|
public final class NewPipeDatabase {
|
||||||
private static volatile AppDatabase databaseInstance;
|
private static volatile AppDatabase databaseInstance;
|
||||||
|
|
||||||
|
@ -22,7 +28,8 @@ public final class NewPipeDatabase {
|
||||||
private static AppDatabase getDatabase(final Context context) {
|
private static AppDatabase getDatabase(final Context context) {
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
|
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,9 +53,20 @@ public final class NewPipeDatabase {
|
||||||
if (databaseInstance == null) {
|
if (databaseInstance == null) {
|
||||||
throw new IllegalStateException("database is not initialized");
|
throw new IllegalStateException("database is not initialized");
|
||||||
}
|
}
|
||||||
Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||||
if (c.moveToFirst() && c.getInt(0) == 1) {
|
if (c.moveToFirst() && c.getInt(0) == 1) {
|
||||||
throw new RuntimeException("Checkpoint was blocked from completing");
|
throw new RuntimeException("Checkpoint was blocked from completing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void close() {
|
||||||
|
if (databaseInstance != null) {
|
||||||
|
synchronized (NewPipeDatabase.class) {
|
||||||
|
if (databaseInstance != null) {
|
||||||
|
databaseInstance.close();
|
||||||
|
databaseInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
178
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
178
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.PendingIntentCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
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
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NewVersionWorker(
|
||||||
|
context: Context,
|
||||||
|
workerParams: WorkerParameters
|
||||||
|
) : Worker(context, workerParams) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to compare the current and latest available app version.
|
||||||
|
* If a newer version is available, we show the update notification.
|
||||||
|
*
|
||||||
|
* @param versionName Name of new version
|
||||||
|
* @param apkLocationUrl Url with the new apk
|
||||||
|
* @param versionCode Code of new version
|
||||||
|
*/
|
||||||
|
private fun compareAppVersionAndShowNotification(
|
||||||
|
versionName: String,
|
||||||
|
apkLocationUrl: String?,
|
||||||
|
versionCode: Int
|
||||||
|
) {
|
||||||
|
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||||
|
if (inputData.getBoolean(IS_MANUAL, false)) {
|
||||||
|
// Show toast stating that the app is up-to-date if the update check was manual.
|
||||||
|
ContextCompat.getMainExecutor(applicationContext).execute {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext, R.string.app_update_unavailable_toast,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pending intent to open the apk location url in the browser.
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val pendingIntent = PendingIntentCompat.getActivity(
|
||||||
|
applicationContext, 0, intent, 0, false
|
||||||
|
)
|
||||||
|
val channelId = applicationContext.getString(R.string.app_update_notification_channel_id)
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setContentTitle(
|
||||||
|
applicationContext.getString(R.string.app_update_available_notification_title)
|
||||||
|
)
|
||||||
|
.setContentText(
|
||||||
|
applicationContext.getString(
|
||||||
|
R.string.app_update_available_notification_text, versionName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
notificationManager.notify(2000, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
|
private fun checkNewVersion() {
|
||||||
|
// Check if the current apk is a github one or not.
|
||||||
|
if (!ReleaseVersionUtil.isReleaseApk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputData.getBoolean(IS_MANUAL, false)) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
// 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 (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a network request to get latest NewPipe data.
|
||||||
|
val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL)
|
||||||
|
handleResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResponse(response: Response) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
try {
|
||||||
|
// Store a timestamp which needs to be exceeded,
|
||||||
|
// before a new request to the API is made.
|
||||||
|
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
|
prefs.edit {
|
||||||
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Could not extract and save new expiry date", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the json from the response.
|
||||||
|
try {
|
||||||
|
val newpipeVersionInfo = JsonParser.`object`()
|
||||||
|
.from(response.responseBody()).getObject("flavors")
|
||||||
|
.getObject("newpipe")
|
||||||
|
|
||||||
|
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.
|
||||||
|
// Do not alarm user and fail silently.
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Could not get NewPipe API: invalid json", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
checkNewVersion()
|
||||||
|
Result.success()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e)
|
||||||
|
Result.failure()
|
||||||
|
} catch (e: ReCaptchaException) {
|
||||||
|
Log.e(TAG, "ReCaptchaException should never happen here.", e)
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
private val TAG = NewVersionWorker::class.java.simpleName
|
||||||
|
private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json"
|
||||||
|
private const val IS_MANUAL = "isManual"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new worker which checks if all conditions for performing a version check are met,
|
||||||
|
* fetches the API endpoint [.NEWPIPE_API_URL] containing info about the latest NewPipe
|
||||||
|
* version and displays a notification about an available update if one is available.
|
||||||
|
* <br></br>
|
||||||
|
* Following conditions need to be met, before data is requested from the server:
|
||||||
|
*
|
||||||
|
* * The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||||
|
* If the signing key differs from the one used upstream, the update cannot be installed.
|
||||||
|
* * The user enabled searching for and notifying about updates in the settings.
|
||||||
|
* * The app did not recently check for updates.
|
||||||
|
* We do not want to make unnecessary connections and DOS our servers.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun enqueueNewVersionCheckingWork(context: Context, isManual: Boolean) {
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<NewVersionWorker>()
|
||||||
|
.setInputData(workDataOf(IS_MANUAL to isManual))
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueue(workRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -31,7 +30,7 @@ public class PanicResponderActivity extends Activity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||||
// TODO: Explicitly clear the search results
|
// TODO: Explicitly clear the search results
|
||||||
// once they are restored when the app restarts
|
// once they are restored when the app restarts
|
||||||
|
@ -40,10 +39,6 @@ public class PanicResponderActivity extends Activity {
|
||||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
finishAndRemoveTask();
|
||||||
finishAndRemoveTask();
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
94
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
94
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
|
||||||
|
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.PopupMenu;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.SparseItemUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class QueueItemMenuUtil {
|
||||||
|
private QueueItemMenuUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openPopupMenu(final PlayQueue playQueue,
|
||||||
|
final PlayQueueItem item,
|
||||||
|
final View view,
|
||||||
|
final boolean hideDetails,
|
||||||
|
final FragmentManager fragmentManager,
|
||||||
|
final Context context) {
|
||||||
|
final ContextThemeWrapper themeWrapper =
|
||||||
|
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||||
|
|
||||||
|
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||||
|
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||||
|
|
||||||
|
if (hideDetails) {
|
||||||
|
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||||
|
switch (menuItem.getItemId()) {
|
||||||
|
case R.id.menu_item_remove:
|
||||||
|
final int index = playQueue.indexOf(item);
|
||||||
|
playQueue.remove(index);
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_details:
|
||||||
|
// playQueue is null since we don't want any queue change
|
||||||
|
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||||
|
item.getUrl(), item.getTitle(), null,
|
||||||
|
false);
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_append_playlist:
|
||||||
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
|
context,
|
||||||
|
List.of(new StreamEntity(item)),
|
||||||
|
dialog -> dialog.show(
|
||||||
|
fragmentManager,
|
||||||
|
"QueueItemMenuUtil@append_playlist"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_channel_details:
|
||||||
|
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||||
|
item.getUrl(), item.getUploaderUrl(),
|
||||||
|
// An intent must be used here.
|
||||||
|
// Opening with FragmentManager transactions is not working,
|
||||||
|
// as PlayQueueActivity doesn't use fragments.
|
||||||
|
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||||
|
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_share:
|
||||||
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
|
item.getThumbnails());
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_download:
|
||||||
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
|
info -> {
|
||||||
|
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
||||||
|
info);
|
||||||
|
downloadDialog.show(fragmentManager, "downloadDialog");
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
popupMenu.show();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,201 +0,0 @@
|
||||||
package org.schabi.newpipe.about;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
|
|
||||||
|
|
||||||
public class AboutActivity extends AppCompatActivity {
|
|
||||||
/**
|
|
||||||
* List of all software components.
|
|
||||||
*/
|
|
||||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
|
||||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
|
||||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
|
||||||
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
|
||||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
|
||||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
|
|
||||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
|
||||||
new SoftwareComponent("Rhino", "2015", "Mozilla",
|
|
||||||
"https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
|
||||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
|
|
||||||
"http://www.acra.ch", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
|
||||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
|
||||||
StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
|
||||||
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
|
||||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
|
||||||
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
|
|
||||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
|
|
||||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
|
|
||||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
|
|
||||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
|
||||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
|
|
||||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
|
|
||||||
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
|
|
||||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link PagerAdapter} that will provide
|
|
||||||
* fragments for each of the sections. We use a
|
|
||||||
* {@link FragmentPagerAdapter} derivative, which will keep every
|
|
||||||
* loaded fragment in memory. If this becomes too memory intensive, it
|
|
||||||
* may be best to switch to a
|
|
||||||
* {@link FragmentStatePagerAdapter}.
|
|
||||||
*/
|
|
||||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link ViewPager} that will host the section contents.
|
|
||||||
*/
|
|
||||||
private ViewPager mViewPager;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
assureCorrectAppLanguage(this);
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
ThemeHelper.setTheme(this);
|
|
||||||
this.setTitle(getString(R.string.title_activity_about));
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_about);
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
// Create the adapter that will return a fragment for each of the three
|
|
||||||
// primary sections of the activity.
|
|
||||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
|
||||||
|
|
||||||
// Set up the ViewPager with the sections adapter.
|
|
||||||
mViewPager = findViewById(R.id.container);
|
|
||||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
|
||||||
|
|
||||||
TabLayout tabLayout = findViewById(R.id.tabs);
|
|
||||||
tabLayout.setupWithViewPager(mViewPager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
|
|
||||||
switch (id) {
|
|
||||||
case android.R.id.home:
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A placeholder fragment containing a simple view.
|
|
||||||
*/
|
|
||||||
public static class AboutFragment extends Fragment {
|
|
||||||
public AboutFragment() { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created a new instance of this fragment for the given section number.
|
|
||||||
*
|
|
||||||
* @return New instance of {@link AboutFragment}
|
|
||||||
*/
|
|
||||||
public static AboutFragment newInstance() {
|
|
||||||
return new AboutFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
|
||||||
final Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
|
||||||
Context context = this.getContext();
|
|
||||||
|
|
||||||
TextView version = rootView.findViewById(R.id.app_version);
|
|
||||||
version.setText(BuildConfig.VERSION_NAME);
|
|
||||||
|
|
||||||
View githubLink = rootView.findViewById(R.id.github_link);
|
|
||||||
githubLink.setOnClickListener(nv ->
|
|
||||||
openUrlInBrowser(context, context.getString(R.string.github_url)));
|
|
||||||
|
|
||||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
|
||||||
donationLink.setOnClickListener(v ->
|
|
||||||
openUrlInBrowser(context, context.getString(R.string.donation_url)));
|
|
||||||
|
|
||||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
|
||||||
websiteLink.setOnClickListener(nv ->
|
|
||||||
openUrlInBrowser(context, context.getString(R.string.website_url)));
|
|
||||||
|
|
||||||
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
|
||||||
privacyPolicyLink.setOnClickListener(v ->
|
|
||||||
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
|
|
||||||
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
|
||||||
* one of the sections/tabs/pages.
|
|
||||||
*/
|
|
||||||
public class SectionsPagerAdapter extends FragmentPagerAdapter {
|
|
||||||
public SectionsPagerAdapter(final FragmentManager fm) {
|
|
||||||
super(fm);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Fragment getItem(final int position) {
|
|
||||||
switch (position) {
|
|
||||||
case 0:
|
|
||||||
return AboutFragment.newInstance();
|
|
||||||
case 1:
|
|
||||||
return LicenseFragment.newInstance(SOFTWARE_COMPONENTS);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
// Show 2 total pages.
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getPageTitle(final int position) {
|
|
||||||
switch (position) {
|
|
||||||
case 0:
|
|
||||||
return getString(R.string.tab_about);
|
|
||||||
case 1:
|
|
||||||
return getString(R.string.tab_licenses);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
199
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Normal file
199
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||||
|
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Localization.assureCorrectAppLanguage(this)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
title = getString(R.string.title_activity_about)
|
||||||
|
|
||||||
|
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
|
setContentView(aboutBinding.root)
|
||||||
|
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
// Create the adapter that will return a fragment for each of the three
|
||||||
|
// primary sections of the activity.
|
||||||
|
val mAboutStateAdapter = AboutStateAdapter(this)
|
||||||
|
// Set up the ViewPager with the sections adapter.
|
||||||
|
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
||||||
|
TabLayoutMediator(
|
||||||
|
aboutBinding.aboutTabLayout,
|
||||||
|
aboutBinding.aboutViewPager2
|
||||||
|
) { tab, position ->
|
||||||
|
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
||||||
|
}.attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == android.R.id.home) {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder fragment containing a simple view.
|
||||||
|
*/
|
||||||
|
class AboutFragment : Fragment() {
|
||||||
|
private fun Button.openLink(@StringRes url: Int) {
|
||||||
|
setOnClickListener {
|
||||||
|
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
||||||
|
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
||||||
|
aboutGithubLink.openLink(R.string.github_url)
|
||||||
|
aboutDonationLink.openLink(R.string.donation_url)
|
||||||
|
aboutWebsiteLink.openLink(R.string.website_url)
|
||||||
|
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||||
|
faqLink.openLink(R.string.faq_url)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
||||||
|
* one of the sections/tabs/pages.
|
||||||
|
*/
|
||||||
|
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
|
private val posAbout = 0
|
||||||
|
private val posLicense = 1
|
||||||
|
private val totalCount = 2
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> AboutFragment()
|
||||||
|
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||||
|
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
// Show 2 total pages.
|
||||||
|
return totalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageTitle(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> R.string.tab_about
|
||||||
|
posLicense -> R.string.tab_licenses
|
||||||
|
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* List of all software components.
|
||||||
|
*/
|
||||||
|
private val SOFTWARE_COMPONENTS = arrayListOf(
|
||||||
|
SoftwareComponent(
|
||||||
|
"ACRA", "2013", "Kevin Gaudin",
|
||||||
|
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
||||||
|
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
||||||
|
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"GigaGet", "2014 - 2015", "Peter Cai",
|
||||||
|
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Groupie", "2016", "Lisa Wray",
|
||||||
|
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Icepick", "2015", "Frankie Sardo",
|
||||||
|
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||||
|
"https://github.com/jhy/jsoup", StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Markwon", "2019", "Dimitry Ivanov",
|
||||||
|
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Material Components for Android", "2016 - 2020", "Google, Inc.",
|
||||||
|
"https://github.com/material-components/material-components-android",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||||
|
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||||
|
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"OkHttp", "2019", "Square, Inc.",
|
||||||
|
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Picasso", "2013", "Square, Inc.",
|
||||||
|
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||||
|
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ProcessPhoenix", "2015", "Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxAndroid", "2015", "The RxAndroid authors",
|
||||||
|
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxBinding", "2015", "Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||||
|
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"SearchPreference", "2018", "ByteHamster",
|
||||||
|
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
package org.schabi.newpipe.about;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for storing information about a software license.
|
|
||||||
*/
|
|
||||||
public class License implements Parcelable {
|
|
||||||
public static final Creator<License> CREATOR = new Creator<License>() {
|
|
||||||
@Override
|
|
||||||
public License createFromParcel(final Parcel source) {
|
|
||||||
return new License(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public License[] newArray(final int size) {
|
|
||||||
return new License[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private final String abbreviation;
|
|
||||||
private final String name;
|
|
||||||
private String filename;
|
|
||||||
|
|
||||||
public License(final String name, final String abbreviation, final String filename) {
|
|
||||||
if (name == null) {
|
|
||||||
throw new NullPointerException("name is null");
|
|
||||||
}
|
|
||||||
if (abbreviation == null) {
|
|
||||||
throw new NullPointerException("abbreviation is null");
|
|
||||||
}
|
|
||||||
if (filename == null) {
|
|
||||||
throw new NullPointerException("filename is null");
|
|
||||||
}
|
|
||||||
this.name = name;
|
|
||||||
this.filename = filename;
|
|
||||||
this.abbreviation = abbreviation;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected License(final Parcel in) {
|
|
||||||
this.filename = in.readString();
|
|
||||||
this.abbreviation = in.readString();
|
|
||||||
this.name = in.readString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getContentUri() {
|
|
||||||
return new Uri.Builder()
|
|
||||||
.scheme("file")
|
|
||||||
.path("/android_asset")
|
|
||||||
.appendPath(filename)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAbbreviation() {
|
|
||||||
return abbreviation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilename() {
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(final Parcel dest, final int flags) {
|
|
||||||
dest.writeString(this.filename);
|
|
||||||
dest.writeString(this.abbreviation);
|
|
||||||
dest.writeString(this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for storing information about a software license.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
|
@ -1,119 +0,0 @@
|
||||||
package org.schabi.newpipe.about;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.ContextMenu;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment containing the software licenses.
|
|
||||||
*/
|
|
||||||
public class LicenseFragment extends Fragment {
|
|
||||||
private static final String ARG_COMPONENTS = "components";
|
|
||||||
private SoftwareComponent[] softwareComponents;
|
|
||||||
private SoftwareComponent componentForContextMenu;
|
|
||||||
|
|
||||||
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
|
|
||||||
if (softwareComponents == null) {
|
|
||||||
throw new NullPointerException("softwareComponents is null");
|
|
||||||
}
|
|
||||||
LicenseFragment fragment = new LicenseFragment();
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents);
|
|
||||||
fragment.setArguments(bundle);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a popup containing the license.
|
|
||||||
*
|
|
||||||
* @param context the context to use
|
|
||||||
* @param license the license to show
|
|
||||||
*/
|
|
||||||
private static void showLicense(final Context context, final License license) {
|
|
||||||
new LicenseFragmentHelper((Activity) context).execute(license);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
softwareComponents = (SoftwareComponent[]) getArguments()
|
|
||||||
.getParcelableArray(ARG_COMPONENTS);
|
|
||||||
|
|
||||||
// Sort components by name
|
|
||||||
Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
|
||||||
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
|
||||||
|
|
||||||
final View licenseLink = rootView.findViewById(R.id.app_read_license);
|
|
||||||
licenseLink.setOnClickListener(v ->
|
|
||||||
showLicense(getActivity(), StandardLicenses.GPL3));
|
|
||||||
|
|
||||||
for (final SoftwareComponent component : softwareComponents) {
|
|
||||||
final View componentView = inflater
|
|
||||||
.inflate(R.layout.item_software_component, container, false);
|
|
||||||
final TextView softwareName = componentView.findViewById(R.id.name);
|
|
||||||
final TextView copyright = componentView.findViewById(R.id.copyright);
|
|
||||||
softwareName.setText(component.getName());
|
|
||||||
copyright.setText(getString(R.string.copyright,
|
|
||||||
component.getYears(),
|
|
||||||
component.getCopyrightOwner(),
|
|
||||||
component.getLicense().getAbbreviation()));
|
|
||||||
|
|
||||||
componentView.setTag(component);
|
|
||||||
componentView.setOnClickListener(v ->
|
|
||||||
showLicense(getActivity(), component.getLicense()));
|
|
||||||
softwareComponentsView.addView(componentView);
|
|
||||||
registerForContextMenu(componentView);
|
|
||||||
}
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(final ContextMenu menu, final View v,
|
|
||||||
final ContextMenu.ContextMenuInfo menuInfo) {
|
|
||||||
final MenuInflater inflater = getActivity().getMenuInflater();
|
|
||||||
final SoftwareComponent component = (SoftwareComponent) v.getTag();
|
|
||||||
menu.setHeaderTitle(component.getName());
|
|
||||||
inflater.inflate(R.menu.software_component, menu);
|
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
|
||||||
componentForContextMenu = (SoftwareComponent) v.getTag();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onContextItemSelected(final MenuItem item) {
|
|
||||||
// item.getMenuInfo() is null so we use the tag of the view
|
|
||||||
final SoftwareComponent component = componentForContextMenu;
|
|
||||||
if (component == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.action_website:
|
|
||||||
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
|
|
||||||
return true;
|
|
||||||
case R.id.action_show_license:
|
|
||||||
showLicense(getActivity(), component.getLicense());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
140
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
140
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
|
import org.schabi.newpipe.ktx.parcelableArrayList
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment containing the software licenses.
|
||||||
|
*/
|
||||||
|
class LicenseFragment : Fragment() {
|
||||||
|
private lateinit var softwareComponents: List<SoftwareComponent>
|
||||||
|
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
||||||
|
.sortedBy { it.name } // Sort components by name
|
||||||
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
compositeDisposable.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||||
|
binding.licensesAppReadLicense.setOnClickListener {
|
||||||
|
compositeDisposable.add(
|
||||||
|
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (component in softwareComponents) {
|
||||||
|
val componentBinding = ItemSoftwareComponentBinding
|
||||||
|
.inflate(inflater, container, false)
|
||||||
|
componentBinding.name.text = component.name
|
||||||
|
componentBinding.copyright.text = getString(
|
||||||
|
R.string.copyright,
|
||||||
|
component.years,
|
||||||
|
component.copyrightOwner,
|
||||||
|
component.license.abbreviation
|
||||||
|
)
|
||||||
|
val root: View = componentBinding.root
|
||||||
|
root.tag = component
|
||||||
|
root.setOnClickListener {
|
||||||
|
compositeDisposable.add(
|
||||||
|
showLicense(component)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.licensesSoftwareComponents.addView(root)
|
||||||
|
registerForContextMenu(root)
|
||||||
|
}
|
||||||
|
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
|
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
softwareComponent: SoftwareComponent
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
val context = requireContext()
|
||||||
|
activeSoftwareComponent = softwareComponent
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData = Base64.encodeToString(
|
||||||
|
formattedLicense.toByteArray(), Base64.NO_PADDING
|
||||||
|
)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
Localization.assureCorrectAppLanguage(context)
|
||||||
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(softwareComponent.name)
|
||||||
|
.setView(webView)
|
||||||
|
.setOnCancelListener { activeSoftwareComponent = null }
|
||||||
|
.setOnDismissListener { activeSoftwareComponent = null }
|
||||||
|
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
||||||
|
|
||||||
|
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
||||||
|
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_COMPONENTS = "components"
|
||||||
|
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
||||||
|
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||||
|
"NewPipe",
|
||||||
|
"2014-2023",
|
||||||
|
"Team NewPipe",
|
||||||
|
"https://newpipe.net/",
|
||||||
|
StandardLicenses.GPL3,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||||
|
val fragment = LicenseFragment()
|
||||||
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,128 +0,0 @@
|
||||||
package org.schabi.newpipe.about;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.util.Base64;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
|
||||||
private final WeakReference<Activity> weakReference;
|
|
||||||
private License license;
|
|
||||||
|
|
||||||
public LicenseFragmentHelper(@Nullable final Activity activity) {
|
|
||||||
weakReference = new WeakReference<>(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the context to use
|
|
||||||
* @param license the license
|
|
||||||
* @return String which contains a HTML formatted license page
|
|
||||||
* styled according to the context's theme
|
|
||||||
*/
|
|
||||||
private static String getFormattedLicense(@NonNull final Context context,
|
|
||||||
@NonNull final License license) {
|
|
||||||
final StringBuilder licenseContent = new StringBuilder();
|
|
||||||
final String webViewData;
|
|
||||||
try {
|
|
||||||
final BufferedReader in = new BufferedReader(new InputStreamReader(
|
|
||||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
|
|
||||||
String str;
|
|
||||||
while ((str = in.readLine()) != null) {
|
|
||||||
licenseContent.append(str);
|
|
||||||
}
|
|
||||||
in.close();
|
|
||||||
|
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
|
||||||
webViewData = licenseContent.toString().replace("</head>",
|
|
||||||
"<style>" + getLicenseStylesheet(context) + "</style></head>");
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Could not get license file: " + license.getFilename(), e);
|
|
||||||
}
|
|
||||||
return webViewData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context
|
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
|
||||||
*/
|
|
||||||
private static String getLicenseStylesheet(final Context context) {
|
|
||||||
final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
|
|
||||||
return "body{padding:12px 15px;margin:0;"
|
|
||||||
+ "background:#" + getHexRGBColor(context, isLightTheme
|
|
||||||
? R.color.light_license_background_color
|
|
||||||
: R.color.dark_license_background_color) + ";"
|
|
||||||
+ "color:#" + getHexRGBColor(context, isLightTheme
|
|
||||||
? R.color.light_license_text_color
|
|
||||||
: R.color.dark_license_text_color) + "}"
|
|
||||||
+ "a[href]{color:#" + getHexRGBColor(context, isLightTheme
|
|
||||||
? R.color.light_youtube_primary_color
|
|
||||||
: R.color.dark_youtube_primary_color) + "}"
|
|
||||||
+ "pre{white-space:pre-wrap}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cast R.color to a hexadecimal color value.
|
|
||||||
*
|
|
||||||
* @param context the context to use
|
|
||||||
* @param color the color number from R.color
|
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
|
||||||
*/
|
|
||||||
private static String getHexRGBColor(final Context context, final int color) {
|
|
||||||
return context.getResources().getString(color).substring(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Activity getActivity() {
|
|
||||||
final Activity activity = weakReference.get();
|
|
||||||
|
|
||||||
if (activity != null && activity.isFinishing()) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return activity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Integer doInBackground(final Object... objects) {
|
|
||||||
license = (License) objects[0];
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(final Integer result) {
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
if (activity == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license)
|
|
||||||
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
|
|
||||||
final WebView webView = new WebView(activity);
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
|
|
||||||
|
|
||||||
final AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
|
||||||
alert.setTitle(license.getName());
|
|
||||||
alert.setView(webView);
|
|
||||||
assureCorrectAppLanguage(activity);
|
|
||||||
alert.setNegativeButton(activity.getString(R.string.finish),
|
|
||||||
(dialog, which) -> dialog.dismiss());
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the context to use
|
||||||
|
* @param license the license
|
||||||
|
* @return String which contains a HTML formatted license page
|
||||||
|
* styled according to the context's theme
|
||||||
|
*/
|
||||||
|
fun getFormattedLicense(context: Context, license: License): String {
|
||||||
|
try {
|
||||||
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
|
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the Android context
|
||||||
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
|
*/
|
||||||
|
fun getLicenseStylesheet(context: Context): String {
|
||||||
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
|
)
|
||||||
|
val licenseTextColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
||||||
|
)
|
||||||
|
val youtubePrimaryColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
||||||
|
)
|
||||||
|
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
||||||
|
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast R.color to a hexadecimal color value.
|
||||||
|
*
|
||||||
|
* @param context the context to use
|
||||||
|
* @param color the color number from R.color
|
||||||
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
|
*/
|
||||||
|
fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
|
return context.getString(color).substring(3)
|
||||||
|
}
|
|
@ -1,83 +0,0 @@
|
||||||
package org.schabi.newpipe.about;
|
|
||||||
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
public class SoftwareComponent implements Parcelable {
|
|
||||||
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
|
||||||
@Override
|
|
||||||
public SoftwareComponent createFromParcel(final Parcel source) {
|
|
||||||
return new SoftwareComponent(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SoftwareComponent[] newArray(final int size) {
|
|
||||||
return new SoftwareComponent[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final License license;
|
|
||||||
private final String name;
|
|
||||||
private final String years;
|
|
||||||
private final String copyrightOwner;
|
|
||||||
private final String link;
|
|
||||||
private final String version;
|
|
||||||
|
|
||||||
public SoftwareComponent(final String name, final String years, final String copyrightOwner,
|
|
||||||
final String link, final License license) {
|
|
||||||
this.name = name;
|
|
||||||
this.years = years;
|
|
||||||
this.copyrightOwner = copyrightOwner;
|
|
||||||
this.link = link;
|
|
||||||
this.license = license;
|
|
||||||
this.version = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected SoftwareComponent(final Parcel in) {
|
|
||||||
this.name = in.readString();
|
|
||||||
this.license = in.readParcelable(License.class.getClassLoader());
|
|
||||||
this.copyrightOwner = in.readString();
|
|
||||||
this.link = in.readString();
|
|
||||||
this.years = in.readString();
|
|
||||||
this.version = in.readString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getYears() {
|
|
||||||
return years;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCopyrightOwner() {
|
|
||||||
return copyrightOwner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLink() {
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public License getLicense() {
|
|
||||||
return license;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(final Parcel dest, final int flags) {
|
|
||||||
dest.writeString(name);
|
|
||||||
dest.writeParcelable(license, flags);
|
|
||||||
dest.writeString(copyrightOwner);
|
|
||||||
dest.writeString(link);
|
|
||||||
dest.writeString(years);
|
|
||||||
dest.writeString(version);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class SoftwareComponent
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
val name: String,
|
||||||
|
val years: String,
|
||||||
|
val copyrightOwner: String,
|
||||||
|
val link: String,
|
||||||
|
val license: License,
|
||||||
|
val version: String? = null
|
||||||
|
) : Parcelable, Serializable
|
|
@ -1,19 +0,0 @@
|
||||||
package org.schabi.newpipe.about;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class containing information about standard software licenses.
|
|
||||||
*/
|
|
||||||
public final class StandardLicenses {
|
|
||||||
public static final License GPL2
|
|
||||||
= new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
|
|
||||||
public static final License GPL3
|
|
||||||
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
|
|
||||||
public static final License APACHE2
|
|
||||||
= new License("Apache License, Version 2.0", "ALv2", "apache2.html");
|
|
||||||
public static final License MPL2
|
|
||||||
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
|
||||||
public static final License MIT
|
|
||||||
= new License("MIT License", "MIT", "mit.html");
|
|
||||||
|
|
||||||
private StandardLicenses() { }
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class containing information about standard software licenses.
|
||||||
|
*/
|
||||||
|
object StandardLicenses {
|
||||||
|
@JvmField
|
||||||
|
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MIT = License("MIT License", "MIT", "mit.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
|
@ -27,8 +29,6 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
|
||||||
|
|
||||||
@TypeConverters({Converters.class})
|
@TypeConverters({Converters.class})
|
||||||
@Database(
|
@Database(
|
||||||
entities = {
|
entities = {
|
||||||
|
@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_3
|
version = DB_VER_9
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
|
|
@ -3,24 +3,20 @@ package org.schabi.newpipe.database;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Delete;
|
import androidx.room.Delete;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Update;
|
import androidx.room.Update;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface BasicDAO<Entity> {
|
public interface BasicDAO<Entity> {
|
||||||
/* Inserts */
|
/* Inserts */
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
@Insert
|
||||||
long insert(Entity entity);
|
long insert(Entity entity);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
@Insert
|
||||||
List<Long> insertAll(Entity... entities);
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
|
||||||
List<Long> insertAll(Collection<Entity> entities);
|
List<Long> insertAll(Collection<Entity> entities);
|
||||||
|
|
||||||
/* Searches */
|
/* Searches */
|
||||||
|
@ -32,9 +28,6 @@ public interface BasicDAO<Entity> {
|
||||||
@Delete
|
@Delete
|
||||||
void delete(Entity entity);
|
void delete(Entity entity);
|
||||||
|
|
||||||
@Delete
|
|
||||||
int delete(Collection<Entity> entities);
|
|
||||||
|
|
||||||
int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
/* Updates */
|
/* Updates */
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
package org.schabi.newpipe.database;
|
|
||||||
|
|
||||||
import androidx.room.TypeConverter;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public final class Converters {
|
|
||||||
private Converters() { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a long value to a date.
|
|
||||||
*
|
|
||||||
* @param value the long value
|
|
||||||
* @return the date
|
|
||||||
*/
|
|
||||||
@TypeConverter
|
|
||||||
public static Date fromTimestamp(final Long value) {
|
|
||||||
return value == null ? null : new Date(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a date to a long value.
|
|
||||||
*
|
|
||||||
* @param date the date
|
|
||||||
* @return the long value
|
|
||||||
*/
|
|
||||||
@TypeConverter
|
|
||||||
public static Long dateToTimestamp(final Date date) {
|
|
||||||
return date == null ? null : date.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
public static StreamType streamTypeOf(final String value) {
|
|
||||||
return StreamType.valueOf(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
public static String stringOf(final StreamType streamType) {
|
|
||||||
return streamType.name();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
|
|
||||||
return feedGroupIcon.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
|
|
||||||
for (FeedGroupIcon icon : FeedGroupIcon.values()) {
|
|
||||||
if (icon.getId() == id) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
|
|
||||||
}
|
|
||||||
}
|
|
52
app/src/main/java/org/schabi/newpipe/database/Converters.kt
Normal file
52
app/src/main/java/org/schabi/newpipe/database/Converters.kt
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
/**
|
||||||
|
* Convert a long value to a [OffsetDateTime].
|
||||||
|
*
|
||||||
|
* @param value the long value
|
||||||
|
* @return the `OffsetDateTime`
|
||||||
|
*/
|
||||||
|
@TypeConverter
|
||||||
|
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? {
|
||||||
|
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a [OffsetDateTime] to a long value.
|
||||||
|
*
|
||||||
|
* @param offsetDateTime the `OffsetDateTime`
|
||||||
|
* @return the long value
|
||||||
|
*/
|
||||||
|
@TypeConverter
|
||||||
|
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? {
|
||||||
|
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun streamTypeOf(value: String): StreamType {
|
||||||
|
return StreamType.valueOf(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringOf(streamType: StreamType): String {
|
||||||
|
return streamType.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun integerOf(feedGroupIcon: FeedGroupIcon): Int {
|
||||||
|
return feedGroupIcon.id
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
|
return FeedGroupIcon.entries.first { it.id == id }
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,15 +6,30 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.room.migration.Migration;
|
import androidx.room.migration.Migration;
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
|
||||||
public final class Migrations {
|
public final class Migrations {
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Test new migrations manually by importing a database from daily usage //
|
||||||
|
// and checking if the migration works (Use the Database Inspector //
|
||||||
|
// https://developer.android.com/studio/inspect/database). //
|
||||||
|
// If you add a migration point it out in the pull request, so that //
|
||||||
|
// others remember to test it themselves. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public static final int DB_VER_1 = 1;
|
public static final int DB_VER_1 = 1;
|
||||||
public static final int DB_VER_2 = 2;
|
public static final int DB_VER_2 = 2;
|
||||||
public static final int DB_VER_3 = 3;
|
public static final int DB_VER_3 = 3;
|
||||||
|
public static final int DB_VER_4 = 4;
|
||||||
|
public static final int DB_VER_5 = 5;
|
||||||
|
public static final int DB_VER_6 = 6;
|
||||||
|
public static final int DB_VER_7 = 7;
|
||||||
|
public static final int DB_VER_8 = 8;
|
||||||
|
public static final int DB_VER_9 = 9;
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
|
||||||
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
||||||
@Override
|
@Override
|
||||||
|
@ -160,5 +175,133 @@ public final class Migrations {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private Migrations() { }
|
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL(
|
||||||
|
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||||
|
+ "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` "
|
||||||
|
+ "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
// Create a new column thumbnail_stream_id
|
||||||
|
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
|
||||||
|
+ "INTEGER NOT NULL DEFAULT -1");
|
||||||
|
|
||||||
|
// Migrate the thumbnail_url to the thumbnail_stream_id
|
||||||
|
database.execSQL("UPDATE playlists SET thumbnail_stream_id = ("
|
||||||
|
+ " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END"
|
||||||
|
+ " FROM ("
|
||||||
|
+ " SELECT p.uid AS playlist_uid, s.uid AS stream_uid"
|
||||||
|
+ " FROM playlists p"
|
||||||
|
+ " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id"
|
||||||
|
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
|
||||||
|
+ " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table"
|
||||||
|
+ " WHERE playlist_uid = playlists.uid)");
|
||||||
|
|
||||||
|
// Remove the thumbnail_url field in the playlist table
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`"
|
||||||
|
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "name TEXT, "
|
||||||
|
+ "is_thumbnail_permanent INTEGER NOT NULL, "
|
||||||
|
+ "thumbnail_stream_id INTEGER NOT NULL)");
|
||||||
|
|
||||||
|
database.execSQL("INSERT INTO playlists_new"
|
||||||
|
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
|
||||||
|
+ " FROM playlists");
|
||||||
|
|
||||||
|
|
||||||
|
database.execSQL("DROP TABLE playlists");
|
||||||
|
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists");
|
||||||
|
database.execSQL("CREATE INDEX IF NOT EXISTS "
|
||||||
|
+ "`index_playlists_name` ON `playlists` (`name`)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||||
|
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
||||||
|
database.execSQL("UPDATE search_history SET search = trim(search)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
try {
|
||||||
|
database.beginTransaction();
|
||||||
|
|
||||||
|
// Update playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
||||||
|
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL)");
|
||||||
|
database.execSQL("INSERT INTO `playlists_tmp` "
|
||||||
|
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "`display_index`) "
|
||||||
|
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "-1 "
|
||||||
|
+ "FROM `playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
||||||
|
|
||||||
|
|
||||||
|
// Update remote_playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL,"
|
||||||
|
+ "`stream_count` INTEGER)");
|
||||||
|
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||||
|
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
||||||
|
+ "`stream_count`)"
|
||||||
|
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
||||||
|
+ "-1, `stream_count` FROM `remote_playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `remote_playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
||||||
|
|
||||||
|
// Create index on the new table.
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
|
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||||
|
|
||||||
|
database.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
database.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private Migrations() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,64 +6,123 @@ import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.reactivex.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import java.util.Date
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class FeedDAO {
|
abstract class FeedDAO {
|
||||||
@Query("DELETE FROM feed")
|
@Query("DELETE FROM feed")
|
||||||
abstract fun deleteAll(): Int
|
abstract fun deleteAll(): Int
|
||||||
|
|
||||||
@Query("""
|
/**
|
||||||
SELECT s.* FROM streams s
|
* @param groupId the group id to get feed streams of; use
|
||||||
|
* [FeedGroupEntity.GROUP_ALL_ID] to not filter by group
|
||||||
|
* @param includePlayed if false, only return all of the live, never-played or non-finished
|
||||||
|
* feed streams (see `@see` items); if true no filter is applied
|
||||||
|
* @param uploadDateBefore get only streams uploaded before this date (useful to filter out
|
||||||
|
* future streams); use null to not filter by upload date
|
||||||
|
* @return the feed streams filtered according to the conditions provided in the parameters
|
||||||
|
* @see StreamStateEntity.isFinished()
|
||||||
|
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||||
|
* @see StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT s.*, sst.progress_time
|
||||||
|
FROM streams s
|
||||||
|
|
||||||
|
LEFT JOIN stream_state sst
|
||||||
|
ON s.uid = sst.stream_id
|
||||||
|
|
||||||
|
LEFT JOIN stream_history sh
|
||||||
|
ON s.uid = sh.stream_id
|
||||||
|
|
||||||
INNER JOIN feed f
|
INNER JOIN feed f
|
||||||
ON s.uid = f.stream_id
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON (
|
||||||
LIMIT 500
|
:groupId <> ${FeedGroupEntity.GROUP_ALL_ID}
|
||||||
""")
|
AND fgs.subscription_id = f.subscription_id
|
||||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
|
||||||
|
|
||||||
@Query("""
|
|
||||||
SELECT s.* FROM streams s
|
|
||||||
|
|
||||||
INNER JOIN feed f
|
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
INNER JOIN feed_group_subscription_join fgs
|
|
||||||
ON fgs.subscription_id = f.subscription_id
|
|
||||||
|
|
||||||
INNER JOIN feed_group fg
|
|
||||||
ON fg.uid = fgs.group_id
|
|
||||||
|
|
||||||
WHERE fgs.group_id = :groupId
|
|
||||||
|
|
||||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
|
||||||
LIMIT 500
|
|
||||||
""")
|
|
||||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
|
||||||
|
|
||||||
@Query("""
|
|
||||||
DELETE FROM feed WHERE
|
|
||||||
|
|
||||||
feed.stream_id IN (
|
|
||||||
SELECT s.uid FROM streams s
|
|
||||||
|
|
||||||
INNER JOIN feed f
|
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
WHERE s.upload_date < :date
|
|
||||||
)
|
)
|
||||||
""")
|
|
||||||
abstract fun unlinkStreamsOlderThan(date: Date)
|
|
||||||
|
|
||||||
@Query("""
|
WHERE (
|
||||||
|
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}
|
||||||
|
OR fgs.group_id = :groupId
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
:includePlayed
|
||||||
|
OR sh.stream_id IS NULL
|
||||||
|
OR sst.stream_id IS NULL
|
||||||
|
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||||
|
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||||
|
OR s.stream_type = 'LIVE_STREAM'
|
||||||
|
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
:includePartiallyPlayed
|
||||||
|
OR sh.stream_id IS NULL
|
||||||
|
OR sst.stream_id IS NULL
|
||||||
|
OR (sst.progress_time <= ${StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}
|
||||||
|
AND sst.progress_time <= s.duration * 1000 / 4)
|
||||||
|
OR (sst.progress_time >= s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||||
|
AND sst.progress_time >= s.duration * 1000 * 3 / 4)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
:uploadDateBefore IS NULL
|
||||||
|
OR s.upload_date IS NULL
|
||||||
|
OR s.upload_date < :uploadDateBefore
|
||||||
|
)
|
||||||
|
|
||||||
|
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||||
|
LIMIT 500
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getStreams(
|
||||||
|
groupId: Long,
|
||||||
|
includePlayed: Boolean,
|
||||||
|
includePartiallyPlayed: Boolean,
|
||||||
|
uploadDateBefore: OffsetDateTime?
|
||||||
|
): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove links to streams that are older than the given date
|
||||||
|
* **but keep at least one stream per uploader**.
|
||||||
|
*
|
||||||
|
* One stream per uploader is kept because it is needed as reference
|
||||||
|
* when fetching new streams to check if they are new or not.
|
||||||
|
* @param offsetDateTime the newest date to keep, older streams are removed
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM feed
|
||||||
|
WHERE feed.stream_id IN (SELECT uid from (
|
||||||
|
SELECT s.uid,
|
||||||
|
(SELECT MAX(upload_date)
|
||||||
|
FROM streams s1
|
||||||
|
INNER JOIN feed f1
|
||||||
|
ON s1.uid = f1.stream_id
|
||||||
|
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
||||||
|
FROM streams s
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.upload_date < :offsetDateTime
|
||||||
|
AND s.upload_date <> max_upload_date))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
DELETE FROM feed
|
DELETE FROM feed
|
||||||
|
|
||||||
WHERE feed.subscription_id = :subscriptionId
|
WHERE feed.subscription_id = :subscriptionId
|
||||||
|
@ -76,7 +135,8 @@ abstract class FeedDAO {
|
||||||
|
|
||||||
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
@ -100,21 +160,24 @@ abstract class FeedDAO {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||||
|
|
||||||
INNER JOIN feed_group_subscription_join fgs
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||||
""")
|
"""
|
||||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>>
|
)
|
||||||
|
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
|
||||||
|
|
||||||
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||||
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<Date>>
|
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||||
abstract fun notLoadedCount(): Flowable<Long>
|
abstract fun notLoadedCount(): Flowable<Long>
|
||||||
|
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
SELECT COUNT(*) FROM subscriptions s
|
SELECT COUNT(*) FROM subscriptions s
|
||||||
|
|
||||||
INNER JOIN feed_group_subscription_join fgs
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
@ -124,20 +187,24 @@ abstract class FeedDAO {
|
||||||
ON s.uid = lu.subscription_id
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
WHERE lu.last_updated IS NULL
|
WHERE lu.last_updated IS NULL
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||||
|
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
SELECT s.* FROM subscriptions s
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
LEFT JOIN feed_last_updated lu
|
LEFT JOIN feed_last_updated lu
|
||||||
ON s.uid = lu.subscription_id
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||||
""")
|
"""
|
||||||
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
)
|
||||||
|
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
@Query("""
|
@Query(
|
||||||
|
"""
|
||||||
SELECT s.* FROM subscriptions s
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
INNER JOIN feed_group_subscription_join fgs
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
@ -147,6 +214,24 @@ abstract class FeedDAO {
|
||||||
ON s.uid = lu.subscription_id
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||||
""")
|
"""
|
||||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
)
|
||||||
|
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
(lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold)
|
||||||
|
AND s.notification_mode = :notificationMode
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getOutdatedWithNotificationMode(
|
||||||
|
outdatedThreshold: OffsetDateTime,
|
||||||
|
@NotificationMode notificationMode: Int
|
||||||
|
): Flowable<List<SubscriptionEntity>>
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.reactivex.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.Maybe
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||||
|
|
||||||
|
|
|
@ -10,21 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
@Entity(tableName = FEED_TABLE,
|
@Entity(
|
||||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
tableName = FEED_TABLE,
|
||||||
indices = [Index(SUBSCRIPTION_ID)],
|
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||||
foreignKeys = [
|
indices = [Index(SUBSCRIPTION_ID)],
|
||||||
ForeignKey(
|
foreignKeys = [
|
||||||
entity = StreamEntity::class,
|
ForeignKey(
|
||||||
parentColumns = [StreamEntity.STREAM_ID],
|
entity = StreamEntity::class,
|
||||||
childColumns = [STREAM_ID],
|
parentColumns = [StreamEntity.STREAM_ID],
|
||||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
|
childColumns = [STREAM_ID],
|
||||||
ForeignKey(
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||||
entity = SubscriptionEntity::class,
|
),
|
||||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
ForeignKey(
|
||||||
childColumns = [SUBSCRIPTION_ID],
|
entity = SubscriptionEntity::class,
|
||||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
]
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||||
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
data class FeedEntity(
|
data class FeedEntity(
|
||||||
@ColumnInfo(name = STREAM_ID)
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
|
|
@ -9,8 +9,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD
|
||||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = FEED_GROUP_TABLE,
|
tableName = FEED_GROUP_TABLE,
|
||||||
indices = [Index(SORT_ORDER)]
|
indices = [Index(SORT_ORDER)]
|
||||||
)
|
)
|
||||||
data class FeedGroupEntity(
|
data class FeedGroupEntity(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe.database.feed.model
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.ForeignKey.CASCADE
|
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
|
||||||
|
@ -11,22 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||||
indices = [Index(SUBSCRIPTION_ID)],
|
indices = [Index(SUBSCRIPTION_ID)],
|
||||||
foreignKeys = [
|
foreignKeys = [
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = FeedGroupEntity::class,
|
entity = FeedGroupEntity::class,
|
||||||
parentColumns = [FeedGroupEntity.ID],
|
parentColumns = [FeedGroupEntity.ID],
|
||||||
childColumns = [GROUP_ID],
|
childColumns = [GROUP_ID],
|
||||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||||
|
),
|
||||||
|
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = SubscriptionEntity::class,
|
entity = SubscriptionEntity::class,
|
||||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
childColumns = [SUBSCRIPTION_ID],
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||||
]
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
data class FeedGroupSubscriptionEntity(
|
data class FeedGroupSubscriptionEntity(
|
||||||
@ColumnInfo(name = GROUP_ID)
|
@ColumnInfo(name = GROUP_ID)
|
||||||
|
|
|
@ -4,20 +4,21 @@ import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import java.util.Date
|
|
||||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = FEED_LAST_UPDATED_TABLE,
|
tableName = FEED_LAST_UPDATED_TABLE,
|
||||||
foreignKeys = [
|
foreignKeys = [
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = SubscriptionEntity::class,
|
entity = SubscriptionEntity::class,
|
||||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
childColumns = [SUBSCRIPTION_ID],
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||||
]
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
data class FeedLastUpdatedEntity(
|
data class FeedLastUpdatedEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
|
@ -25,9 +26,8 @@ data class FeedLastUpdatedEntity(
|
||||||
var subscriptionId: Long,
|
var subscriptionId: Long,
|
||||||
|
|
||||||
@ColumnInfo(name = LAST_UPDATED)
|
@ColumnInfo(name = LAST_UPDATED)
|
||||||
var lastUpdated: Date? = null
|
var lastUpdated: OffsetDateTime? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
|
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||||
|
@ -19,6 +19,7 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
|
||||||
@Dao
|
@Dao
|
||||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||||
|
String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC";
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME
|
@Query("SELECT * FROM " + TABLE_NAME
|
||||||
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||||
|
@ -36,16 +37,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||||
@Override
|
@Override
|
||||||
Flowable<List<SearchHistoryEntry>> getAll();
|
Flowable<List<SearchHistoryEntry>> getAll();
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
|
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
|
||||||
+ " LIMIT :limit")
|
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||||
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
Flowable<List<String>> getUniqueEntries(int limit);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME
|
@Query("SELECT * FROM " + TABLE_NAME
|
||||||
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||||
@Override
|
@Override
|
||||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||||
+ " GROUP BY " + SEARCH + " LIMIT :limit")
|
+ " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||||
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
Flowable<List<String>> getSimilarEntries(String query, int limit);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.database.history.dao;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
|
@ -10,7 +11,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||||
|
@ -20,6 +21,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA
|
||||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||||
|
@ -64,6 +68,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
public abstract int deleteStreamHistory(long streamId);
|
public abstract int deleteStreamHistory(long streamId);
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE
|
@Query("SELECT * FROM " + STREAM_TABLE
|
||||||
|
|
||||||
// Select the latest entry and watch count for each stream id on history table
|
// Select the latest entry and watch count for each stream id on history table
|
||||||
|
@ -73,6 +78,12 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||||
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||||
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||||
|
|
||||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
|
||||||
|
+ " LEFT JOIN "
|
||||||
|
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||||
|
+ STREAM_PROGRESS_MILLIS
|
||||||
|
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
package org.schabi.newpipe.database.history.model;
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
|
||||||
import androidx.room.Entity;
|
|
||||||
import androidx.room.Ignore;
|
|
||||||
import androidx.room.Index;
|
|
||||||
import androidx.room.PrimaryKey;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
|
||||||
|
|
||||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
|
||||||
indices = {@Index(value = SEARCH)})
|
|
||||||
public class SearchHistoryEntry {
|
|
||||||
public static final String ID = "id";
|
|
||||||
public static final String TABLE_NAME = "search_history";
|
|
||||||
public static final String SERVICE_ID = "service_id";
|
|
||||||
public static final String CREATION_DATE = "creation_date";
|
|
||||||
public static final String SEARCH = "search";
|
|
||||||
|
|
||||||
@ColumnInfo(name = ID)
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
private long id;
|
|
||||||
|
|
||||||
@ColumnInfo(name = CREATION_DATE)
|
|
||||||
private Date creationDate;
|
|
||||||
|
|
||||||
@ColumnInfo(name = SERVICE_ID)
|
|
||||||
private int serviceId;
|
|
||||||
|
|
||||||
@ColumnInfo(name = SEARCH)
|
|
||||||
private String search;
|
|
||||||
|
|
||||||
public SearchHistoryEntry(final Date creationDate, final int serviceId, final String search) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.creationDate = creationDate;
|
|
||||||
this.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(final long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getCreationDate() {
|
|
||||||
return creationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreationDate(final Date creationDate) {
|
|
||||||
this.creationDate = creationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getServiceId() {
|
|
||||||
return serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setServiceId(final int serviceId) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSearch() {
|
|
||||||
return search;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSearch(final String search) {
|
|
||||||
this.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
|
|
||||||
return getServiceId() == otherEntry.getServiceId()
|
|
||||||
&& getSearch().equals(otherEntry.getSearch());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = SearchHistoryEntry.TABLE_NAME,
|
||||||
|
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
||||||
|
)
|
||||||
|
data class SearchHistoryEntry(
|
||||||
|
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
|
||||||
|
@field:ColumnInfo(
|
||||||
|
name = SERVICE_ID
|
||||||
|
) var serviceId: Int,
|
||||||
|
@field:ColumnInfo(name = SEARCH) var search: String?
|
||||||
|
) {
|
||||||
|
@ColumnInfo(name = ID)
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var id: Long = 0
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
||||||
|
return (
|
||||||
|
serviceId == otherEntry.serviceId &&
|
||||||
|
search == otherEntry.search
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = "id"
|
||||||
|
const val TABLE_NAME = "search_history"
|
||||||
|
const val SERVICE_ID = "service_id"
|
||||||
|
const val CREATION_DATE = "creation_date"
|
||||||
|
const val SEARCH = "search"
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,11 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.ForeignKey;
|
import androidx.room.ForeignKey;
|
||||||
import androidx.room.Ignore;
|
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
import static androidx.room.ForeignKey.CASCADE;
|
import static androidx.room.ForeignKey.CASCADE;
|
||||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||||
|
@ -37,23 +36,24 @@ public class StreamHistoryEntity {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
||||||
private Date accessDate;
|
private OffsetDateTime accessDate;
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||||
private long repeatCount;
|
private long repeatCount;
|
||||||
|
|
||||||
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate,
|
/**
|
||||||
|
* @param streamUid the stream id this history item will refer to
|
||||||
|
* @param accessDate the last time the stream was accessed
|
||||||
|
* @param repeatCount the total number of views this stream received
|
||||||
|
*/
|
||||||
|
public StreamHistoryEntity(final long streamUid,
|
||||||
|
@NonNull final OffsetDateTime accessDate,
|
||||||
final long repeatCount) {
|
final long repeatCount) {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
this.accessDate = accessDate;
|
this.accessDate = accessDate;
|
||||||
this.repeatCount = repeatCount;
|
this.repeatCount = repeatCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate) {
|
|
||||||
this(streamUid, accessDate, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getStreamUid() {
|
public long getStreamUid() {
|
||||||
return streamUid;
|
return streamUid;
|
||||||
}
|
}
|
||||||
|
@ -62,11 +62,12 @@ public class StreamHistoryEntity {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getAccessDate() {
|
@NonNull
|
||||||
|
public OffsetDateTime getAccessDate() {
|
||||||
return accessDate;
|
return accessDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAccessDate(@NonNull final Date accessDate) {
|
public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
|
||||||
this.accessDate = accessDate;
|
this.accessDate = accessDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import java.util.Date
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
data class StreamHistoryEntry(
|
data class StreamHistoryEntry(
|
||||||
@Embedded
|
@Embedded
|
||||||
|
@ -13,7 +13,7 @@ data class StreamHistoryEntry(
|
||||||
val streamId: Long,
|
val streamId: Long,
|
||||||
|
|
||||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||||
val accessDate: Date,
|
val accessDate: OffsetDateTime,
|
||||||
|
|
||||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||||
val repeatCount: Long
|
val repeatCount: Long
|
||||||
|
@ -25,6 +25,6 @@ data class StreamHistoryEntry(
|
||||||
|
|
||||||
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
||||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||||
accessDate.compareTo(other.accessDate) == 0
|
accessDate.isEqual(other.accessDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
|
||||||
|
* how many times a specific stream is already contained inside a local playlist. Used to be able
|
||||||
|
* to grey out playlists which already contain the current stream in the playlist append dialog.
|
||||||
|
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
|
||||||
|
*/
|
||||||
|
public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
||||||
|
public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
|
||||||
|
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||||
|
public final long timesStreamIsContained;
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||||
|
public PlaylistDuplicatesEntry(final long uid,
|
||||||
|
final String name,
|
||||||
|
final String thumbnailUrl,
|
||||||
|
final boolean isThumbnailPermanent,
|
||||||
|
final long thumbnailStreamId,
|
||||||
|
final long displayIndex,
|
||||||
|
final long streamCount,
|
||||||
|
final long timesStreamIsContained) {
|
||||||
|
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||||
|
streamCount);
|
||||||
|
this.timesStreamIsContained = timesStreamIsContained;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,33 +1,13 @@
|
||||||
package org.schabi.newpipe.database.playlist;
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
|
||||||
static List<PlaylistLocalItem> merge(
|
long getDisplayIndex();
|
||||||
final List<PlaylistMetadataEntry> localPlaylists,
|
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
|
||||||
final List<PlaylistLocalItem> items = new ArrayList<>(
|
|
||||||
localPlaylists.size() + remotePlaylists.size());
|
|
||||||
items.addAll(localPlaylists);
|
|
||||||
items.addAll(remotePlaylists);
|
|
||||||
|
|
||||||
Collections.sort(items, (left, right) -> {
|
long getUid();
|
||||||
final String on1 = left.getOrderingName();
|
|
||||||
final String on2 = right.getOrderingName();
|
|
||||||
if (on1 == null) {
|
|
||||||
return on2 == null ? 0 : 1;
|
|
||||||
} else {
|
|
||||||
return on2 == null ? -1 : on1.compareToIgnoreCase(on2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
void setDisplayIndex(long displayIndex);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,40 @@ package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
|
||||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
public final long uid;
|
private final long uid;
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
public final String name;
|
public final String name;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
private final boolean isThumbnailPermanent;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
private final long thumbnailStreamId;
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
public final String thumbnailUrl;
|
public final String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
public final long streamCount;
|
public final long streamCount;
|
||||||
|
|
||||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||||
final long streamCount) {
|
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
||||||
|
final long displayIndex, final long streamCount) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,4 +48,27 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public String getOrderingName() {
|
public String getOrderingName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isThumbnailPermanent() {
|
||||||
|
return isThumbnailPermanent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getThumbnailStreamId() {
|
||||||
|
return thumbnailStreamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,17 @@ import androidx.room.Embedded
|
||||||
import org.schabi.newpipe.database.LocalItem
|
import org.schabi.newpipe.database.LocalItem
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
class PlaylistStreamEntry(
|
data class PlaylistStreamEntry(
|
||||||
@Embedded
|
@Embedded
|
||||||
val streamEntity: StreamEntity,
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||||
|
val progressMillis: Long,
|
||||||
|
|
||||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||||
val streamId: Long,
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ -23,7 +28,8 @@ class PlaylistStreamEntry(
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,35 +2,52 @@ package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
||||||
public abstract Flowable<List<PlaylistEntity>> getAll();
|
Flowable<List<PlaylistEntity>> getAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
||||||
public abstract int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
default Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract int deletePlaylist(long playlistId);
|
int deletePlaylist(long playlistId);
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||||
|
Flowable<Long> getCount();
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
default long upsertPlaylist(final PlaylistEntity playlist) {
|
||||||
|
final long playlistId = playlist.getUid();
|
||||||
|
|
||||||
|
if (playlistId == -1) {
|
||||||
|
// This situation is probably impossible.
|
||||||
|
return insert(playlist);
|
||||||
|
} else {
|
||||||
|
update(playlist);
|
||||||
|
return playlistId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,39 +9,48 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
|
public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
||||||
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
|
Flowable<List<PlaylistRemoteEntity>> getAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
||||||
public abstract int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
|
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
|
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
|
||||||
|
|
||||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
abstract Long getPlaylistIdInternal(long serviceId, String url);
|
Long getPlaylistIdInternal(long serviceId, String url);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
public long upsert(final PlaylistRemoteEntity playlist) {
|
default long upsert(final PlaylistRemoteEntity playlist) {
|
||||||
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||||
|
|
||||||
if (playlistId == null) {
|
if (playlistId == null) {
|
||||||
|
@ -55,5 +64,5 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
|
||||||
|
|
||||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract int deletePlaylist(long playlistId);
|
int deletePlaylist(long playlistId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,29 @@ package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||||
import androidx.room.Transaction;
|
import androidx.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||||
|
@ -24,31 +32,47 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||||
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
|
Flowable<List<PlaylistStreamEntity>> getAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||||
public abstract int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
default Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract void deleteBatch(long playlistId);
|
void deleteBatch(long playlistId);
|
||||||
|
|
||||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
|
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
|
||||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
|
Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||||
|
|
||||||
|
@Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID
|
||||||
|
+ " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END"
|
||||||
|
+ " FROM " + STREAM_TABLE
|
||||||
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId "
|
||||||
|
+ " LIMIT 1"
|
||||||
|
)
|
||||||
|
Flowable<Long> getAutomaticThumbnailStreamId(long playlistId);
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
||||||
// get ids of streams of the given playlist
|
// get ids of streams of the given playlist
|
||||||
|
@ -58,17 +82,78 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||||
|
|
||||||
// then merge with the stream metadata
|
// then merge with the stream metadata
|
||||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
|
||||||
|
+ " LEFT JOIN "
|
||||||
|
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||||
|
+ STREAM_PROGRESS_MILLIS
|
||||||
|
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||||
|
|
||||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
|
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
|
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
|
||||||
|
+ " FROM " + STREAM_TABLE
|
||||||
|
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
|
||||||
|
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||||
|
|
||||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
||||||
|
+ " FROM " + PLAYLIST_TABLE
|
||||||
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
|
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
|
+ " GROUP BY " + PLAYLIST_ID
|
||||||
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
|
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT *, MIN(" + JOIN_INDEX + ")"
|
||||||
|
+ " FROM " + STREAM_TABLE + " INNER JOIN"
|
||||||
|
+ " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
|
||||||
|
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
|
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
+ " LEFT JOIN "
|
||||||
|
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||||
|
+ STREAM_PROGRESS_MILLIS
|
||||||
|
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||||
|
+ " GROUP BY " + STREAM_ID
|
||||||
|
+ " ORDER BY MIN(" + JOIN_INDEX + ") ASC")
|
||||||
|
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
|
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
|
||||||
|
+ " FROM " + STREAM_TABLE
|
||||||
|
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
|
||||||
|
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||||
|
|
||||||
|
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
|
||||||
|
+ "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
|
||||||
|
+ PLAYLIST_TIMES_STREAM_IS_CONTAINED
|
||||||
|
|
||||||
+ " FROM " + PLAYLIST_TABLE
|
+ " FROM " + PLAYLIST_TABLE
|
||||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
|
|
||||||
|
+ " LEFT JOIN " + STREAM_TABLE
|
||||||
|
+ " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
+ " AND :streamUrl = :streamUrl"
|
||||||
|
|
||||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,28 @@ package org.schabi.newpipe.database.playlist.model;
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Index;
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
@Entity(tableName = PLAYLIST_TABLE,
|
import org.schabi.newpipe.R;
|
||||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
|
||||||
|
@Entity(tableName = PLAYLIST_TABLE)
|
||||||
public class PlaylistEntity {
|
public class PlaylistEntity {
|
||||||
|
|
||||||
|
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||||
|
+ R.drawable.placeholder_thumbnail_playlist;
|
||||||
|
public static final long DEFAULT_THUMBNAIL_ID = -1;
|
||||||
|
|
||||||
public static final String PLAYLIST_TABLE = "playlists";
|
public static final String PLAYLIST_TABLE = "playlists";
|
||||||
public static final String PLAYLIST_ID = "uid";
|
public static final String PLAYLIST_ID = "uid";
|
||||||
public static final String PLAYLIST_NAME = "name";
|
public static final String PLAYLIST_NAME = "name";
|
||||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
|
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||||
|
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
|
@ -23,12 +32,30 @@ public class PlaylistEntity {
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
private String thumbnailUrl;
|
private boolean isThumbnailPermanent;
|
||||||
|
|
||||||
public PlaylistEntity(final String name, final String thumbnailUrl) {
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
private long thumbnailStreamId;
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
|
|
||||||
|
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||||
|
final long thumbnailStreamId, final long displayIndex) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistEntity(final PlaylistMetadataEntry item) {
|
||||||
|
this.uid = item.getUid();
|
||||||
|
this.name = item.name;
|
||||||
|
this.isThumbnailPermanent = item.isThumbnailPermanent();
|
||||||
|
this.thumbnailStreamId = item.getThumbnailStreamId();
|
||||||
|
this.displayIndex = item.getDisplayIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
|
@ -47,11 +74,27 @@ public class PlaylistEntity {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getThumbnailUrl() {
|
public long getThumbnailStreamId() {
|
||||||
return thumbnailUrl;
|
return thumbnailStreamId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
public void setThumbnailStreamId(final long thumbnailStreamId) {
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getIsThumbnailPermanent() {
|
||||||
|
return isThumbnailPermanent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsThumbnailPermanent(final boolean isThumbnailSet) {
|
||||||
|
this.isThumbnailPermanent = isThumbnailSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user