Compare commits
1786 Commits
Author | SHA1 | Date |
---|---|---|
TobiGr | fafd471606 | |
TobiGr | 4f477ad72b | |
Tobi | 964e429978 | |
dependabot[bot] | 10c6965a28 | |
TobiGr | e54f38f5e7 | |
Tobi | 5dd5c7a65b | |
Tobi | 37438ff82f | |
TobiGr | bba3b6c69b | |
TobiGr | b40a5784ed | |
dependabot[bot] | c6da4004e2 | |
dependabot[bot] | f26e84d39f | |
Tobi | ec3e8378c6 | |
TobiGr | 8d2a7a5281 | |
TobiGr | 7c29dbc965 | |
Stypox | fbe9e6223a | |
Stypox | 4e9e7cb29c | |
Stypox | 9d0dd36034 | |
Stypox | d4e6d22e64 | |
AudricV | 74bf000473 | |
AudricV | f9792cf3a9 | |
AudricV | f40fc0aa4f | |
AudricV | 2a3c6f80d2 | |
AudricV | 657b4377aa | |
AudricV | 7bf50bf1cb | |
AudricV | 27dc1b1f50 | |
AudricV | e380bb4bc3 | |
Audric V | 6c3c2e25d7 | |
Stypox | 02274d5395 | |
Stypox | 3f7b2653e3 | |
Stypox | a90237816a | |
Stypox | b80c3f5d51 | |
Stypox | 09732d6785 | |
AudricV | 293c3e9e47 | |
Stypox | e5b30ae8c3 | |
Stypox | 23fc7aa209 | |
Stypox | fb468a23f4 | |
Stypox | 6589e2c15d | |
Tobi | ad71864b23 | |
Stypox | c57016b79b | |
Tobi | adcc1f17ee | |
TobiGr | 51ddacc81d | |
TobiGr | 8392d50ba6 | |
TobiGr | aaccfecda8 | |
TobiGr | 73f0c63a9d | |
Tobi | 896a55e319 | |
dependabot[bot] | e58fc652e0 | |
Tobi | e3f2c9aec7 | |
Tobi | 6b0fc14c04 | |
dependabot[bot] | d579b608e5 | |
TobiGr | fe47a4311f | |
TobiGr | 15e0e74b48 | |
dependabot[bot] | da04eded5d | |
Profpatsch | 7408173246 | |
Stypox | aaf3231fc7 | |
Stypox | 137e924035 | |
Stypox | cc9ade962e | |
Stypox | 3402cdb666 | |
petlyh | 6dc25f7b97 | |
petlyh | 4408e2d0ac | |
TobiGr | 9ab932e394 | |
Stypox | 9d66debf3c | |
Tobi | 038ebdedc4 | |
TobiGr | 61d237de02 | |
petlyh | 2b2c1546d1 | |
Tobi | 1e93b1dc20 | |
dependabot[bot] | 3400af99b3 | |
Tobi | 1f8a044462 | |
dependabot[bot] | 1470aa7303 | |
TobiGr | 8f9ebdcb77 | |
Stypox | 1553931027 | |
Stypox | b2ec1b15fb | |
dependabot[bot] | 151ee99da3 | |
dragfyre | 65e7bc5b95 | |
Stypox | f276caf54a | |
Stypox | fc54fb2fdb | |
Stypox | 0518487d26 | |
Stypox | 5b59a1a8c5 | |
Stypox | b8e12dd76c | |
Stypox | 83c1737f70 | |
Stypox | 2938067c2c | |
Stypox | 91419ec6e8 | |
Audric V | 678c98f24c | |
AudricV | ec0194cfbf | |
AudricV | 00a0f1a103 | |
AudricV | 06838d7245 | |
AudricV | 642bb01388 | |
AudricV | bedc9e5bc0 | |
AudricV | 5fa22ae25b | |
AudricV | 29dc7625f2 | |
AudricV | 56ab35423e | |
AudricV | 9dc1eab28c | |
AudricV | ad3d187ac7 | |
AudricV | e111814401 | |
AudricV | fc45941ead | |
AudricV | 0bcb241c38 | |
AudricV | 6ba8251be1 | |
AudricV | 7dea2d0d27 | |
AudricV | 3782d9a02a | |
AudricV | b71ce1123f | |
AudricV | ff8ed7247f | |
AudricV | ec838d7421 | |
AudricV | 2c941794c0 | |
AudricV | d97c9e0db1 | |
AudricV | 8a9ebcc373 | |
Tobi | eac850ca10 | |
Tobi | 5ab1f784e8 | |
dependabot[bot] | 9d7bcba050 | |
dependabot[bot] | e26065148a | |
FineFindus | 34b05a0dda | |
TobiGr | 0821f09114 | |
FineFindus | c1784a4bdb | |
TobiGr | f9846352ea | |
Tobi | d6f5cba6e2 | |
TobiGr | 9d63c75623 | |
TobiGr | d49f8411d7 | |
Stypox | bb132167d5 | |
AudricV | c98695fcea | |
AudricV | ac00459c1a | |
FineFindus | dd7b2d9798 | |
Youssif Shaaban Alsager | 917554acc4 | |
Tobi | 8b0068f8f4 | |
Christian | fc67d49f59 | |
Stypox | 289db1178a | |
AudricV | 6ed22099a2 | |
AudricV | 714b141ecb | |
AudricV | 588c6a8422 | |
AudricV | 1fa85ec6ca | |
AudricV | a04bc320de | |
AudricV | 7de3753a81 | |
AudricV | 6884d191cd | |
Tobi | 3be76a6406 | |
TobiGr | 17790328cd | |
Isira Seneviratne | 4bc8ae7812 | |
Tobi | 90aed06a63 | |
TobiGr | cf49f4a31c | |
Tobi | 7c7ceaceab | |
dependabot[bot] | 72c475d944 | |
Stypox | 1f08d28ae5 | |
AudricV | e8bfd20170 | |
AudricV | 0292c4f3e8 | |
AudricV | 2578f22054 | |
AudricV | ba5315c72d | |
AudricV | 1d72bac53d | |
AudricV | 93a210394d | |
AudricV | 2c436d428c | |
AudricV | d381f3b70b | |
AudricV | 434e885708 | |
AudricV | 5158472852 | |
AudricV | 70fb3aa38e | |
AudricV | e16d521b7b | |
AudricV | 306068a63b | |
AudricV | 2f40861428 | |
AudricV | 71cda03c4c | |
AudricV | 7e01eaac33 | |
AudricV | 4b80d737a4 | |
AudricV | 4e6fb368bc | |
AudricV | 0a6011a50e | |
AudricV | 6f8331524b | |
AudricV | 81c0d80a54 | |
AudricV | 31da5beb51 | |
AudricV | a3a74cd566 | |
AudricV | 7f818217d2 | |
AudricV | 266cd1f76b | |
AudricV | c1981ed54f | |
AudricV | 4cc99f9ce1 | |
AudricV | adfad086ac | |
AudricV | d56b880cae | |
AudricV | 9d8098576e | |
AudricV | 0f4a5a8184 | |
AudricV | ca1d4a6fa4 | |
AudricV | 2f3ee8a3f2 | |
AudricV | 78ce65769f | |
AudricV | d85454186a | |
Stypox | 7294675aea | |
Stypox | 93a90b816d | |
Stypox | 44b664af15 | |
AudricV | 2f7bfd3e7f | |
AudricV | b147904571 | |
AudricV | 1852031a0b | |
AudricV | 698c710685 | |
AudricV | 8237052ef5 | |
AudricV | 162c261577 | |
AudricV | e2f4ee47b9 | |
AudricV | e6f371fb94 | |
Stypox | 7936987955 | |
Stypox | 6d2227111f | |
Stypox | ee625c325c | |
Stypox | 276c293889 | |
Stypox | 9d3761a371 | |
Stypox | 95a3cc0a17 | |
Stypox | e34b4f1978 | |
Stypox | ef67c7cd74 | |
Stypox | a104cf3227 | |
Stypox | bb47f05f89 | |
Stypox | 468bcc045d | |
Stypox | 35f3a4ad01 | |
AudricV | e7d64099a7 | |
AudricV | 684101c47d | |
AudricV | eaf2600ce0 | |
AudricV | 0ee2072de5 | |
AudricV | d3801dd0e9 | |
AudricV | 8baec04611 | |
AudricV | e0ba29cd19 | |
AudricV | 18846baba7 | |
ThetaDev | c70a0e3543 | |
AudricV | 7366eab156 | |
AudricV | 4586067934 | |
AudricV | d4bfe791ee | |
AudricV | 6f7d1f079f | |
AudricV | 1e8474b22d | |
AudricV | 652c2c8408 | |
AudricV | de823a6b68 | |
AudricV | 76fb9dcdd7 | |
AudricV | 946eb9bd91 | |
AudricV | 356a888d6c | |
Stypox | 3faaf4301c | |
Stypox | 8fb6ba36fa | |
Stypox | 2947257111 | |
Stypox | 485bfbca9d | |
Stypox | 7c70fef197 | |
Stypox | de0a9bb797 | |
TobiGr | 340095515d | |
TobiGr | fe27d6a0ec | |
Kavin | 25082d78b0 | |
TobiGr | aa6c17dc77 | |
TobiGr | 2fb9922a15 | |
AudricV | a3d160edab | |
Stypox | 5492343b8e | |
Tobi | 9a59afbcf5 | |
AudricV | bb1ab166bf | |
AudricV | f1fa84b4e3 | |
Tobi | 39a911db9f | |
dependabot[bot] | bda3a3fc5d | |
AudricV | 522c78160f | |
AudricV | 7528eb2bd9 | |
AudricV | 164c8e3abb | |
Tobi | 8ebbe1b7e1 | |
FireMasterK | 6db0d116fe | |
Tobi | 986a3f7747 | |
Tobi | d6132a9ed9 | |
Tobi | 87eb29a25d | |
AudricV | 4e22c5ee87 | |
Kavin | d961d349c3 | |
ThetaDev | ad97f08048 | |
Tobi | d294ccb433 | |
Tobi | 5d112aa772 | |
TobiGr | 0e01d90562 | |
TobiGr | 5809904cf7 | |
TobiGr | 53dfd871e2 | |
Stypox | 5eb886243f | |
TobiGr | 19ce06fe00 | |
Audric V | 533121fb81 | |
Audric V | 92a0024424 | |
Tobi | 3c036a9c03 | |
TobiGr | c70bb83801 | |
TobiGr | ca0ce00753 | |
TobiGr | b218bf69bd | |
chunky programmer | 81f29116ba | |
chunky programmer | e147867d41 | |
chunky programmer | 5ab6cd7420 | |
TobiGr | d358ba1c41 | |
TobiGr | aff3e795f8 | |
ThetaDev | 3673d4ae01 | |
ThetaDev | 0addb98cd7 | |
ThetaDev | 24eba62305 | |
Kavin | a9ca5c49e4 | |
Stypox | 00408db959 | |
dependabot[bot] | 108f8a7a17 | |
Tobi | 11565db17f | |
AudricV | cf6040ddb3 | |
AudricV | 593122342f | |
AudricV | e923fca440 | |
AudricV | 945165a3c0 | |
Stypox | d61dc27406 | |
Stypox | 51cba5a287 | |
Stypox | f819c2fe34 | |
dependabot[bot] | 4b92c5a34d | |
Stypox | 2deb023da4 | |
ThetaDev | 4aada7f91b | |
Tobi | b2b426048f | |
ThetaDev | 47aa9fed40 | |
ThetaDev | 20370395c5 | |
Stypox | 7dba6e3891 | |
petlyh | e6aad117e7 | |
TobiGr | 8495ad619e | |
fynngodau | 69705138e4 | |
Björn Sigurbergsson | 1b6fe5edd6 | |
ThetaDev | 8d1303e18f | |
Tobi | 44ae139d33 | |
Tobi | d5e9df5381 | |
Tobi | 0486ad7a08 | |
AudricV | 80a6fc2c63 | |
petlyh | 5a9b6ed2e3 | |
Stypox | 6bdd698c25 | |
Stypox | 19e4b216c9 | |
Stypox | b1298490c0 | |
petlyh | 9dc1832733 | |
fynngodau | 3fdb6ee476 | |
AudricV | bd79b921e8 | |
AudricV | 51f9b39953 | |
AudricV | 95b3f5e391 | |
AudricV | 30a0f8c510 | |
AudricV | 7f0269c4c7 | |
AudricV | 034f82dae7 | |
AudricV | 05e8cb39f7 | |
AudricV | bf30d70152 | |
AudricV | 76b7c19c5d | |
AudricV | 3bb5eeef30 | |
AudricV | 14bf3fb05b | |
AudricV | f92426560c | |
AudricV | a63f289667 | |
AudricV | 9483dcd9fa | |
AudricV | 1556adbb2d | |
Stypox | 99ab9777ad | |
Stypox | 39c2e1fda0 | |
petlyh | f7a7a236fb | |
dependabot[bot] | f5599ff08d | |
Stypox | 7e793c11ae | |
Stypox | e920ab3f5f | |
TobiGr | 3f7df9536e | |
Stypox | 999fb7f812 | |
Stypox | 3519d4c367 | |
Stypox | 9aca710e86 | |
Stypox | 76eeabac45 | |
AudricV | 676622f6df | |
AudricV | 2a24d407d5 | |
Tobi | 0e4e6a9bac | |
AudricV | ba24976e41 | |
AudricV | 57f850bc2d | |
AudricV | 1f4ed9dce9 | |
Tobi | c589a2c1a2 | |
Tobi | c2b36fd405 | |
TobiGr | 72573932cf | |
TobiGr | f50b7275af | |
Kunal | 9bdad40b06 | |
Stypox | 5945057227 | |
Stypox | 7293991832 | |
Stypox | ff94e9f30b | |
Stypox | c1040bccac | |
Stypox | 6ccc43e57e | |
dependabot[bot] | 1bc44cb949 | |
dependabot[bot] | f43049985e | |
Stypox | 33f9f2cf66 | |
TobiGr | 56aab4d971 | |
Kavin | 22a47da8c7 | |
Kavin | 98a90fd9c8 | |
Kavin | 2974dfaa48 | |
Kavin | 64d24aa09e | |
Kavin | 67ef4f4c30 | |
FireMasterK | 22f71b010c | |
FireMasterK | 656b7c1cd9 | |
FireMasterK | 981aee4092 | |
Stypox | 45636b0d00 | |
Stypox | 219c5c5be5 | |
Stypox | 259de3cba6 | |
Stypox | 991394b53a | |
Stypox | ce15f7cc50 | |
Isira Seneviratne | d8ce08d969 | |
Kavin | 01acf79436 | |
TobiGr | 292e0d8ce7 | |
TobiGr | 2a8729aeb2 | |
TobiGr | d75a997611 | |
TobiGr | dea6d8ce4c | |
Stypox | 95cc6aefbb | |
Stypox | 7b54457789 | |
AudricV | f45966d449 | |
AudricV | d5437e0bc5 | |
Tobi | 88e07e555d | |
AudricV | 0766b1d211 | |
Tobi | 896d7e09eb | |
TobiGr | cd3262745d | |
TobiGr | 4e66b2287e | |
Tobi | 41c8dce452 | |
Isira Seneviratne | 2bca56f0df | |
Isira Seneviratne | 3b80547976 | |
Tobi | a1128ecbdc | |
ThetaDev | 016623131e | |
Kavin | 2e08eaad96 | |
Kavin | abf08e1496 | |
Tobi | 72d5ed3318 | |
Kavin | 57e7a6fb7c | |
Kavin | 1d3d7fa5c3 | |
Kavin | 52fda37915 | |
Kavin | b566084cac | |
Tobi | 40f1ec4a54 | |
Stypox | 5f6101698b | |
Tobi | f8162b049d | |
Tobi | ad7c3c2374 | |
Tobi | 1da0190056 | |
Stypox | 60fb30f835 | |
Kavin | 5abea22225 | |
Kavin | 7874d3c8aa | |
Kavin | faf28f5c11 | |
Kavin | a3ed947b2d | |
Kavin | c043597255 | |
TobiGr | 4680df0bdf | |
TobiGr | 9de8405c9f | |
Stypox | 34d79bd267 | |
AudricV | 2ec296e674 | |
AudricV | 3891542ca1 | |
AudricV | b2862f3cd1 | |
AudricV | e9a0d3bd95 | |
AudricV | b9e463de49 | |
AudricV | 65d6321e3d | |
ThetaDev | 5daabd1793 | |
Kavin | c953e23414 | |
Tobi | 2211a24b69 | |
Tobi | f6eefdc76c | |
Kavin | a1f22be0dd | |
Kavin | 9bdad55508 | |
Kavin | 86f06b333a | |
Kavin | b16e6082e1 | |
Kavin | 30909da1df | |
Kavin | 6d59cdbe3a | |
Tobi | 43d1c1f8b1 | |
Isira Seneviratne | e4d982c7ea | |
Isira Seneviratne | 416089146e | |
Isira Seneviratne | ff5f223d3f | |
Isira Seneviratne | ddbce3b83d | |
Isira Seneviratne | 366f5c1632 | |
Tobi | 0ea16c0b73 | |
dependabot[bot] | 0b82fade51 | |
Luigi Russo | c9635218e2 | |
Isira Seneviratne | 1be270768f | |
Isira Seneviratne | 316d8573fa | |
AudricV | 6a2c680d8f | |
AudricV | e66fed41d6 | |
AudricV | aa9a8ca23c | |
AudricV | eb07d70a2c | |
AudricV | 20cd8e8a4a | |
AudricV | a34f060ba0 | |
AudricV | 724f669ff7 | |
AudricV | 61ce041bda | |
AudricV | ffffb04439 | |
ThetaDev | 592e1d6386 | |
AudricV | 31bf7046b7 | |
ThetaDev | 099b53cc4f | |
AudricV | e4c24d4c36 | |
Theta-Dev | 20e4a35814 | |
AudricV | 4cae66f1f9 | |
Tobi | eb40bb8458 | |
chowder | b1a899fd47 | |
Stypox | a4db106a66 | |
Kavin | b441910257 | |
chowder | 3fdc0e72cc | |
Kavin | 6a256d0631 | |
Tobi | 430504b4b5 | |
Kavin | f9bd08c649 | |
Caleb | 9282c3c13b | |
Caleb | c5216f7c12 | |
Caleb | 04795fe5d2 | |
chowder | 975b38a0b9 | |
chowder | 4cccd33f3d | |
chowder | 09544ceb23 | |
chowder | 644cc6cd76 | |
chowder | daf5674951 | |
Tobi | 3d314169b9 | |
AudricV | 65125a3eb4 | |
AudricV | 7258a53225 | |
AudricV | e37e8b358e | |
AudricV | 60e97cd274 | |
AudricV | 4bc90cd9d8 | |
AudricV | c230d84df1 | |
Tobi | 9ffdd0948b | |
dependabot[bot] | 24137bb15f | |
xz-dev | 0ffcb32d9c | |
Isira Seneviratne | b90a566dd8 | |
Isira Seneviratne | b232c29d22 | |
TobiGr | 4d136599bd | |
Tobi | a822e91909 | |
TobiGr | 02810a7db7 | |
Tobi | 2d50369c77 | |
Tobi | 54092fc3c7 | |
Tobi | d7c23ef118 | |
dependabot[bot] | 08339d6b0c | |
Tobi | 5537e84100 | |
dependabot[bot] | 719664593f | |
TurtleArmyMc | bf70d32eb4 | |
Stypox | 5c710da160 | |
AudricV | 8067c43837 | |
AudricV | abcee87167 | |
ThetaDev | 7244be7627 | |
Isira Seneviratne | 0e31c86aee | |
Stypox | a99af9bb6e | |
Kavin | 4e14644c41 | |
Stypox | 14ef430546 | |
Stypox | 93ef73e2f2 | |
AudricV | d8ddeb549c | |
AudricV | 884da40f65 | |
AudricV | 58b09cf78b | |
AudricV | 119b9129e2 | |
ThetaDev | 4905f74700 | |
Isira Seneviratne | e101d58ae7 | |
Stypox | 6a858368c8 | |
Stypox | fa25a7280f | |
Stypox | 25f0ce7305 | |
Stypox | d8e10dd3ad | |
dependabot[bot] | 1d7a3a90a4 | |
Isira Seneviratne | 41254ae12a | |
Isira Seneviratne | 943b7c033b | |
litetex | 20914a6035 | |
litetex | d6577e5e0b | |
litetex | 0a5ad90724 | |
AudricV | 03d9a4fe9d | |
litetex | 0beb55a232 | |
litetex | 9e93d6b193 | |
litetex | ed0a07af4e | |
litetex | 641a447d9b | |
litetex | 8ff7a90f52 | |
TacoTheDank | d8339d4060 | |
AudricV | cb64a480cd | |
litetex | 5bbea3a8f2 | |
litetex | da06166065 | |
litetex | 2e36ab1578 | |
litetex | 2a8a623643 | |
litetex | 938e69a16a | |
litetex | 844de3e378 | |
litetex | c1a72b8240 | |
litetex | fc27b8a5b8 | |
Stypox | 17bad6cedf | |
litetex | ecfc370685 | |
litetex | 2b6fe294b2 | |
litetex | d6586da614 | |
litetex | bf3ae5e679 | |
litetex | 504f81036e | |
litetex | e7c12258f4 | |
litetex | 9e8724df4d | |
Tobi | 76aad92fa5 | |
AudricV | 472f5d9e9c | |
AudricV | 7bdca33a87 | |
AudricV | c82317e318 | |
AudricV | d0549a5a52 | |
AudricV | d7e678aca2 | |
AudricV | 6a885ef5ab | |
AudricV | 5b548340e8 | |
ThetaDev | 52ded6e3d7 | |
Stypox | d12003651b | |
Isira Seneviratne | 7daca10a06 | |
Stypox | 2906be22af | |
Stypox | 4ddb96a86f | |
Isira Seneviratne | 64771c5712 | |
Stypox | fc8b5ebbc6 | |
Stypox | 4a4939d89c | |
Stypox | c336bd58a5 | |
dependabot[bot] | 325af31e5f | |
dependabot[bot] | d905636021 | |
Isira Seneviratne | 1af6b8eedb | |
Isira Seneviratne | ff60e05c76 | |
Isira Seneviratne | 682a4263e5 | |
Stypox | 8c5f014a6f | |
Stypox | 954a294e27 | |
Mohammed Anas | f57049d2c0 | |
Stypox | 5bd7bf20cc | |
Stypox | 5ab74b3631 | |
dependabot[bot] | 122365005a | |
mhmdanas | 414186cff5 | |
dependabot[bot] | e9b4be3e3c | |
Stypox | 5219a705ba | |
AudricV | 090debd83b | |
litetex | a26bcc55c4 | |
dependabot[bot] | 424eb1c559 | |
litetex | 2712c3d788 | |
dependabot[bot] | 281d2b9f81 | |
litetex | f775155d25 | |
litetex | 9d625dd75f | |
Gábor Lipták | 28c9340d69 | |
Stypox | 1b51eab664 | |
AudricV | 301a795ed3 | |
AudricV | e960a417ec | |
Stypox | c8a77da2ab | |
Kavin | 7635aeed2c | |
TiA4f8R | 287d1dfd63 | |
Stypox | b3c620f0d8 | |
Stypox | d652e05874 | |
Stypox | 044639c32b | |
litetex | c33d392958 | |
TiA4f8R | fffbbee7f3 | |
TiA4f8R | f7b1515290 | |
TiA4f8R | f17f7b9842 | |
TiA4f8R | 301b9fa024 | |
TiA4f8R | 2f3920c648 | |
TiA4f8R | 4158fc46a0 | |
TiA4f8R | 54d323c2ae | |
Stypox | 2321822844 | |
Stypox | cfc13f4a6f | |
Stypox | 00bbe5eb4d | |
Stypox | 4da05afe11 | |
Stypox | 3708ab9ed5 | |
Stypox | 5c83409039 | |
Stypox | 8226fd044f | |
Stypox | ba68b8c014 | |
Stypox | 159d05c91b | |
Stypox | 50272db946 | |
TiA4f8R | 07b045f20d | |
TiA4f8R | 436ddde29f | |
TiA4f8R | d64d7bbd01 | |
TiA4f8R | 2fb1a412a6 | |
TiA4f8R | f61e2092a1 | |
TiA4f8R | aa4c10e751 | |
TiA4f8R | 6985167e63 | |
TiA4f8R | f6ec7f9a61 | |
TiA4f8R | 7477ed0f3d | |
TiA4f8R | a857684442 | |
TiA4f8R | 4330b5f7be | |
TiA4f8R | 881969f1da | |
TiA4f8R | d5f3637fc3 | |
TiA4f8R | 7c67d46e09 | |
TiA4f8R | ad993b920f | |
TiA4f8R | 2f061b8dbd | |
Stypox | 1dc80957d8 | |
TiA4f8R | c34b5e3a8b | |
litetex | 2015eb374a | |
litetex | f69b0ff77b | |
Stypox | 23fa31a1ec | |
TiA4f8R | 3c3cd78676 | |
Stypox | 2e1c5c119d | |
Stypox | 598ebb92ea | |
litetex | 5db4d1faf3 | |
litetex | fe30eb43a9 | |
Stypox | c2b5370517 | |
Stypox | 7c78c39230 | |
Stypox | ac1c22d81c | |
Stypox | 3f5c8962ac | |
TiA4f8R | 9f9af35adb | |
Fynn Godau | c38c016de5 | |
litetex | 5f8f3929bd | |
Nickoriginal | b5f3f9eb90 | |
litetex | 9f665db4e1 | |
Stypox | 52fa2d939a | |
Stypox | 95a4b05acc | |
Tobi | b77c72fb88 | |
Stypox | dcb7483dcf | |
Robin | bebdc55ad4 | |
TiA4f8R | ef49cd0007 | |
dependabot[bot] | 00b51a2bf8 | |
TiA4f8R | 2e92d718a2 | |
TiA4f8R | b30e341559 | |
TiA4f8R | 70812fa611 | |
TiA4f8R | 67288a0191 | |
TiA4f8R | 11b5a222c4 | |
TiA4f8R | 9ca647a750 | |
TiA4f8R | dfa4239661 | |
TiA4f8R | 6d27996ac4 | |
TiA4f8R | 2e3da445e6 | |
TiA4f8R | 1dad3bfe8b | |
TiA4f8R | 3d38459cf3 | |
litetex | 349ba8db7f | |
TiA4f8R | 52376949e5 | |
TiA4f8R | d0d91e6690 | |
TiA4f8R | b6bc521f0d | |
TiA4f8R | 26f93f5bb0 | |
TiA4f8R | 7d07924de8 | |
TiA4f8R | 05b7fee23b | |
TiA4f8R | 83f374bff1 | |
Stypox | 8c1041def6 | |
Stypox | adbbdc7a5b | |
Stypox | 24e83997b4 | |
Stypox | 349990fd48 | |
litetex | 3bf7aa3762 | |
litetex | 5a18730845 | |
litetex | af82edf9dc | |
litetex | 29408f9356 | |
litetex | fcee247f9f | |
litetex | 0fceb4686b | |
litetex | 70b20f0d6b | |
litetex | 6a6c9359af | |
litetex | 26596215fa | |
litetex | 53962bfd7d | |
litetex | 66dc5e8bb8 | |
litetex | 5d58156cde | |
litetex | 7598b40957 | |
litetex | 164e21b5af | |
litetex | 49dfdae5d2 | |
litetex | 69a58fd3d1 | |
litetex | 639be7adda | |
litetex | 2bd4299563 | |
litetex | 606a386a98 | |
litetex | 4d1a1c8fb8 | |
litetex | ba43dbaa28 | |
litetex | ff436e5740 | |
litetex | bb49f7d857 | |
litetex | 9c07e8a664 | |
litetex | 93d6e5c4d5 | |
litetex | 804e57004f | |
litetex | 33347ac18b | |
litetex | ec5b54c38b | |
litetex | 8771af7ba5 | |
litetex | 01cfde0285 | |
Stypox | bdadcfa1f7 | |
Stypox | 740a37a2de | |
Stypox | 9dc17cd1ca | |
Stypox | 9ab32cb2e7 | |
Stypox | 9f7e06c817 | |
Stypox | 3a94839359 | |
Stypox | 08dff33002 | |
Stypox | c2446ecff0 | |
Stypox | d79e20340c | |
Stypox | ca7c63f273 | |
Stypox | 1d5f22e41f | |
Stypox | 87d2834986 | |
Stypox | bd7b362040 | |
Stypox | 8aba2b47b0 | |
Stypox | e4951a0623 | |
Stypox | 37690058d2 | |
litetex | 9284569c84 | |
litetex | 358e619a27 | |
dependabot[bot] | a2c9ad104c | |
XiangRongLin | aa6b7272a4 | |
Stypox | 843945f2a6 | |
litetex | fa76098750 | |
litetex | faaf4cbec3 | |
XiangRongLin | 7f2ea133f0 | |
Stypox | 09ddb6adbb | |
Stypox | 8201b3b90e | |
Stypox | 13f7900816 | |
Stypox | 279f3a20fe | |
Stypox | d660c04838 | |
Stypox | 401082abe4 | |
Stypox | 63ed06a710 | |
Stypox | f19660e7d9 | |
Stypox | 8f9d5b858e | |
Stypox | 34a4484c72 | |
Stypox | 50db871d89 | |
Stypox | 638da1756c | |
Stypox | 53673d64c6 | |
Stypox | d8f2031619 | |
litetex | cc2e4d7104 | |
litetex | cd8088b217 | |
TiA4f8R | c7757c0994 | |
TiA4f8R | 35e082248e | |
TiA4f8R | 8b3f90eb7e | |
TiA4f8R | 58a247907e | |
TiA4f8R | fc6b45ee36 | |
Stypox | 73d1fd472f | |
Stypox | ef71a5fa0f | |
Stypox | 0c37c75981 | |
Stypox | 40aa5104b1 | |
litetex | ba56be8ef1 | |
XiangRongLin | e726437da3 | |
litetex | e7aee0ca57 | |
litetex | d806984aa8 | |
litetex | 2aa5f98c26 | |
Tobi | a267093e76 | |
dependabot[bot] | fdb603e9ac | |
FireMasterK | 60cc71e944 | |
Stypox | 3a6269bca6 | |
dependabot[bot] | e953794703 | |
Stypox | dd8687f9fe | |
FireMasterK | e6d334765d | |
FireMasterK | 6950b362f2 | |
FireMasterK | 5b0ec694a6 | |
FireMasterK | d290d2e393 | |
FireMasterK | ab49cb6e18 | |
Kavin | 4bd59f65f4 | |
FireMasterK | 6f3f608ab6 | |
Stypox | 69e18c80cb | |
litetex | 1a67ea100a | |
litetex | f79ce1f52a | |
TobiGr | dfe8716f5f | |
TobiGr | d07e16aadd | |
TobiGr | d337e537dc | |
Tobi | 5c8fbeb166 | |
dependabot[bot] | 89900431ab | |
Tobi | 0d6aebce25 | |
dependabot[bot] | a2a41e3bf2 | |
XiangRongLin | 545522b80f | |
TobiGr | ebc129c392 | |
TobiGr | 5306f7a988 | |
Abdu Ameen | 38e9da8d44 | |
Abdu Ameen | 36ebb633a4 | |
Tobi | 65129e65a8 | |
TobiGr | 047d75a4c9 | |
Abdu Ameen | 50a4b026f8 | |
Abdu Ameen | a9174f6b9f | |
dependabot[bot] | a4399fd4ad | |
Stypox | 59e0aeba88 | |
Mohammed Anas | 65df39b3e3 | |
litetex | a579ef2651 | |
litetex | a32edce750 | |
litetex | 602068937d | |
litetex | dd3f3da828 | |
litetex | a8ebd2cead | |
litetex | 066afd8629 | |
litetex | 3712a669b1 | |
litetex | 4291a90251 | |
litetex | 4995709871 | |
litetex | a2cbdf0991 | |
litetex | 10f6cc7194 | |
litetex | 148ba6641d | |
litetex | 4d0a689cb6 | |
litetex | a6961c5efa | |
litetex | 65687f3b9b | |
litetex | 1e2e0029fc | |
litetex | 15b98ffdb4 | |
dependabot[bot] | fe432425df | |
dependabot[bot] | 36663dcb37 | |
litetex | 12a6a48b15 | |
litetex | 7484b6a098 | |
litetex | 795e6df596 | |
litetex | 4dd737c9a0 | |
litetex | 7489502cc8 | |
Tobi | 1e0c409bec | |
litetex | d9b2a2042a | |
litetex | a3a0c1ee85 | |
litetex | 5c288eb8d7 | |
litetex | 75bb83a1b8 | |
litetex | f276bacce5 | |
litetex | 7f99c1c193 | |
litetex | 652fdf2c36 | |
litetex | 3c669d3cb5 | |
litetex | f735788a39 | |
litetex | f6f2724634 | |
bopol | 56c8af710e | |
bopol | c4eca91be9 | |
TobiGr | 5028396405 | |
dependabot[bot] | 1e0c802d8d | |
Tobi | 68d7e4e20a | |
bopol | 8e4f9995bf | |
opusforlife2 | cb80a646d9 | |
litetex | 7e7b78f1b3 | |
bopol | 396aecef19 | |
bopol | 38ad1eaac5 | |
bopol | 026751624f | |
bopol | e0b8e142fc | |
bopol | 599a91c88c | |
dependabot[bot] | 198d37090b | |
litetex | 4f60225ddc | |
mhmdanas | 3e8e2a1532 | |
Tobi | b425394e7a | |
Tobi | 4e9d9bffd0 | |
dependabot[bot] | 1867a381a8 | |
FireMaskterK | 62313962a0 | |
Kavin | d1add05bc1 | |
Tobi | 0b0c39aec2 | |
TobiGr | 8dcb74724a | |
dependabot[bot] | 3ca9f51b30 | |
TobiGr | d8ec3ce370 | |
FireMasterK | 94efe86c71 | |
Tobi | a9d214478d | |
TobiGr | ce8cabb9f8 | |
FireMasterK | 6aabdc6d16 | |
Kavin | 0aad09fa22 | |
Tobi | a031289a63 | |
dependabot[bot] | 82ae322cb8 | |
Tobi | 62b87552f5 | |
FireMasterK | feb09e9997 | |
Tobi | f0aa46b008 | |
Tobi | c8037f5e00 | |
FireMasterK | 857b2f39e8 | |
FireMasterK | 1db463b55f | |
Kavin | db6b3b2c29 | |
FireMasterK | a0c1dcc8d8 | |
FireMasterK | 0ba03c552c | |
FireMasterK | b9fad4fcc8 | |
Stypox | 68f1fa994a | |
TobiGr | b644f1d67b | |
Stypox | a5e9eeb790 | |
Tobi | 9570882c73 | |
Stypox | db6c729678 | |
talanc | 94a29fd63f | |
dependabot[bot] | 816bc7137f | |
TobiGr | 6d487e0dad | |
Tobi | 7823dcd99d | |
dependabot[bot] | d75d54e1c6 | |
dependabot[bot] | 72393995bb | |
Stypox | d8179dc35a | |
Tobi | 89bab241b9 | |
XiangRongLin | 61acdae4b8 | |
FireMasterK | 4f86b8ce43 | |
Stypox | 52910ce970 | |
TiA4f8R | f8309d546a | |
magicfelix | 0e16091ce0 | |
Tobi | b6f951edb4 | |
gardenapple | 50bba32239 | |
Tobi | 78f2192cbb | |
TacoTheDank | 4a7c1723a6 | |
TacoTheDank | 8919a53d95 | |
TacoTheDank | cf9e678223 | |
FireMasterK | 71b9fd0076 | |
Tobi | 4b147863ec | |
FireMasterK | ee32317315 | |
FireMasterK | e8e9e6cd00 | |
FireMasterK | 666b45958e | |
FireMasterK | f6d054e5da | |
FireMasterK | ed84658055 | |
FireMasterK | ea7e6526fd | |
FireMasterK | 4385cc1b7c | |
Stypox | 8ca5e5e364 | |
Stypox | bb3815d19b | |
Stypox | 5b38b3ae97 | |
Stypox | 4b5907e18c | |
Stypox | 5a88263785 | |
TiA4f8R | 79cc5c8d12 | |
TiA4f8R | b74a39c176 | |
TiA4f8R | f8197da09e | |
FireMasterK | f3e4c9d689 | |
TiA4f8R | 286d839a3b | |
TiA4f8R | 693f654f02 | |
TiA4f8R | d55109258d | |
FireMasterK | 2eeb0a3403 | |
TiA4f8R | 7753556e66 | |
TiA4f8R | 2320aecb7c | |
TiA4f8R | 629dcd63b2 | |
TiA4f8R | 3adac6a150 | |
TiA4f8R | 8aa60d7e8f | |
TiA4f8R | 27bd797f1e | |
TiA4f8R | 609919db59 | |
TiA4f8R | 4299d806a2 | |
TiA4f8R | 1a6b8da438 | |
TiA4f8R | d8177b57f6 | |
TiA4f8R | a6a2c6eb80 | |
TiA4f8R | 81013e5a8e | |
TiA4f8R | cc798523cd | |
TiA4f8R | accd5ddef3 | |
TiA4f8R | 632772d17f | |
TiA4f8R | 657f165771 | |
TiA4f8R | 8c1c7281b0 | |
TiA4f8R | e97a685989 | |
TiA4f8R | 34a9ccb0fd | |
TiA4f8R | 54d4551ca6 | |
TiA4f8R | a59c2a3577 | |
TiA4f8R | 6921e80ded | |
TiA4f8R | 70927ddade | |
TiA4f8R | 318bc46a8c | |
TiA4f8R | 947baec805 | |
TiA4f8R | 7474049fd1 | |
TiA4f8R | c32bc6e534 | |
TiA4f8R | b52732a1c3 | |
TiA4f8R | ae5abc0c5d | |
TiA4f8R | 0f9e9b8b4b | |
TiA4f8R | 14569c4aa9 | |
TiA4f8R | 3017dde67e | |
TiA4f8R | 013b902535 | |
TiA4f8R | e7d589edbf | |
TiA4f8R | f73c923f60 | |
TiA4f8R | effcdaa4f2 | |
TiA4f8R | 4d682834c3 | |
TiA4f8R | f46cfb0f26 | |
TiA4f8R | e075dd5a63 | |
TiA4f8R | b49ae547a3 | |
TiA4f8R | 58ce9b04a1 | |
TiA4f8R | 991b2c7d73 | |
TiA4f8R | 9ab9c66ddf | |
TiA4f8R | 77c031a88a | |
TiA4f8R | f461224b2b | |
TiA4f8R | a12c69da7d | |
TiA4f8R | 5794eb2350 | |
Stypox | c97a19d719 | |
Stypox | 06a5219c9c | |
XiangRongLin | 37df225556 | |
FireMasterK | f4aad8b014 | |
FireMasterK | 88c11db4c5 | |
FireMasterK | fd19c53f65 | |
FireMasterK | a685941bba | |
XiangRongLin | 852a65ff18 | |
XiangRongLin | 48d897e6ad | |
XiangRongLin | 60794aea31 | |
XiangRongLin | 2967d1ae6a | |
XiangRongLin | 1c78976900 | |
TobiGr | b62fe7141e | |
TobiGr | dbc4e01c17 | |
TobiGr | 6335823843 | |
TobiGr | 7c6ff0a38f | |
TobiGr | 8bfcb0ad59 | |
Tobi | 027dc65434 | |
Tobi | 14c179f343 | |
Tobi | 394c02ad06 | |
TobiGr | 785ff4aa32 | |
TobiGr | 79f2d74b04 | |
TobiGr | d70adfdb8f | |
TiA4f8R | d13f531b6f | |
TiA4f8R | 32055147e0 | |
TobiGr | 1c30a2725e | |
XiangRongLin | 3a3d1d7f2b | |
XiangRongLin | a683c8d278 | |
XiangRongLin | a02ee2e952 | |
Tobi | ada67d136a | |
litetex | 0c12b396e5 | |
litetex | ecf4232ce3 | |
litetex | 0f9ed020a4 | |
litetex | 17ccaf4b87 | |
litetex | 4e41e172ea | |
litetex | 09b670d745 | |
litetex | b086655d54 | |
litetex | 93b0c5b15f | |
XiangRongLin | 6956b72af7 | |
XiangRongLin | a189f685dc | |
XiangRongLin | fcdb9bdbeb | |
XiangRongLin | 80cf8b3acd | |
XiangRongLin | a86a30103f | |
bopol | c38a06e8dc | |
Tobi | b45bb411e8 | |
TobiGr | 6fd93cdb31 | |
litetex | fdebf3c6cd | |
litetex | 6860543b07 | |
litetex | a59fead0d7 | |
litetex | 688a1c316b | |
litetex | ca33f4f60b | |
Tobi | f324772254 | |
TobiGr | dbdc962a07 | |
TobiGr | 9ca6dc26ed | |
Tobi | a023f08774 | |
XiangRongLin | b9549ace25 | |
XiangRongLin | 5982431ffb | |
bopol | 7d7cc087e2 | |
bopol | 3966178979 | |
bopol | b475f09ba7 | |
TiA4f8R | d1c56be744 | |
bopol | b96b2a6eba | |
bopol | c5e8bd368d | |
bopol | e9a992b0a9 | |
bopol | 35c1bdd012 | |
dependabot[bot] | ad403ce73e | |
bopol | 65fd8740aa | |
bopol | 361f142621 | |
bopol | 58f109ddec | |
bopol | fe29c78fe1 | |
bopol | e9412fdadc | |
XiangRongLin | 1510dd5187 | |
TobiGr | 1f9a99c931 | |
Tobi | c998012c28 | |
dependabot[bot] | 38d0339753 | |
dependabot[bot] | 5a46878b09 | |
bopol | 598de2dd5f | |
Tobi | 0cb48e36bd | |
TobiGr | 6762fe3357 | |
Tobi | a27575021d | |
TobiGr | f7f727d19c | |
TobiGr | b70c0f93c7 | |
Tobi | d4186d100b | |
TobiGr | 89e0514d8b | |
TobiGr | 80d3052033 | |
TobiGr | 98825a2f01 | |
TobiGr | fa444c8298 | |
Tobi | 8c42a48673 | |
Stypox | 2158ca4060 | |
Stypox | 13f192704d | |
XiangRongLin | 0ad51e76fb | |
Tobi | 143fd3c81c | |
XiangRongLin | faa503c7dd | |
XiangRongLin | db81384ae0 | |
TiA4f8R | ac31f3a883 | |
Tobi | 7e4332e0cf | |
bopol | ff11c2df2a | |
TobiGr | 1ff56a85df | |
TobiGr | 858e007804 | |
Tobi | 519bba70b3 | |
litetex | 289b84e68b | |
litetex | e81b0e2885 | |
TobiGr | f4cb32cef5 | |
XiangRongLin | bf2af4f316 | |
litetex | 8c96545e57 | |
litetex | 12fb18c310 | |
litetex | 8e08a2aac7 | |
litetex | 020acfed71 | |
litetex | b310922fc0 | |
litetex | 86bb9efb5d | |
litetex | 46654f5fca | |
litetex | 2174685c5c | |
litetex | 01cfb55505 | |
litetex | bedcd87abb | |
litetex | 545c0a6f42 | |
Tobi | ff005122bf | |
TiA4f8R | a00fdcbd3d | |
TiA4f8R | 6b607eb38d | |
TiA4f8R | 4552ea9c9f | |
TiA4f8R | 86308d0603 | |
TiA4f8R | c5c190500c | |
litetex | 89ecc4e2e8 | |
litetex | 7791907480 | |
Tobi | 636e27333b | |
Tobi | fadf677c42 | |
XiangRongLin | 172394cd52 | |
XiangRongLin | 18c8cac40c | |
Hassan Igbaria | e8b5364745 | |
litetex | c3b837fe3b | |
litetex | b934c7ccbb | |
litetex | 10cf081145 | |
TobiGr | 1acc53b8db | |
Tobi | 3a3ade20f4 | |
Fynn Godau | 85a4270bd9 | |
TobiGr | ec50570053 | |
TobiGr | f4404b5cc8 | |
TobiGr | fc998589dc | |
TobiGr | bb3861ddce | |
Tobi | 284362fc15 | |
TobiGr | dc678a9f6d | |
Tobi | 7cf8edd5ac | |
TobiGr | 9c12dc5609 | |
Fynn Godau | 9dc7a1d5ee | |
TobiGr | 8f023c1ec7 | |
Tobi | 7f202db8b1 | |
fynngodau | 6db4bea8ca | |
Tobi | c14f6db14a | |
Fynn Godau | 3671876721 | |
Fynn Godau | 90b5c00599 | |
TobiGr | 1fe645704f | |
TobiGr | 9a2b814d8f | |
TobiGr | 50005ce937 | |
Tobi | 82d11386df | |
TobiGr | 318cec7625 | |
TobiGr | 55e0f8e725 | |
Tobi | e747a89be2 | |
TobiGr | 33173eb3e6 | |
TobiGr | b94b316558 | |
TobiGr | 4b8d4a84b6 | |
XiangRongLin | db7ad9c27d | |
XiangRongLin | 4833df60e5 | |
TobiGr | 682ec27737 | |
XiangRongLin | eafad3ad6b | |
TobiGr | 080d1e318d | |
TobiGr | 4ee270fe01 | |
TobiGr | 883f16e0ad | |
XiangRongLin | aee3838ed5 | |
XiangRongLin | 90afd6162a | |
XiangRongLin | 7b06c696e2 | |
XiangRongLin | eda1b6e199 | |
XiangRongLin | 3af26a2821 | |
Tobi | 4e0be60ddc | |
XiangRongLin | 7b36469807 | |
Tobi | b481a228cd | |
XiangRongLin | 3d70b6d46a | |
XiangRongLin | 1925dcf4dc | |
Fynn Godau | c877712647 | |
Fynn Godau | 14f6f1b7c3 | |
Stypox | 9dbc152d44 | |
TobiGr | 1498e1905e | |
Tobi | 7ea2cd73d3 | |
Fynn Godau | 705f6c6e33 | |
XiangRongLin | 564d74c250 | |
TobiGr | 657b00ca11 | |
TobiGr | 8fbd1a2bcf | |
TobiGr | f2e7c7ebd6 | |
TobiGr | 48fd8ca7b3 | |
TobiGr | 22bd16fca3 | |
TobiGr | 6a0f6e846a | |
TobiGr | b9282bbe94 | |
TobiGr | 46eab1ec17 | |
TobiGr | c3e23559d7 | |
TobiGr | e4c40ae6d1 | |
XiangRongLin | af2183855a | |
TotalCaesar659 | cf3f54f438 | |
Fynn Godau | d663be5a78 | |
Fynn Godau | dbcf61c6f7 | |
Stypox | b4dee6d08f | |
TobiGr | e61ceef005 | |
Tobi | e2500fb7cc | |
TobiGr | 070a40e181 | |
TobiGr | b029779217 | |
TobiGr | bbee15474d | |
Tobi | d4f83a1782 | |
TiA4f8R | ae283314da | |
TiA4f8R | abb790f465 | |
TiA4f8R | 772de53a66 | |
TiA4f8R | efe2b964f0 | |
Tobi | 12835bfae1 | |
Tobi | cae6cea551 | |
Stypox | ce7cbbc9a0 | |
XiangRongLin | 9009f1e277 | |
bopol | 152221c7fb | |
bopol | 8806fb4e6b | |
bopol | c47cc54908 | |
Stypox | f71cfd489c | |
TiA4f8R | 379d7312fa | |
TiA4f8R | 0e3e420a25 | |
TiA4f8R | d61d9d116d | |
TiA4f8R | a7b15b51e6 | |
TiA4f8R | 0438828e36 | |
TiA4f8R | 3bd08a2880 | |
TiA4f8R | cbacd3c0a5 | |
TiA4f8R | 26f1b4e7dc | |
Tobi | def745b801 | |
Fynn Godau | 2e57a8f24f | |
TobiGr | 70d9e389b9 | |
TobiGr | f9d06252f2 | |
Tobi | 7e6f464407 | |
Tobi | ac51134aed | |
Tobi | 021da75f24 | |
TobiGr | 22fa131922 | |
TiA4f8R | e55284bb8f | |
TobiGr | d0a1041afd | |
TiA4f8R | 890cbba625 | |
TobiGr | da3cfa967d | |
TiA4f8R | 448b68700c | |
TobiGr | 22e6f33f0a | |
TobiGr | 21158744b1 | |
TiA4f8R | f15d7837a1 | |
TiA4f8R | 4e45aef2b3 | |
TiA4f8R | 59d6d3f04e | |
TiA4f8R | 3925204658 | |
TiA4f8R | 771bb1a2cb | |
TiA4f8R | 35325d980d | |
TobiGr | cb07ffa1eb | |
Tobi | bc0cda68d1 | |
Tobi | a3c6fceef5 | |
bopol | ff5273b882 | |
Tobi | ec1127dd6a | |
bopol | ed850d0688 | |
TobiGr | 440a808b8a | |
Tobi | b608587e4d | |
golfinq | 05213175cd | |
golfinq | 8bceb57a6c | |
golfinq | ae48bdea4c | |
TiA4f8R | b27efdc9a4 | |
TobiGr | 2b671b15ce | |
TobiGr | 33594d4aff | |
TobiGr | fb2f37d223 | |
XiangRongLin | 9256b3b848 | |
XiangRongLin | 506cc5fbe9 | |
XiangRongLin | 03b00ff1d6 | |
FireMasterK | 668b080b3a | |
XiangRongLin | 43b46bd408 | |
XiangRongLin | beb05bd05c | |
XiangRongLin | e13e237392 | |
Tobi | 5d594cfded | |
Tobi | 965a66bc87 | |
TiA4f8R | b2cf41496d | |
TobiGr | a1688fe953 | |
TobiGr | 70814dcfef | |
TobiGr | b9e8ee8450 | |
TobiGr | c07db80ef0 | |
TobiGr | adde4332d1 | |
FireMasterK | 42bdd8adc5 | |
FireMasterK | e8bccfaf5d | |
bopol | a9aa385bb3 | |
bopol | 0c831afa7b | |
TobiGr | ea120a4637 | |
XiangRongLin | adf9d7d10f | |
XiangRongLin | ea52030613 | |
TobiGr | 02920fafa8 | |
TobiGr | 91e9309486 | |
TobiGr | fa61b864f2 | |
TobiGr | 98268e351c | |
FireMasterK | 959d5d7db7 | |
bopol | 27a20e41cd | |
FireMasterK | 5333d8a98b | |
FireMasterK | 88890d81a5 | |
bopol | 9c1a7f7df6 | |
bopol | 1a322ad8ed | |
bopol | db253e202b | |
FireMasterK | c24afa2cbb | |
FireMasterK | 8d54401233 | |
FireMasterK | 4d096be14a | |
FireMasterK | 5b70645d85 | |
FireMasterK | e975d33fbe | |
FireMasterK | f31b2a68fd | |
FireMasterK | d4945ac55c | |
FireMasterK | 3f6a601be8 | |
FireMasterK | a7c9905183 | |
FireMasterK | 0c0f2d74bc | |
FireMasterK | d1054338d3 | |
FireMasterK | 11eb4932f4 | |
FireMasterK | 525e345ed8 | |
FireMasterK | e844d2aed3 | |
FireMasterK | b713a7af8c | |
bopol | 64f6b0478f | |
XiangRongLin | a64579da2f | |
XiangRongLin | b99cb8624f | |
XiangRongLin | 37366c65ca | |
XiangRongLin | c327922cc0 | |
XiangRongLin | 26d50aca05 | |
TobiGr | 54aa5b3042 | |
TobiGr | 54b8e54f80 | |
TobiGr | 16973126a0 | |
bopol | 84380e4c3a | |
bopol | 41f689b099 | |
XiangRongLin | a9594f531f | |
XiangRongLin | a28e0226c5 | |
XiangRongLin | 05cb22c654 | |
XiangRongLin | d9b4c2779f | |
XiangRongLin | 137272b7a4 | |
TobiGr | 5bf9fddba9 | |
TobiGr | e062c8cb0d | |
bopol | 416cf17b7a | |
bopol | 7a3d9bdb7d | |
bopol | 557934cb17 | |
bopol | 9ca52ca68a | |
XiangRongLin | 50e5718bd1 | |
Tobi | d7486f251a | |
TobiGr | 432c68cdec | |
TobiGr | 5eb75d72d3 | |
XiangRongLin | a88b2e6af4 | |
bopol | 8d89c82caa | |
bopol | ee2012c948 | |
bopol | 8db40db4de | |
bopol | 81317cf6bc | |
bopol | f15c0fcfed | |
XiangRongLin | 971128c464 | |
XiangRongLin | 92dea0806a | |
XiangRongLin | d6b9930c18 | |
XiangRongLin | b555f38fa5 | |
XiangRongLin | f352b0c7af | |
XiangRongLin | f45b5610bf | |
XiangRongLin | 19737e06ee | |
XiangRongLin | 84eb4b30c9 | |
XiangRongLin | b43f3474f5 | |
Tobi | 8d7b62914c | |
bopol | 2859c7ba5c | |
bopol | 73353996d7 | |
bopol | a931e31239 | |
TobiGr | d116680ea3 | |
TobiGr | f8d769617a | |
Tobi | c2525916ac | |
bopol | b0f356dd76 | |
bopol | fcdb50b825 | |
bopol | 44c54d403a | |
TiA4f8R | 1414a6f178 | |
Tobias Groza | bfa639950e | |
TobiGr | d728c4fad0 | |
Stypox | a64dfd7343 | |
Tobias Groza | c00ee75d74 | |
bopol | 2662ceca22 | |
Tobias Groza | 10e8c543e8 | |
bopol | 66e4eb2f96 | |
Tobias Groza | 69f155d292 | |
bopol | 67ddfefdc6 | |
bopol | 53d3f7989b | |
XiangRongLin | 88e4c8667b | |
XiangRongLin | eecfe09f2c | |
XiangRongLin | a0288b8fd6 | |
Tobias Groza | beb705013c | |
XiangRongLin | 7a6d930e7b | |
TobiGr | 07570554b6 | |
TobiGr | 78c2113094 | |
Stypox | b9ba95614b | |
XiangRongLin | a6cfe11ae9 | |
Stypox | 48a9993edd | |
Stypox | 588fbcc61f | |
XiangRongLin | 6888e7773f | |
XiangRongLin | e5dc62d9bd | |
Stypox | 7c774c84ce | |
Stypox | c2c4d97efd | |
XiangRongLin | 4f81d9d689 | |
Stypox | 951159f75f | |
Tobias Groza | cc51c5f6a3 | |
XiangRongLin | 6ca7123d5d | |
vkay94 | 11dcfe638b | |
Tobias Groza | c2ff6723d7 | |
Tobias Groza | 6773ec8699 | |
TobiGr | 0c4836c800 | |
XiangRongLin | 4af50c6870 | |
XiangRongLin | 3455f0f23c | |
bopol | 5292cc1636 | |
bopol | f3c22da618 | |
Tobias Groza | da968cf9c5 | |
XiangRongLin | 63c237db41 | |
XiangRongLin | e82cb1efea | |
XiangRongLin | 1ea6c6ce54 | |
XiangRongLin | f91916c017 | |
XiangRongLin | 255c726f20 | |
XiangRongLin | 35e299759e | |
XiangRongLin | f447a7a450 | |
XiangRongLin | f8aa989c42 | |
XiangRongLin | 285c26eafa | |
XiangRongLin | e6e8e39def | |
XiangRongLin | 7c40fb8bf7 | |
XiangRongLin | 1bcb9c76a7 | |
XiangRongLin | 4dad3d60d6 | |
XiangRongLin | fe3902c669 | |
TobiGr | 2d93b23723 | |
TobiGr | 52f3bd15f2 | |
Tobias Groza | ab62464e86 | |
bopol | 0bcea9c2cd | |
bopol | 0e45c25ea9 | |
TobiGr | fae67fbd45 | |
Tobias Groza | c4b9183684 | |
TobiGr | 4671e68304 | |
TobiGr | d63d20f943 | |
FireMasterK | 1ba689900e | |
TobiGr | 2cbc3ccb1a | |
TobiGr | a896ec41b9 | |
TobiGr | 5dc9a76e3c | |
TobiGr | edf8dd0e92 | |
Tobias Groza | 3c8c8e7307 | |
TobiGr | 288ddab65b | |
TiA4f8R | d30caeb091 | |
TobiGr | deb9af7bf5 | |
TobiGr | 21c6a7884c | |
Tobias Groza | 1259521d53 | |
TobiGr | b4e1913971 | |
TobiGr | 80f4d42226 | |
TobiGr | ed9402c002 | |
TobiGr | 674f2227f3 | |
TobiGr | 090dade5b3 | |
TobiGr | 0cfefe222a | |
TobiGr | d1a0686d2f | |
TobiGr | 41699798fe | |
TobiGr | b2154c6e87 | |
TobiGr | ed6ae329b3 | |
TobiGr | b2837698f5 | |
Tobias Groza | b5e50cc9fb | |
TobiGr | d9e2da53c3 | |
TobiGr | abee0a8df1 | |
TobiGr | 3033c0b993 | |
TobiGr | b62144b49d | |
TobiGr | c9d9bd1e24 | |
bopol | 3ae924a7f1 | |
TobiGr | 942d840624 | |
TobiGr | 865c42e273 | |
Tobias Groza | c682ea0d18 | |
Fynn Godau | c9e9953bb0 | |
TobiGr | 41a8ed625d | |
TobiGr | bc6de14952 | |
TobiGr | 50903730b1 | |
Tobias Groza | 853a65a1a6 | |
TobiGr | 838dc1ab54 | |
Tobias Groza | 7e481cf81a | |
bopol | 8d3436565f | |
Tobias Groza | 22a415156f | |
TobiGr | 74b46fed2d | |
TobiGr | 116e921d6c | |
Tobias Groza | e8cc302d5f | |
TobiGr | df16a8646f | |
XiangRongLin | cdcb66b93e | |
XiangRongLin | ba8782a9ed | |
Stypox | 85fa006214 | |
XiangRongLin | f90f6fcf92 | |
Xiang Rong Lin | a338e4e08e | |
Xiang Rong Lin | 22d2f7e400 | |
Stypox | 421935401f | |
Xiang Rong Lin | 3ff8619bcc | |
Xiang Rong Lin | 822cf307f7 | |
Xiang Rong Lin | df38b1926c | |
Xiang Rong Lin | 68a3948af6 | |
Xiang Rong Lin | ec6b99c082 | |
Xiang Rong Lin | 0ff054acb4 | |
Xiang Rong Lin | a376792a5d | |
Xiang Rong Lin | 327a5730a8 | |
Xiang Rong Lin | d74265c846 | |
Xiang Rong Lin | 0efb854d27 | |
vkay94 | 2ba27b39af | |
Stypox | 2b622fd2f1 | |
vkay94 | 9a7a224a54 | |
vkay94 | d3eea4f4be | |
Isira Seneviratne | 57be1f1123 | |
Isira Seneviratne | 4c19a88612 | |
Isira Seneviratne | b8f64595a2 | |
TobiGr | b3835bd616 | |
Tobias Groza | 6fbcdd24ee | |
vkay94 | 9dbacbc618 | |
Fynn Godau | 04dd3d4d32 | |
Tobias Groza | 8ade913d9b | |
TheAssassin | ba3e2302bc | |
TobiGr | 175df679e0 | |
Tobias Groza | b7a995187f | |
Fynn Godau | be562b8436 | |
TobiGr | c91e21b37c | |
TobiGr | 99e7ef013e | |
fynngodau | 8c369b0f79 | |
TobiGr | 5a3f96b967 | |
Tobias Groza | 650f0920fe | |
TobiGr | 334e1e9b53 | |
Fynn Godau | 186936d041 | |
Fynn Godau | 6bc7e3420e | |
TobiGr | 3582a5189f | |
XiangRongLin | 5bceff0083 | |
XiangRongLin | 8347e14952 | |
opusforlife2 | 2174055ef8 | |
opusforlife2 | c0afd5213a | |
Tobias Groza | 6701b0fe71 | |
Stypox | 89a77ae74a | |
Stypox | 827f7bd137 | |
Stypox | b42a196f35 | |
Isira Seneviratne | 4fe28d7e3a | |
Isira Seneviratne | 9cf9e7e980 | |
Isira Seneviratne | fe31a90cb3 | |
Tobias Groza | b13c7e1c1e | |
Tobias Groza | ac50068bbe | |
bopol | 345e136f6c | |
Stypox | 501ec30152 | |
Tobias Groza | 564a965810 | |
Bri@n | 82746d172f | |
Stypox | 2f02c0e6a4 | |
bopol | f69b3ef05d | |
TobiGr | be9e160333 | |
Tobias Groza | 8cbdec675b | |
Isira Seneviratne | fcdfe7d939 | |
Isira Seneviratne | 4f04cfccca | |
Isira Seneviratne | ee3af63c04 | |
Isira Seneviratne | 0526a5148d | |
Isira Seneviratne | b2d0c098a3 | |
Stypox | 6cc50b57e3 | |
Stypox | b242e1d113 | |
Stypox | 9e53cf0b56 | |
Stypox | 3fe55b30ba | |
Stypox | c190a3029b | |
Isira Seneviratne | 35c7b5640c | |
Stypox | a9303b24a5 | |
bopol | b21e59925d | |
Isira Seneviratne | 61dcbbb800 | |
Stypox | bcc01b99c7 | |
Stypox | 30ed4f2d63 | |
TobiGr | 62912ee834 | |
TobiGr | 94176c5d68 | |
Tobias Groza | 19c0e8700d | |
Tobias Groza | 354426fe5d | |
Scratch | 947ce3ee10 | |
bopol | 6dc5ab4015 | |
bopol | db0ef83d6b | |
Stypox | 943334ba49 | |
bopol | c1e9857960 | |
bopol | a39a2cca82 | |
Tobias Groza | a8665fd97f | |
bopol | 0a12300c5e | |
bopol | 01f49e8f66 | |
Tobias Groza | 14c0c37d64 | |
Stypox | df26f61cfc | |
Kiril Isakov | 366ed71523 | |
Stypox | 8026304a0a | |
Stypox | 57e7994c9e | |
Stypox | f11fe87688 | |
Stypox | 3c55ea9321 | |
Stypox | 880ff04a5c | |
Stypox | d0b14644bb | |
Stypox | 8ce711f40f | |
Stypox | 6127826571 | |
Stypox | a087b092b4 | |
Stypox | a4097d8d01 | |
Stypox | 8dc3f28618 | |
Stypox | 68d23defba | |
Stypox | 3191bd6c70 | |
Stypox | 55bc01d1ce | |
Stypox | 06430c4749 | |
Stypox | fcb9b6f855 | |
Stypox | af5b8b1915 | |
Stypox | 07a90d116a | |
Stypox | 0c980b2d64 | |
Stypox | d130fd79c3 | |
Stypox | 492db83ccf | |
Stypox | 3b2a1829d4 | |
Stypox | b461da792f | |
Stypox | aeeae87641 | |
Stypox | 7ae3cb6d07 | |
Stypox | 4349be13af | |
Stypox | 3b2cfb4ca2 | |
Stypox | 072bae321f | |
Stypox | 7cd410f3fc | |
Stypox | 7fb867c166 | |
Stypox | 1d7a86e664 | |
Bartosz Rumiński | 29695aed0a | |
Bartosz Rumiński | 0e67d820bc | |
Bartosz Rumiński | d3f80d1538 | |
Tobias Groza | 3058af0e43 | |
Stypox | 6887d59570 | |
Scratch | e945f711c1 | |
TobiGr | 350eed6214 | |
Tobias Groza | f82eda9f7d | |
wb9688 | be9a6f931c | |
Tobias Groza | 1a9bc8caf1 | |
Scratch | 6a70cb9d50 | |
Tobias Groza | 9a785f9a7e | |
Scratch | 538f5d3973 | |
Tobias Groza | 527945eadb | |
Stypox | 19e862657a | |
Bartosz Rumiński | e3f996e014 | |
Bartosz Rumiński | 5ab1b053d2 | |
Bartosz Rumiński | 7abb4b3713 | |
Bartosz Rumiński | 9d63211a66 | |
Bartosz Rumiński | 8c38a5509e | |
Bartosz Rumiński | 4e04991762 | |
Fynn Godau | 81b5e7cf3d | |
TobiGr | acb04eb351 | |
Stypox | 2463884aa8 | |
wb9688 | ebbfe7f6d4 | |
Tobias Groza | 6633f26ec5 | |
Tobias Groza | 070cd92857 | |
wb9688 | 7657c2ed1a | |
mhmdanas | 1a63dcb355 | |
Tobias Groza | de8edbe7a5 | |
mhmdanas | f0f1c009b2 | |
Fynn Godau | cfe88a74c1 | |
TobiGr | 0fb73301e3 | |
Fynn Godau | 932d094d6a | |
Fynn Godau | c12ef3a02d | |
TobiGr | 5ac80624a4 | |
TobiGr | b4481dfec2 | |
Tobias Groza | 32dff1541f | |
TobiGr | ec778200d9 | |
Tobias Groza | 8627d01006 | |
wb9688 | 667dce034c | |
wb9688 | f345f667e2 | |
wb9688 | 4c987a5302 | |
wb9688 | 81459e289f | |
Tobias Groza | df28a087de | |
wb9688 | fc3a63fec5 | |
wb9688 | ff560e907a | |
TheAssassin | 186193d366 | |
Tobias Groza | a70cb0283f | |
wb9688 | 0a5a905bc7 | |
Tobias Groza | e5d23a89de | |
wb9688 | 17ba8a57fa | |
wb9688 | 9b6fe1dea6 | |
wb9688 | 4cc312086a | |
wb9688 | e3bfdba135 | |
wb9688 | 156a26b64b | |
Tobias Groza | 54d9e5a2f8 | |
bopol | 261471e7f4 | |
bopol | eb3901acee | |
Tobias Groza | c0ceb5cb27 | |
Tobias Groza | f4ed7ce922 | |
Tobias Groza | 339f1d9c43 | |
Tobias Groza | 970bc7f69d | |
Tobias Groza | dbf6ab9e91 | |
Robin | 576754982e | |
yausername | 89e0e3ee2d | |
Mauricio Colli | 7505e828cc | |
Mauricio Colli | d2f1c0f40d | |
Robin | b63ae93495 | |
Tobias Groza | 92f6754f0f | |
wb9688 | e02d3c37b2 | |
Stypox | 5808439c3f | |
wb9688 | 3239aa84f2 | |
wb9688 | a65e46e4b1 | |
Tobias Groza | 5f523254cb | |
Fynn Godau | 96de834b67 | |
Fynn Godau | 39b55b5b42 | |
Fynn Godau | 9a555d97e5 | |
Fynn Godau | e13f341a45 | |
Fynn Godau | 4dd9540782 | |
Fynn Godau | 52103ac61f | |
Fynn Godau | 8fa81537c0 | |
Fynn Godau | 0950a95577 | |
Fynn Godau | e6ecd91929 | |
Fynn Godau | 27e7aad159 | |
fynngodau | ea49202f64 | |
bopol | 063f4be766 | |
Fynn Godau | 34b6928124 | |
Fynn Godau | 1be20ceeec | |
Fynn Godau | 6822fe3dc8 | |
Fynn Godau | e98bff8ae9 | |
Fynn Godau | 8f6c00f8d8 | |
Fynn Godau | de776561b0 | |
Fynn Godau | 3940138fc5 | |
Fynn Godau | 9fa9d920a9 | |
Fynn Godau | dd955c7f0c | |
wb9688 | 33b1121fc7 | |
TobiGr | 3aa6b93e91 | |
Tobias Groza | bda83fe6a5 | |
wb9688 | e39147202c | |
wb9688 | 98055a3c3c | |
Tobias Groza | bdb0f2ae6b | |
Tobias Groza | cf18cdb2f5 | |
wb9688 | ab77961b0a | |
wb9688 | 4d683e7655 | |
Tobias Groza | 0b4977bb0c | |
Tobias Groza | 0f8057ae56 | |
wb9688 | 384d6acd5d | |
wb9688 | 8f71bde776 | |
wb9688 | aa277e1b6a | |
wb9688 | 036cc7e26e | |
Fynn Godau | 89b0639faa | |
Fynn Godau | e08256ebc6 | |
Fynn Godau | 692b2d06f4 | |
Fynn Godau | fff21caa87 | |
wb9688 | 7ef3e80493 | |
Stypox | 60e6df7e6b | |
wb9688 | efafbbb88a | |
wb9688 | baf5dd3e8f | |
wb9688 | 102f04e297 | |
bopol | cbf1163b80 | |
Tobias Groza | b40ccb5075 | |
bopol | aeb2a6ff6f | |
B0pol | 3a858e6640 | |
bopol | adaf196c99 | |
bopol | de26e00079 | |
bopol | 202a73516c | |
Tobias Groza | 3cae32b6db | |
wb9688 | 1de0876fb4 | |
bopol | 7cae95bf09 | |
bopol | 1a6e92ebf6 | |
bopol | cf99107745 | |
bopol | 5bab9d9fc0 | |
Tobias Groza | f3913e241e | |
Tobias Groza | 3ddd6e4d7b | |
wb9688 | 7320108c66 | |
wb9688 | 44d382b4bd | |
Roy Yosef | a408661771 | |
Roy Yosef | 1de1f97cf1 | |
Roy Yosef | 4234740baa | |
bopol | 6cff5de400 | |
bopol | 5760366d70 | |
bopol | 93e868634e | |
bopol | 3757541a9c | |
bopol | 79483a6dc0 | |
bopol | e8d58e09c9 | |
bopol | 5d1c3b3fa2 | |
bopol | 29b639b454 | |
wb9688 | 665c69b530 | |
Stypox | a02bfbeae5 | |
bopol | bc13e0c616 | |
TobiGr | 4086715a68 | |
TobiGr | 8a79b14077 | |
Tobias Groza | fc3a69ed54 | |
wb9688 | 6aae99f832 | |
Tobias Groza | 2780e716b3 | |
wb9688 | a1eabc7589 | |
Fynn Godau | e7b046db9d | |
Fynn Godau | a3e8e1c70e | |
Fynn Godau | 9bc7a470ab | |
Tobias Groza | 49157fcf0d | |
bopol | 2564bcf399 | |
Fynn Godau | f71bd29d58 | |
Fynn Godau | 5009d9f53b | |
Fynn Godau | b5e251c82f | |
Fynn Godau | 8c70dab8c5 | |
Fynn Godau | 10ae3db118 | |
Fynn Godau | 9201e0d3ea | |
Fynn Godau | 965bce00cf | |
Fynn Godau | 82099592c7 | |
Fynn Godau | 00c0333059 | |
Fynn Godau | 433d72c26a | |
Fynn Godau | 67de0285e1 | |
Fynn Godau | c133190c53 | |
Fynn Godau | b100b9873f | |
wb9688 | b51699a20e | |
wb9688 | 74f0ab1792 | |
Fynn Godau | 9c239371f4 | |
Fynn Godau | 5c0a03328a | |
bopol | 0cbbc2a1f9 | |
wb9688 | 979c5a7502 | |
bopol | d4352f9b84 | |
wb9688 | 1b522304c1 | |
Roy Yosef | 4afe657f6f | |
Roy Yosef | 2c9f1260eb | |
bopol | 636c430743 | |
Roy Yosef | b6e6f403a8 | |
wb9688 | a5155fb562 | |
bopol | a20d53dd98 | |
bopol | 9a7c6b7ab0 | |
bopol | a22104cbda | |
bopol | 093762e793 | |
Tobias Groza | 094b87c537 | |
Tobias Groza | a7d3d14910 | |
Tobias Groza | e3263666bd | |
Tobias Groza | b81e22ddc9 | |
wb9688 | bce27a0e22 | |
opusforlife2 | 4a1d8c804c | |
bopol | 1eb3deb7fd | |
Tobias Groza | cef7c96d7d | |
wb9688 | 8a9e137385 | |
wb9688 | 2d0c301440 | |
wb9688 | bf24caa5f2 | |
wb9688 | 2af610e3e7 | |
wb9688 | c7f7bd2442 | |
wb9688 | ac15df4548 | |
wb9688 | cf0f2aff3e | |
wb9688 | aa8cea47f3 | |
wb9688 | dd434cca01 | |
wb9688 | 5a775a4bbe | |
wb9688 | d58c0f230d | |
wb9688 | dc29d87962 | |
wb9688 | c852b13d5a | |
wb9688 | eb48524411 | |
wb9688 | 2b9b2a78e8 | |
wb9688 | fb9b9691b7 | |
wb9688 | 4ddbdf0aee | |
wb9688 | 1762a527c9 | |
Tobias Groza | 04af575764 | |
Fynn Godau | a1523eb293 | |
fynngodau | b78f788017 | |
Fynn Godau | 36a316eea5 | |
Fynn Godau | 623e6a8c63 | |
Fynn Godau | 7b5702bdc2 | |
Fynn Godau | ba967f1a15 | |
Fynn Godau | 46e1f3922c | |
Fynn Godau | f0d36dfa7e | |
Fynn Godau | 808c8aa087 | |
Fynn Godau | 13ef11e9c8 | |
Fynn Godau | db02850d4f | |
Fynn Godau | 13e4908b83 | |
Fynn Godau | 9b16baffb7 | |
Fynn Godau | ce2a88e56f | |
Fynn Godau | e12ddaef7f | |
Fynn Godau | 655df356a2 | |
Fynn Godau | ba700bfb3e | |
Fynn Godau | d05b14ae48 | |
Fynn Godau | 794ca5eeae | |
Fynn Godau | 91c0ec7cea | |
Fynn Godau | d5cdc20be1 | |
Fynn Godau | 7730eb2ea1 | |
Fynn Godau | a42c77425d | |
Fynn Godau | 5281456899 | |
Fynn Godau | c3d127ccd9 | |
Fynn Godau | 80d67e22b7 | |
Fynn Godau | 43dc3c3d4c | |
Fynn Godau | a579337c9a |
|
@ -1,3 +1,3 @@
|
||||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||||
- [ ] I did test the API against [NewPipe](https://github.com/TeamNewPipe/NewPipe).
|
- [ ] I have tested the API against [NewPipe](https://github.com/TeamNewPipe/NewPipe).
|
||||||
- [ ] I agree to ASAP create a PULL request for [NewPipe](https://github.com/TeamNewPipe/NewPipe) for making in compatible when I changed the api.
|
- [ ] I agree to create a pull request for [NewPipe](https://github.com/TeamNewPipe/NewPipe) as soon as possible to make it compatible with the changed API.
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for Gradle
|
||||||
|
- package-ecosystem: "gradle"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
|
@ -0,0 +1,51 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# once per day
|
||||||
|
- cron: 0 0 * * *
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: set up JDK 11
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '11'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Gradle dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.gradle/caches
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle
|
||||||
|
|
||||||
|
# See gradle file for difference between downloaders
|
||||||
|
- name: Build and run Tests
|
||||||
|
run: |
|
||||||
|
if [[ "$GITHUB_EVENT_NAME" == 'schedule' ]]; then
|
||||||
|
echo running with real downloader
|
||||||
|
./gradlew check --stacktrace -Ddownloader=REAL
|
||||||
|
else
|
||||||
|
echo running with mock downloader
|
||||||
|
./gradlew check --stacktrace -Ddownloader=MOCK
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload test reports when failure occurs
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: NewPipeExtractor-test-reports
|
||||||
|
path: extractor/build/reports/tests/test/**
|
|
@ -0,0 +1,38 @@
|
||||||
|
name: Build and deploy JavaDocs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# The generated docs are written to the `gh-pages` branch.
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy-docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: set up JDK 11
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '11'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Gradle dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.gradle/caches
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle
|
||||||
|
|
||||||
|
- name: Build JavaDocs
|
||||||
|
run: ./gradlew aggregatedJavadocs
|
||||||
|
|
||||||
|
- name: Deploy JavaDocs
|
||||||
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./build/docs
|
|
@ -16,3 +16,11 @@ gradle-app.setting
|
||||||
|
|
||||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||||
# gradle/wrapper/gradle-wrapper.properties
|
# gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
# vscode / eclipse files
|
||||||
|
*.classpath
|
||||||
|
*.project
|
||||||
|
*.settings
|
||||||
|
**/bin
|
||||||
|
**.vscode
|
||||||
|
*.code-workspace
|
||||||
|
|
15
.travis.yml
15
.travis.yml
|
@ -1,15 +0,0 @@
|
||||||
language: java
|
|
||||||
jdk:
|
|
||||||
- openjdk8
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./gradlew check
|
|
||||||
- ./gradlew aggregatedJavadocs
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: pages
|
|
||||||
skip_cleanup: true
|
|
||||||
github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard
|
|
||||||
local_dir: build/docs
|
|
||||||
on:
|
|
||||||
branch: master
|
|
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>.
|
||||||
|
|
33
README.md
33
README.md
|
@ -1,6 +1,6 @@
|
||||||
# NewPipe Extractor
|
# NewPipe Extractor
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/TeamNewPipe/NewPipeExtractor.svg?branch=master)](https://travis-ci.org/TeamNewPipe/NewPipeExtractor) [![JIT Pack Badge](https://jitpack.io/v/TeamNewPipe/NewPipeExtractor.svg)](https://jitpack.io/#TeamNewPipe/NewPipeExtractor) [Documentation](https://teamnewpipe.github.io/documentation/)
|
[![CI](https://github.com/TeamNewPipe/NewPipeExtractor/actions/workflows/ci.yml/badge.svg?branch=dev&event=schedule)](https://github.com/TeamNewPipe/NewPipeExtractor/actions/workflows/ci.yml) [![JIT Pack Badge](https://jitpack.io/v/TeamNewPipe/NewPipeExtractor.svg)](https://jitpack.io/#TeamNewPipe/NewPipeExtractor) [JDoc](https://teamnewpipe.github.io/NewPipeExtractor/javadoc/) • [Documentation](https://teamnewpipe.github.io/documentation/)
|
||||||
|
|
||||||
NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of [NewPipe](https://github.com/TeamNewPipe/NewPipe), but could be used independently.
|
NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of [NewPipe](https://github.com/TeamNewPipe/NewPipe), but could be used independently.
|
||||||
|
|
||||||
|
@ -11,11 +11,31 @@ NewPipe Extractor is available at JitPack's Maven repo.
|
||||||
If you're using Gradle, you could add NewPipe Extractor as a dependency with the following steps:
|
If you're using Gradle, you could add NewPipe Extractor as a dependency with the following steps:
|
||||||
|
|
||||||
1. Add `maven { url 'https://jitpack.io' }` to the `repositories` in your `build.gradle`.
|
1. Add `maven { url 'https://jitpack.io' }` to the `repositories` in your `build.gradle`.
|
||||||
2. Add `compile 'com.github.TeamNewPipe:NewPipeExtractor:v0.11.0'`the `dependencies` in your `build.gradle`. Replace `v0.11.0` with the latest release.
|
2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:INSERT_VERSION_HERE'` to the `dependencies` in your `build.gradle`. Replace `INSERT_VERSION_HERE` with the [latest release](https://github.com/TeamNewPipe/NewPipeExtractor/releases/latest).
|
||||||
|
3. If you are using tools to minimize your project, make sure to keep the files below, by e.g. adding the following lines to your proguard file:
|
||||||
|
```
|
||||||
|
## Rules for NewPipeExtractor
|
||||||
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** To use NewPipe Extractor in Android projects with a `minSdk` below 26, [API desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) is required. If the `minSdk` is below 19, the `desugar_jdk_libs_nio` artifact is required, which requires Android Gradle Plugin (AGP) version 7.4.0.
|
||||||
|
|
||||||
### Testing changes
|
### Testing changes
|
||||||
|
|
||||||
To test changes quickly you can build the library locally. Using the local Maven repository is a good approach, here's a gist of how to use it:
|
To test changes quickly you can build the library locally. A good approach would be to add something like the following to your `settings.gradle`:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
includeBuild('../NewPipeExtractor') {
|
||||||
|
dependencySubstitution {
|
||||||
|
substitute module('com.github.TeamNewPipe:NewPipeExtractor') with project(':extractor')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Another approach would be to use the local Maven repository, here's a gist of how to use it:
|
||||||
|
|
||||||
1. Add `mavenLocal()` in your project `repositories` list (usually as the first entry to give priority above the others).
|
1. Add `mavenLocal()` in your project `repositories` list (usually as the first entry to give priority above the others).
|
||||||
2. It's _recommended_ that you change the `version` of this library (e.g. `LOCAL_SNAPSHOT`).
|
2. It's _recommended_ that you change the `version` of this library (e.g. `LOCAL_SNAPSHOT`).
|
||||||
|
@ -30,14 +50,15 @@ The following sites are currently supported:
|
||||||
|
|
||||||
- YouTube
|
- YouTube
|
||||||
- SoundCloud
|
- SoundCloud
|
||||||
- MediaCCC
|
- media.ccc.de
|
||||||
- PeerTube (no P2P)
|
- PeerTube (no P2P)
|
||||||
|
- Bandcamp
|
||||||
|
|
||||||
## 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 Extractor is Free Software: You can use, study share and improve it at your
|
||||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
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
|
[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
|
published by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
|
43
build.gradle
43
build.gradle
|
@ -1,26 +1,47 @@
|
||||||
allprojects {
|
allprojects {
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
apply plugin: 'maven'
|
apply plugin: 'maven-publish'
|
||||||
|
|
||||||
sourceCompatibility = 1.7
|
compileJava.options.encoding = 'UTF-8'
|
||||||
targetCompatibility = 1.7
|
compileTestJava.options.encoding = 'UTF-8'
|
||||||
|
|
||||||
version 'v0.19.0'
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
|
||||||
|
version 'v0.24.0'
|
||||||
group 'com.github.TeamNewPipe'
|
group 'com.github.TeamNewPipe'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
mavenCentral()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
mavenJava(MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
nanojsonVersion = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||||
|
spotbugsVersion = "4.8.5"
|
||||||
|
junitVersion = "5.10.2"
|
||||||
|
checkstyleVersion = "10.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':extractor')
|
api project(':extractor')
|
||||||
implementation project(':timeago-parser')
|
implementation project(':timeago-parser')
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||||
classifier = 'sources'
|
archiveClassifier.set('sources')
|
||||||
from sourceSets.main.allSource
|
from sourceSets.main.allSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +63,14 @@ task aggregatedJavadocs(type: Javadoc, group: 'Documentation') {
|
||||||
destinationDir = file("$buildDir/docs/javadoc")
|
destinationDir = file("$buildDir/docs/javadoc")
|
||||||
title = "$project.name $version"
|
title = "$project.name $version"
|
||||||
// options.memberLevel = JavadocMemberLevel.PRIVATE
|
// options.memberLevel = JavadocMemberLevel.PRIVATE
|
||||||
options.links 'https://docs.oracle.com/javase/7/docs/api/'
|
options.links 'https://docs.oracle.com/javase/8/docs/api/'
|
||||||
options.encoding 'UTF-8'
|
options.encoding 'UTF-8'
|
||||||
|
// Fixes unknown tag @implNote; the other two were added precautionary
|
||||||
|
options.tags = [
|
||||||
|
"apiNote:a:API Note:",
|
||||||
|
"implSpec:a:Implementation Requirements:",
|
||||||
|
"implNote:a:Implementation Note:"
|
||||||
|
]
|
||||||
|
|
||||||
subprojects.each { project ->
|
subprojects.each { project ->
|
||||||
project.tasks.withType(Javadoc).each { javadocTask ->
|
project.tasks.withType(Javadoc).each { javadocTask ->
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||||
|
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||||
|
<module name="Checker">
|
||||||
|
<!--
|
||||||
|
If you set the basedir property below, then all reported file
|
||||||
|
names will be relative to the specified directory. See
|
||||||
|
https://checkstyle.org/5.x/config.html#Checker
|
||||||
|
|
||||||
|
<property name="basedir" value="${basedir}"/>
|
||||||
|
-->
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
|
||||||
|
<property name="fileExtensions" value="java, properties, xml"/>
|
||||||
|
|
||||||
|
<!-- Excludes all 'module-info.java' files -->
|
||||||
|
<!-- See https://checkstyle.org/config_filefilters.html -->
|
||||||
|
<module name="BeforeExecutionExclusionFileFilter">
|
||||||
|
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Checks that a package-info.java file exists for each package. -->
|
||||||
|
<!-- See https://checkstyle.org/config_javadoc.html#JavadocPackage -->
|
||||||
|
<!--<module name="JavadocPackage"/>-->
|
||||||
|
|
||||||
|
<!-- Checks whether files end with a new line. -->
|
||||||
|
<!-- See https://checkstyle.org/config_misc.html#NewlineAtEndOfFile -->
|
||||||
|
<module name="NewlineAtEndOfFile"/>
|
||||||
|
|
||||||
|
<!-- Checks that property files contain the same keys. -->
|
||||||
|
<!-- See https://checkstyle.org/config_misc.html#Translation -->
|
||||||
|
<module name="Translation"/>
|
||||||
|
|
||||||
|
<!-- Checks for Size Violations. -->
|
||||||
|
<!-- See https://checkstyle.org/config_sizes.html -->
|
||||||
|
<module name="FileLength"/>
|
||||||
|
<module name="LineLength">
|
||||||
|
<property name="max" value="100"/>
|
||||||
|
<property name="fileExtensions" value="java"/>
|
||||||
|
<!-- Also allow links in javadocs to be longer (the default would just cover imports) -->
|
||||||
|
<property name="ignorePattern" value="^((package|import) .*)|( *\* (@see )?<a href ?\= ?".*">)$"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Checks for whitespace -->
|
||||||
|
<!-- See https://checkstyle.org/config_whitespace.html -->
|
||||||
|
<module name="FileTabCharacter"/>
|
||||||
|
|
||||||
|
<!-- Miscellaneous other checks. -->
|
||||||
|
<!-- See https://checkstyle.org/config_misc.html -->
|
||||||
|
<module name="RegexpSingleline">
|
||||||
|
<property name="format" value="\s+$"/>
|
||||||
|
<property name="minimum" value="0"/>
|
||||||
|
<property name="maximum" value="0"/>
|
||||||
|
<property name="message" value="Line has trailing spaces."/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Checks for Headers -->
|
||||||
|
<!-- See https://checkstyle.org/config_header.html -->
|
||||||
|
<!-- <module name="Header"> -->
|
||||||
|
<!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->
|
||||||
|
<!-- <property name="fileExtensions" value="java"/> -->
|
||||||
|
<!-- </module> -->
|
||||||
|
|
||||||
|
<module name="SuppressWarningsFilter" />
|
||||||
|
|
||||||
|
<module name="SuppressWithPlainTextCommentFilter"/>
|
||||||
|
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<!-- Checks for Javadoc comments. -->
|
||||||
|
<!-- See https://checkstyle.org/config_javadoc.html -->
|
||||||
|
<module name="InvalidJavadocPosition"/>
|
||||||
|
<module name="JavadocMethod">
|
||||||
|
<property name="allowMissingParamTags" value="true"/>
|
||||||
|
<property name="allowMissingReturnTag" value="true"/>
|
||||||
|
</module>
|
||||||
|
<module name="JavadocType"/>
|
||||||
|
<!--<module name="JavadocVariable"/>-->
|
||||||
|
<module name="JavadocStyle">
|
||||||
|
<property name="checkFirstSentence" value="false"/>
|
||||||
|
</module>
|
||||||
|
<!--<module name="MissingJavadocMethod"/>-->
|
||||||
|
|
||||||
|
<!-- Checks for Naming Conventions. -->
|
||||||
|
<!-- See https://checkstyle.org/config_naming.html -->
|
||||||
|
<module name="ConstantName"/>
|
||||||
|
<module name="LocalFinalVariableName"/>
|
||||||
|
<module name="LocalVariableName"/>
|
||||||
|
<module name="MemberName">
|
||||||
|
<property name="format" value="^(TAG|DEBUG|[a-z][a-zA-Z0-9]*)$"/>
|
||||||
|
</module>
|
||||||
|
<module name="MethodName"/>
|
||||||
|
<module name="PackageName"/>
|
||||||
|
<module name="ParameterName"/>
|
||||||
|
<module name="StaticVariableName"/>
|
||||||
|
<module name="TypeName"/>
|
||||||
|
|
||||||
|
<!-- Checks for imports -->
|
||||||
|
<!-- See https://checkstyle.org/config_import.html -->
|
||||||
|
<module name="AvoidStarImport"/>
|
||||||
|
<module name="IllegalImport"> <!-- defaults to sun.* packages -->
|
||||||
|
<property name="illegalClasses" value="
|
||||||
|
org.jetbrains.annotations.Nullable,
|
||||||
|
org.jetbrains.annotations.NotNull,
|
||||||
|
androidx.annotation.Nullable,
|
||||||
|
androidx.annotation.NonNull,
|
||||||
|
io.reactivex.rxjava3.annotations.NonNull,
|
||||||
|
io.reactivex.rxjava3.annotations.Nullable" />
|
||||||
|
</module>
|
||||||
|
<module name="RedundantImport"/>
|
||||||
|
<module name="UnusedImports"/>
|
||||||
|
|
||||||
|
<!-- Checks for Size Violations. -->
|
||||||
|
<!-- See https://checkstyle.org/config_sizes.html -->
|
||||||
|
<module name="MethodLength">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
<module name="ParameterNumber">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Checks for whitespace -->
|
||||||
|
<!-- See https://checkstyle.org/config_whitespace.html -->
|
||||||
|
<module name="EmptyForIteratorPad"/>
|
||||||
|
<module name="GenericWhitespace"/>
|
||||||
|
<module name="MethodParamPad"/>
|
||||||
|
<module name="NoWhitespaceAfter"/>
|
||||||
|
<module name="NoWhitespaceBefore"/>
|
||||||
|
<module name="OperatorWrap"/>
|
||||||
|
<module name="ParenPad"/>
|
||||||
|
<module name="TypecastParenPad"/>
|
||||||
|
<module name="WhitespaceAfter"/>
|
||||||
|
<module name="WhitespaceAround"/>
|
||||||
|
|
||||||
|
<!-- Modifier Checks -->
|
||||||
|
<!-- See https://checkstyle.org/config_modifiers.html -->
|
||||||
|
<module name="ModifierOrder"/>
|
||||||
|
<module name="RedundantModifier"/>
|
||||||
|
|
||||||
|
<!-- Checks for blocks. You know, those {}'s -->
|
||||||
|
<!-- See https://checkstyle.org/config_blocks.html -->
|
||||||
|
<module name="AvoidNestedBlocks"/>
|
||||||
|
<module name="EmptyBlock"/>
|
||||||
|
<module name="LeftCurly"/>
|
||||||
|
<module name="NeedBraces"/>
|
||||||
|
<module name="RightCurly"/>
|
||||||
|
|
||||||
|
<!-- Checks for common coding problems -->
|
||||||
|
<!-- See https://checkstyle.org/config_coding.html -->
|
||||||
|
<module name="EmptyStatement"/>
|
||||||
|
<module name="EqualsHashCode">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>
|
||||||
|
<module name="HiddenField">
|
||||||
|
<property name="ignoreConstructorParameter" value="true"/>
|
||||||
|
<property name="ignoreSetter" value="true"/>
|
||||||
|
</module>
|
||||||
|
<module name="IllegalInstantiation"/>
|
||||||
|
<module name="InnerAssignment"/>
|
||||||
|
<!--<module name="MagicNumber"/>-->
|
||||||
|
<!--<module name="MissingSwitchDefault">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>-->
|
||||||
|
<module name="MultipleVariableDeclarations"/>
|
||||||
|
<module name="SimplifyBooleanExpression"/>
|
||||||
|
<module name="SimplifyBooleanReturn"/>
|
||||||
|
<module name="FinalLocalVariable">
|
||||||
|
<property name="tokens" value="VARIABLE_DEF,PARAMETER_DEF"/>
|
||||||
|
<property name="validateEnhancedForLoopVariable" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Checks for class design -->
|
||||||
|
<!-- See https://checkstyle.org/config_design.html -->
|
||||||
|
<!--<module name="DesignForExtension"/>-->
|
||||||
|
<module name="FinalClass"/>
|
||||||
|
<module name="HideUtilityClassConstructor"/>
|
||||||
|
<module name="InterfaceIsType"/>
|
||||||
|
<!--<module name="VisibilityModifier">
|
||||||
|
<property name="ignoreAnnotationCanonicalNames" value="State,ColumnInfo"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>-->
|
||||||
|
|
||||||
|
<!-- Miscellaneous other checks. -->
|
||||||
|
<!-- See https://checkstyle.org/config_misc.html -->
|
||||||
|
<module name="ArrayTypeStyle"/>
|
||||||
|
<module name="FinalParameters"/>
|
||||||
|
<!--<module name="TodoComment">
|
||||||
|
<property name="format" value="(TODO:|FIXME:)"/>
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
</module>-->
|
||||||
|
<module name="UpperEll"/>
|
||||||
|
|
||||||
|
<module name="SuppressWarningsHolder" />
|
||||||
|
</module>
|
||||||
|
</module>
|
15
copyright
15
copyright
|
@ -1,15 +0,0 @@
|
||||||
Copyright: 2017 Christian Schabesberger <chris.schabesberger@mailbox.com>
|
|
||||||
|
|
||||||
License: GPL-3.0+
|
|
||||||
This program 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.
|
|
||||||
|
|
||||||
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
@ -1,11 +1,45 @@
|
||||||
|
plugins {
|
||||||
|
id 'checkstyle'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
// Pass on downloader type to tests for different CI jobs. See DownloaderFactory.java and ci.yml
|
||||||
|
if (System.properties.containsKey('downloader')) {
|
||||||
|
systemProperty('downloader', System.getProperty('downloader'))
|
||||||
|
}
|
||||||
|
useJUnitPlatform()
|
||||||
|
dependsOn checkstyleMain // run checkstyle when testing
|
||||||
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||||
|
ignoreFailures false
|
||||||
|
showViolations true
|
||||||
|
toolVersion checkstyleVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
checkstyleTest {
|
||||||
|
enabled false // do not checkstyle test files
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':timeago-parser')
|
implementation project(':timeago-parser')
|
||||||
|
|
||||||
implementation 'com.grack:nanojson:1.1'
|
implementation "com.github.TeamNewPipe:nanojson:$nanojsonVersion"
|
||||||
implementation 'org.jsoup:jsoup:1.9.2'
|
implementation 'org.jsoup:jsoup:1.17.2'
|
||||||
implementation 'org.mozilla:rhino:1.7.7.1'
|
implementation "com.github.spotbugs:spotbugs-annotations:$spotbugsVersion"
|
||||||
implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0'
|
|
||||||
implementation 'org.nibor.autolink:autolink:0.8.0'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||||
}
|
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
||||||
|
implementation 'org.mozilla:rhino:1.7.15'
|
||||||
|
|
||||||
|
checkstyle "com.puppycrawl.tools:checkstyle:$checkstyleVersion"
|
||||||
|
|
||||||
|
testImplementation platform("org.junit:junit-bom:$junitVersion")
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||||
|
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter-params'
|
||||||
|
|
||||||
|
testImplementation "com.squareup.okhttp3:okhttp:3.12.13"
|
||||||
|
testImplementation 'com.google.code.gson:gson:2.10.1'
|
||||||
|
}
|
||||||
|
|
|
@ -10,12 +10,15 @@ import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public abstract class Extractor {
|
public abstract class Extractor {
|
||||||
/**
|
/**
|
||||||
* {@link StreamingService} currently related to this extractor.<br>
|
* {@link StreamingService} currently related to this extractor.<br>
|
||||||
* Useful for getting other things from a service (like the url handlers for cleaning/accepting/get id from urls).
|
* Useful for getting other things from a service (like the url handlers for
|
||||||
|
* cleaning/accepting/get id from urls).
|
||||||
*/
|
*/
|
||||||
private final StreamingService service;
|
private final StreamingService service;
|
||||||
private final LinkHandler linkHandler;
|
private final LinkHandler linkHandler;
|
||||||
|
@ -26,19 +29,18 @@ public abstract class Extractor {
|
||||||
private ContentCountry forcedContentCountry = null;
|
private ContentCountry forcedContentCountry = null;
|
||||||
|
|
||||||
private boolean pageFetched = false;
|
private boolean pageFetched = false;
|
||||||
|
// called like this to prevent checkstyle errors about "hiding a field"
|
||||||
private final Downloader downloader;
|
private final Downloader downloader;
|
||||||
|
|
||||||
public Extractor(final StreamingService service, final LinkHandler linkHandler) {
|
protected Extractor(final StreamingService service, final LinkHandler linkHandler) {
|
||||||
if (service == null) throw new NullPointerException("service is null");
|
this.service = Objects.requireNonNull(service, "service is null");
|
||||||
if (linkHandler == null) throw new NullPointerException("LinkHandler is null");
|
this.linkHandler = Objects.requireNonNull(linkHandler, "LinkHandler is null");
|
||||||
this.service = service;
|
this.downloader = Objects.requireNonNull(NewPipe.getDownloader(), "downloader is null");
|
||||||
this.linkHandler = linkHandler;
|
|
||||||
this.downloader = NewPipe.getDownloader();
|
|
||||||
if (downloader == null) throw new NullPointerException("downloader is null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The {@link LinkHandler} of the current extractor object (e.g. a ChannelExtractor should return a channel url handler).
|
* @return The {@link LinkHandler} of the current extractor object (e.g. a ChannelExtractor
|
||||||
|
* should return a channel url handler).
|
||||||
*/
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public LinkHandler getLinkHandler() {
|
public LinkHandler getLinkHandler() {
|
||||||
|
@ -52,13 +54,17 @@ public abstract class Extractor {
|
||||||
* @throws ExtractionException if the pages content is not understood
|
* @throws ExtractionException if the pages content is not understood
|
||||||
*/
|
*/
|
||||||
public void fetchPage() throws IOException, ExtractionException {
|
public void fetchPage() throws IOException, ExtractionException {
|
||||||
if (pageFetched) return;
|
if (pageFetched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onFetchPage(downloader);
|
onFetchPage(downloader);
|
||||||
pageFetched = true;
|
pageFetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertPageFetched() {
|
protected void assertPageFetched() {
|
||||||
if (!pageFetched) throw new IllegalStateException("Page is not fetched. Make sure you call fetchPage()");
|
if (!pageFetched) {
|
||||||
|
throw new IllegalStateException("Page is not fetched. Make sure you call fetchPage()");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isPageFetched() {
|
protected boolean isPageFetched() {
|
||||||
|
@ -68,11 +74,13 @@ public abstract class Extractor {
|
||||||
/**
|
/**
|
||||||
* Fetch the current page.
|
* Fetch the current page.
|
||||||
*
|
*
|
||||||
* @param downloader the download to use
|
* @param downloader the downloader to use
|
||||||
* @throws IOException if the page can not be loaded
|
* @throws IOException if the page can not be loaded
|
||||||
* @throws ExtractionException if the pages content is not understood
|
* @throws ExtractionException if the pages content is not understood
|
||||||
*/
|
*/
|
||||||
public abstract void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException;
|
@SuppressWarnings("HiddenField")
|
||||||
|
public abstract void onFetchPage(@Nonnull Downloader downloader)
|
||||||
|
throws IOException, ExtractionException;
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public String getId() throws ParsingException {
|
public String getId() throws ParsingException {
|
||||||
|
@ -120,11 +128,11 @@ public abstract class Extractor {
|
||||||
// Localization
|
// Localization
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void forceLocalization(Localization localization) {
|
public void forceLocalization(final Localization localization) {
|
||||||
this.forcedLocalization = localization;
|
this.forcedLocalization = localization;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void forceContentCountry(ContentCountry contentCountry) {
|
public void forceContentCountry(final ContentCountry contentCountry) {
|
||||||
this.forcedContentCountry = contentCountry;
|
this.forcedContentCountry = contentCountry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +143,8 @@ public abstract class Extractor {
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public ContentCountry getExtractorContentCountry() {
|
public ContentCountry getExtractorContentCountry() {
|
||||||
return forcedContentCountry == null ? getService().getContentCountry() : forcedContentCountry;
|
return forcedContentCountry == null ? getService().getContentCountry()
|
||||||
|
: forcedContentCountry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing images in the extractor.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An image has four properties: its URL, its height, its width and its estimated quality level.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Depending of the services, the height, the width or both properties may be not known.
|
||||||
|
* Implementations <b>must use</b> the relevant unknown constants in this case
|
||||||
|
* ({@link #HEIGHT_UNKNOWN} and {@link #WIDTH_UNKNOWN}), to ensure properly the lack of knowledge
|
||||||
|
* of one or both of these properties to extractor clients.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* They should also respect the ranges defined in the estimated image resolution levels as much as
|
||||||
|
* possible, to ensure consistency to extractor clients.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class Image implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant representing that the height of an {@link Image} is unknown.
|
||||||
|
*/
|
||||||
|
public static final int HEIGHT_UNKNOWN = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant representing that the width of an {@link Image} is unknown.
|
||||||
|
*/
|
||||||
|
public static final int WIDTH_UNKNOWN = -1;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private final String url;
|
||||||
|
private final int height;
|
||||||
|
private final int width;
|
||||||
|
@Nonnull
|
||||||
|
private final ResolutionLevel estimatedResolutionLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an {@link Image} instance.
|
||||||
|
*
|
||||||
|
* @param url the URL to the image, which should be not null or empty
|
||||||
|
* @param height the image's height
|
||||||
|
* @param width the image's width
|
||||||
|
* @param estimatedResolutionLevel the image's estimated resolution level, which must not be
|
||||||
|
* null
|
||||||
|
* @throws NullPointerException if {@code estimatedResolutionLevel} is null
|
||||||
|
*/
|
||||||
|
public Image(@Nonnull final String url,
|
||||||
|
final int height,
|
||||||
|
final int width,
|
||||||
|
@Nonnull final ResolutionLevel estimatedResolutionLevel)
|
||||||
|
throws NullPointerException {
|
||||||
|
this.url = url;
|
||||||
|
this.height = height;
|
||||||
|
this.width = width;
|
||||||
|
this.estimatedResolutionLevel = Objects.requireNonNull(
|
||||||
|
estimatedResolutionLevel, "estimatedResolutionLevel is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL of this {@link Image}.
|
||||||
|
*
|
||||||
|
* @return the {@link Image}'s URL.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the height of this {@link Image}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If it is unknown, {@link #HEIGHT_UNKNOWN} is returned instead.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the {@link Image}'s height or {@link #HEIGHT_UNKNOWN}
|
||||||
|
*/
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the width of this {@link Image}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If it is unknown, {@link #WIDTH_UNKNOWN} is returned instead.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the {@link Image}'s width or {@link #WIDTH_UNKNOWN}
|
||||||
|
*/
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the estimated resolution level of this image.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If it is unknown, {@link ResolutionLevel#UNKNOWN} is returned instead.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the estimated resolution level, which is never {@code null}
|
||||||
|
* @see ResolutionLevel
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public ResolutionLevel getEstimatedResolutionLevel() {
|
||||||
|
return estimatedResolutionLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string representation of this {@link Image} instance.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The representation will be in the following format, where {@code url}, {@code height},
|
||||||
|
* {@code width} and {@code estimatedResolutionLevel} represent the corresponding properties:
|
||||||
|
* <br>
|
||||||
|
* <br>
|
||||||
|
* {@code Image {url=url, height='height, width=width,
|
||||||
|
* estimatedResolutionLevel=estimatedResolutionLevel}'}
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a string representation of this {@link Image} instance
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Image {" + "url=" + url + ", height=" + height + ", width=" + width
|
||||||
|
+ ", estimatedResolutionLevel=" + estimatedResolutionLevel + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The estimated resolution level of an {@link Image}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Some services don't return the size of their images, but we may know for a specific image
|
||||||
|
* type that a service returns, according to real data, an approximation of the resolution
|
||||||
|
* level.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public enum ResolutionLevel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The high resolution level.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This level applies to images with a height greater than or equal to 720px.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
HIGH,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The medium resolution level.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This level applies to images with a height between 175px inclusive and 720px exclusive.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
MEDIUM,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The low resolution level.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This level applies to images with a height between 1px inclusive and 175px exclusive.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
LOW,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unknown resolution level.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This value is returned when the extractor doesn't know what resolution level an image
|
||||||
|
* could have, for example if the extractor loops in an array of images with different
|
||||||
|
* resolution levels without knowing the height.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
UNKNOWN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link ResolutionLevel} based from the given height.
|
||||||
|
*
|
||||||
|
* @param heightPx the height from which returning the good {@link ResolutionLevel}
|
||||||
|
* @return the {@link ResolutionLevel} corresponding to the height provided. See the
|
||||||
|
* {@link ResolutionLevel} values for details about what value is returned.
|
||||||
|
*/
|
||||||
|
public static ResolutionLevel fromHeight(final int heightPx) {
|
||||||
|
if (heightPx <= 0) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heightPx < 175) {
|
||||||
|
return LOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heightPx < 720) {
|
||||||
|
return MEDIUM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HIGH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.extractor;
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -16,7 +17,8 @@ public abstract class Info implements Serializable {
|
||||||
*/
|
*/
|
||||||
private final String id;
|
private final String id;
|
||||||
/**
|
/**
|
||||||
* Different than the {@link #originalUrl} in the sense that it <i>may</i> be set as a cleaned url.
|
* Different than the {@link #originalUrl} in the sense that it <i>may</i> be set as a cleaned
|
||||||
|
* url.
|
||||||
*
|
*
|
||||||
* @see LinkHandler#getUrl()
|
* @see LinkHandler#getUrl()
|
||||||
* @see Extractor#getOriginalUrl()
|
* @see Extractor#getOriginalUrl()
|
||||||
|
@ -32,15 +34,19 @@ public abstract class Info implements Serializable {
|
||||||
|
|
||||||
private final List<Throwable> errors = new ArrayList<>();
|
private final List<Throwable> errors = new ArrayList<>();
|
||||||
|
|
||||||
public void addError(Throwable throwable) {
|
public void addError(final Throwable throwable) {
|
||||||
this.errors.add(throwable);
|
this.errors.add(throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addAllErrors(Collection<Throwable> errors) {
|
public void addAllErrors(final Collection<Throwable> throwables) {
|
||||||
this.errors.addAll(errors);
|
this.errors.addAll(throwables);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Info(int serviceId, String id, String url, String originalUrl, String name) {
|
public Info(final int serviceId,
|
||||||
|
final String id,
|
||||||
|
final String url,
|
||||||
|
final String originalUrl,
|
||||||
|
final String name) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
@ -48,7 +54,7 @@ public abstract class Info implements Serializable {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Info(int serviceId, LinkHandler linkHandler, String name) {
|
public Info(final int serviceId, final LinkHandler linkHandler, final String name) {
|
||||||
this(serviceId,
|
this(serviceId,
|
||||||
linkHandler.getId(),
|
linkHandler.getId(),
|
||||||
linkHandler.getUrl(),
|
linkHandler.getUrl(),
|
||||||
|
@ -58,20 +64,31 @@ public abstract class Info implements Serializable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
final String ifDifferentString = !url.equals(originalUrl) ? " (originalUrl=\"" + originalUrl + "\")" : "";
|
final String ifDifferentString
|
||||||
return getClass().getSimpleName() + "[url=\"" + url + "\"" + ifDifferentString + ", name=\"" + name + "\"]";
|
= url.equals(originalUrl) ? "" : " (originalUrl=\"" + originalUrl + "\")";
|
||||||
|
return getClass().getSimpleName() + "[url=\"" + url + "\"" + ifDifferentString
|
||||||
|
+ ", name=\"" + name + "\"]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// if you use an api and want to handle the website url
|
// if you use an api and want to handle the website url
|
||||||
// overriding original url is essential
|
// overriding original url is essential
|
||||||
public void setOriginalUrl(String url) {
|
public void setOriginalUrl(final String originalUrl) {
|
||||||
originalUrl = url;
|
this.originalUrl = originalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getServiceId() {
|
public int getServiceId() {
|
||||||
return serviceId;
|
return serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StreamingService getService() {
|
||||||
|
try {
|
||||||
|
return NewPipe.getService(serviceId);
|
||||||
|
} catch (final ExtractionException e) {
|
||||||
|
// this should be unreachable, as serviceId certainly refers to a valid service
|
||||||
|
throw new RuntimeException("Info object has invalid service id", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,41 @@
|
||||||
package org.schabi.newpipe.extractor;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 11.02.17.
|
* Created by Christian Schabesberger on 11.02.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* InfoItem.java is part of NewPipe.
|
* InfoItem.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class InfoItem implements Serializable {
|
public abstract class InfoItem implements Serializable {
|
||||||
private final InfoType infoType;
|
private final InfoType infoType;
|
||||||
private final int serviceId;
|
private final int serviceId;
|
||||||
private final String url;
|
private final String url;
|
||||||
private final String name;
|
private final String name;
|
||||||
private String thumbnailUrl;
|
@Nonnull
|
||||||
|
private List<Image> thumbnails = List.of();
|
||||||
|
|
||||||
public InfoItem(InfoType infoType, int serviceId, String url, String name) {
|
public InfoItem(final InfoType infoType,
|
||||||
|
final int serviceId,
|
||||||
|
final String url,
|
||||||
|
final String name) {
|
||||||
this.infoType = infoType;
|
this.infoType = infoType;
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
@ -52,12 +58,13 @@ public abstract class InfoItem implements Serializable {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThumbnailUrl(String thumbnailUrl) {
|
public void setThumbnails(@Nonnull final List<Image> thumbnails) {
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnails = thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getThumbnailUrl() {
|
@Nonnull
|
||||||
return thumbnailUrl;
|
public List<Image> getThumbnails() {
|
||||||
|
return thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,8 +2,12 @@ package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface InfoItemExtractor {
|
public interface InfoItemExtractor {
|
||||||
String getName() throws ParsingException;
|
String getName() throws ParsingException;
|
||||||
String getUrl() throws ParsingException;
|
String getUrl() throws ParsingException;
|
||||||
String getThumbnailUrl() throws ParsingException;
|
@Nonnull
|
||||||
|
List<Image> getThumbnails() throws ParsingException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,46 +3,63 @@ package org.schabi.newpipe.extractor;
|
||||||
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
|
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* InfoItemsCollector.java is part of NewPipe.
|
* InfoItemsCollector.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemExtractor> implements Collector<I, E> {
|
public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemExtractor>
|
||||||
|
implements Collector<I, E> {
|
||||||
|
|
||||||
private final List<I> itemList = new ArrayList<>();
|
private final List<I> itemList = new ArrayList<>();
|
||||||
private final List<Throwable> errors = new ArrayList<>();
|
private final List<Throwable> errors = new ArrayList<>();
|
||||||
private final int serviceId;
|
private final int serviceId;
|
||||||
|
@Nullable
|
||||||
|
private final Comparator<I> comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new collector with no comparator / sorting function
|
||||||
|
* @param serviceId the service id
|
||||||
|
*/
|
||||||
|
public InfoItemsCollector(final int serviceId) {
|
||||||
|
this(serviceId, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new collector
|
* Create a new collector
|
||||||
* @param serviceId the service id
|
* @param serviceId the service id
|
||||||
*/
|
*/
|
||||||
public InfoItemsCollector(int serviceId) {
|
public InfoItemsCollector(final int serviceId, @Nullable final Comparator<I> comparator) {
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
|
this.comparator = comparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<I> getItems() {
|
public List<I> getItems() {
|
||||||
|
if (comparator != null) {
|
||||||
|
itemList.sort(comparator);
|
||||||
|
}
|
||||||
return Collections.unmodifiableList(itemList);
|
return Collections.unmodifiableList(itemList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +78,7 @@ public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemE
|
||||||
* Add an error
|
* Add an error
|
||||||
* @param error the error
|
* @param error the error
|
||||||
*/
|
*/
|
||||||
protected void addError(Exception error) {
|
protected void addError(final Exception error) {
|
||||||
errors.add(error);
|
errors.add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +86,7 @@ public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemE
|
||||||
* Add an item
|
* Add an item
|
||||||
* @param item the item
|
* @param item the item
|
||||||
*/
|
*/
|
||||||
protected void addItem(I item) {
|
protected void addItem(final I item) {
|
||||||
itemList.add(item);
|
itemList.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,12 +99,12 @@ public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemE
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void commit(E extractor) {
|
public void commit(final E extractor) {
|
||||||
try {
|
try {
|
||||||
addItem(extract(extractor));
|
addItem(extract(extractor));
|
||||||
} catch (FoundAdException ae) {
|
} catch (final FoundAdException ae) {
|
||||||
// found an ad. Maybe a debug line could be placed here
|
// found an ad. Maybe a debug line could be placed here
|
||||||
} catch (ParsingException e) {
|
} catch (final ParsingException e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,53 +3,57 @@ package org.schabi.newpipe.extractor;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class to extractors that have a list (e.g. playlists, users).
|
* Base class to extractors that have a list (e.g. playlists, users).
|
||||||
|
* @param <R> the info item type this list extractor provides
|
||||||
*/
|
*/
|
||||||
public abstract class ListExtractor<R extends InfoItem> extends Extractor {
|
public abstract class ListExtractor<R extends InfoItem> extends Extractor {
|
||||||
|
/**
|
||||||
|
* Constant that should be returned whenever
|
||||||
|
* a list has an unknown number of items.
|
||||||
|
*/
|
||||||
|
public static final long ITEM_COUNT_UNKNOWN = -1;
|
||||||
|
/**
|
||||||
|
* Constant that should be returned whenever a list has an
|
||||||
|
* infinite number of items. For example a YouTube mix.
|
||||||
|
*/
|
||||||
|
public static final long ITEM_COUNT_INFINITE = -2;
|
||||||
|
/**
|
||||||
|
* Constant that should be returned whenever a list
|
||||||
|
* has an unknown number of items bigger than 100.
|
||||||
|
*/
|
||||||
|
public static final long ITEM_COUNT_MORE_THAN_100 = -3;
|
||||||
|
|
||||||
public ListExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
public ListExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link InfoItemsPage InfoItemsPage} corresponding to the initial page where the items are from the initial request and
|
* A {@link InfoItemsPage InfoItemsPage} corresponding to the initial page
|
||||||
* the nextPageUrl relative to it.
|
* where the items are from the initial request and the nextPage relative to it.
|
||||||
*
|
*
|
||||||
* @return a {@link InfoItemsPage} corresponding to the initial page
|
* @return a {@link InfoItemsPage} corresponding to the initial page
|
||||||
*/
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public abstract InfoItemsPage<R> getInitialPage() throws IOException, ExtractionException;
|
public abstract InfoItemsPage<R> getInitialPage() throws IOException, ExtractionException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an url that can be used to get the next page relative to the initial one.
|
|
||||||
* <p>Usually, these links will only work in the implementation itself.</p>
|
|
||||||
*
|
|
||||||
* @return an url pointing to the next page relative to the initial page
|
|
||||||
* @see #getPage(String)
|
|
||||||
*/
|
|
||||||
public abstract String getNextPageUrl() throws IOException, ExtractionException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of items corresponding to the specific requested page.
|
* Get a list of items corresponding to the specific requested page.
|
||||||
*
|
*
|
||||||
* @param pageUrl any page url got from the exclusive implementation of the list extractor
|
* @param page any page got from the exclusive implementation of the list extractor
|
||||||
* @return a {@link InfoItemsPage} corresponding to the requested page
|
* @return a {@link InfoItemsPage} corresponding to the requested page
|
||||||
* @see #getNextPageUrl()
|
* @see InfoItemsPage#getNextPage()
|
||||||
* @see InfoItemsPage#getNextPageUrl()
|
|
||||||
*/
|
*/
|
||||||
public abstract InfoItemsPage<R> getPage(final String pageUrl) throws IOException, ExtractionException;
|
public abstract InfoItemsPage<R> getPage(Page page) throws IOException, ExtractionException;
|
||||||
|
|
||||||
public boolean hasNextPage() throws IOException, ExtractionException {
|
|
||||||
final String nextPageUrl = getNextPageUrl();
|
|
||||||
return nextPageUrl != null && !nextPageUrl.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public ListLinkHandler getLinkHandler() {
|
public ListLinkHandler getLinkHandler() {
|
||||||
return (ListLinkHandler) super.getLinkHandler();
|
return (ListLinkHandler) super.getLinkHandler();
|
||||||
|
@ -61,23 +65,24 @@ public abstract class ListExtractor<R extends InfoItem> extends Extractor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that is used to wrap a list of gathered items and eventual errors, it
|
* A class that is used to wrap a list of gathered items and eventual errors, it
|
||||||
* also contains a field that points to the next available page ({@link #nextPageUrl}).
|
* also contains a field that points to the next available page ({@link #nextPage}).
|
||||||
|
* @param <T> the info item type that this page is supposed to store and provide
|
||||||
*/
|
*/
|
||||||
public static class InfoItemsPage<T extends InfoItem> {
|
public static class InfoItemsPage<T extends InfoItem> {
|
||||||
private static final InfoItemsPage<InfoItem> EMPTY =
|
private static final InfoItemsPage<InfoItem> EMPTY =
|
||||||
new InfoItemsPage<>(Collections.<InfoItem>emptyList(), "", Collections.<Throwable>emptyList());
|
new InfoItemsPage<>(Collections.emptyList(), null, Collections.emptyList());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A convenient method that returns a representation of an empty page.
|
* A convenient method that returns a representation of an empty page.
|
||||||
*
|
*
|
||||||
* @return a type-safe page with the list of items and errors empty and the nextPageUrl set to an empty string.
|
* @return a type-safe page with the list of items and errors empty and the nextPage set to
|
||||||
|
* {@code null}.
|
||||||
*/
|
*/
|
||||||
public static <T extends InfoItem> InfoItemsPage<T> emptyPage() {
|
public static <T extends InfoItem> InfoItemsPage<T> emptyPage() {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
return (InfoItemsPage<T>) EMPTY;
|
return (InfoItemsPage<T>) EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current list of items of this page
|
* The current list of items of this page
|
||||||
*/
|
*/
|
||||||
|
@ -86,40 +91,42 @@ public abstract class ListExtractor<R extends InfoItem> extends Extractor {
|
||||||
/**
|
/**
|
||||||
* Url pointing to the next page relative to this one
|
* Url pointing to the next page relative to this one
|
||||||
*
|
*
|
||||||
* @see ListExtractor#getPage(String)
|
* @see ListExtractor#getPage(Page)
|
||||||
|
* @see Page
|
||||||
*/
|
*/
|
||||||
private final String nextPageUrl;
|
private final Page nextPage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Errors that happened during the extraction
|
* Errors that happened during the extraction
|
||||||
*/
|
*/
|
||||||
private final List<Throwable> errors;
|
private final List<Throwable> errors;
|
||||||
|
|
||||||
public InfoItemsPage(InfoItemsCollector<T, ?> collector, String nextPageUrl) {
|
public InfoItemsPage(final InfoItemsCollector<T, ?> collector, final Page nextPage) {
|
||||||
this(collector.getItems(), nextPageUrl, collector.getErrors());
|
this(collector.getItems(), nextPage, collector.getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
public InfoItemsPage(List<T> itemsList, String nextPageUrl, List<Throwable> errors) {
|
public InfoItemsPage(final List<T> itemsList,
|
||||||
|
final Page nextPage,
|
||||||
|
final List<Throwable> errors) {
|
||||||
this.itemsList = itemsList;
|
this.itemsList = itemsList;
|
||||||
this.nextPageUrl = nextPageUrl;
|
this.nextPage = nextPage;
|
||||||
this.errors = errors;
|
this.errors = errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasNextPage() {
|
public boolean hasNextPage() {
|
||||||
return nextPageUrl != null && !nextPageUrl.isEmpty();
|
return Page.isValid(nextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<T> getItems() {
|
public List<T> getItems() {
|
||||||
return itemsList;
|
return itemsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNextPageUrl() {
|
public Page getNextPage() {
|
||||||
return nextPageUrl;
|
return nextPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Throwable> getErrors() {
|
public List<Throwable> getErrors() {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,23 +6,25 @@ import java.util.List;
|
||||||
|
|
||||||
public abstract class ListInfo<T extends InfoItem> extends Info {
|
public abstract class ListInfo<T extends InfoItem> extends Info {
|
||||||
private List<T> relatedItems;
|
private List<T> relatedItems;
|
||||||
private String nextPageUrl = null;
|
private Page nextPage = null;
|
||||||
private final List<String> contentFilters;
|
private final List<String> contentFilters;
|
||||||
private final String sortFilter;
|
private final String sortFilter;
|
||||||
|
|
||||||
public ListInfo(int serviceId,
|
public ListInfo(final int serviceId,
|
||||||
String id,
|
final String id,
|
||||||
String url,
|
final String url,
|
||||||
String originalUrl,
|
final String originalUrl,
|
||||||
String name,
|
final String name,
|
||||||
List<String> contentFilter,
|
final List<String> contentFilter,
|
||||||
String sortFilter) {
|
final String sortFilter) {
|
||||||
super(serviceId, id, url, originalUrl, name);
|
super(serviceId, id, url, originalUrl, name);
|
||||||
this.contentFilters = contentFilter;
|
this.contentFilters = contentFilter;
|
||||||
this.sortFilter = sortFilter;
|
this.sortFilter = sortFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
|
public ListInfo(final int serviceId,
|
||||||
|
final ListLinkHandler listUrlIdHandler,
|
||||||
|
final String name) {
|
||||||
super(serviceId, listUrlIdHandler, name);
|
super(serviceId, listUrlIdHandler, name);
|
||||||
this.contentFilters = listUrlIdHandler.getContentFilters();
|
this.contentFilters = listUrlIdHandler.getContentFilters();
|
||||||
this.sortFilter = listUrlIdHandler.getSortFilter();
|
this.sortFilter = listUrlIdHandler.getSortFilter();
|
||||||
|
@ -32,20 +34,20 @@ public abstract class ListInfo<T extends InfoItem> extends Info {
|
||||||
return relatedItems;
|
return relatedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRelatedItems(List<T> relatedItems) {
|
public void setRelatedItems(final List<T> relatedItems) {
|
||||||
this.relatedItems = relatedItems;
|
this.relatedItems = relatedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasNextPage() {
|
public boolean hasNextPage() {
|
||||||
return nextPageUrl != null && !nextPageUrl.isEmpty();
|
return Page.isValid(nextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNextPageUrl() {
|
public Page getNextPage() {
|
||||||
return nextPageUrl;
|
return nextPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setNextPageUrl(String pageUrl) {
|
public void setNextPage(final Page page) {
|
||||||
this.nextPageUrl = pageUrl;
|
this.nextPage = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getContentFilters() {
|
public List<String> getContentFilters() {
|
||||||
|
|
|
@ -3,115 +3,158 @@ package org.schabi.newpipe.extractor;
|
||||||
/*
|
/*
|
||||||
* Created by Adam Howard on 08/11/15.
|
* Created by Adam Howard on 08/11/15.
|
||||||
*
|
*
|
||||||
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
|
* Copyright (c) 2015 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* and Adam Howard <achdisposable1@gmail.com> 2015
|
* and Adam Howard <achdisposable1@gmail.com>
|
||||||
*
|
*
|
||||||
* MediaFormat.java is part of NewPipe.
|
* MediaFormat.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static data about various media formats support by NewPipe, eg mime type, extension
|
* Static data about various media formats support by NewPipe, eg mime type, extension
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@SuppressWarnings("MethodParamPad") // we want the media format table below to be aligned
|
||||||
public enum MediaFormat {
|
public enum MediaFormat {
|
||||||
|
// @formatter:off
|
||||||
//video and audio combined formats
|
//video and audio combined formats
|
||||||
// id name suffix mime type
|
// id name suffix mimeType
|
||||||
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
||||||
v3GPP (0x10, "3GPP", "3gp", "video/3gpp"),
|
v3GPP (0x10, "3GPP", "3gp", "video/3gpp"),
|
||||||
WEBM (0x20, "WebM", "webm", "video/webm"),
|
WEBM (0x20, "WebM", "webm", "video/webm"),
|
||||||
// audio formats
|
// audio formats
|
||||||
M4A (0x100, "m4a", "m4a", "audio/mp4"),
|
M4A (0x100, "m4a", "m4a", "audio/mp4"),
|
||||||
WEBMA (0x200, "WebM", "webm", "audio/webm"),
|
WEBMA (0x200, "WebM", "webm", "audio/webm"),
|
||||||
MP3 (0x300, "MP3", "mp3", "audio/mpeg"),
|
MP3 (0x300, "MP3", "mp3", "audio/mpeg"),
|
||||||
OPUS (0x400, "opus", "opus", "audio/opus"),
|
MP2 (0x310, "MP2", "mp2", "audio/mpeg"),
|
||||||
OGG (0x500, "ogg", "ogg", "audio/ogg"),
|
OPUS (0x400, "opus", "opus", "audio/opus"),
|
||||||
WEBMA_OPUS (0x200, "WebM Opus", "webm", "audio/webm"),
|
OGG (0x500, "ogg", "ogg", "audio/ogg"),
|
||||||
|
WEBMA_OPUS(0x200, "WebM Opus", "webm", "audio/webm"),
|
||||||
|
AIFF (0x600, "AIFF", "aiff", "audio/aiff"),
|
||||||
|
/**
|
||||||
|
* Same as {@link MediaFormat#AIFF}, just with the shorter suffix/file extension
|
||||||
|
*/
|
||||||
|
AIF (0x600, "AIFF", "aif", "audio/aiff"),
|
||||||
|
WAV (0x700, "WAV", "wav", "audio/wav"),
|
||||||
|
FLAC (0x800, "FLAC", "flac", "audio/flac"),
|
||||||
|
ALAC (0x900, "ALAC", "alac", "audio/alac"),
|
||||||
// subtitles formats
|
// subtitles formats
|
||||||
VTT (0x1000, "WebVTT", "vtt", "text/vtt"),
|
VTT (0x1000, "WebVTT", "vtt", "text/vtt"),
|
||||||
TTML (0x2000, "Timed Text Markup Language", "ttml", "application/ttml+xml"),
|
TTML (0x2000, "Timed Text Markup Language", "ttml", "application/ttml+xml"),
|
||||||
TRANSCRIPT1 (0x3000, "TranScript v1", "srv1", "text/xml"),
|
TRANSCRIPT1(0x3000, "TranScript v1", "srv1", "text/xml"),
|
||||||
TRANSCRIPT2 (0x4000, "TranScript v2", "srv2", "text/xml"),
|
TRANSCRIPT2(0x4000, "TranScript v2", "srv2", "text/xml"),
|
||||||
TRANSCRIPT3 (0x5000, "TranScript v3", "srv3", "text/xml"),
|
TRANSCRIPT3(0x5000, "TranScript v3", "srv3", "text/xml"),
|
||||||
SRT (0x6000, "SubRip file format", "srt", "text/srt");
|
SRT (0x6000, "SubRip file format", "srt", "text/srt");
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
public final int id;
|
public final int id;
|
||||||
|
@Nonnull
|
||||||
public final String name;
|
public final String name;
|
||||||
|
@Nonnull
|
||||||
public final String suffix;
|
public final String suffix;
|
||||||
|
@Nonnull
|
||||||
public final String mimeType;
|
public final String mimeType;
|
||||||
|
|
||||||
MediaFormat(int id, String name, String suffix, String mimeType) {
|
MediaFormat(final int id, @Nonnull final String name,
|
||||||
|
@Nonnull final String suffix, @Nonnull final String mimeType) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.suffix = suffix;
|
this.suffix = suffix;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static <T> T getById(final int id,
|
||||||
|
final Function<MediaFormat, T> field,
|
||||||
|
final T orElse) {
|
||||||
|
return Arrays.stream(MediaFormat.values())
|
||||||
|
.filter(mediaFormat -> mediaFormat.id == id)
|
||||||
|
.map(field)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(orElse);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the friendly name of the media format with the supplied id
|
* Return the friendly name of the media format with the supplied id
|
||||||
*
|
*
|
||||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||||
* @return the friendly name of the MediaFormat associated with this ids,
|
* @return the friendly name of the MediaFormat associated with this ids,
|
||||||
* or an empty String if none match it.
|
* or an empty String if none match it.
|
||||||
*/
|
*/
|
||||||
public static String getNameById(int ident) {
|
@Nonnull
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
public static String getNameById(final int id) {
|
||||||
if (vf.id == ident) return vf.name;
|
return getById(id, MediaFormat::getName, "");
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the file extension of the media format with the supplied id
|
* Return the file extension of the media format with the supplied id
|
||||||
*
|
*
|
||||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||||
* @return the file extension of the MediaFormat associated with this ids,
|
* @return the file extension of the MediaFormat associated with this ids,
|
||||||
* or an empty String if none match it.
|
* or an empty String if none match it.
|
||||||
*/
|
*/
|
||||||
public static String getSuffixById(int ident) {
|
@Nonnull
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
public static String getSuffixById(final int id) {
|
||||||
if (vf.id == ident) return vf.suffix;
|
return getById(id, MediaFormat::getSuffix, "");
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the MIME type of the media format with the supplied id
|
* Return the MIME type of the media format with the supplied id
|
||||||
*
|
*
|
||||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||||
* @return the MIME type of the MediaFormat associated with this ids,
|
* @return the MIME type of the MediaFormat associated with this ids,
|
||||||
* or an empty String if none match it.
|
* or an empty String if none match it.
|
||||||
*/
|
*/
|
||||||
public static String getMimeById(int ident) {
|
@Nullable
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
public static String getMimeById(final int id) {
|
||||||
if (vf.id == ident) return vf.mimeType;
|
return getById(id, MediaFormat::getMimeType, null);
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the MediaFormat with the supplied mime type
|
* Return the first {@link MediaFormat} with the supplied mime type.
|
||||||
|
* There might be more formats which have the same mime type.
|
||||||
|
* To retrieve those, use {@link #getAllFromMimeType(String)}.
|
||||||
*
|
*
|
||||||
* @return MediaFormat associated with this mime type,
|
* @return MediaFormat associated with this mime type,
|
||||||
* or null if none match it.
|
* or null if none match it.
|
||||||
*/
|
*/
|
||||||
public static MediaFormat getFromMimeType(String mimeType) {
|
@Nullable
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
public static MediaFormat getFromMimeType(final String mimeType) {
|
||||||
if (vf.mimeType.equals(mimeType)) return vf;
|
return Arrays.stream(MediaFormat.values())
|
||||||
}
|
.filter(mediaFormat -> mediaFormat.mimeType.equals(mimeType))
|
||||||
return null;
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all media formats which have the given mime type.
|
||||||
|
* @param mimeType the mime type to search for
|
||||||
|
* @return a modifiable {@link List} which contains the {@link MediaFormat}s
|
||||||
|
* that have the given mime type.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static List<MediaFormat> getAllFromMimeType(final String mimeType) {
|
||||||
|
return Arrays.stream(MediaFormat.values())
|
||||||
|
.filter(mediaFormat -> mediaFormat.mimeType.equals(mimeType))
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,18 +163,21 @@ public enum MediaFormat {
|
||||||
* @param id the id
|
* @param id the id
|
||||||
* @return the id of the media format or null.
|
* @return the id of the media format or null.
|
||||||
*/
|
*/
|
||||||
public static MediaFormat getFormatById(int id) {
|
@Nullable
|
||||||
for (MediaFormat vf : values()) {
|
public static MediaFormat getFormatById(final int id) {
|
||||||
if (vf.id == id) return vf;
|
return getById(id, mediaFormat -> mediaFormat, null);
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaFormat getFromSuffix(String suffix) {
|
/**
|
||||||
for (MediaFormat vf : values()) {
|
* Get the first media format that has the given suffix/file extension.
|
||||||
if (vf.suffix.equals(suffix)) return vf;
|
* @return the matching {@link MediaFormat} or {@code null} if no associated format is found
|
||||||
}
|
*/
|
||||||
return null;
|
@Nullable
|
||||||
|
public static MediaFormat getFromSuffix(final String suffix) {
|
||||||
|
return Arrays.stream(MediaFormat.values())
|
||||||
|
.filter(mediaFormat -> mediaFormat.suffix.equals(suffix))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,6 +185,7 @@ public enum MediaFormat {
|
||||||
*
|
*
|
||||||
* @return the name of the format
|
* @return the name of the format
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
@ -148,6 +195,7 @@ public enum MediaFormat {
|
||||||
*
|
*
|
||||||
* @return the filename extension
|
* @return the filename extension
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public String getSuffix() {
|
public String getSuffix() {
|
||||||
return suffix;
|
return suffix;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +205,7 @@ public enum MediaFormat {
|
||||||
*
|
*
|
||||||
* @return the mime type
|
* @return the mime type
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public String getMimeType() {
|
public String getMimeType() {
|
||||||
return mimeType;
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class MetaInfo implements Serializable {
|
||||||
|
|
||||||
|
private String title = "";
|
||||||
|
private Description content;
|
||||||
|
private List<URL> urls = new ArrayList<>();
|
||||||
|
private List<String> urlTexts = new ArrayList<>();
|
||||||
|
|
||||||
|
public MetaInfo(@Nonnull final String title,
|
||||||
|
@Nonnull final Description content,
|
||||||
|
@Nonnull final List<URL> urls,
|
||||||
|
@Nonnull final List<String> urlTexts) {
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
this.urls = urls;
|
||||||
|
this.urlTexts = urlTexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Title of the info. Can be empty.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(@Nonnull final String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Description getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(@Nonnull final Description content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<URL> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrls(@Nonnull final List<URL> urls) {
|
||||||
|
this.urls = urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUrl(@Nonnull final URL url) {
|
||||||
|
urls.add(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<String> getUrlTexts() {
|
||||||
|
return urlTexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrlTexts(@Nonnull final List<String> urlTexts) {
|
||||||
|
this.urlTexts = urlTexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUrlText(@Nonnull final String urlText) {
|
||||||
|
urlTexts.add(urlText);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,5 @@
|
||||||
package org.schabi.newpipe.extractor.search;
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
@ -18,25 +15,26 @@ import java.util.List;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* InfoItemsSearchCollector.java is part of NewPipe.
|
* InfoItemsSearchCollector.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collector for search results
|
* A collector that can handle many extractor types, to be used when a list contains items of
|
||||||
|
* different types (e.g. search)
|
||||||
* <p>
|
* <p>
|
||||||
* This collector can handle the following extractor types:
|
* This collector can handle the following extractor types:
|
||||||
* <ul>
|
* <ul>
|
||||||
|
@ -44,15 +42,15 @@ import java.util.List;
|
||||||
* <li>{@link ChannelInfoItemExtractor}</li>
|
* <li>{@link ChannelInfoItemExtractor}</li>
|
||||||
* <li>{@link PlaylistInfoItemExtractor}</li>
|
* <li>{@link PlaylistInfoItemExtractor}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* Calling {@link #extract(InfoItemExtractor)} or {@link #commit(Object)} with any
|
* Calling {@link #extract(InfoItemExtractor)} or {@link #commit(InfoItemExtractor)} with any
|
||||||
* other extractor type will raise an exception.
|
* other extractor type will raise an exception.
|
||||||
*/
|
*/
|
||||||
public class InfoItemsSearchCollector extends InfoItemsCollector<InfoItem, InfoItemExtractor> {
|
public class MultiInfoItemsCollector extends InfoItemsCollector<InfoItem, InfoItemExtractor> {
|
||||||
private final StreamInfoItemsCollector streamCollector;
|
private final StreamInfoItemsCollector streamCollector;
|
||||||
private final ChannelInfoItemsCollector userCollector;
|
private final ChannelInfoItemsCollector userCollector;
|
||||||
private final PlaylistInfoItemsCollector playlistCollector;
|
private final PlaylistInfoItemsCollector playlistCollector;
|
||||||
|
|
||||||
public InfoItemsSearchCollector(int serviceId) {
|
public MultiInfoItemsCollector(final int serviceId) {
|
||||||
super(serviceId);
|
super(serviceId);
|
||||||
streamCollector = new StreamInfoItemsCollector(serviceId);
|
streamCollector = new StreamInfoItemsCollector(serviceId);
|
||||||
userCollector = new ChannelInfoItemsCollector(serviceId);
|
userCollector = new ChannelInfoItemsCollector(serviceId);
|
||||||
|
@ -78,7 +76,7 @@ public class InfoItemsSearchCollector extends InfoItemsCollector<InfoItem, InfoI
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfoItem extract(InfoItemExtractor extractor) throws ParsingException {
|
public InfoItem extract(final InfoItemExtractor extractor) throws ParsingException {
|
||||||
// Use the corresponding collector for each item extractor type
|
// Use the corresponding collector for each item extractor type
|
||||||
if (extractor instanceof StreamInfoItemExtractor) {
|
if (extractor instanceof StreamInfoItemExtractor) {
|
||||||
return streamCollector.extract((StreamInfoItemExtractor) extractor);
|
return streamCollector.extract((StreamInfoItemExtractor) extractor);
|
|
@ -3,21 +3,21 @@ package org.schabi.newpipe.extractor;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 23.08.15.
|
* Created by Christian Schabesberger on 23.08.15.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2015 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* NewPipe.java is part of NewPipe.
|
* NewPipe Extractor.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
@ -25,14 +25,15 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to streaming services supported by NewPipe.
|
* Provides access to streaming services supported by NewPipe.
|
||||||
*/
|
*/
|
||||||
public class NewPipe {
|
public final class NewPipe {
|
||||||
private static Downloader downloader;
|
private static Downloader downloader;
|
||||||
private static Localization preferredLocalization;
|
private static Localization preferredLocalization;
|
||||||
private static ContentCountry preferredContentCountry;
|
private static ContentCountry preferredContentCountry;
|
||||||
|
@ -40,19 +41,16 @@ public class NewPipe {
|
||||||
private NewPipe() {
|
private NewPipe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(Downloader d) {
|
public static void init(final Downloader d) {
|
||||||
downloader = d;
|
init(d, Localization.DEFAULT);
|
||||||
preferredLocalization = Localization.DEFAULT;
|
|
||||||
preferredContentCountry = ContentCountry.DEFAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(Downloader d, Localization l) {
|
public static void init(final Downloader d, final Localization l) {
|
||||||
downloader = d;
|
init(d, l, l.getCountryCode().isEmpty()
|
||||||
preferredLocalization = l;
|
? ContentCountry.DEFAULT : new ContentCountry(l.getCountryCode()));
|
||||||
preferredContentCountry = l.getCountryCode().isEmpty() ? ContentCountry.DEFAULT : new ContentCountry(l.getCountryCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(Downloader d, Localization l, ContentCountry c) {
|
public static void init(final Downloader d, final Localization l, final ContentCountry c) {
|
||||||
downloader = d;
|
downloader = d;
|
||||||
preferredLocalization = l;
|
preferredLocalization = l;
|
||||||
preferredContentCountry = c;
|
preferredContentCountry = c;
|
||||||
|
@ -70,26 +68,24 @@ public class NewPipe {
|
||||||
return ServiceList.all();
|
return ServiceList.all();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamingService getService(int serviceId) throws ExtractionException {
|
public static StreamingService getService(final int serviceId) throws ExtractionException {
|
||||||
for (StreamingService service : ServiceList.all()) {
|
return ServiceList.all().stream()
|
||||||
if (service.getServiceId() == serviceId) {
|
.filter(service -> service.getServiceId() == serviceId)
|
||||||
return service;
|
.findFirst()
|
||||||
}
|
.orElseThrow(() -> new ExtractionException(
|
||||||
}
|
"There's no service with the id = \"" + serviceId + "\""));
|
||||||
throw new ExtractionException("There's no service with the id = \"" + serviceId + "\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamingService getService(String serviceName) throws ExtractionException {
|
public static StreamingService getService(final String serviceName) throws ExtractionException {
|
||||||
for (StreamingService service : ServiceList.all()) {
|
return ServiceList.all().stream()
|
||||||
if (service.getServiceInfo().getName().equals(serviceName)) {
|
.filter(service -> service.getServiceInfo().getName().equals(serviceName))
|
||||||
return service;
|
.findFirst()
|
||||||
}
|
.orElseThrow(() -> new ExtractionException(
|
||||||
}
|
"There's no service with the name = \"" + serviceName + "\""));
|
||||||
throw new ExtractionException("There's no service with the name = \"" + serviceName + "\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamingService getServiceByUrl(String url) throws ExtractionException {
|
public static StreamingService getServiceByUrl(final String url) throws ExtractionException {
|
||||||
for (StreamingService service : ServiceList.all()) {
|
for (final StreamingService service : ServiceList.all()) {
|
||||||
if (service.getLinkTypeByUrl(url) != StreamingService.LinkType.NONE) {
|
if (service.getLinkTypeByUrl(url) != StreamingService.LinkType.NONE) {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
@ -97,43 +93,25 @@ public class NewPipe {
|
||||||
throw new ExtractionException("No service can handle the url = \"" + url + "\"");
|
throw new ExtractionException("No service can handle the url = \"" + url + "\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getIdOfService(String serviceName) {
|
|
||||||
try {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
return getService(serviceName).getServiceId();
|
|
||||||
} catch (ExtractionException ignored) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getNameOfService(int id) {
|
|
||||||
try {
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
return getService(id).getServiceInfo().getName();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Service id not known");
|
|
||||||
e.printStackTrace();
|
|
||||||
return "<unknown>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Localization
|
// Localization
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static void setupLocalization(Localization preferredLocalization) {
|
public static void setupLocalization(final Localization thePreferredLocalization) {
|
||||||
setupLocalization(preferredLocalization, null);
|
setupLocalization(thePreferredLocalization, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setupLocalization(Localization preferredLocalization, @Nullable ContentCountry preferredContentCountry) {
|
public static void setupLocalization(
|
||||||
NewPipe.preferredLocalization = preferredLocalization;
|
final Localization thePreferredLocalization,
|
||||||
|
@Nullable final ContentCountry thePreferredContentCountry) {
|
||||||
|
NewPipe.preferredLocalization = thePreferredLocalization;
|
||||||
|
|
||||||
if (preferredContentCountry != null) {
|
if (thePreferredContentCountry != null) {
|
||||||
NewPipe.preferredContentCountry = preferredContentCountry;
|
NewPipe.preferredContentCountry = thePreferredContentCountry;
|
||||||
} else {
|
} else {
|
||||||
NewPipe.preferredContentCountry = preferredLocalization.getCountryCode().isEmpty()
|
NewPipe.preferredContentCountry = thePreferredLocalization.getCountryCode().isEmpty()
|
||||||
? ContentCountry.DEFAULT
|
? ContentCountry.DEFAULT
|
||||||
: new ContentCountry(preferredLocalization.getCountryCode());
|
: new ContentCountry(thePreferredLocalization.getCountryCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +120,7 @@ public class NewPipe {
|
||||||
return preferredLocalization == null ? Localization.DEFAULT : preferredLocalization;
|
return preferredLocalization == null ? Localization.DEFAULT : preferredLocalization;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setPreferredLocalization(Localization preferredLocalization) {
|
public static void setPreferredLocalization(final Localization preferredLocalization) {
|
||||||
NewPipe.preferredLocalization = preferredLocalization;
|
NewPipe.preferredLocalization = preferredLocalization;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +129,7 @@ public class NewPipe {
|
||||||
return preferredContentCountry == null ? ContentCountry.DEFAULT : preferredContentCountry;
|
return preferredContentCountry == null ? ContentCountry.DEFAULT : preferredContentCountry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setPreferredContentCountry(ContentCountry preferredContentCountry) {
|
public static void setPreferredContentCountry(final ContentCountry preferredContentCountry) {
|
||||||
NewPipe.preferredContentCountry = preferredContentCountry;
|
NewPipe.preferredContentCountry = preferredContentCountry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
|
public class Page implements Serializable {
|
||||||
|
private final String url;
|
||||||
|
private final String id;
|
||||||
|
private final List<String> ids;
|
||||||
|
private final Map<String, String> cookies;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final byte[] body;
|
||||||
|
|
||||||
|
public Page(final String url,
|
||||||
|
final String id,
|
||||||
|
final List<String> ids,
|
||||||
|
final Map<String, String> cookies,
|
||||||
|
@Nullable final byte[] body) {
|
||||||
|
this.url = url;
|
||||||
|
this.id = id;
|
||||||
|
this.ids = ids;
|
||||||
|
this.cookies = cookies;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final String url) {
|
||||||
|
this(url, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final String url, final String id) {
|
||||||
|
this(url, id, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final String url, final String id, final byte[] body) {
|
||||||
|
this(url, id, null, null, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final String url, final byte[] body) {
|
||||||
|
this(url, null, null, null, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final String url, final Map<String, String> cookies) {
|
||||||
|
this(url, null, null, cookies, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final List<String> ids) {
|
||||||
|
this(null, null, ids, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page(final List<String> ids, final Map<String, String> cookies) {
|
||||||
|
this(null, null, ids, cookies, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getIds() {
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getCookies() {
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isValid(final Page page) {
|
||||||
|
return page != null && (!isNullOrEmpty(page.getUrl())
|
||||||
|
|| !isNullOrEmpty(page.getIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public byte[] getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,56 +1,52 @@
|
||||||
package org.schabi.newpipe.extractor;
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.BandcampService;
|
||||||
import org.schabi.newpipe.extractor.services.media_ccc.MediaCCCService;
|
import org.schabi.newpipe.extractor.services.media_ccc.MediaCCCService;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeService;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeService;
|
||||||
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudService;
|
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudService;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2018 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ServiceList.java is part of NewPipe.
|
* ServiceList.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of supported services.
|
* A list of supported services.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({"ConstantName", "InnerAssignment"}) // keep unusual names and inner assignments
|
||||||
public final class ServiceList {
|
public final class ServiceList {
|
||||||
private ServiceList() {
|
private ServiceList() {
|
||||||
//no instance
|
// no instance
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final YoutubeService YouTube;
|
public static final YoutubeService YouTube = new YoutubeService(0);
|
||||||
public static final SoundcloudService SoundCloud;
|
public static final SoundcloudService SoundCloud = new SoundcloudService(1);
|
||||||
public static final MediaCCCService MediaCCC;
|
public static final MediaCCCService MediaCCC = new MediaCCCService(2);
|
||||||
public static final PeertubeService PeerTube;
|
public static final PeertubeService PeerTube = new PeertubeService(3);
|
||||||
|
public static final BandcampService Bandcamp = new BandcampService(4);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When creating a new service, put this service in the end of this list,
|
* When creating a new service, put this service in the end of this list,
|
||||||
* and give it the next free id.
|
* and give it the next free id.
|
||||||
*/
|
*/
|
||||||
private static final List<StreamingService> SERVICES = Collections.unmodifiableList(
|
private static final List<StreamingService> SERVICES = List.of(
|
||||||
Arrays.asList(
|
YouTube, SoundCloud, MediaCCC, PeerTube, Bandcamp);
|
||||||
YouTube = new YoutubeService(0),
|
|
||||||
SoundCloud = new SoundcloudService(1),
|
|
||||||
MediaCCC = new MediaCCCService(2),
|
|
||||||
PeerTube = new PeertubeService(3)
|
|
||||||
));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the supported services.
|
* Get all the supported services.
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
package org.schabi.newpipe.extractor;
|
package org.schabi.newpipe.extractor;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.*;
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||||
|
@ -16,27 +22,28 @@ import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2018 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* StreamingService.java is part of NewPipe.
|
* StreamingService.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public abstract class StreamingService {
|
public abstract class StreamingService {
|
||||||
|
@ -54,7 +61,7 @@ public abstract class StreamingService {
|
||||||
* @param name the name of the service
|
* @param name the name of the service
|
||||||
* @param mediaCapabilities the type of media this service can handle
|
* @param mediaCapabilities the type of media this service can handle
|
||||||
*/
|
*/
|
||||||
public ServiceInfo(String name, List<MediaCapability> mediaCapabilities) {
|
public ServiceInfo(final String name, final List<MediaCapability> mediaCapabilities) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.mediaCapabilities = Collections.unmodifiableList(mediaCapabilities);
|
this.mediaCapabilities = Collections.unmodifiableList(mediaCapabilities);
|
||||||
}
|
}
|
||||||
|
@ -73,8 +80,8 @@ public abstract class StreamingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LinkType will be used to determine which type of URL you are handling, and therefore which part
|
* LinkType will be used to determine which type of URL you are handling, and therefore which
|
||||||
* of NewPipe should handle a certain URL.
|
* part of NewPipe should handle a certain URL.
|
||||||
*/
|
*/
|
||||||
public enum LinkType {
|
public enum LinkType {
|
||||||
NONE,
|
NONE,
|
||||||
|
@ -89,14 +96,15 @@ public abstract class StreamingService {
|
||||||
/**
|
/**
|
||||||
* Creates a new Streaming service.
|
* Creates a new Streaming service.
|
||||||
* If you Implement one do not set id within your implementation of this extractor, instead
|
* If you Implement one do not set id within your implementation of this extractor, instead
|
||||||
* set the id when you put the extractor into
|
* set the id when you put the extractor into {@link ServiceList}
|
||||||
* <a href="https://teamnewpipe.github.io/NewPipeExtractor/javadoc/org/schabi/newpipe/extractor/ServiceList.html">ServiceList</a>.
|
|
||||||
* All other parameters can be set directly from the overriding constructor.
|
* All other parameters can be set directly from the overriding constructor.
|
||||||
* @param id the number of the service to identify him within the NewPipe frontend
|
* @param id the number of the service to identify him within the NewPipe frontend
|
||||||
* @param name the name of the service
|
* @param name the name of the service
|
||||||
* @param capabilities the type of media this service can handle
|
* @param capabilities the type of media this service can handle
|
||||||
*/
|
*/
|
||||||
public StreamingService(int id, String name, List<ServiceInfo.MediaCapability> capabilities) {
|
public StreamingService(final int id,
|
||||||
|
final String name,
|
||||||
|
final List<ServiceInfo.MediaCapability> capabilities) {
|
||||||
this.serviceId = id;
|
this.serviceId = id;
|
||||||
this.serviceInfo = new ServiceInfo(name, capabilities);
|
this.serviceInfo = new ServiceInfo(name, capabilities);
|
||||||
}
|
}
|
||||||
|
@ -133,6 +141,14 @@ public abstract class StreamingService {
|
||||||
*/
|
*/
|
||||||
public abstract ListLinkHandlerFactory getChannelLHFactory();
|
public abstract ListLinkHandlerFactory getChannelLHFactory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must return a new instance of an implementation of ListLinkHandlerFactory for channel tabs.
|
||||||
|
* If support for channel tabs is not given null must be returned.
|
||||||
|
*
|
||||||
|
* @return an instance of a ListLinkHandlerFactory for channels or null
|
||||||
|
*/
|
||||||
|
public abstract ListLinkHandlerFactory getChannelTabLHFactory();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must return a new instance of an implementation of ListLinkHandlerFactory for playlists.
|
* Must return a new instance of an implementation of ListLinkHandlerFactory for playlists.
|
||||||
* If support for playlists is not given null must be returned.
|
* If support for playlists is not given null must be returned.
|
||||||
|
@ -171,22 +187,21 @@ public abstract class StreamingService {
|
||||||
public abstract SubscriptionExtractor getSubscriptionExtractor();
|
public abstract SubscriptionExtractor getSubscriptionExtractor();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method decides which strategy will be chosen to fetch the feed. In YouTube, for example, a separate feed
|
* This method decides which strategy will be chosen to fetch the feed. In YouTube, for example,
|
||||||
* exists which is lightweight and made specifically to be used like this.
|
* a separate feed exists which is lightweight and made specifically to be used like this.
|
||||||
* <p>
|
* <p>
|
||||||
* In services which there's no other way to retrieve them, null should be returned.
|
* In services which there's no other way to retrieve them, null should be returned.
|
||||||
*
|
*
|
||||||
* @return a {@link FeedExtractor} instance or null.
|
* @return a {@link FeedExtractor} instance or null.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public FeedExtractor getFeedExtractor(String url) throws ExtractionException {
|
public FeedExtractor getFeedExtractor(final String url) throws ExtractionException {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must create a new instance of a KioskList implementation.
|
* Must create a new instance of a KioskList implementation.
|
||||||
* @return a new KioskList instance
|
* @return a new KioskList instance
|
||||||
* @throws ExtractionException
|
|
||||||
*/
|
*/
|
||||||
public abstract KioskList getKioskList() throws ExtractionException;
|
public abstract KioskList getKioskList() throws ExtractionException;
|
||||||
|
|
||||||
|
@ -194,49 +209,61 @@ public abstract class StreamingService {
|
||||||
* Must create a new instance of a ChannelExtractor implementation.
|
* Must create a new instance of a ChannelExtractor implementation.
|
||||||
* @param linkHandler is pointing to the channel which should be handled by this new instance.
|
* @param linkHandler is pointing to the channel which should be handled by this new instance.
|
||||||
* @return a new ChannelExtractor
|
* @return a new ChannelExtractor
|
||||||
* @throws ExtractionException
|
|
||||||
*/
|
*/
|
||||||
public abstract ChannelExtractor getChannelExtractor(ListLinkHandler linkHandler) throws ExtractionException;
|
public abstract ChannelExtractor getChannelExtractor(ListLinkHandler linkHandler)
|
||||||
|
throws ExtractionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must create a new instance of a ChannelTabExtractor implementation.
|
||||||
|
*
|
||||||
|
* @param linkHandler is pointing to the channel which should be handled by this new instance.
|
||||||
|
* @return a new ChannelTabExtractor
|
||||||
|
*/
|
||||||
|
public abstract ChannelTabExtractor getChannelTabExtractor(ListLinkHandler linkHandler)
|
||||||
|
throws ExtractionException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must crete a new instance of a PlaylistExtractor implementation.
|
* Must crete a new instance of a PlaylistExtractor implementation.
|
||||||
* @param linkHandler is pointing to the playlist which should be handled by this new instance.
|
* @param linkHandler is pointing to the playlist which should be handled by this new instance.
|
||||||
* @return a new PlaylistExtractor
|
* @return a new PlaylistExtractor
|
||||||
* @throws ExtractionException
|
|
||||||
*/
|
*/
|
||||||
public abstract PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler) throws ExtractionException;
|
public abstract PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler)
|
||||||
|
throws ExtractionException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must create a new instance of a StreamExtractor implementation.
|
* Must create a new instance of a StreamExtractor implementation.
|
||||||
* @param linkHandler is pointing to the stream which should be handled by this new instance.
|
* @param linkHandler is pointing to the stream which should be handled by this new instance.
|
||||||
* @return a new StreamExtractor
|
* @return a new StreamExtractor
|
||||||
* @throws ExtractionException
|
|
||||||
*/
|
*/
|
||||||
public abstract StreamExtractor getStreamExtractor(LinkHandler linkHandler) throws ExtractionException;
|
public abstract StreamExtractor getStreamExtractor(LinkHandler linkHandler)
|
||||||
|
throws ExtractionException;
|
||||||
|
|
||||||
public abstract CommentsExtractor getCommentsExtractor(ListLinkHandler linkHandler) throws ExtractionException;
|
public abstract CommentsExtractor getCommentsExtractor(ListLinkHandler linkHandler)
|
||||||
|
throws ExtractionException;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Extractors without link handler
|
// Extractors without link handler
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public SearchExtractor getSearchExtractor(String query,
|
public SearchExtractor getSearchExtractor(final String query,
|
||||||
List<String> contentFilter,
|
final List<String> contentFilter,
|
||||||
String sortFilter) throws ExtractionException {
|
final String sortFilter) throws ExtractionException {
|
||||||
return getSearchExtractor(getSearchQHFactory()
|
return getSearchExtractor(getSearchQHFactory()
|
||||||
.fromQuery(query, contentFilter, sortFilter));
|
.fromQuery(query, contentFilter, sortFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelExtractor getChannelExtractor(String id,
|
public ChannelExtractor getChannelExtractor(final String id,
|
||||||
List<String> contentFilter,
|
final List<String> contentFilter,
|
||||||
String sortFilter) throws ExtractionException {
|
final String sortFilter)
|
||||||
|
throws ExtractionException {
|
||||||
return getChannelExtractor(getChannelLHFactory()
|
return getChannelExtractor(getChannelLHFactory()
|
||||||
.fromQuery(id, contentFilter, sortFilter));
|
.fromQuery(id, contentFilter, sortFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaylistExtractor getPlaylistExtractor(String id,
|
public PlaylistExtractor getPlaylistExtractor(final String id,
|
||||||
List<String> contentFilter,
|
final List<String> contentFilter,
|
||||||
String sortFilter) throws ExtractionException {
|
final String sortFilter)
|
||||||
|
throws ExtractionException {
|
||||||
return getPlaylistExtractor(getPlaylistLHFactory()
|
return getPlaylistExtractor(getPlaylistLHFactory()
|
||||||
.fromQuery(id, contentFilter, sortFilter));
|
.fromQuery(id, contentFilter, sortFilter));
|
||||||
}
|
}
|
||||||
|
@ -245,28 +272,42 @@ public abstract class StreamingService {
|
||||||
// Short extractors overloads
|
// Short extractors overloads
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public SearchExtractor getSearchExtractor(String query) throws ExtractionException {
|
public SearchExtractor getSearchExtractor(final String query) throws ExtractionException {
|
||||||
return getSearchExtractor(getSearchQHFactory().fromQuery(query));
|
return getSearchExtractor(getSearchQHFactory().fromQuery(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelExtractor getChannelExtractor(String url) throws ExtractionException {
|
public ChannelExtractor getChannelExtractor(final String url) throws ExtractionException {
|
||||||
return getChannelExtractor(getChannelLHFactory().fromUrl(url));
|
return getChannelExtractor(getChannelLHFactory().fromUrl(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaylistExtractor getPlaylistExtractor(String url) throws ExtractionException {
|
public ChannelTabExtractor getChannelTabExtractorFromId(final String id, final String tab)
|
||||||
|
throws ExtractionException {
|
||||||
|
return getChannelTabExtractor(getChannelTabLHFactory().fromQuery(
|
||||||
|
id, Collections.singletonList(tab), ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelTabExtractor getChannelTabExtractorFromIdAndBaseUrl(final String id,
|
||||||
|
final String tab,
|
||||||
|
final String baseUrl)
|
||||||
|
throws ExtractionException {
|
||||||
|
return getChannelTabExtractor(getChannelTabLHFactory().fromQuery(
|
||||||
|
id, Collections.singletonList(tab), "", baseUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistExtractor getPlaylistExtractor(final String url) throws ExtractionException {
|
||||||
return getPlaylistExtractor(getPlaylistLHFactory().fromUrl(url));
|
return getPlaylistExtractor(getPlaylistLHFactory().fromUrl(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamExtractor getStreamExtractor(String url) throws ExtractionException {
|
public StreamExtractor getStreamExtractor(final String url) throws ExtractionException {
|
||||||
return getStreamExtractor(getStreamLHFactory().fromUrl(url));
|
return getStreamExtractor(getStreamLHFactory().fromUrl(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommentsExtractor getCommentsExtractor(String url) throws ExtractionException {
|
public CommentsExtractor getCommentsExtractor(final String url) throws ExtractionException {
|
||||||
ListLinkHandlerFactory llhf = getCommentsLHFactory();
|
final ListLinkHandlerFactory listLinkHandlerFactory = getCommentsLHFactory();
|
||||||
if (llhf == null) {
|
if (listLinkHandlerFactory == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return getCommentsExtractor(llhf.fromUrl(url));
|
return getCommentsExtractor(listLinkHandlerFactory.fromUrl(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -277,18 +318,19 @@ public abstract class StreamingService {
|
||||||
* Figures out where the link is pointing to (a channel, a video, a playlist, etc.)
|
* Figures out where the link is pointing to (a channel, a video, a playlist, etc.)
|
||||||
* @param url the url on which it should be decided of which link type it is
|
* @param url the url on which it should be decided of which link type it is
|
||||||
* @return the link type of url
|
* @return the link type of url
|
||||||
* @throws ParsingException
|
|
||||||
*/
|
*/
|
||||||
public final LinkType getLinkTypeByUrl(String url) throws ParsingException {
|
public final LinkType getLinkTypeByUrl(final String url) throws ParsingException {
|
||||||
LinkHandlerFactory sH = getStreamLHFactory();
|
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
|
||||||
LinkHandlerFactory cH = getChannelLHFactory();
|
|
||||||
LinkHandlerFactory pH = getPlaylistLHFactory();
|
|
||||||
|
|
||||||
if (sH != null && sH.acceptUrl(url)) {
|
final LinkHandlerFactory sH = getStreamLHFactory();
|
||||||
|
final LinkHandlerFactory cH = getChannelLHFactory();
|
||||||
|
final LinkHandlerFactory pH = getPlaylistLHFactory();
|
||||||
|
|
||||||
|
if (sH != null && sH.acceptUrl(polishedUrl)) {
|
||||||
return LinkType.STREAM;
|
return LinkType.STREAM;
|
||||||
} else if (cH != null && cH.acceptUrl(url)) {
|
} else if (cH != null && cH.acceptUrl(polishedUrl)) {
|
||||||
return LinkType.CHANNEL;
|
return LinkType.CHANNEL;
|
||||||
} else if (pH != null && pH.acceptUrl(url)) {
|
} else if (pH != null && pH.acceptUrl(polishedUrl)) {
|
||||||
return LinkType.PLAYLIST;
|
return LinkType.PLAYLIST;
|
||||||
} else {
|
} else {
|
||||||
return LinkType.NONE;
|
return LinkType.NONE;
|
||||||
|
@ -318,7 +360,8 @@ public abstract class StreamingService {
|
||||||
* the user prefer (using {@link NewPipe#getPreferredLocalization()}), then it will:
|
* the user prefer (using {@link NewPipe#getPreferredLocalization()}), then it will:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Check if the exactly localization is supported by this service.</li>
|
* <li>Check if the exactly localization is supported by this service.</li>
|
||||||
* <li>If not, check if a less specific localization is available, using only the language code.</li>
|
* <li>If not, check if a less specific localization is available, using only the language
|
||||||
|
* code.</li>
|
||||||
* <li>Fallback to the {@link Localization#DEFAULT default} localization.</li>
|
* <li>Fallback to the {@link Localization#DEFAULT default} localization.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
|
@ -331,8 +374,9 @@ public abstract class StreamingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to the first supported language that matches the preferred language
|
// Fallback to the first supported language that matches the preferred language
|
||||||
for (Localization supportedLanguage : getSupportedLocalizations()) {
|
for (final Localization supportedLanguage : getSupportedLocalizations()) {
|
||||||
if (supportedLanguage.getLanguageCode().equals(preferredLocalization.getLanguageCode())) {
|
if (supportedLanguage.getLanguageCode()
|
||||||
|
.equals(preferredLocalization.getLanguageCode())) {
|
||||||
return supportedLanguage;
|
return supportedLanguage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -341,8 +385,8 @@ public abstract class StreamingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the country that should be used to fetch content in this service. It will get which country
|
* Returns the country that should be used to fetch content in this service. It will get which
|
||||||
* the user prefer (using {@link NewPipe#getPreferredContentCountry()}), then it will:
|
* country the user prefer (using {@link NewPipe#getPreferredContentCountry()}), then it will:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Check if the country is supported by this service.</li>
|
* <li>Check if the country is supported by this service.</li>
|
||||||
* <li>If not, fallback to the {@link ContentCountry#DEFAULT default} country.</li>
|
* <li>If not, fallback to the {@link ContentCountry#DEFAULT default} country.</li>
|
||||||
|
@ -359,14 +403,15 @@ public abstract class StreamingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an instance of the time ago parser using the patterns related to the passed localization.<br>
|
* Get an instance of the time ago parser using the patterns related to the passed localization.
|
||||||
* <br>
|
* <br><br>
|
||||||
* Just like {@link #getLocalization()}, it will also try to fallback to a less specific localization if
|
* Just like {@link #getLocalization()}, it will also try to fallback to a less specific
|
||||||
* the exact one is not available/supported.
|
* localization if the exact one is not available/supported.
|
||||||
*
|
*
|
||||||
* @throws IllegalArgumentException if the localization is not supported (parsing patterns are not present).
|
* @throws IllegalArgumentException if the localization is not supported (parsing patterns are
|
||||||
|
* not present).
|
||||||
*/
|
*/
|
||||||
public TimeAgoParser getTimeAgoParser(Localization localization) {
|
public TimeAgoParser getTimeAgoParser(final Localization localization) {
|
||||||
final TimeAgoParser targetParser = TimeAgoPatternsManager.getTimeAgoParserFor(localization);
|
final TimeAgoParser targetParser = TimeAgoPatternsManager.getTimeAgoParserFor(localization);
|
||||||
|
|
||||||
if (targetParser != null) {
|
if (targetParser != null) {
|
||||||
|
@ -374,15 +419,18 @@ public abstract class StreamingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!localization.getCountryCode().isEmpty()) {
|
if (!localization.getCountryCode().isEmpty()) {
|
||||||
final Localization lessSpecificLocalization = new Localization(localization.getLanguageCode());
|
final Localization lessSpecificLocalization
|
||||||
final TimeAgoParser lessSpecificParser = TimeAgoPatternsManager.getTimeAgoParserFor(lessSpecificLocalization);
|
= new Localization(localization.getLanguageCode());
|
||||||
|
final TimeAgoParser lessSpecificParser
|
||||||
|
= TimeAgoPatternsManager.getTimeAgoParserFor(lessSpecificLocalization);
|
||||||
|
|
||||||
if (lessSpecificParser != null) {
|
if (lessSpecificParser != null) {
|
||||||
return lessSpecificParser;
|
return lessSpecificParser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("Localization is not supported (\"" + localization.toString() + "\")");
|
throw new IllegalArgumentException(
|
||||||
|
"Localization is not supported (\"" + localization + "\")");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,58 @@
|
||||||
package org.schabi.newpipe.extractor.channel;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 25.07.16.
|
* Created by Christian Schabesberger on 25.07.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ChannelExtractor.java is part of NewPipe.
|
* ChannelExtractor.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public abstract class ChannelExtractor extends ListExtractor<StreamInfoItem> {
|
package org.schabi.newpipe.extractor.channel;
|
||||||
|
|
||||||
public ChannelExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
import org.schabi.newpipe.extractor.Extractor;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class ChannelExtractor extends Extractor {
|
||||||
|
|
||||||
|
public static final long UNKNOWN_SUBSCRIBER_COUNT = -1;
|
||||||
|
|
||||||
|
protected ChannelExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract String getAvatarUrl() throws ParsingException;
|
@Nonnull
|
||||||
public abstract String getBannerUrl() throws ParsingException;
|
public abstract List<Image> getAvatars() throws ParsingException;
|
||||||
|
@Nonnull
|
||||||
|
public abstract List<Image> getBanners() throws ParsingException;
|
||||||
public abstract String getFeedUrl() throws ParsingException;
|
public abstract String getFeedUrl() throws ParsingException;
|
||||||
public abstract long getSubscriberCount() throws ParsingException;
|
public abstract long getSubscriberCount() throws ParsingException;
|
||||||
public abstract String getDescription() throws ParsingException;
|
public abstract String getDescription() throws ParsingException;
|
||||||
|
public abstract String getParentChannelName() throws ParsingException;
|
||||||
|
public abstract String getParentChannelUrl() throws ParsingException;
|
||||||
|
@Nonnull
|
||||||
|
public abstract List<Image> getParentChannelAvatars() throws ParsingException;
|
||||||
|
public abstract boolean isVerified() throws ParsingException;
|
||||||
|
@Nonnull
|
||||||
|
public abstract List<ListLinkHandler> getTabs() throws ParsingException;
|
||||||
|
@Nonnull
|
||||||
|
public List<String> getTags() throws ParsingException {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,59 +1,60 @@
|
||||||
package org.schabi.newpipe.extractor.channel;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 31.07.16.
|
* Created by Christian Schabesberger on 31.07.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ChannelInfo.java is part of NewPipe.
|
* ChannelInfo.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ChannelInfo extends ListInfo<StreamInfoItem> {
|
package org.schabi.newpipe.extractor.channel;
|
||||||
|
|
||||||
public ChannelInfo(int serviceId, String id, String url, String originalUrl, String name, ListLinkHandler listLinkHandler) {
|
import org.schabi.newpipe.extractor.Info;
|
||||||
super(serviceId, id, url, originalUrl, name, listLinkHandler.getContentFilters(), listLinkHandler.getSortFilter());
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class ChannelInfo extends Info {
|
||||||
|
|
||||||
|
public ChannelInfo(final int serviceId,
|
||||||
|
final String id,
|
||||||
|
final String url,
|
||||||
|
final String originalUrl,
|
||||||
|
final String name) {
|
||||||
|
super(serviceId, id, url, originalUrl, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ChannelInfo getInfo(String url) throws IOException, ExtractionException {
|
public static ChannelInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ChannelInfo getInfo(StreamingService service, String url) throws IOException, ExtractionException {
|
public static ChannelInfo getInfo(final StreamingService service, final String url)
|
||||||
ChannelExtractor extractor = service.getChannelExtractor(url);
|
throws IOException, ExtractionException {
|
||||||
|
final ChannelExtractor extractor = service.getChannelExtractor(url);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
return getInfo(extractor);
|
return getInfo(extractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfoItemsPage<StreamInfoItem> getMoreItems(StreamingService service,
|
public static ChannelInfo getInfo(final ChannelExtractor extractor)
|
||||||
String url,
|
throws IOException, ExtractionException {
|
||||||
String pageUrl) throws IOException, ExtractionException {
|
|
||||||
return service.getChannelExtractor(url).getPage(pageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ChannelInfo getInfo(ChannelExtractor extractor) throws IOException, ExtractionException {
|
|
||||||
|
|
||||||
final int serviceId = extractor.getServiceId();
|
final int serviceId = extractor.getServiceId();
|
||||||
final String id = extractor.getId();
|
final String id = extractor.getId();
|
||||||
|
@ -61,70 +62,141 @@ public class ChannelInfo extends ListInfo<StreamInfoItem> {
|
||||||
final String originalUrl = extractor.getOriginalUrl();
|
final String originalUrl = extractor.getOriginalUrl();
|
||||||
final String name = extractor.getName();
|
final String name = extractor.getName();
|
||||||
|
|
||||||
final ChannelInfo info = new ChannelInfo(serviceId, id, url, originalUrl, name, extractor.getLinkHandler());
|
final ChannelInfo info = new ChannelInfo(serviceId, id, url, originalUrl, name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setAvatarUrl(extractor.getAvatarUrl());
|
info.setAvatars(extractor.getAvatars());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setBannerUrl(extractor.getBannerUrl());
|
info.setBanners(extractor.getBanners());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setFeedUrl(extractor.getFeedUrl());
|
info.setFeedUrl(extractor.getFeedUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final InfoItemsPage<StreamInfoItem> itemsPage = ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
|
||||||
info.setRelatedItems(itemsPage.getItems());
|
|
||||||
info.setNextPageUrl(itemsPage.getNextPageUrl());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setSubscriberCount(extractor.getSubscriberCount());
|
info.setSubscriberCount(extractor.getSubscriberCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setDescription(extractor.getDescription());
|
info.setDescription(extractor.getDescription());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setParentChannelName(extractor.getParentChannelName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setParentChannelUrl(extractor.getParentChannelUrl());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setParentChannelAvatars(extractor.getParentChannelAvatars());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setVerified(extractor.isVerified());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setTabs(extractor.getTabs());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setTags(extractor.getTags());
|
||||||
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String avatarUrl;
|
private String parentChannelName;
|
||||||
private String bannerUrl;
|
private String parentChannelUrl;
|
||||||
private String feedUrl;
|
private String feedUrl;
|
||||||
private long subscriberCount = -1;
|
private long subscriberCount = -1;
|
||||||
private String description;
|
private String description;
|
||||||
private String[] donationLinks;
|
private String[] donationLinks;
|
||||||
|
@Nonnull
|
||||||
|
private List<Image> avatars = List.of();
|
||||||
|
@Nonnull
|
||||||
|
private List<Image> banners = List.of();
|
||||||
|
@Nonnull
|
||||||
|
private List<Image> parentChannelAvatars = List.of();
|
||||||
|
private boolean verified;
|
||||||
|
private List<ListLinkHandler> tabs = List.of();
|
||||||
|
private List<String> tags = List.of();
|
||||||
|
|
||||||
public String getAvatarUrl() {
|
public String getParentChannelName() {
|
||||||
return avatarUrl;
|
return parentChannelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAvatarUrl(String avatarUrl) {
|
public void setParentChannelName(final String parentChannelName) {
|
||||||
this.avatarUrl = avatarUrl;
|
this.parentChannelName = parentChannelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBannerUrl() {
|
public String getParentChannelUrl() {
|
||||||
return bannerUrl;
|
return parentChannelUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBannerUrl(String bannerUrl) {
|
public void setParentChannelUrl(final String parentChannelUrl) {
|
||||||
this.bannerUrl = bannerUrl;
|
this.parentChannelUrl = parentChannelUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getParentChannelAvatars() {
|
||||||
|
return parentChannelAvatars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParentChannelAvatars(@Nonnull final List<Image> parentChannelAvatars) {
|
||||||
|
this.parentChannelAvatars = parentChannelAvatars;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getAvatars() {
|
||||||
|
return avatars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvatars(@Nonnull final List<Image> avatars) {
|
||||||
|
this.avatars = avatars;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getBanners() {
|
||||||
|
return banners;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBanners(@Nonnull final List<Image> banners) {
|
||||||
|
this.banners = banners;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFeedUrl() {
|
public String getFeedUrl() {
|
||||||
return feedUrl;
|
return feedUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeedUrl(String feedUrl) {
|
public void setFeedUrl(final String feedUrl) {
|
||||||
this.feedUrl = feedUrl;
|
this.feedUrl = feedUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +204,7 @@ public class ChannelInfo extends ListInfo<StreamInfoItem> {
|
||||||
return subscriberCount;
|
return subscriberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubscriberCount(long subscriberCount) {
|
public void setSubscriberCount(final long subscriberCount) {
|
||||||
this.subscriberCount = subscriberCount;
|
this.subscriberCount = subscriberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +212,7 @@ public class ChannelInfo extends ListInfo<StreamInfoItem> {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public void setDescription(final String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +220,33 @@ public class ChannelInfo extends ListInfo<StreamInfoItem> {
|
||||||
return donationLinks;
|
return donationLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDonationLinks(String[] donationLinks) {
|
public void setDonationLinks(final String[] donationLinks) {
|
||||||
this.donationLinks = donationLinks;
|
this.donationLinks = donationLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isVerified() {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVerified(final boolean verified) {
|
||||||
|
this.verified = verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<ListLinkHandler> getTabs() {
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTabs(@Nonnull final List<ListLinkHandler> tabs) {
|
||||||
|
this.tabs = tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<String> getTags() {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTags(@Nonnull final List<String> tags) {
|
||||||
|
this.tags = tags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,21 +5,21 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 11.02.17.
|
* Created by Christian Schabesberger on 11.02.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ChannelInfoItem.java is part of NewPipe.
|
* ChannelInfoItem.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ChannelInfoItem extends InfoItem {
|
public class ChannelInfoItem extends InfoItem {
|
||||||
|
@ -27,9 +27,9 @@ public class ChannelInfoItem extends InfoItem {
|
||||||
private String description;
|
private String description;
|
||||||
private long subscriberCount = -1;
|
private long subscriberCount = -1;
|
||||||
private long streamCount = -1;
|
private long streamCount = -1;
|
||||||
|
private boolean verified = false;
|
||||||
|
|
||||||
|
public ChannelInfoItem(final int serviceId, final String url, final String name) {
|
||||||
public ChannelInfoItem(int serviceId, String url, String name) {
|
|
||||||
super(InfoType.CHANNEL, serviceId, url, name);
|
super(InfoType.CHANNEL, serviceId, url, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ public class ChannelInfoItem extends InfoItem {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public void setDescription(final String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,15 +45,23 @@ public class ChannelInfoItem extends InfoItem {
|
||||||
return subscriberCount;
|
return subscriberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubscriberCount(long subscriber_count) {
|
public void setSubscriberCount(final long subscriberCount) {
|
||||||
this.subscriberCount = subscriber_count;
|
this.subscriberCount = subscriberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getStreamCount() {
|
public long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamCount(long stream_count) {
|
public void setStreamCount(final long streamCount) {
|
||||||
this.streamCount = stream_count;
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVerified() {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVerified(final boolean verified) {
|
||||||
|
this.verified = verified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,26 +6,29 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ChannelInfoItemExtractor.java is part of NewPipe.
|
* ChannelInfoItemExtractor.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public interface ChannelInfoItemExtractor extends InfoItemExtractor {
|
public interface ChannelInfoItemExtractor extends InfoItemExtractor {
|
||||||
String getDescription() throws ParsingException;
|
String getDescription() throws ParsingException;
|
||||||
|
|
||||||
long getSubscriberCount() throws ParsingException;
|
long getSubscriberCount() throws ParsingException;
|
||||||
|
|
||||||
long getStreamCount() throws ParsingException;
|
long getStreamCount() throws ParsingException;
|
||||||
|
|
||||||
|
boolean isVerified() throws ParsingException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +1,67 @@
|
||||||
package org.schabi.newpipe.extractor.channel;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ChannelInfoItemsCollector.java is part of NewPipe.
|
* ChannelInfoItemsCollector.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ChannelInfoItemsCollector extends InfoItemsCollector<ChannelInfoItem, ChannelInfoItemExtractor> {
|
package org.schabi.newpipe.extractor.channel;
|
||||||
public ChannelInfoItemsCollector(int serviceId) {
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
public final class ChannelInfoItemsCollector
|
||||||
|
extends InfoItemsCollector<ChannelInfoItem, ChannelInfoItemExtractor> {
|
||||||
|
public ChannelInfoItemsCollector(final int serviceId) {
|
||||||
super(serviceId);
|
super(serviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelInfoItem extract(ChannelInfoItemExtractor extractor) throws ParsingException {
|
public ChannelInfoItem extract(final ChannelInfoItemExtractor extractor)
|
||||||
// important information
|
throws ParsingException {
|
||||||
int serviceId = getServiceId();
|
final ChannelInfoItem resultItem = new ChannelInfoItem(
|
||||||
String name = extractor.getName();
|
getServiceId(), extractor.getUrl(), extractor.getName());
|
||||||
String url = extractor.getUrl();
|
|
||||||
|
|
||||||
ChannelInfoItem resultItem = new ChannelInfoItem(serviceId, url, name);
|
|
||||||
|
|
||||||
|
|
||||||
// optional information
|
// optional information
|
||||||
try {
|
try {
|
||||||
resultItem.setSubscriberCount(extractor.getSubscriberCount());
|
resultItem.setSubscriberCount(extractor.getSubscriberCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setStreamCount(extractor.getStreamCount());
|
resultItem.setStreamCount(extractor.getStreamCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
|
resultItem.setThumbnails(extractor.getThumbnails());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setDescription(extractor.getDescription());
|
resultItem.setDescription(extractor.getDescription());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
resultItem.setVerified(extractor.isVerified());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
return resultItem;
|
return resultItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.schabi.newpipe.extractor.channel.tabs;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ListExtractor} of {@link InfoItem}s for tabs of channels.
|
||||||
|
*/
|
||||||
|
public abstract class ChannelTabExtractor extends ListExtractor<InfoItem> {
|
||||||
|
|
||||||
|
protected ChannelTabExtractor(@Nonnull final StreamingService service,
|
||||||
|
@Nonnull final ListLinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return getLinkHandler().getContentFilters().get(0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.schabi.newpipe.extractor.channel.tabs;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ChannelTabInfo extends ListInfo<InfoItem> {
|
||||||
|
|
||||||
|
public ChannelTabInfo(final int serviceId,
|
||||||
|
@Nonnull final ListLinkHandler linkHandler) {
|
||||||
|
super(serviceId, linkHandler, linkHandler.getContentFilters().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link ChannelTabInfo} instance from the given service and tab handler.
|
||||||
|
*
|
||||||
|
* @param service streaming service
|
||||||
|
* @param linkHandler Channel tab handler (from {@link ChannelInfo})
|
||||||
|
* @return the extracted {@link ChannelTabInfo}
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static ChannelTabInfo getInfo(@Nonnull final StreamingService service,
|
||||||
|
@Nonnull final ListLinkHandler linkHandler)
|
||||||
|
throws ExtractionException, IOException {
|
||||||
|
final ChannelTabExtractor extractor = service.getChannelTabExtractor(linkHandler);
|
||||||
|
extractor.fetchPage();
|
||||||
|
return getInfo(extractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link ChannelTabInfo} instance from a {@link ChannelTabExtractor}.
|
||||||
|
*
|
||||||
|
* @param extractor an extractor where {@code fetchPage()} was already got called on
|
||||||
|
* @return the extracted {@link ChannelTabInfo}
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static ChannelTabInfo getInfo(@Nonnull final ChannelTabExtractor extractor) {
|
||||||
|
final ChannelTabInfo info =
|
||||||
|
new ChannelTabInfo(extractor.getServiceId(), extractor.getLinkHandler());
|
||||||
|
|
||||||
|
try {
|
||||||
|
info.setOriginalUrl(extractor.getOriginalUrl());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ListExtractor.InfoItemsPage<InfoItem> page
|
||||||
|
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||||
|
info.setRelatedItems(page.getItems());
|
||||||
|
info.setNextPage(page.getNextPage());
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ListExtractor.InfoItemsPage<InfoItem> getMoreItems(
|
||||||
|
@Nonnull final StreamingService service,
|
||||||
|
@Nonnull final ListLinkHandler linkHandler,
|
||||||
|
@Nonnull final Page page) throws ExtractionException, IOException {
|
||||||
|
return service.getChannelTabExtractor(linkHandler).getPage(page);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.schabi.newpipe.extractor.channel.tabs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants of channel tabs supported by the extractor.
|
||||||
|
*/
|
||||||
|
public final class ChannelTabs {
|
||||||
|
public static final String VIDEOS = "videos";
|
||||||
|
public static final String TRACKS = "tracks";
|
||||||
|
public static final String SHORTS = "shorts";
|
||||||
|
public static final String LIVESTREAMS = "livestreams";
|
||||||
|
public static final String CHANNELS = "channels";
|
||||||
|
public static final String PLAYLISTS = "playlists";
|
||||||
|
public static final String ALBUMS = "albums";
|
||||||
|
|
||||||
|
private ChannelTabs() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,13 +2,36 @@ package org.schabi.newpipe.extractor.comments;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem> {
|
public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem> {
|
||||||
|
|
||||||
public CommentsExtractor(StreamingService service, ListLinkHandler uiHandler) {
|
public CommentsExtractor(final StreamingService service, final ListLinkHandler uiHandler) {
|
||||||
super(service, uiHandler);
|
super(service, uiHandler);
|
||||||
// TODO Auto-generated constructor stub
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||||
|
* @return <code>true</code> if the comments are disabled otherwise <code>false</code> (default)
|
||||||
|
*/
|
||||||
|
public boolean isCommentsDisabled() throws ExtractionException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the total number of comments
|
||||||
|
*/
|
||||||
|
public int getCommentsCount() throws ExtractionException {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return "Comments";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.comments;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
@ -10,62 +11,116 @@ import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
||||||
|
|
||||||
private CommentsInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
|
private CommentsInfo(
|
||||||
|
final int serviceId,
|
||||||
|
final ListLinkHandler listUrlIdHandler,
|
||||||
|
final String name) {
|
||||||
super(serviceId, listUrlIdHandler, name);
|
super(serviceId, listUrlIdHandler, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommentsInfo getInfo(String url) throws IOException, ExtractionException {
|
public static CommentsInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommentsInfo getInfo(StreamingService serviceByUrl, String url) throws ExtractionException, IOException {
|
public static CommentsInfo getInfo(final StreamingService service, final String url)
|
||||||
return getInfo(serviceByUrl.getCommentsExtractor(url));
|
throws ExtractionException, IOException {
|
||||||
|
return getInfo(service.getCommentsExtractor(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CommentsInfo getInfo(CommentsExtractor commentsExtractor) throws IOException, ExtractionException {
|
public static CommentsInfo getInfo(final CommentsExtractor commentsExtractor)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
// for services which do not have a comments extractor
|
// for services which do not have a comments extractor
|
||||||
if (null == commentsExtractor) {
|
if (commentsExtractor == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsExtractor.fetchPage();
|
commentsExtractor.fetchPage();
|
||||||
String name = commentsExtractor.getName();
|
|
||||||
int serviceId = commentsExtractor.getServiceId();
|
final String name = commentsExtractor.getName();
|
||||||
ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();
|
final int serviceId = commentsExtractor.getServiceId();
|
||||||
CommentsInfo commentsInfo = new CommentsInfo(serviceId, listUrlIdHandler, name);
|
final ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();
|
||||||
|
|
||||||
|
final CommentsInfo commentsInfo = new CommentsInfo(serviceId, listUrlIdHandler, name);
|
||||||
commentsInfo.setCommentsExtractor(commentsExtractor);
|
commentsInfo.setCommentsExtractor(commentsExtractor);
|
||||||
InfoItemsPage<CommentsInfoItem> initialCommentsPage = ExtractorHelper.getItemsPageOrLogError(commentsInfo,
|
final InfoItemsPage<CommentsInfoItem> initialCommentsPage =
|
||||||
commentsExtractor);
|
ExtractorHelper.getItemsPageOrLogError(commentsInfo, commentsExtractor);
|
||||||
|
commentsInfo.setCommentsDisabled(commentsExtractor.isCommentsDisabled());
|
||||||
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
|
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
|
||||||
commentsInfo.setNextPageUrl(initialCommentsPage.getNextPageUrl());
|
try {
|
||||||
|
commentsInfo.setCommentsCount(commentsExtractor.getCommentsCount());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
commentsInfo.addError(e);
|
||||||
|
}
|
||||||
|
commentsInfo.setNextPage(initialCommentsPage.getNextPage());
|
||||||
|
|
||||||
return commentsInfo;
|
return commentsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(CommentsInfo commentsInfo, String pageUrl)
|
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||||
throws ExtractionException, IOException {
|
final CommentsInfo commentsInfo,
|
||||||
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo, pageUrl);
|
final Page page) throws ExtractionException, IOException {
|
||||||
|
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo.getUrl(),
|
||||||
|
page);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(StreamingService service, CommentsInfo commentsInfo,
|
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||||
String pageUrl) throws IOException, ExtractionException {
|
final StreamingService service,
|
||||||
if (null == commentsInfo.getCommentsExtractor()) {
|
final CommentsInfo commentsInfo,
|
||||||
commentsInfo.setCommentsExtractor(service.getCommentsExtractor(commentsInfo.getUrl()));
|
final Page page) throws IOException, ExtractionException {
|
||||||
commentsInfo.getCommentsExtractor().fetchPage();
|
return getMoreItems(service, commentsInfo.getUrl(), page);
|
||||||
}
|
}
|
||||||
return commentsInfo.getCommentsExtractor().getPage(pageUrl);
|
|
||||||
|
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||||
|
final StreamingService service,
|
||||||
|
final String url,
|
||||||
|
final Page page) throws IOException, ExtractionException {
|
||||||
|
return service.getCommentsExtractor(url).getPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
private transient CommentsExtractor commentsExtractor;
|
private transient CommentsExtractor commentsExtractor;
|
||||||
|
private boolean commentsDisabled = false;
|
||||||
|
private int commentsCount;
|
||||||
|
|
||||||
public CommentsExtractor getCommentsExtractor() {
|
public CommentsExtractor getCommentsExtractor() {
|
||||||
return commentsExtractor;
|
return commentsExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCommentsExtractor(CommentsExtractor commentsExtractor) {
|
public void setCommentsExtractor(final CommentsExtractor commentsExtractor) {
|
||||||
this.commentsExtractor = commentsExtractor;
|
this.commentsExtractor = commentsExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if the comments are disabled otherwise {@code false} (default)
|
||||||
|
* @see CommentsExtractor#isCommentsDisabled()
|
||||||
|
*/
|
||||||
|
public boolean isCommentsDisabled() {
|
||||||
|
return commentsDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param commentsDisabled {@code true} if the comments are disabled otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public void setCommentsDisabled(final boolean commentsDisabled) {
|
||||||
|
this.commentsDisabled = commentsDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of comments.
|
||||||
|
*
|
||||||
|
* @return the total number of comments
|
||||||
|
*/
|
||||||
|
public int getCommentsCount() {
|
||||||
|
return commentsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the total number of comments.
|
||||||
|
*
|
||||||
|
* @param commentsCount the commentsCount to set.
|
||||||
|
*/
|
||||||
|
public void setCommentsCount(final int commentsCount) {
|
||||||
|
this.commentsCount = commentsCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,45 @@
|
||||||
package org.schabi.newpipe.extractor.comments;
|
package org.schabi.newpipe.extractor.comments;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class CommentsInfoItem extends InfoItem {
|
public class CommentsInfoItem extends InfoItem {
|
||||||
|
|
||||||
private String commentId;
|
private String commentId;
|
||||||
private String commentText;
|
@Nonnull
|
||||||
private String authorName;
|
private Description commentText = Description.EMPTY_DESCRIPTION;
|
||||||
private String authorThumbnail;
|
private String uploaderName;
|
||||||
private String authorEndpoint;
|
@Nonnull
|
||||||
private String textualPublishedTime;
|
private List<Image> uploaderAvatars = List.of();
|
||||||
@Nullable private DateWrapper publishedTime;
|
private String uploaderUrl;
|
||||||
|
private boolean uploaderVerified;
|
||||||
|
private String textualUploadDate;
|
||||||
|
@Nullable
|
||||||
|
private DateWrapper uploadDate;
|
||||||
private int likeCount;
|
private int likeCount;
|
||||||
|
private String textualLikeCount;
|
||||||
|
private boolean heartedByUploader;
|
||||||
|
private boolean pinned;
|
||||||
|
private int streamPosition;
|
||||||
|
private int replyCount;
|
||||||
|
@Nullable
|
||||||
|
private Page replies;
|
||||||
|
private boolean isChannelOwner;
|
||||||
|
private boolean creatorReply;
|
||||||
|
|
||||||
public CommentsInfoItem(int serviceId, String url, String name) {
|
public static final int NO_LIKE_COUNT = -1;
|
||||||
|
public static final int NO_STREAM_POSITION = -1;
|
||||||
|
|
||||||
|
public static final int UNKNOWN_REPLY_COUNT = -1;
|
||||||
|
|
||||||
|
public CommentsInfoItem(final int serviceId, final String url, final String name) {
|
||||||
super(InfoType.COMMENT, serviceId, url, name);
|
super(InfoType.COMMENT, serviceId, url, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,64 +47,151 @@ public class CommentsInfoItem extends InfoItem {
|
||||||
return commentId;
|
return commentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCommentId(String commentId) {
|
public void setCommentId(final String commentId) {
|
||||||
this.commentId = commentId;
|
this.commentId = commentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCommentText() {
|
@Nonnull
|
||||||
|
public Description getCommentText() {
|
||||||
return commentText;
|
return commentText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCommentText(String commentText) {
|
public void setCommentText(@Nonnull final Description commentText) {
|
||||||
this.commentText = commentText;
|
this.commentText = commentText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthorName() {
|
public String getUploaderName() {
|
||||||
return authorName;
|
return uploaderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAuthorName(String authorName) {
|
public void setUploaderName(final String uploaderName) {
|
||||||
this.authorName = authorName;
|
this.uploaderName = uploaderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthorThumbnail() {
|
@Nonnull
|
||||||
return authorThumbnail;
|
public List<Image> getUploaderAvatars() {
|
||||||
|
return uploaderAvatars;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAuthorThumbnail(String authorThumbnail) {
|
public void setUploaderAvatars(@Nonnull final List<Image> uploaderAvatars) {
|
||||||
this.authorThumbnail = authorThumbnail;
|
this.uploaderAvatars = uploaderAvatars;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthorEndpoint() {
|
public String getUploaderUrl() {
|
||||||
return authorEndpoint;
|
return uploaderUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAuthorEndpoint(String authorEndpoint) {
|
public void setUploaderUrl(final String uploaderUrl) {
|
||||||
this.authorEndpoint = authorEndpoint;
|
this.uploaderUrl = uploaderUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTextualPublishedTime() {
|
public String getTextualUploadDate() {
|
||||||
return textualPublishedTime;
|
return textualUploadDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTextualPublishedTime(String textualPublishedTime) {
|
public void setTextualUploadDate(final String textualUploadDate) {
|
||||||
this.textualPublishedTime = textualPublishedTime;
|
this.textualUploadDate = textualUploadDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public DateWrapper getPublishedTime() {
|
public DateWrapper getUploadDate() {
|
||||||
return publishedTime;
|
return uploadDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPublishedTime(@Nullable DateWrapper publishedTime) {
|
public void setUploadDate(@Nullable final DateWrapper uploadDate) {
|
||||||
this.publishedTime = publishedTime;
|
this.uploadDate = uploadDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the comment's like count or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is
|
||||||
|
* unavailable
|
||||||
|
*/
|
||||||
public int getLikeCount() {
|
public int getLikeCount() {
|
||||||
return likeCount;
|
return likeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLikeCount(int likeCount) {
|
public void setLikeCount(final int likeCount) {
|
||||||
this.likeCount = likeCount;
|
this.likeCount = likeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTextualLikeCount() {
|
||||||
|
return textualLikeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTextualLikeCount(final String textualLikeCount) {
|
||||||
|
this.textualLikeCount = textualLikeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeartedByUploader(final boolean isHeartedByUploader) {
|
||||||
|
this.heartedByUploader = isHeartedByUploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHeartedByUploader() {
|
||||||
|
return this.heartedByUploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPinned() {
|
||||||
|
return pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPinned(final boolean pinned) {
|
||||||
|
this.pinned = pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploaderVerified(final boolean uploaderVerified) {
|
||||||
|
this.uploaderVerified = uploaderVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUploaderVerified() {
|
||||||
|
return uploaderVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamPosition(final int streamPosition) {
|
||||||
|
this.streamPosition = streamPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the playback position of the stream to which this comment belongs.
|
||||||
|
* This is not supported by all services.
|
||||||
|
*
|
||||||
|
* @return the playback position in seconds or {@link #NO_STREAM_POSITION} if not available
|
||||||
|
*/
|
||||||
|
public int getStreamPosition() {
|
||||||
|
return streamPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReplyCount(final int replyCount) {
|
||||||
|
this.replyCount = replyCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReplyCount() {
|
||||||
|
return replyCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReplies(@Nullable final Page replies) {
|
||||||
|
this.replies = replies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Page getReplies() {
|
||||||
|
return this.replies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannelOwner(final boolean channelOwner) {
|
||||||
|
this.isChannelOwner = channelOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isChannelOwner() {
|
||||||
|
return isChannelOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setCreatorReply(final boolean creatorReply) {
|
||||||
|
this.creatorReply = creatorReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasCreatorReply() {
|
||||||
|
return creatorReply;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,152 @@
|
||||||
package org.schabi.newpipe.extractor.comments;
|
package org.schabi.newpipe.extractor.comments;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCommentsInfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface CommentsInfoItemExtractor extends InfoItemExtractor {
|
public interface CommentsInfoItemExtractor extends InfoItemExtractor {
|
||||||
String getCommentId() throws ParsingException;
|
|
||||||
String getCommentText() throws ParsingException;
|
/**
|
||||||
String getAuthorName() throws ParsingException;
|
* Return the like count of the comment,
|
||||||
String getAuthorThumbnail() throws ParsingException;
|
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable.
|
||||||
String getAuthorEndpoint() throws ParsingException;
|
*
|
||||||
String getTextualPublishedTime() throws ParsingException;
|
* <br>
|
||||||
|
* <p>
|
||||||
|
* NOTE: Currently only implemented for YT {@link
|
||||||
|
* YoutubeCommentsInfoItemExtractor#getLikeCount()}
|
||||||
|
* with limitations (only approximate like count is returned)
|
||||||
|
*
|
||||||
|
* @return the comment's like count
|
||||||
|
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable
|
||||||
|
* @see StreamExtractor#getLikeCount()
|
||||||
|
*/
|
||||||
|
default int getLikeCount() throws ParsingException {
|
||||||
|
return CommentsInfoItem.NO_LIKE_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unmodified like count given by the service
|
||||||
|
* <br>
|
||||||
|
* It may be language dependent
|
||||||
|
*/
|
||||||
|
default String getTextualLikeCount() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text of the comment
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
default Description getCommentText() throws ParsingException {
|
||||||
|
return Description.EMPTY_DESCRIPTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The upload date given by the service, unmodified
|
||||||
|
*
|
||||||
|
* @see StreamExtractor#getTextualUploadDate()
|
||||||
|
*/
|
||||||
|
default String getTextualUploadDate() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The upload date wrapped with DateWrapper class
|
||||||
|
*
|
||||||
|
* @see StreamExtractor#getUploadDate()
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
DateWrapper getPublishedTime() throws ParsingException;
|
default DateWrapper getUploadDate() throws ParsingException {
|
||||||
int getLikeCount() throws ParsingException;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default String getCommentId() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
default String getUploaderUrl() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
default String getUploaderName() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
default List<Image> getUploaderAvatars() throws ParsingException {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the comment has been hearted by the uploader
|
||||||
|
*/
|
||||||
|
default boolean isHeartedByUploader() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the comment is pinned
|
||||||
|
*/
|
||||||
|
default boolean isPinned() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the uploader is verified by the service
|
||||||
|
*/
|
||||||
|
default boolean isUploaderVerified() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The playback position of the stream to which this comment belongs.
|
||||||
|
*
|
||||||
|
* @see CommentsInfoItem#getStreamPosition()
|
||||||
|
*/
|
||||||
|
default int getStreamPosition() throws ParsingException {
|
||||||
|
return CommentsInfoItem.NO_STREAM_POSITION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The count of comment replies.
|
||||||
|
*
|
||||||
|
* @return the count of the replies
|
||||||
|
* or {@link CommentsInfoItem#UNKNOWN_REPLY_COUNT} if replies are not supported
|
||||||
|
*/
|
||||||
|
default int getReplyCount() throws ParsingException {
|
||||||
|
return CommentsInfoItem.UNKNOWN_REPLY_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The continuation page which is used to get comment replies from.
|
||||||
|
*
|
||||||
|
* @return the continuation Page for the replies, or null if replies are not supported
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
default Page getReplies() throws ParsingException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the comment was made by the channel owner.
|
||||||
|
*/
|
||||||
|
default boolean isChannelOwner() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the comment was replied to by the creator.
|
||||||
|
*/
|
||||||
|
default boolean hasCreatorReply() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,94 +1,133 @@
|
||||||
package org.schabi.newpipe.extractor.comments;
|
package org.schabi.newpipe.extractor.comments;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
public class CommentsInfoItemsCollector extends InfoItemsCollector<CommentsInfoItem, CommentsInfoItemExtractor> {
|
public final class CommentsInfoItemsCollector
|
||||||
|
extends InfoItemsCollector<CommentsInfoItem, CommentsInfoItemExtractor> {
|
||||||
|
|
||||||
public CommentsInfoItemsCollector(int serviceId) {
|
public CommentsInfoItemsCollector(final int serviceId) {
|
||||||
super(serviceId);
|
super(serviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommentsInfoItem extract(CommentsInfoItemExtractor extractor) throws ParsingException {
|
public CommentsInfoItem extract(final CommentsInfoItemExtractor extractor)
|
||||||
|
throws ParsingException {
|
||||||
// important information
|
final CommentsInfoItem resultItem = new CommentsInfoItem(
|
||||||
int serviceId = getServiceId();
|
getServiceId(), extractor.getUrl(), extractor.getName());
|
||||||
String url = extractor.getUrl();
|
|
||||||
String name = extractor.getName();
|
|
||||||
|
|
||||||
CommentsInfoItem resultItem = new CommentsInfoItem(serviceId, url, name);
|
|
||||||
|
|
||||||
// optional information
|
// optional information
|
||||||
try {
|
try {
|
||||||
resultItem.setCommentId(extractor.getCommentId());
|
resultItem.setCommentId(extractor.getCommentId());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setCommentText(extractor.getCommentText());
|
resultItem.setCommentText(extractor.getCommentText());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setAuthorName(extractor.getAuthorName());
|
resultItem.setUploaderName(extractor.getUploaderName());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setAuthorThumbnail(extractor.getAuthorThumbnail());
|
resultItem.setUploaderAvatars(extractor.getUploaderAvatars());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setAuthorEndpoint(extractor.getAuthorEndpoint());
|
resultItem.setUploaderUrl(extractor.getUploaderUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setTextualPublishedTime(extractor.getTextualPublishedTime());
|
resultItem.setTextualUploadDate(extractor.getTextualUploadDate());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setPublishedTime(extractor.getPublishedTime());
|
resultItem.setUploadDate(extractor.getUploadDate());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setLikeCount(extractor.getLikeCount());
|
resultItem.setLikeCount(extractor.getLikeCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
|
resultItem.setTextualLikeCount(extractor.getTextualLikeCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
resultItem.setThumbnails(extractor.getThumbnails());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setHeartedByUploader(extractor.isHeartedByUploader());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setPinned(extractor.isPinned());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setStreamPosition(extractor.getStreamPosition());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setReplyCount(extractor.getReplyCount());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setReplies(extractor.getReplies());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setChannelOwner(extractor.isChannelOwner());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultItem.setCreatorReply(extractor.hasCreatorReply());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return resultItem;
|
return resultItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void commit(CommentsInfoItemExtractor extractor) {
|
public void commit(final CommentsInfoItemExtractor extractor) {
|
||||||
try {
|
try {
|
||||||
addItem(extract(extractor));
|
addItem(extract(extractor));
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CommentsInfoItem> getCommentsInfoItemList() {
|
public List<CommentsInfoItem> getCommentsInfoItemList() {
|
||||||
List<CommentsInfoItem> siiList = new Vector<>();
|
return new ArrayList<>(super.getItems());
|
||||||
for (InfoItem ii : super.getItems()) {
|
|
||||||
if (ii instanceof CommentsInfoItem) {
|
|
||||||
siiList.add((CommentsInfoItem) ii);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return siiList;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -19,13 +21,14 @@ public abstract class Downloader {
|
||||||
/**
|
/**
|
||||||
* Do a GET request to get the resource that the url is pointing to.<br>
|
* Do a GET request to get the resource that the url is pointing to.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* This method calls {@link #get(String, Map, Localization)} with the default preferred localization. It should only be
|
* This method calls {@link #get(String, Map, Localization)} with the default preferred
|
||||||
* used when the resource that will be fetched won't be affected by the localization.
|
* localization. It should only be used when the resource that will be fetched won't be affected
|
||||||
|
* by the localization.
|
||||||
*
|
*
|
||||||
* @param url the URL that is pointing to the wanted resource
|
* @param url the URL that is pointing to the wanted resource
|
||||||
* @return the result of the GET request
|
* @return the result of the GET request
|
||||||
*/
|
*/
|
||||||
public Response get(String url) throws IOException, ReCaptchaException {
|
public Response get(final String url) throws IOException, ReCaptchaException {
|
||||||
return get(url, null, NewPipe.getPreferredLocalization());
|
return get(url, null, NewPipe.getPreferredLocalization());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +41,8 @@ public abstract class Downloader {
|
||||||
* @param localization the source of the value of the {@code Accept-Language} header
|
* @param localization the source of the value of the {@code Accept-Language} header
|
||||||
* @return the result of the GET request
|
* @return the result of the GET request
|
||||||
*/
|
*/
|
||||||
public Response get(String url, @Nullable Localization localization) throws IOException, ReCaptchaException {
|
public Response get(final String url, final Localization localization)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
return get(url, null, localization);
|
return get(url, null, localization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +54,8 @@ public abstract class Downloader {
|
||||||
* Any default headers <b>should</b> be overridden by these.
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
* @return the result of the GET request
|
* @return the result of the GET request
|
||||||
*/
|
*/
|
||||||
public Response get(String url, @Nullable Map<String, List<String>> headers) throws IOException, ReCaptchaException {
|
public Response get(final String url, @Nullable final Map<String, List<String>> headers)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
return get(url, headers, NewPipe.getPreferredLocalization());
|
return get(url, headers, NewPipe.getPreferredLocalization());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +70,9 @@ public abstract class Downloader {
|
||||||
* @param localization the source of the value of the {@code Accept-Language} header
|
* @param localization the source of the value of the {@code Accept-Language} header
|
||||||
* @return the result of the GET request
|
* @return the result of the GET request
|
||||||
*/
|
*/
|
||||||
public Response get(String url, @Nullable Map<String, List<String>> headers, @Nullable Localization localization)
|
public Response get(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
final Localization localization)
|
||||||
throws IOException, ReCaptchaException {
|
throws IOException, ReCaptchaException {
|
||||||
return execute(Request.newBuilder()
|
return execute(Request.newBuilder()
|
||||||
.get(url)
|
.get(url)
|
||||||
|
@ -80,7 +87,7 @@ public abstract class Downloader {
|
||||||
* @param url the URL that is pointing to the wanted resource
|
* @param url the URL that is pointing to the wanted resource
|
||||||
* @return the result of the HEAD request
|
* @return the result of the HEAD request
|
||||||
*/
|
*/
|
||||||
public Response head(String url) throws IOException, ReCaptchaException {
|
public Response head(final String url) throws IOException, ReCaptchaException {
|
||||||
return head(url, null);
|
return head(url, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +99,7 @@ public abstract class Downloader {
|
||||||
* Any default headers <b>should</b> be overridden by these.
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
* @return the result of the HEAD request
|
* @return the result of the HEAD request
|
||||||
*/
|
*/
|
||||||
public Response head(String url, @Nullable Map<String, List<String>> headers)
|
public Response head(final String url, @Nullable final Map<String, List<String>> headers)
|
||||||
throws IOException, ReCaptchaException {
|
throws IOException, ReCaptchaException {
|
||||||
return execute(Request.newBuilder()
|
return execute(Request.newBuilder()
|
||||||
.head(url)
|
.head(url)
|
||||||
|
@ -107,9 +114,11 @@ public abstract class Downloader {
|
||||||
* @param headers a list of headers that will be used in the request.
|
* @param headers a list of headers that will be used in the request.
|
||||||
* Any default headers <b>should</b> be overridden by these.
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
* @param dataToSend byte array that will be sent when doing the request.
|
* @param dataToSend byte array that will be sent when doing the request.
|
||||||
* @return the result of the GET request
|
* @return the result of the POST request
|
||||||
*/
|
*/
|
||||||
public Response post(String url, @Nullable Map<String, List<String>> headers, @Nullable byte[] dataToSend)
|
public Response post(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
@Nullable final byte[] dataToSend)
|
||||||
throws IOException, ReCaptchaException {
|
throws IOException, ReCaptchaException {
|
||||||
return post(url, headers, dataToSend, NewPipe.getPreferredLocalization());
|
return post(url, headers, dataToSend, NewPipe.getPreferredLocalization());
|
||||||
}
|
}
|
||||||
|
@ -124,9 +133,12 @@ public abstract class Downloader {
|
||||||
* Any default headers <b>should</b> be overridden by these.
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
* @param dataToSend byte array that will be sent when doing the request.
|
* @param dataToSend byte array that will be sent when doing the request.
|
||||||
* @param localization the source of the value of the {@code Accept-Language} header
|
* @param localization the source of the value of the {@code Accept-Language} header
|
||||||
* @return the result of the GET request
|
* @return the result of the POST request
|
||||||
*/
|
*/
|
||||||
public Response post(String url, @Nullable Map<String, List<String>> headers, @Nullable byte[] dataToSend, @Nullable Localization localization)
|
public Response post(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
@Nullable final byte[] dataToSend,
|
||||||
|
final Localization localization)
|
||||||
throws IOException, ReCaptchaException {
|
throws IOException, ReCaptchaException {
|
||||||
return execute(Request.newBuilder()
|
return execute(Request.newBuilder()
|
||||||
.post(url, dataToSend)
|
.post(url, dataToSend)
|
||||||
|
@ -135,10 +147,100 @@ public abstract class Downloader {
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient method to send a POST request using the specified value of the
|
||||||
|
* {@code Content-Type} header with a given {@link Localization}.
|
||||||
|
*
|
||||||
|
* @param url the URL that is pointing to the wanted resource
|
||||||
|
* @param headers a list of headers that will be used in the request.
|
||||||
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
|
* @param dataToSend byte array that will be sent when doing the request.
|
||||||
|
* @param localization the source of the value of the {@code Accept-Language} header
|
||||||
|
* @param contentType the mime type of the body sent, which will be set as the value of the
|
||||||
|
* {@code Content-Type} header
|
||||||
|
* @return the result of the POST request
|
||||||
|
* @see #post(String, Map, byte[], Localization)
|
||||||
|
*/
|
||||||
|
public Response postWithContentType(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
@Nullable final byte[] dataToSend,
|
||||||
|
final Localization localization,
|
||||||
|
final String contentType)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
|
final Map<String, List<String>> actualHeaders = new HashMap<>();
|
||||||
|
if (headers != null) {
|
||||||
|
actualHeaders.putAll(headers);
|
||||||
|
}
|
||||||
|
actualHeaders.put("Content-Type", Collections.singletonList(contentType));
|
||||||
|
return post(url, actualHeaders, dataToSend, localization);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient method to send a POST request using the specified value of the
|
||||||
|
* {@code Content-Type} header.
|
||||||
|
*
|
||||||
|
* @param url the URL that is pointing to the wanted resource
|
||||||
|
* @param headers a list of headers that will be used in the request.
|
||||||
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
|
* @param dataToSend byte array that will be sent when doing the request.
|
||||||
|
* @param contentType the mime type of the body sent, which will be set as the value of the
|
||||||
|
* {@code Content-Type} header
|
||||||
|
* @return the result of the POST request
|
||||||
|
* @see #post(String, Map, byte[], Localization)
|
||||||
|
*/
|
||||||
|
public Response postWithContentType(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
@Nullable final byte[] dataToSend,
|
||||||
|
final String contentType)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
|
return postWithContentType(url, headers, dataToSend, NewPipe.getPreferredLocalization(),
|
||||||
|
contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient method to send a POST request the JSON mime type as the value of the
|
||||||
|
* {@code Content-Type} header with a given {@link Localization}.
|
||||||
|
*
|
||||||
|
* @param url the URL that is pointing to the wanted resource
|
||||||
|
* @param headers a list of headers that will be used in the request.
|
||||||
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
|
* @param dataToSend byte array that will be sent when doing the request.
|
||||||
|
* @param localization the source of the value of the {@code Accept-Language} header
|
||||||
|
* @return the result of the POST request
|
||||||
|
* @see #post(String, Map, byte[], Localization)
|
||||||
|
*/
|
||||||
|
public Response postWithContentTypeJson(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
@Nullable final byte[] dataToSend,
|
||||||
|
final Localization localization)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
|
return postWithContentType(url, headers, dataToSend, localization, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient method to send a POST request the JSON mime type as the value of the
|
||||||
|
* {@code Content-Type} header.
|
||||||
|
*
|
||||||
|
* @param url the URL that is pointing to the wanted resource
|
||||||
|
* @param headers a list of headers that will be used in the request.
|
||||||
|
* Any default headers <b>should</b> be overridden by these.
|
||||||
|
* @param dataToSend byte array that will be sent when doing the request.
|
||||||
|
* @return the result of the POST request
|
||||||
|
* @see #post(String, Map, byte[], Localization)
|
||||||
|
*/
|
||||||
|
public Response postWithContentTypeJson(final String url,
|
||||||
|
@Nullable final Map<String, List<String>> headers,
|
||||||
|
@Nullable final byte[] dataToSend)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
|
return postWithContentTypeJson(url, headers, dataToSend,
|
||||||
|
NewPipe.getPreferredLocalization());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a request using the specified {@link Request} object.
|
* Do a request using the specified {@link Request} object.
|
||||||
*
|
*
|
||||||
* @return the result of the request
|
* @return the result of the request
|
||||||
*/
|
*/
|
||||||
public abstract Response execute(@Nonnull Request request) throws IOException, ReCaptchaException;
|
public abstract Response execute(@Nonnull Request request)
|
||||||
|
throws IOException, ReCaptchaException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,42 +2,53 @@ package org.schabi.newpipe.extractor.downloader;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that holds request information used when {@link Downloader#execute(Request) executing} a request.
|
* An object that holds request information used when {@link Downloader#execute(Request) executing}
|
||||||
|
* a request.
|
||||||
*/
|
*/
|
||||||
public class Request {
|
public class Request {
|
||||||
private final String httpMethod;
|
private final String httpMethod;
|
||||||
private final String url;
|
private final String url;
|
||||||
private final Map<String, List<String>> headers;
|
private final Map<String, List<String>> headers;
|
||||||
@Nullable private final byte[] dataToSend;
|
@Nullable
|
||||||
@Nullable private final Localization localization;
|
private final byte[] dataToSend;
|
||||||
|
@Nullable
|
||||||
|
private final Localization localization;
|
||||||
|
|
||||||
public Request(String httpMethod, String url, Map<String, List<String>> headers, @Nullable byte[] dataToSend,
|
public Request(final String httpMethod,
|
||||||
@Nullable Localization localization, boolean automaticLocalizationHeader) {
|
final String url,
|
||||||
if (httpMethod == null) throw new IllegalArgumentException("Request's httpMethod is null");
|
@Nullable final Map<String, List<String>> headers,
|
||||||
if (url == null) throw new IllegalArgumentException("Request's url is null");
|
@Nullable final byte[] dataToSend,
|
||||||
|
@Nullable final Localization localization,
|
||||||
this.httpMethod = httpMethod;
|
final boolean automaticLocalizationHeader) {
|
||||||
this.url = url;
|
this.httpMethod = Objects.requireNonNull(httpMethod, "Request's httpMethod is null");
|
||||||
|
this.url = Objects.requireNonNull(url, "Request's url is null");
|
||||||
this.dataToSend = dataToSend;
|
this.dataToSend = dataToSend;
|
||||||
this.localization = localization;
|
this.localization = localization;
|
||||||
|
|
||||||
Map<String, List<String>> headersToSet = null;
|
final Map<String, List<String>> actualHeaders = new LinkedHashMap<>();
|
||||||
if (headers == null) headers = Collections.emptyMap();
|
if (headers != null) {
|
||||||
|
actualHeaders.putAll(headers);
|
||||||
|
}
|
||||||
if (automaticLocalizationHeader && localization != null) {
|
if (automaticLocalizationHeader && localization != null) {
|
||||||
headersToSet = new LinkedHashMap<>(headersFromLocalization(localization));
|
actualHeaders.putAll(getHeadersFromLocalization(localization));
|
||||||
headersToSet.putAll(headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.headers = Collections.unmodifiableMap(headersToSet == null ? headers : headersToSet);
|
this.headers = Collections.unmodifiableMap(actualHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Request(Builder builder) {
|
private Request(final Builder builder) {
|
||||||
this(builder.httpMethod, builder.url, builder.headers, builder.dataToSend,
|
this(builder.httpMethod, builder.url, builder.headers, builder.dataToSend,
|
||||||
builder.localization, builder.automaticLocalizationHeader);
|
builder.localization, builder.automaticLocalizationHeader);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +91,7 @@ public class Request {
|
||||||
* A localization object that should be used when executing a request.<br>
|
* A localization object that should be used when executing a request.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* Usually the {@code Accept-Language} will be set to this value (a helper
|
* Usually the {@code Accept-Language} will be set to this value (a helper
|
||||||
* method to do this easily: {@link Request#headersFromLocalization(Localization)}).
|
* method to do this easily: {@link Request#getHeadersFromLocalization(Localization)}).
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public Localization localization() {
|
public Localization localization() {
|
||||||
|
@ -94,7 +105,7 @@ public class Request {
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private String httpMethod;
|
private String httpMethod;
|
||||||
private String url;
|
private String url;
|
||||||
private Map<String, List<String>> headers = new LinkedHashMap<>();
|
private final Map<String, List<String>> headers = new LinkedHashMap<>();
|
||||||
private byte[] dataToSend;
|
private byte[] dataToSend;
|
||||||
private Localization localization;
|
private Localization localization;
|
||||||
private boolean automaticLocalizationHeader = true;
|
private boolean automaticLocalizationHeader = true;
|
||||||
|
@ -105,30 +116,29 @@ public class Request {
|
||||||
/**
|
/**
|
||||||
* A http method (i.e. {@code GET, POST, HEAD}).
|
* A http method (i.e. {@code GET, POST, HEAD}).
|
||||||
*/
|
*/
|
||||||
public Builder httpMethod(String httpMethod) {
|
public Builder httpMethod(final String httpMethodToSet) {
|
||||||
this.httpMethod = httpMethod;
|
this.httpMethod = httpMethodToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL that is pointing to the wanted resource.
|
* The URL that is pointing to the wanted resource.
|
||||||
*/
|
*/
|
||||||
public Builder url(String url) {
|
public Builder url(final String urlToSet) {
|
||||||
this.url = url;
|
this.url = urlToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of headers that will be used in the request.<br>
|
* A list of headers that will be used in the request.<br>
|
||||||
* Any default headers that the implementation may have, <b>should</b> be overridden by these.
|
* Any default headers that the implementation may have, <b>should</b> be overridden by
|
||||||
|
* these.
|
||||||
*/
|
*/
|
||||||
public Builder headers(@Nullable Map<String, List<String>> headers) {
|
public Builder headers(@Nullable final Map<String, List<String>> headersToSet) {
|
||||||
if (headers == null) {
|
|
||||||
this.headers.clear();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
this.headers.clear();
|
this.headers.clear();
|
||||||
this.headers.putAll(headers);
|
if (headersToSet != null) {
|
||||||
|
this.headers.putAll(headersToSet);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,8 +149,8 @@ public class Request {
|
||||||
* The implementation should make note of some recommended headers
|
* The implementation should make note of some recommended headers
|
||||||
* (for example, {@code Content-Length} in a post request).
|
* (for example, {@code Content-Length} in a post request).
|
||||||
*/
|
*/
|
||||||
public Builder dataToSend(byte[] dataToSend) {
|
public Builder dataToSend(final byte[] dataToSendToSet) {
|
||||||
this.dataToSend = dataToSend;
|
this.dataToSend = dataToSendToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,18 +158,18 @@ public class Request {
|
||||||
* A localization object that should be used when executing a request.<br>
|
* A localization object that should be used when executing a request.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* Usually the {@code Accept-Language} will be set to this value (a helper
|
* Usually the {@code Accept-Language} will be set to this value (a helper
|
||||||
* method to do this easily: {@link Request#headersFromLocalization(Localization)}).
|
* method to do this easily: {@link Request#getHeadersFromLocalization(Localization)}).
|
||||||
*/
|
*/
|
||||||
public Builder localization(Localization localization) {
|
public Builder localization(final Localization localizationToSet) {
|
||||||
this.localization = localization;
|
this.localization = localizationToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If localization headers should automatically be included in the request.
|
* If localization headers should automatically be included in the request.
|
||||||
*/
|
*/
|
||||||
public Builder automaticLocalizationHeader(boolean automaticLocalizationHeader) {
|
public Builder automaticLocalizationHeader(final boolean automaticLocalizationHeaderToSet) {
|
||||||
this.automaticLocalizationHeader = automaticLocalizationHeader;
|
this.automaticLocalizationHeader = automaticLocalizationHeaderToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,22 +182,22 @@ public class Request {
|
||||||
// Http Methods Utils
|
// Http Methods Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public Builder get(String url) {
|
public Builder get(final String urlToSet) {
|
||||||
this.httpMethod = "GET";
|
this.httpMethod = "GET";
|
||||||
this.url = url;
|
this.url = urlToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder head(String url) {
|
public Builder head(final String urlToSet) {
|
||||||
this.httpMethod = "HEAD";
|
this.httpMethod = "HEAD";
|
||||||
this.url = url;
|
this.url = urlToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder post(String url, @Nullable byte[] dataToSend) {
|
public Builder post(final String urlToSet, @Nullable final byte[] dataToSendToSet) {
|
||||||
this.httpMethod = "POST";
|
this.httpMethod = "POST";
|
||||||
this.url = url;
|
this.url = urlToSet;
|
||||||
this.dataToSend = dataToSend;
|
this.dataToSend = dataToSendToSet;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,13 +205,13 @@ public class Request {
|
||||||
// Additional Headers Utils
|
// Additional Headers Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public Builder setHeaders(String headerName, List<String> headerValueList) {
|
public Builder setHeaders(final String headerName, final List<String> headerValueList) {
|
||||||
this.headers.remove(headerName);
|
this.headers.remove(headerName);
|
||||||
this.headers.put(headerName, headerValueList);
|
this.headers.put(headerName, headerValueList);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder addHeaders(String headerName, List<String> headerValueList) {
|
public Builder addHeaders(final String headerName, final List<String> headerValueList) {
|
||||||
@Nullable List<String> currentHeaderValueList = this.headers.get(headerName);
|
@Nullable List<String> currentHeaderValueList = this.headers.get(headerName);
|
||||||
if (currentHeaderValueList == null) {
|
if (currentHeaderValueList == null) {
|
||||||
currentHeaderValueList = new ArrayList<>();
|
currentHeaderValueList = new ArrayList<>();
|
||||||
|
@ -212,11 +222,11 @@ public class Request {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder setHeader(String headerName, String headerValue) {
|
public Builder setHeader(final String headerName, final String headerValue) {
|
||||||
return setHeaders(headerName, Collections.singletonList(headerValue));
|
return setHeaders(headerName, Collections.singletonList(headerValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder addHeader(String headerName, String headerValue) {
|
public Builder addHeader(final String headerName, final String headerValue) {
|
||||||
return addHeaders(headerName, Collections.singletonList(headerValue));
|
return addHeaders(headerName, Collections.singletonList(headerValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,17 +238,43 @@ public class Request {
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public static Map<String, List<String>> headersFromLocalization(@Nullable Localization localization) {
|
public static Map<String, List<String>> getHeadersFromLocalization(
|
||||||
if (localization == null) return Collections.emptyMap();
|
@Nullable final Localization localization) {
|
||||||
|
if (localization == null) {
|
||||||
final Map<String, List<String>> headers = new LinkedHashMap<>();
|
return Collections.emptyMap();
|
||||||
if (!localization.getCountryCode().isEmpty()) {
|
|
||||||
headers.put("Accept-Language", Collections.singletonList(localization.getLocalizationCode() +
|
|
||||||
", " + localization.getLanguageCode() + ";q=0.9"));
|
|
||||||
} else {
|
|
||||||
headers.put("Accept-Language", Collections.singletonList(localization.getLanguageCode()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
final String languageCode = localization.getLanguageCode();
|
||||||
|
final List<String> languageCodeList = Collections.singletonList(
|
||||||
|
localization.getCountryCode().isEmpty() ? languageCode
|
||||||
|
: localization.getLocalizationCode() + ", " + languageCode + ";q=0.9");
|
||||||
|
return Collections.singletonMap("Accept-Language", languageCodeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Generated
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Request request = (Request) o;
|
||||||
|
return httpMethod.equals(request.httpMethod)
|
||||||
|
&& url.equals(request.url)
|
||||||
|
&& headers.equals(request.headers)
|
||||||
|
&& Arrays.equals(dataToSend, request.dataToSend)
|
||||||
|
&& Objects.equals(localization, request.localization);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = Objects.hash(httpMethod, url, headers, localization);
|
||||||
|
result = 31 * result + Arrays.hashCode(dataToSend);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,14 @@ public class Response {
|
||||||
|
|
||||||
private final String latestUrl;
|
private final String latestUrl;
|
||||||
|
|
||||||
public Response(int responseCode, String responseMessage, Map<String, List<String>> responseHeaders,
|
public Response(final int responseCode,
|
||||||
@Nullable String responseBody, @Nullable String latestUrl) {
|
final String responseMessage,
|
||||||
|
@Nullable final Map<String, List<String>> responseHeaders,
|
||||||
|
@Nullable final String responseBody,
|
||||||
|
@Nullable final String latestUrl) {
|
||||||
this.responseCode = responseCode;
|
this.responseCode = responseCode;
|
||||||
this.responseMessage = responseMessage;
|
this.responseMessage = responseMessage;
|
||||||
this.responseHeaders = responseHeaders != null ? responseHeaders : Collections.<String, List<String>>emptyMap();
|
this.responseHeaders = responseHeaders == null ? Collections.emptyMap() : responseHeaders;
|
||||||
|
|
||||||
this.responseBody = responseBody == null ? "" : responseBody;
|
this.responseBody = responseBody == null ? "" : responseBody;
|
||||||
this.latestUrl = latestUrl;
|
this.latestUrl = latestUrl;
|
||||||
|
@ -60,19 +63,18 @@ public class Response {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For easy access to some header value that (usually) don't repeat itself.
|
* For easy access to some header value that (usually) don't repeat itself.
|
||||||
* <p>For getting all the values associated to the header, use {@link #responseHeaders()} (e.g. {@code Set-Cookie}).
|
* <p>For getting all the values associated to the header, use {@link #responseHeaders()} (e.g.
|
||||||
|
* {@code Set-Cookie}).
|
||||||
*
|
*
|
||||||
* @param name the name of the header
|
* @param name the name of the header
|
||||||
* @return the first value assigned to this header
|
* @return the first value assigned to this header
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getHeader(String name) {
|
public String getHeader(final String name) {
|
||||||
for (Map.Entry<String, List<String>> headerEntry : responseHeaders.entrySet()) {
|
for (final Map.Entry<String, List<String>> headerEntry : responseHeaders.entrySet()) {
|
||||||
final String key = headerEntry.getKey();
|
final String key = headerEntry.getKey();
|
||||||
if (key != null && key.equalsIgnoreCase(name)) {
|
if (key != null && key.equalsIgnoreCase(name) && !headerEntry.getValue().isEmpty()) {
|
||||||
if (headerEntry.getValue().size() > 0) {
|
return headerEntry.getValue().get(0);
|
||||||
return headerEntry.getValue().get(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class AccountTerminatedException extends ContentNotAvailableException {
|
||||||
|
|
||||||
|
private Reason reason = Reason.UNKNOWN;
|
||||||
|
|
||||||
|
public AccountTerminatedException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTerminatedException(final String message, final Reason reason) {
|
||||||
|
super(message);
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountTerminatedException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason for the violation. There should also be more info in the exception's message.
|
||||||
|
*/
|
||||||
|
public Reason getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Reason {
|
||||||
|
UNKNOWN,
|
||||||
|
VIOLATION
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class AgeRestrictedContentException extends ContentNotAvailableException {
|
||||||
|
public AgeRestrictedContentException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgeRestrictedContentException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
package org.schabi.newpipe.extractor.exceptions;
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
public class ContentNotAvailableException extends ParsingException {
|
public class ContentNotAvailableException extends ParsingException {
|
||||||
public ContentNotAvailableException(String message) {
|
public ContentNotAvailableException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentNotAvailableException(String message, Throwable cause) {
|
public ContentNotAvailableException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class ContentNotSupportedException extends ParsingException {
|
||||||
|
public ContentNotSupportedException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentNotSupportedException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,33 +3,33 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 30.01.16.
|
* Created by Christian Schabesberger on 30.01.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ExtractionException.java is part of NewPipe.
|
* ExtractionException.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ExtractionException extends Exception {
|
public class ExtractionException extends Exception {
|
||||||
public ExtractionException(String message) {
|
public ExtractionException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtractionException(Throwable cause) {
|
public ExtractionException(final Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtractionException(String message, Throwable cause) {
|
public ExtractionException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,29 +3,29 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.09.16.
|
* Created by Christian Schabesberger on 12.09.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* FoundAdException.java is part of NewPipe.
|
* FoundAdException.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class FoundAdException extends ParsingException {
|
public class FoundAdException extends ParsingException {
|
||||||
public FoundAdException(String message) {
|
public FoundAdException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FoundAdException(String message, Throwable cause) {
|
public FoundAdException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class GeographicRestrictionException extends ContentNotAvailableException {
|
||||||
|
public GeographicRestrictionException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeographicRestrictionException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class PaidContentException extends ContentNotAvailableException {
|
||||||
|
public PaidContentException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaidContentException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,30 +3,30 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 31.01.16.
|
* Created by Christian Schabesberger on 31.01.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ParsingException.java is part of NewPipe.
|
* ParsingException.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
public class ParsingException extends ExtractionException {
|
public class ParsingException extends ExtractionException {
|
||||||
public ParsingException(String message) {
|
public ParsingException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParsingException(String message, Throwable cause) {
|
public ParsingException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class PrivateContentException extends ContentNotAvailableException {
|
||||||
|
public PrivateContentException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateContentException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,27 +3,27 @@ package org.schabi.newpipe.extractor.exceptions;
|
||||||
/*
|
/*
|
||||||
* Created by beneth <bmauduit@beneth.fr> on 07.12.16.
|
* Created by beneth <bmauduit@beneth.fr> on 07.12.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* ReCaptchaException.java is part of NewPipe.
|
* ReCaptchaException.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ReCaptchaException extends ExtractionException {
|
public class ReCaptchaException extends ExtractionException {
|
||||||
private String url;
|
private final String url;
|
||||||
|
|
||||||
public ReCaptchaException(String message, String url) {
|
public ReCaptchaException(final String message, final String url) {
|
||||||
super(message);
|
super(message);
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class SoundCloudGoPlusContentException extends ContentNotAvailableException {
|
||||||
|
public SoundCloudGoPlusContentException() {
|
||||||
|
super("This track is a SoundCloud Go+ track");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundCloudGoPlusContentException(final Throwable cause) {
|
||||||
|
super("This track is a SoundCloud Go+ track", cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public final class UnsupportedTabException extends UnsupportedOperationException {
|
||||||
|
public UnsupportedTabException(final String unsupportedTab) {
|
||||||
|
super("Unsupported tab " + unsupportedTab);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.extractor.exceptions;
|
||||||
|
|
||||||
|
public class YoutubeMusicPremiumContentException extends ContentNotAvailableException {
|
||||||
|
public YoutubeMusicPremiumContentException() {
|
||||||
|
super("This video is a YouTube Music Premium video");
|
||||||
|
}
|
||||||
|
|
||||||
|
public YoutubeMusicPremiumContentException(final Throwable cause) {
|
||||||
|
super("This video is a YouTube Music Premium video", cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
* YouTube is an example of a service that has this alternative available.
|
* YouTube is an example of a service that has this alternative available.
|
||||||
*/
|
*/
|
||||||
public abstract class FeedExtractor extends ListExtractor<StreamInfoItem> {
|
public abstract class FeedExtractor extends ListExtractor<StreamInfoItem> {
|
||||||
public FeedExtractor(StreamingService service, ListLinkHandler listLinkHandler) {
|
public FeedExtractor(final StreamingService service, final ListLinkHandler listLinkHandler) {
|
||||||
super(service, listLinkHandler);
|
super(service, listLinkHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,26 +13,35 @@ import java.util.List;
|
||||||
|
|
||||||
public class FeedInfo extends ListInfo<StreamInfoItem> {
|
public class FeedInfo extends ListInfo<StreamInfoItem> {
|
||||||
|
|
||||||
public FeedInfo(int serviceId, String id, String url, String originalUrl, String name, List<String> contentFilter, String sortFilter) {
|
public FeedInfo(final int serviceId,
|
||||||
|
final String id,
|
||||||
|
final String url,
|
||||||
|
final String originalUrl,
|
||||||
|
final String name,
|
||||||
|
final List<String> contentFilter,
|
||||||
|
final String sortFilter) {
|
||||||
super(serviceId, id, url, originalUrl, name, contentFilter, sortFilter);
|
super(serviceId, id, url, originalUrl, name, contentFilter, sortFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FeedInfo getInfo(String url) throws IOException, ExtractionException {
|
public static FeedInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FeedInfo getInfo(StreamingService service, String url) throws IOException, ExtractionException {
|
public static FeedInfo getInfo(final StreamingService service, final String url)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
final FeedExtractor extractor = service.getFeedExtractor(url);
|
final FeedExtractor extractor = service.getFeedExtractor(url);
|
||||||
|
|
||||||
if (extractor == null) {
|
if (extractor == null) {
|
||||||
throw new IllegalArgumentException("Service \"" + service.getServiceInfo().getName() + "\" doesn't support FeedExtractor.");
|
throw new IllegalArgumentException("Service \"" + service.getServiceInfo().getName()
|
||||||
|
+ "\" doesn't support FeedExtractor.");
|
||||||
}
|
}
|
||||||
|
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
return getInfo(extractor);
|
return getInfo(extractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FeedInfo getInfo(FeedExtractor extractor) throws IOException, ExtractionException {
|
public static FeedInfo getInfo(final FeedExtractor extractor)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
|
|
||||||
final int serviceId = extractor.getServiceId();
|
final int serviceId = extractor.getServiceId();
|
||||||
|
@ -43,9 +52,10 @@ public class FeedInfo extends ListInfo<StreamInfoItem> {
|
||||||
|
|
||||||
final FeedInfo info = new FeedInfo(serviceId, id, url, originalUrl, name, null, null);
|
final FeedInfo info = new FeedInfo(serviceId, id, url, originalUrl, name, null, null);
|
||||||
|
|
||||||
final InfoItemsPage<StreamInfoItem> itemsPage = ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
final InfoItemsPage<StreamInfoItem> itemsPage
|
||||||
|
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||||
info.setRelatedItems(itemsPage.getItems());
|
info.setRelatedItems(itemsPage.getItems());
|
||||||
info.setNextPageUrl(itemsPage.getNextPageUrl());
|
info.setNextPage(itemsPage.getNextPage());
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,21 @@ package org.schabi.newpipe.extractor.kiosk;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.08.17.
|
* Created by Christian Schabesberger on 12.08.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* KioskExtractor.java is part of NewPipe.
|
* KioskExtractor.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
@ -31,9 +31,9 @@ import javax.annotation.Nonnull;
|
||||||
public abstract class KioskExtractor<T extends InfoItem> extends ListExtractor<T> {
|
public abstract class KioskExtractor<T extends InfoItem> extends ListExtractor<T> {
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
||||||
public KioskExtractor(StreamingService streamingService,
|
public KioskExtractor(final StreamingService streamingService,
|
||||||
ListLinkHandler linkHandler,
|
final ListLinkHandler linkHandler,
|
||||||
String kioskId) {
|
final String kioskId) {
|
||||||
super(streamingService, linkHandler);
|
super(streamingService, linkHandler);
|
||||||
this.id = kioskId;
|
this.id = kioskId;
|
||||||
}
|
}
|
||||||
|
@ -45,12 +45,11 @@ public abstract class KioskExtractor<T extends InfoItem> extends ListExtractor<T
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id should be the name of the kiosk, tho Id is used for identifing it in the frontend,
|
* Id should be the name of the kiosk, tho Id is used for identifying it in the frontend,
|
||||||
* so id should be kept in english.
|
* so id should be kept in english.
|
||||||
* In order to get the name of the kiosk in the desired language we have to
|
* In order to get the name of the kiosk in the desired language we have to
|
||||||
* crawl if from the website.
|
* crawl if from the website.
|
||||||
* @return the tranlsated version of id
|
* @return the translated version of id
|
||||||
* @throws ParsingException
|
|
||||||
*/
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -3,57 +3,53 @@ package org.schabi.newpipe.extractor.kiosk;
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.08.17.
|
* Created by Christian Schabesberger on 12.08.17.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2017 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* KioskInfo.java is part of NewPipe.
|
* KioskInfo.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class KioskInfo extends ListInfo<StreamInfoItem> {
|
public final class KioskInfo extends ListInfo<StreamInfoItem> {
|
||||||
|
private KioskInfo(final int serviceId, final ListLinkHandler linkHandler, final String name) {
|
||||||
private KioskInfo(int serviceId, ListLinkHandler linkHandler, String name) throws ParsingException {
|
|
||||||
super(serviceId, linkHandler, name);
|
super(serviceId, linkHandler, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ListExtractor.InfoItemsPage<StreamInfoItem> getMoreItems(StreamingService service,
|
public static ListExtractor.InfoItemsPage<StreamInfoItem> getMoreItems(
|
||||||
String url,
|
final StreamingService service, final String url, final Page page)
|
||||||
String pageUrl)
|
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
KioskList kl = service.getKioskList();
|
return service.getKioskList().getExtractorByUrl(url, page).getPage(page);
|
||||||
KioskExtractor extractor = kl.getExtractorByUrl(url, pageUrl);
|
|
||||||
return extractor.getPage(pageUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KioskInfo getInfo(String url) throws IOException, ExtractionException {
|
public static KioskInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KioskInfo getInfo(StreamingService service, String url) throws IOException, ExtractionException {
|
public static KioskInfo getInfo(final StreamingService service, final String url)
|
||||||
KioskList kl = service.getKioskList();
|
throws IOException, ExtractionException {
|
||||||
KioskExtractor extractor = kl.getExtractorByUrl(url, null);
|
final KioskExtractor extractor = service.getKioskList().getExtractorByUrl(url, null);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
return getInfo(extractor);
|
return getInfo(extractor);
|
||||||
}
|
}
|
||||||
|
@ -63,15 +59,16 @@ public class KioskInfo extends ListInfo<StreamInfoItem> {
|
||||||
*
|
*
|
||||||
* @param extractor an extractor where fetchPage() was already got called on.
|
* @param extractor an extractor where fetchPage() was already got called on.
|
||||||
*/
|
*/
|
||||||
public static KioskInfo getInfo(KioskExtractor extractor) throws ExtractionException {
|
public static KioskInfo getInfo(final KioskExtractor extractor) throws ExtractionException {
|
||||||
|
|
||||||
final KioskInfo info = new KioskInfo(extractor.getServiceId(),
|
final KioskInfo info = new KioskInfo(extractor.getServiceId(),
|
||||||
extractor.getLinkHandler(),
|
extractor.getLinkHandler(),
|
||||||
extractor.getName());
|
extractor.getName());
|
||||||
|
|
||||||
final ListExtractor.InfoItemsPage<StreamInfoItem> itemsPage = ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
final ListExtractor.InfoItemsPage<StreamInfoItem> itemsPage
|
||||||
|
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||||
info.setRelatedItems(itemsPage.getItems());
|
info.setRelatedItems(itemsPage.getItems());
|
||||||
info.setNextPageUrl(itemsPage.getNextPageUrl());
|
info.setNextPage(itemsPage.getNextPage());
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
package org.schabi.newpipe.extractor.kiosk;
|
package org.schabi.newpipe.extractor.kiosk;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public class KioskList {
|
public class KioskList {
|
||||||
|
|
||||||
public interface KioskExtractorFactory {
|
public interface KioskExtractorFactory {
|
||||||
KioskExtractor createNewKiosk(final StreamingService streamingService,
|
KioskExtractor createNewKiosk(StreamingService streamingService,
|
||||||
final String url,
|
String url,
|
||||||
final String kioskId)
|
String kioskId)
|
||||||
throws ExtractionException, IOException;
|
throws ExtractionException, IOException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +35,8 @@ public class KioskList {
|
||||||
@Nullable
|
@Nullable
|
||||||
private ContentCountry forcedContentCountry;
|
private ContentCountry forcedContentCountry;
|
||||||
|
|
||||||
private class KioskEntry {
|
private static class KioskEntry {
|
||||||
public KioskEntry(KioskExtractorFactory ef, ListLinkHandlerFactory h) {
|
KioskEntry(final KioskExtractorFactory ef, final ListLinkHandlerFactory h) {
|
||||||
extractorFactory = ef;
|
extractorFactory = ef;
|
||||||
handlerFactory = h;
|
handlerFactory = h;
|
||||||
}
|
}
|
||||||
|
@ -41,11 +45,13 @@ public class KioskList {
|
||||||
final ListLinkHandlerFactory handlerFactory;
|
final ListLinkHandlerFactory handlerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskList(StreamingService service) {
|
public KioskList(final StreamingService service) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addKioskEntry(KioskExtractorFactory extractorFactory, ListLinkHandlerFactory handlerFactory, String id)
|
public void addKioskEntry(final KioskExtractorFactory extractorFactory,
|
||||||
|
final ListLinkHandlerFactory handlerFactory,
|
||||||
|
final String id)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
if (kioskList.get(id) != null) {
|
if (kioskList.get(id) != null) {
|
||||||
throw new Exception("Kiosk with type " + id + " already exists.");
|
throw new Exception("Kiosk with type " + id + " already exists.");
|
||||||
|
@ -53,29 +59,30 @@ public class KioskList {
|
||||||
kioskList.put(id, new KioskEntry(extractorFactory, handlerFactory));
|
kioskList.put(id, new KioskEntry(extractorFactory, handlerFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultKiosk(String kioskType) {
|
public void setDefaultKiosk(final String kioskType) {
|
||||||
defaultKiosk = kioskType;
|
defaultKiosk = kioskType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getDefaultKioskExtractor()
|
public KioskExtractor getDefaultKioskExtractor()
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
return getDefaultKioskExtractor("");
|
return getDefaultKioskExtractor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getDefaultKioskExtractor(String nextPageUrl)
|
public KioskExtractor getDefaultKioskExtractor(final Page nextPage)
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
return getDefaultKioskExtractor(nextPageUrl, NewPipe.getPreferredLocalization());
|
return getDefaultKioskExtractor(nextPage, NewPipe.getPreferredLocalization());
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getDefaultKioskExtractor(String nextPageUrl, Localization localization)
|
public KioskExtractor getDefaultKioskExtractor(final Page nextPage,
|
||||||
|
final Localization localization)
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
if (defaultKiosk != null && !defaultKiosk.equals("")) {
|
if (!isNullOrEmpty(defaultKiosk)) {
|
||||||
return getExtractorById(defaultKiosk, nextPageUrl, localization);
|
return getExtractorById(defaultKiosk, nextPage, localization);
|
||||||
} else {
|
} else {
|
||||||
if (!kioskList.isEmpty()) {
|
final String first = kioskList.keySet().stream().findAny().orElse(null);
|
||||||
|
if (first != null) {
|
||||||
// if not set get any entry
|
// if not set get any entry
|
||||||
Object[] keySet = kioskList.keySet().toArray();
|
return getExtractorById(first, nextPage, localization);
|
||||||
return getExtractorById(keySet[0].toString(), nextPageUrl, localization);
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -86,22 +93,28 @@ public class KioskList {
|
||||||
return defaultKiosk;
|
return defaultKiosk;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getExtractorById(String kioskId, String nextPageUrl)
|
public KioskExtractor getExtractorById(final String kioskId, final Page nextPage)
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
return getExtractorById(kioskId, nextPageUrl, NewPipe.getPreferredLocalization());
|
return getExtractorById(kioskId, nextPage, NewPipe.getPreferredLocalization());
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getExtractorById(String kioskId, String nextPageUrl, Localization localization)
|
public KioskExtractor getExtractorById(final String kioskId,
|
||||||
|
final Page nextPage,
|
||||||
|
final Localization localization)
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
KioskEntry ke = kioskList.get(kioskId);
|
final KioskEntry ke = kioskList.get(kioskId);
|
||||||
if (ke == null) {
|
if (ke == null) {
|
||||||
throw new ExtractionException("No kiosk found with the type: " + kioskId);
|
throw new ExtractionException("No kiosk found with the type: " + kioskId);
|
||||||
} else {
|
} else {
|
||||||
final KioskExtractor kioskExtractor = ke.extractorFactory.createNewKiosk(service,
|
final KioskExtractor kioskExtractor = ke.extractorFactory.createNewKiosk(service,
|
||||||
ke.handlerFactory.fromId(kioskId).getUrl(), kioskId);
|
ke.handlerFactory.fromId(kioskId).getUrl(), kioskId);
|
||||||
|
|
||||||
if (forcedLocalization != null) kioskExtractor.forceLocalization(forcedLocalization);
|
if (forcedLocalization != null) {
|
||||||
if (forcedContentCountry != null) kioskExtractor.forceContentCountry(forcedContentCountry);
|
kioskExtractor.forceLocalization(forcedLocalization);
|
||||||
|
}
|
||||||
|
if (forcedContentCountry != null) {
|
||||||
|
kioskExtractor.forceContentCountry(forcedContentCountry);
|
||||||
|
}
|
||||||
|
|
||||||
return kioskExtractor;
|
return kioskExtractor;
|
||||||
}
|
}
|
||||||
|
@ -111,31 +124,33 @@ public class KioskList {
|
||||||
return kioskList.keySet();
|
return kioskList.keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getExtractorByUrl(String url, String nextPageUrl)
|
public KioskExtractor getExtractorByUrl(final String url, final Page nextPage)
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
return getExtractorByUrl(url, nextPageUrl, NewPipe.getPreferredLocalization());
|
return getExtractorByUrl(url, nextPage, NewPipe.getPreferredLocalization());
|
||||||
}
|
}
|
||||||
|
|
||||||
public KioskExtractor getExtractorByUrl(String url, String nextPageUrl, Localization localization)
|
public KioskExtractor getExtractorByUrl(final String url,
|
||||||
|
final Page nextPage,
|
||||||
|
final Localization localization)
|
||||||
throws ExtractionException, IOException {
|
throws ExtractionException, IOException {
|
||||||
for (Map.Entry<String, KioskEntry> e : kioskList.entrySet()) {
|
for (final Map.Entry<String, KioskEntry> e : kioskList.entrySet()) {
|
||||||
KioskEntry ke = e.getValue();
|
final KioskEntry ke = e.getValue();
|
||||||
if (ke.handlerFactory.acceptUrl(url)) {
|
if (ke.handlerFactory.acceptUrl(url)) {
|
||||||
return getExtractorById(ke.handlerFactory.getId(url), nextPageUrl, localization);
|
return getExtractorById(ke.handlerFactory.getId(url), nextPage, localization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new ExtractionException("Could not find a kiosk that fits to the url: " + url);
|
throw new ExtractionException("Could not find a kiosk that fits to the url: " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListLinkHandlerFactory getListLinkHandlerFactoryByType(String type) {
|
public ListLinkHandlerFactory getListLinkHandlerFactoryByType(final String type) {
|
||||||
return kioskList.get(type).handlerFactory;
|
return kioskList.get(type).handlerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void forceLocalization(@Nullable Localization localization) {
|
public void forceLocalization(@Nullable final Localization localization) {
|
||||||
this.forcedLocalization = localization;
|
this.forcedLocalization = localization;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void forceContentCountry(@Nullable ContentCountry contentCountry) {
|
public void forceContentCountry(@Nullable final ContentCountry contentCountry) {
|
||||||
this.forcedContentCountry = contentCountry;
|
this.forcedContentCountry = contentCountry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,13 @@ public class LinkHandler implements Serializable {
|
||||||
protected final String url;
|
protected final String url;
|
||||||
protected final String id;
|
protected final String id;
|
||||||
|
|
||||||
public LinkHandler(String originalUrl, String url, String id) {
|
public LinkHandler(final String originalUrl, final String url, final String id) {
|
||||||
this.originalUrl = originalUrl;
|
this.originalUrl = originalUrl;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkHandler(LinkHandler handler) {
|
public LinkHandler(final LinkHandler handler) {
|
||||||
this(handler.originalUrl, handler.url, handler.id);
|
this(handler.originalUrl, handler.url, handler.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
package org.schabi.newpipe.extractor.linkhandler;
|
package org.schabi.newpipe.extractor.linkhandler;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 26.07.16.
|
* Created by Christian Schabesberger on 26.07.16.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
* Copyright (C) 2016 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||||
* LinkHandlerFactory.java is part of NewPipe.
|
* LinkHandlerFactory.java is part of NewPipe Extractor.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public abstract class LinkHandlerFactory {
|
public abstract class LinkHandlerFactory {
|
||||||
|
@ -30,11 +31,14 @@ public abstract class LinkHandlerFactory {
|
||||||
// To Override
|
// To Override
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
|
||||||
public abstract String getId(String url) throws ParsingException;
|
public abstract String getId(String url) throws ParsingException, UnsupportedOperationException;
|
||||||
public abstract String getUrl(String id) throws ParsingException;
|
|
||||||
public abstract boolean onAcceptUrl(final String url) throws ParsingException;
|
|
||||||
|
|
||||||
public String getUrl(String id, String baseUrl) throws ParsingException {
|
public abstract String getUrl(String id) throws ParsingException, UnsupportedOperationException;
|
||||||
|
|
||||||
|
public abstract boolean onAcceptUrl(String url) throws ParsingException;
|
||||||
|
|
||||||
|
public String getUrl(final String id, final String baseUrl)
|
||||||
|
throws ParsingException, UnsupportedOperationException {
|
||||||
return getUrl(id);
|
return getUrl(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,30 +46,52 @@ public abstract class LinkHandlerFactory {
|
||||||
// Logic
|
// Logic
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
|
||||||
public LinkHandler fromUrl(String url) throws ParsingException {
|
/**
|
||||||
if (url == null) throw new IllegalArgumentException("url can not be null");
|
* Builds a {@link LinkHandler} from a url.<br>
|
||||||
final String baseUrl = Utils.getBaseUrl(url);
|
* Be sure to call {@link Utils#followGoogleRedirectIfNeeded(String)} on the url if overriding
|
||||||
return fromUrl(url, baseUrl);
|
* this function.
|
||||||
|
*
|
||||||
|
* @param url the url to extract path and id from
|
||||||
|
* @return a {@link LinkHandler} complete with information
|
||||||
|
*/
|
||||||
|
public LinkHandler fromUrl(final String url) throws ParsingException {
|
||||||
|
if (Utils.isNullOrEmpty(url)) {
|
||||||
|
throw new IllegalArgumentException("The url is null or empty");
|
||||||
|
}
|
||||||
|
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
|
||||||
|
final String baseUrl = Utils.getBaseUrl(polishedUrl);
|
||||||
|
return fromUrl(polishedUrl, baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkHandler fromUrl(String url, String baseUrl) throws ParsingException {
|
/**
|
||||||
if (url == null) throw new IllegalArgumentException("url can not be null");
|
* Builds a {@link LinkHandler} from an URL and a base URL. The URL is expected to be already
|
||||||
|
* polished from Google search redirects (otherwise how could {@code baseUrl} have been
|
||||||
|
* extracted?).<br>
|
||||||
|
* So do not call {@link Utils#followGoogleRedirectIfNeeded(String)} on the URL if overriding
|
||||||
|
* this function, since that should be done in {@link #fromUrl(String)}.
|
||||||
|
*
|
||||||
|
* @param url the URL without Google search redirects to extract id from
|
||||||
|
* @param baseUrl the base URL
|
||||||
|
* @return a {@link LinkHandler} complete with information
|
||||||
|
*/
|
||||||
|
public LinkHandler fromUrl(final String url, final String baseUrl) throws ParsingException {
|
||||||
|
Objects.requireNonNull(url, "URL cannot be null");
|
||||||
if (!acceptUrl(url)) {
|
if (!acceptUrl(url)) {
|
||||||
throw new ParsingException("Malformed unacceptable url: " + url);
|
throw new ParsingException("URL not accepted: " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String id = getId(url);
|
final String id = getId(url);
|
||||||
return new LinkHandler(url, getUrl(id, baseUrl), id);
|
return new LinkHandler(url, getUrl(id, baseUrl), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkHandler fromId(String id) throws ParsingException {
|
public LinkHandler fromId(final String id) throws ParsingException {
|
||||||
if (id == null) throw new IllegalArgumentException("id can not be null");
|
Objects.requireNonNull(id, "ID cannot be null");
|
||||||
final String url = getUrl(id);
|
final String url = getUrl(id);
|
||||||
return new LinkHandler(url, url, id);
|
return new LinkHandler(url, url, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkHandler fromId(String id, String baseUrl) throws ParsingException {
|
public LinkHandler fromId(final String id, final String baseUrl) throws ParsingException {
|
||||||
if (id == null) throw new IllegalArgumentException("id can not be null");
|
Objects.requireNonNull(id, "ID cannot be null");
|
||||||
final String url = getUrl(id, baseUrl);
|
final String url = getUrl(id, baseUrl);
|
||||||
return new LinkHandler(url, url, id);
|
return new LinkHandler(url, url, id);
|
||||||
}
|
}
|
||||||
|
@ -76,11 +102,6 @@ public abstract class LinkHandlerFactory {
|
||||||
* Return false if this service shall not allow to be called through ACTIONs.
|
* Return false if this service shall not allow to be called through ACTIONs.
|
||||||
*/
|
*/
|
||||||
public boolean acceptUrl(final String url) throws ParsingException {
|
public boolean acceptUrl(final String url) throws ParsingException {
|
||||||
try {
|
return onAcceptUrl(url);
|
||||||
return onAcceptUrl(url);
|
|
||||||
} catch (FoundAdException fe) {
|
|
||||||
throw fe;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,17 +7,17 @@ public class ListLinkHandler extends LinkHandler {
|
||||||
protected final List<String> contentFilters;
|
protected final List<String> contentFilters;
|
||||||
protected final String sortFilter;
|
protected final String sortFilter;
|
||||||
|
|
||||||
public ListLinkHandler(String originalUrl,
|
public ListLinkHandler(final String originalUrl,
|
||||||
String url,
|
final String url,
|
||||||
String id,
|
final String id,
|
||||||
List<String> contentFilters,
|
final List<String> contentFilters,
|
||||||
String sortFilter) {
|
final String sortFilter) {
|
||||||
super(originalUrl, url, id);
|
super(originalUrl, url, id);
|
||||||
this.contentFilters = Collections.unmodifiableList(contentFilters);
|
this.contentFilters = Collections.unmodifiableList(contentFilters);
|
||||||
this.sortFilter = sortFilter;
|
this.sortFilter = sortFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListLinkHandler(ListLinkHandler handler) {
|
public ListLinkHandler(final ListLinkHandler handler) {
|
||||||
this(handler.originalUrl,
|
this(handler.originalUrl,
|
||||||
handler.url,
|
handler.url,
|
||||||
handler.id,
|
handler.id,
|
||||||
|
@ -25,14 +25,12 @@ public class ListLinkHandler extends LinkHandler {
|
||||||
handler.sortFilter);
|
handler.sortFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListLinkHandler(LinkHandler handler,
|
public ListLinkHandler(final LinkHandler handler) {
|
||||||
List<String> contentFilters,
|
|
||||||
String sortFilter) {
|
|
||||||
this(handler.originalUrl,
|
this(handler.originalUrl,
|
||||||
handler.url,
|
handler.url,
|
||||||
handler.id,
|
handler.id,
|
||||||
contentFilters,
|
Collections.emptyList(),
|
||||||
sortFilter);
|
"");
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getContentFilters() {
|
public List<String> getContentFilters() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
|
public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
|
||||||
|
|
||||||
|
@ -12,17 +13,14 @@ public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
|
||||||
// To Override
|
// To Override
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
|
||||||
public List<String> getContentFilter(String url) throws ParsingException {
|
public abstract String getUrl(String id, List<String> contentFilter, String sortFilter)
|
||||||
return new ArrayList<>(0);
|
throws ParsingException, UnsupportedOperationException;
|
||||||
}
|
|
||||||
|
|
||||||
public String getSortFilter(String url) throws ParsingException {
|
public String getUrl(final String id,
|
||||||
return "";
|
final List<String> contentFilter,
|
||||||
}
|
final String sortFilter,
|
||||||
|
final String baseUrl)
|
||||||
public abstract String getUrl(String id, List<String> contentFilter, String sortFilter) throws ParsingException;
|
throws ParsingException, UnsupportedOperationException {
|
||||||
|
|
||||||
public String getUrl(String id, List<String> contentFilter, String sortFilter, String baseUrl) throws ParsingException {
|
|
||||||
return getUrl(id, contentFilter, sortFilter);
|
return getUrl(id, contentFilter, sortFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,61 +29,62 @@ public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListLinkHandler fromUrl(String url) throws ParsingException {
|
public ListLinkHandler fromUrl(final String url) throws ParsingException {
|
||||||
String baseUrl = Utils.getBaseUrl(url);
|
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
|
||||||
return fromUrl(url, baseUrl);
|
final String baseUrl = Utils.getBaseUrl(polishedUrl);
|
||||||
|
return fromUrl(polishedUrl, baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListLinkHandler fromUrl(String url, String baseUrl) throws ParsingException {
|
public ListLinkHandler fromUrl(final String url, final String baseUrl) throws ParsingException {
|
||||||
if (url == null) throw new IllegalArgumentException("url may not be null");
|
Objects.requireNonNull(url, "URL may not be null");
|
||||||
|
return new ListLinkHandler(super.fromUrl(url, baseUrl));
|
||||||
return new ListLinkHandler(super.fromUrl(url, baseUrl), getContentFilter(url), getSortFilter(url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListLinkHandler fromId(String id) throws ParsingException {
|
public ListLinkHandler fromId(final String id) throws ParsingException {
|
||||||
return new ListLinkHandler(super.fromId(id), new ArrayList<String>(0), "");
|
return new ListLinkHandler(super.fromId(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListLinkHandler fromId(String id, String baseUrl) throws ParsingException {
|
public ListLinkHandler fromId(final String id, final String baseUrl) throws ParsingException {
|
||||||
return new ListLinkHandler(super.fromId(id, baseUrl), new ArrayList<String>(0), "");
|
return new ListLinkHandler(super.fromId(id, baseUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListLinkHandler fromQuery(String id,
|
public ListLinkHandler fromQuery(final String id,
|
||||||
List<String> contentFilters,
|
final List<String> contentFilters,
|
||||||
String sortFilter) throws ParsingException {
|
final String sortFilter) throws ParsingException {
|
||||||
final String url = getUrl(id, contentFilters, sortFilter);
|
final String url = getUrl(id, contentFilters, sortFilter);
|
||||||
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
|
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListLinkHandler fromQuery(String id,
|
public ListLinkHandler fromQuery(final String id,
|
||||||
List<String> contentFilters,
|
final List<String> contentFilters,
|
||||||
String sortFilter, String baseUrl) throws ParsingException {
|
final String sortFilter,
|
||||||
|
final String baseUrl) throws ParsingException {
|
||||||
final String url = getUrl(id, contentFilters, sortFilter, baseUrl);
|
final String url = getUrl(id, contentFilters, sortFilter, baseUrl);
|
||||||
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
|
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For making ListLinkHandlerFactory compatible with LinkHandlerFactory we need to override this,
|
* For making ListLinkHandlerFactory compatible with LinkHandlerFactory we need to override
|
||||||
* however it should not be overridden by the actual implementation.
|
* this, however it should not be overridden by the actual implementation.
|
||||||
*
|
*
|
||||||
* @param id
|
* @return the url corresponding to id without any filters applied
|
||||||
* @return the url coresponding to id without any filters applied
|
|
||||||
*/
|
*/
|
||||||
public String getUrl(String id) throws ParsingException {
|
public String getUrl(final String id) throws ParsingException, UnsupportedOperationException {
|
||||||
return getUrl(id, new ArrayList<String>(0), "");
|
return getUrl(id, new ArrayList<>(0), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl(String id, String baseUrl) throws ParsingException {
|
public String getUrl(final String id, final String baseUrl) throws ParsingException {
|
||||||
return getUrl(id, new ArrayList<String>(0), "", baseUrl);
|
return getUrl(id, new ArrayList<>(0), "", baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will returns content filter the corresponding extractor can handle like "channels", "videos", "music", etc.
|
* Will returns content filter the corresponding extractor can handle like "channels", "videos",
|
||||||
|
* "music", etc.
|
||||||
*
|
*
|
||||||
* @return filter that can be applied when building a query for getting a list
|
* @return filter that can be applied when building a query for getting a list
|
||||||
*/
|
*/
|
||||||
|
@ -94,7 +93,8 @@ public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will returns sort filter the corresponding extractor can handle like "A-Z", "oldest first", "size", etc.
|
* Will returns sort filter the corresponding extractor can handle like "A-Z", "oldest first",
|
||||||
|
* "size", etc.
|
||||||
*
|
*
|
||||||
* @return filter that can be applied when building a query for getting a list
|
* @return filter that can be applied when building a query for getting a list
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.schabi.newpipe.extractor.linkhandler;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ListLinkHandler} which can be used to be returned from {@link
|
||||||
|
* org.schabi.newpipe.extractor.channel.ChannelInfo#getTabs() ChannelInfo#getTabs()} when a
|
||||||
|
* specific tab's data has already been fetched.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class allows passing a builder for a {@link ChannelTabExtractor} that can hold references
|
||||||
|
* to variables.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Note: a service that wishes to use this class in one of its {@link
|
||||||
|
* org.schabi.newpipe.extractor.channel.ChannelExtractor ChannelExtractor}s must also add the
|
||||||
|
* following snippet of code in the service's
|
||||||
|
* {@link StreamingService#getChannelTabExtractor(ListLinkHandler)}:
|
||||||
|
* <pre>
|
||||||
|
* if (linkHandler instanceof ReadyChannelTabListLinkHandler) {
|
||||||
|
* return ((ReadyChannelTabListLinkHandler) linkHandler).getChannelTabExtractor(this);
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class ReadyChannelTabListLinkHandler extends ListLinkHandler {
|
||||||
|
|
||||||
|
public interface ChannelTabExtractorBuilder extends Serializable {
|
||||||
|
@Nonnull
|
||||||
|
ChannelTabExtractor build(@Nonnull StreamingService service,
|
||||||
|
@Nonnull ListLinkHandler linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ChannelTabExtractorBuilder extractorBuilder;
|
||||||
|
|
||||||
|
public ReadyChannelTabListLinkHandler(
|
||||||
|
final String url,
|
||||||
|
final String channelId,
|
||||||
|
@Nonnull final String channelTab,
|
||||||
|
@Nonnull final ChannelTabExtractorBuilder extractorBuilder) {
|
||||||
|
super(url, url, channelId, List.of(channelTab), "");
|
||||||
|
this.extractorBuilder = extractorBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public ChannelTabExtractor getChannelTabExtractor(@Nonnull final StreamingService service) {
|
||||||
|
return extractorBuilder.build(service, new ListLinkHandler(this));
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,15 +4,15 @@ import java.util.List;
|
||||||
|
|
||||||
public class SearchQueryHandler extends ListLinkHandler {
|
public class SearchQueryHandler extends ListLinkHandler {
|
||||||
|
|
||||||
public SearchQueryHandler(String originalUrl,
|
public SearchQueryHandler(final String originalUrl,
|
||||||
String url,
|
final String url,
|
||||||
String searchString,
|
final String searchString,
|
||||||
List<String> contentFilters,
|
final List<String> contentFilters,
|
||||||
String sortFilter) {
|
final String sortFilter) {
|
||||||
super(originalUrl, url, searchString, contentFilters, sortFilter);
|
super(originalUrl, url, searchString, contentFilters, sortFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchQueryHandler(ListLinkHandler handler) {
|
public SearchQueryHandler(final ListLinkHandler handler) {
|
||||||
this(handler.originalUrl,
|
this(handler.originalUrl,
|
||||||
handler.url,
|
handler.url,
|
||||||
handler.id,
|
handler.id,
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.schabi.newpipe.extractor.linkhandler;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class SearchQueryHandlerFactory extends ListLinkHandlerFactory {
|
public abstract class SearchQueryHandlerFactory extends ListLinkHandlerFactory {
|
||||||
|
@ -12,9 +12,11 @@ public abstract class SearchQueryHandlerFactory extends ListLinkHandlerFactory {
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public abstract String getUrl(String querry, List<String> contentFilter, String sortFilter) throws ParsingException;
|
public abstract String getUrl(String query, List<String> contentFilter, String sortFilter)
|
||||||
|
throws ParsingException, UnsupportedOperationException;
|
||||||
|
|
||||||
public String getSearchString(String url) {
|
@SuppressWarnings("unused")
|
||||||
|
public String getSearchString(final String url) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,29 +25,26 @@ public abstract class SearchQueryHandlerFactory extends ListLinkHandlerFactory {
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId(String url) {
|
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
|
||||||
return getSearchString(url);
|
return getSearchString(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SearchQueryHandler fromQuery(String querry,
|
public SearchQueryHandler fromQuery(final String query,
|
||||||
List<String> contentFilter,
|
final List<String> contentFilter,
|
||||||
String sortFilter) throws ParsingException {
|
final String sortFilter) throws ParsingException {
|
||||||
return new SearchQueryHandler(super.fromQuery(querry, contentFilter, sortFilter));
|
return new SearchQueryHandler(super.fromQuery(query, contentFilter, sortFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchQueryHandler fromQuery(String querry) throws ParsingException {
|
public SearchQueryHandler fromQuery(final String query) throws ParsingException {
|
||||||
return fromQuery(querry, new ArrayList<String>(0), "");
|
return fromQuery(query, Collections.emptyList(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's not mandatory for NewPipe to handle the Url
|
* It's not mandatory for NewPipe to handle the Url
|
||||||
*
|
|
||||||
* @param url
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean onAcceptUrl(String url) {
|
public boolean onAcceptUrl(final String url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.extractor.localization;
|
package org.schabi.newpipe.extractor.localization;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -9,23 +10,26 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* Represents a country that should be used when fetching content.
|
* Represents a country that should be used when fetching content.
|
||||||
* <p>
|
* <p>
|
||||||
* YouTube, for example, give different results in their feed depending on which country is selected.
|
* YouTube, for example, give different results in their feed depending on which country is
|
||||||
|
* selected.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public class ContentCountry implements Serializable {
|
public class ContentCountry implements Serializable {
|
||||||
public static final ContentCountry DEFAULT = new ContentCountry(Localization.DEFAULT.getCountryCode());
|
public static final ContentCountry DEFAULT =
|
||||||
|
new ContentCountry(Localization.DEFAULT.getCountryCode());
|
||||||
|
|
||||||
@Nonnull private final String countryCode;
|
@Nonnull
|
||||||
|
private final String countryCode;
|
||||||
|
|
||||||
public static List<ContentCountry> listFrom(String... countryCodeList) {
|
public static List<ContentCountry> listFrom(final String... countryCodeList) {
|
||||||
final List<ContentCountry> toReturn = new ArrayList<>();
|
final List<ContentCountry> toReturn = new ArrayList<>();
|
||||||
for (String countryCode : countryCodeList) {
|
for (final String countryCode : countryCodeList) {
|
||||||
toReturn.add(new ContentCountry(countryCode));
|
toReturn.add(new ContentCountry(countryCode));
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableList(toReturn);
|
return Collections.unmodifiableList(toReturn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentCountry(@Nonnull String countryCode) {
|
public ContentCountry(@Nonnull final String countryCode) {
|
||||||
this.countryCode = countryCode;
|
this.countryCode = countryCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,11 +44,15 @@ public class ContentCountry implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(final Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) {
|
||||||
if (!(o instanceof ContentCountry)) return false;
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof ContentCountry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ContentCountry that = (ContentCountry) o;
|
final ContentCountry that = (ContentCountry) o;
|
||||||
|
|
||||||
return countryCode.equals(that.countryCode);
|
return countryCode.equals(that.countryCode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,70 @@
|
||||||
package org.schabi.newpipe.extractor.localization;
|
package org.schabi.newpipe.extractor.localization;
|
||||||
|
|
||||||
import edu.umd.cs.findbugs.annotations.NonNull;
|
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper class that provides a field to describe if the date is precise or just an approximation.
|
* A wrapper class that provides a field to describe if the date/time is precise or just an
|
||||||
|
* approximation.
|
||||||
*/
|
*/
|
||||||
public class DateWrapper implements Serializable {
|
public class DateWrapper implements Serializable {
|
||||||
@NonNull private final Calendar date;
|
@Nonnull
|
||||||
|
private final OffsetDateTime offsetDateTime;
|
||||||
private final boolean isApproximation;
|
private final boolean isApproximation;
|
||||||
|
|
||||||
public DateWrapper(@NonNull Calendar date) {
|
/**
|
||||||
this(date, false);
|
* @deprecated Use {@link #DateWrapper(OffsetDateTime)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public DateWrapper(@Nonnull final Calendar calendar) {
|
||||||
|
//noinspection deprecation
|
||||||
|
this(calendar, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateWrapper(@NonNull Calendar date, boolean isApproximation) {
|
/**
|
||||||
this.date = date;
|
* @deprecated Use {@link #DateWrapper(OffsetDateTime, boolean)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public DateWrapper(@Nonnull final Calendar calendar, final boolean isApproximation) {
|
||||||
|
this(OffsetDateTime.ofInstant(calendar.toInstant(), ZoneOffset.UTC), isApproximation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime) {
|
||||||
|
this(offsetDateTime, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime,
|
||||||
|
final boolean isApproximation) {
|
||||||
|
this.offsetDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
this.isApproximation = isApproximation;
|
this.isApproximation = isApproximation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the wrapped date.
|
* @return the wrapped date/time as a {@link Calendar}.
|
||||||
|
* @deprecated use {@link #offsetDateTime()} instead.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@Deprecated
|
||||||
|
@Nonnull
|
||||||
public Calendar date() {
|
public Calendar date() {
|
||||||
return date;
|
return GregorianCalendar.from(offsetDateTime.toZonedDateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the date is considered is precise or just an approximation (e.g. service only returns an approximation
|
* @return the wrapped date/time.
|
||||||
* like 2 weeks ago instead of a precise date).
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public OffsetDateTime offsetDateTime() {
|
||||||
|
return offsetDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return if the date is considered is precise or just an approximation (e.g. service only
|
||||||
|
* returns an approximation like 2 weeks ago instead of a precise date).
|
||||||
*/
|
*/
|
||||||
public boolean isApproximation() {
|
public boolean isApproximation() {
|
||||||
return isApproximation;
|
return isApproximation;
|
||||||
|
|
|
@ -1,57 +1,67 @@
|
||||||
package org.schabi.newpipe.extractor.localization;
|
package org.schabi.newpipe.extractor.localization;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import javax.annotation.Nullable;
|
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
public class Localization implements Serializable {
|
public class Localization implements Serializable {
|
||||||
public static final Localization DEFAULT = new Localization("en", "GB");
|
public static final Localization DEFAULT = new Localization("en", "GB");
|
||||||
|
|
||||||
@Nonnull private final String languageCode;
|
@Nonnull
|
||||||
@Nullable private final String countryCode;
|
private final String languageCode;
|
||||||
|
@Nullable
|
||||||
|
private final String countryCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param localizationCodeList a list of localization code, formatted like {@link #getLocalizationCode()}
|
* @param localizationCodeList a list of localization code, formatted like {@link
|
||||||
|
* #getLocalizationCode()}
|
||||||
|
* @throws IllegalArgumentException If any of the localizationCodeList is formatted incorrectly
|
||||||
|
* @return list of Localization objects
|
||||||
*/
|
*/
|
||||||
public static List<Localization> listFrom(String... localizationCodeList) {
|
@Nonnull
|
||||||
|
public static List<Localization> listFrom(final String... localizationCodeList) {
|
||||||
final List<Localization> toReturn = new ArrayList<>();
|
final List<Localization> toReturn = new ArrayList<>();
|
||||||
for (String localizationCode : localizationCodeList) {
|
for (final String localizationCode : localizationCodeList) {
|
||||||
toReturn.add(fromLocalizationCode(localizationCode));
|
toReturn.add(fromLocalizationCode(localizationCode)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException(
|
||||||
|
"Not a localization code: " + localizationCode
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableList(toReturn);
|
return Collections.unmodifiableList(toReturn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param localizationCode a localization code, formatted like {@link #getLocalizationCode()}
|
* @param localizationCode a localization code, formatted like {@link #getLocalizationCode()}
|
||||||
|
* @return A Localization, if the code was valid.
|
||||||
*/
|
*/
|
||||||
public static Localization fromLocalizationCode(String localizationCode) {
|
@Nonnull
|
||||||
final int indexSeparator = localizationCode.indexOf("-");
|
public static Optional<Localization> fromLocalizationCode(final String localizationCode) {
|
||||||
|
return LocaleCompat.forLanguageTag(localizationCode).map(Localization::fromLocale);
|
||||||
final String languageCode, countryCode;
|
|
||||||
if (indexSeparator != -1) {
|
|
||||||
languageCode = localizationCode.substring(0, indexSeparator);
|
|
||||||
countryCode = localizationCode.substring(indexSeparator + 1);
|
|
||||||
} else {
|
|
||||||
languageCode = localizationCode;
|
|
||||||
countryCode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Localization(languageCode, countryCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Localization(@Nonnull String languageCode, @Nullable String countryCode) {
|
public Localization(@Nonnull final String languageCode, @Nullable final String countryCode) {
|
||||||
this.languageCode = languageCode;
|
this.languageCode = languageCode;
|
||||||
this.countryCode = countryCode;
|
this.countryCode = countryCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Localization(@Nonnull String languageCode) {
|
public Localization(@Nonnull final String languageCode) {
|
||||||
this(languageCode, null);
|
this(languageCode, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
public String getLanguageCode() {
|
public String getLanguageCode() {
|
||||||
return languageCode;
|
return languageCode;
|
||||||
}
|
}
|
||||||
|
@ -61,17 +71,15 @@ public class Localization implements Serializable {
|
||||||
return countryCode == null ? "" : countryCode;
|
return countryCode == null ? "" : countryCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Locale asLocale() {
|
public static Localization fromLocale(@Nonnull final Locale locale) {
|
||||||
return new Locale(getLanguageCode(), getCountryCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Localization fromLocale(@Nonnull Locale locale) {
|
|
||||||
return new Localization(locale.getLanguage(), locale.getCountry());
|
return new Localization(locale.getLanguage(), locale.getCountry());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a formatted string in the form of: {@code language-Country}, or
|
* Return a formatted string in the form of: {@code language-Country}, or
|
||||||
* just {@code language} if country is {@code null}.
|
* just {@code language} if country is {@code null}.
|
||||||
|
*
|
||||||
|
* @return A correctly formatted localizationCode for this localization.
|
||||||
*/
|
*/
|
||||||
public String getLocalizationCode() {
|
public String getLocalizationCode() {
|
||||||
return languageCode + (countryCode == null ? "" : "-" + countryCode);
|
return languageCode + (countryCode == null ? "" : "-" + countryCode);
|
||||||
|
@ -83,20 +91,47 @@ public class Localization implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(final Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) {
|
||||||
if (!(o instanceof Localization)) return false;
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof Localization)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Localization that = (Localization) o;
|
final Localization that = (Localization) o;
|
||||||
|
|
||||||
if (!languageCode.equals(that.languageCode)) return false;
|
return languageCode.equals(that.languageCode)
|
||||||
return countryCode != null ? countryCode.equals(that.countryCode) : that.countryCode == null;
|
&& Objects.equals(countryCode, that.countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = languageCode.hashCode();
|
int result = languageCode.hashCode();
|
||||||
result = 31 * result + (countryCode != null ? countryCode.hashCode() : 0);
|
result = 31 * result + Objects.hashCode(countryCode);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a three letter language code (ISO 639-2/T) to a Locale
|
||||||
|
* because limits of Java Locale class.
|
||||||
|
*
|
||||||
|
* @param code a three letter language code
|
||||||
|
* @return the Locale corresponding
|
||||||
|
*/
|
||||||
|
public static Locale getLocaleFromThreeLetterCode(@Nonnull final String code)
|
||||||
|
throws ParsingException {
|
||||||
|
final String[] languages = Locale.getISOLanguages();
|
||||||
|
final Map<String, Locale> localeMap = new HashMap<>(languages.length);
|
||||||
|
for (final String language : languages) {
|
||||||
|
final Locale locale = new Locale(language);
|
||||||
|
localeMap.put(locale.getISO3Language(), locale);
|
||||||
|
}
|
||||||
|
if (localeMap.containsKey(code)) {
|
||||||
|
return localeMap.get(code);
|
||||||
|
} else {
|
||||||
|
throw new ParsingException(
|
||||||
|
"Could not get Locale from this three letter language code" + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,28 @@ package org.schabi.newpipe.extractor.localization;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
|
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
|
||||||
import org.schabi.newpipe.extractor.timeago.TimeAgoUnit;
|
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
import org.schabi.newpipe.extractor.utils.Parser;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Collection;
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.MatchResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper class that is meant to be used by services that need to parse upload dates in the
|
* A helper class that is meant to be used by services that need to parse durations such as
|
||||||
* format '2 days ago' or similar.
|
* {@code 23 seconds} and/or upload dates in the format {@code 2 days ago} or similar.
|
||||||
*/
|
*/
|
||||||
public class TimeAgoParser {
|
public class TimeAgoParser {
|
||||||
|
|
||||||
|
private static final Pattern DURATION_PATTERN = Pattern.compile("(?:(\\d+) )?([A-z]+)");
|
||||||
|
|
||||||
private final PatternsHolder patternsHolder;
|
private final PatternsHolder patternsHolder;
|
||||||
private final Calendar consistentNow;
|
private final OffsetDateTime now;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a helper to parse upload dates in the format '2 days ago'.
|
* Creates a helper to parse upload dates in the format '2 days ago'.
|
||||||
|
@ -24,16 +31,17 @@ public class TimeAgoParser {
|
||||||
* Instantiate a new {@link TimeAgoParser} every time you extract a new batch of items.
|
* Instantiate a new {@link TimeAgoParser} every time you extract a new batch of items.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param patternsHolder An object that holds the "time ago" patterns, special cases, and the language word separator.
|
* @param patternsHolder An object that holds the "time ago" patterns, special cases, and the
|
||||||
|
* language word separator.
|
||||||
*/
|
*/
|
||||||
public TimeAgoParser(PatternsHolder patternsHolder) {
|
public TimeAgoParser(final PatternsHolder patternsHolder) {
|
||||||
this.patternsHolder = patternsHolder;
|
this.patternsHolder = patternsHolder;
|
||||||
consistentNow = Calendar.getInstance();
|
now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a textual date in the format '2 days ago' into a Calendar representation which is then wrapped in a
|
* Parses a textual date in the format '2 days ago' into a Calendar representation which is then
|
||||||
* {@link DateWrapper} object.
|
* wrapped in a {@link DateWrapper} object.
|
||||||
* <p>
|
* <p>
|
||||||
* Beginning with days ago, the date is considered as an approximation.
|
* Beginning with days ago, the date is considered as an approximation.
|
||||||
*
|
*
|
||||||
|
@ -41,136 +49,140 @@ public class TimeAgoParser {
|
||||||
* @return The parsed time (can be approximated)
|
* @return The parsed time (can be approximated)
|
||||||
* @throws ParsingException if the time unit could not be recognized
|
* @throws ParsingException if the time unit could not be recognized
|
||||||
*/
|
*/
|
||||||
public DateWrapper parse(String textualDate) throws ParsingException {
|
public DateWrapper parse(final String textualDate) throws ParsingException {
|
||||||
for (Map.Entry<TimeAgoUnit, Map<String, Integer>> caseUnitEntry : patternsHolder.specialCases().entrySet()) {
|
for (final Map.Entry<ChronoUnit, Map<String, Integer>> caseUnitEntry
|
||||||
final TimeAgoUnit timeAgoUnit = caseUnitEntry.getKey();
|
: patternsHolder.specialCases().entrySet()) {
|
||||||
for (Map.Entry<String, Integer> caseMapToAmountEntry : caseUnitEntry.getValue().entrySet()) {
|
final ChronoUnit chronoUnit = caseUnitEntry.getKey();
|
||||||
|
for (final Map.Entry<String, Integer> caseMapToAmountEntry
|
||||||
|
: caseUnitEntry.getValue().entrySet()) {
|
||||||
final String caseText = caseMapToAmountEntry.getKey();
|
final String caseText = caseMapToAmountEntry.getKey();
|
||||||
final Integer caseAmount = caseMapToAmountEntry.getValue();
|
final Integer caseAmount = caseMapToAmountEntry.getValue();
|
||||||
|
|
||||||
if (textualDateMatches(textualDate, caseText)) {
|
if (textualDateMatches(textualDate, caseText)) {
|
||||||
return getResultFor(caseAmount, timeAgoUnit);
|
return getResultFor(caseAmount, chronoUnit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int timeAgoAmount;
|
return getResultFor(parseTimeAgoAmount(textualDate), parseChronoUnit(textualDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a textual duration into a duration computer number.
|
||||||
|
*
|
||||||
|
* @param textualDuration the textual duration to parse
|
||||||
|
* @return the textual duration parsed, as a primitive {@code long}
|
||||||
|
* @throws ParsingException if the textual duration could not be parsed
|
||||||
|
*/
|
||||||
|
public long parseDuration(final String textualDuration) throws ParsingException {
|
||||||
|
// We can't use Matcher.results, as it is only available on Android 14 and above
|
||||||
|
final Matcher matcher = DURATION_PATTERN.matcher(textualDuration);
|
||||||
|
final List<MatchResult> results = new ArrayList<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
results.add(matcher.toMatchResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.stream()
|
||||||
|
.map(match -> {
|
||||||
|
final String digits = match.group(1);
|
||||||
|
final String word = match.group(2);
|
||||||
|
|
||||||
|
int amount;
|
||||||
|
try {
|
||||||
|
amount = Integer.parseInt(digits);
|
||||||
|
} catch (final NumberFormatException ignored) {
|
||||||
|
amount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ChronoUnit unit;
|
||||||
|
try {
|
||||||
|
unit = parseChronoUnit(word);
|
||||||
|
} catch (final ParsingException ignored) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount * unit.getDuration().getSeconds();
|
||||||
|
})
|
||||||
|
.filter(n -> n > 0)
|
||||||
|
.reduce(Long::sum)
|
||||||
|
.orElseThrow(() -> new ParsingException(
|
||||||
|
"Could not parse duration \"" + textualDuration + "\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseTimeAgoAmount(final String textualDate) {
|
||||||
try {
|
try {
|
||||||
timeAgoAmount = parseTimeAgoAmount(textualDate);
|
return Integer.parseInt(textualDate.replaceAll("\\D+", ""));
|
||||||
} catch (NumberFormatException e) {
|
} catch (final NumberFormatException ignored) {
|
||||||
// If there is no valid number in the textual date,
|
// If there is no valid number in the textual date,
|
||||||
// assume it is 1 (as in 'a second ago').
|
// assume it is 1 (as in 'a second ago').
|
||||||
timeAgoAmount = 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
final TimeAgoUnit timeAgoUnit = parseTimeAgoUnit(textualDate);
|
|
||||||
return getResultFor(timeAgoAmount, timeAgoUnit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int parseTimeAgoAmount(String textualDate) throws NumberFormatException {
|
private ChronoUnit parseChronoUnit(final String textualDate) throws ParsingException {
|
||||||
String timeValueStr = textualDate.replaceAll("\\D+", "");
|
return patternsHolder.asMap().entrySet().stream()
|
||||||
return Integer.parseInt(timeValueStr);
|
.filter(e -> e.getValue().stream()
|
||||||
|
.anyMatch(agoPhrase -> textualDateMatches(textualDate, agoPhrase)))
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new ParsingException("Unable to parse the date: " + textualDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeAgoUnit parseTimeAgoUnit(String textualDate) throws ParsingException {
|
private boolean textualDateMatches(final String textualDate, final String agoPhrase) {
|
||||||
for (Map.Entry<TimeAgoUnit, Collection<String>> entry : patternsHolder.asMap().entrySet()) {
|
|
||||||
final TimeAgoUnit timeAgoUnit = entry.getKey();
|
|
||||||
|
|
||||||
for (String agoPhrase : entry.getValue()) {
|
|
||||||
if (textualDateMatches(textualDate, agoPhrase)) {
|
|
||||||
return timeAgoUnit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ParsingException("Unable to parse the date: " + textualDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean textualDateMatches(String textualDate, String agoPhrase) {
|
|
||||||
if (textualDate.equals(agoPhrase)) {
|
if (textualDate.equals(agoPhrase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patternsHolder.wordSeparator().isEmpty()) {
|
if (patternsHolder.wordSeparator().isEmpty()) {
|
||||||
return textualDate.toLowerCase().contains(agoPhrase.toLowerCase());
|
return textualDate.toLowerCase().contains(agoPhrase.toLowerCase());
|
||||||
} else {
|
|
||||||
final String escapedPhrase = Pattern.quote(agoPhrase.toLowerCase());
|
|
||||||
final String escapedSeparator;
|
|
||||||
if (patternsHolder.wordSeparator().equals(" ")) {
|
|
||||||
// From JDK8 → \h - Treat horizontal spaces as a normal one (non-breaking space, thin space, etc.)
|
|
||||||
escapedSeparator = "[ \\t\\xA0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000]";
|
|
||||||
} else {
|
|
||||||
escapedSeparator = Pattern.quote(patternsHolder.wordSeparator());
|
|
||||||
}
|
|
||||||
|
|
||||||
// (^|separator)pattern($|separator)
|
|
||||||
// Check if the pattern is surrounded by separators or start/end of the string.
|
|
||||||
final String pattern =
|
|
||||||
"(^|" + escapedSeparator + ")" + escapedPhrase + "($|" + escapedSeparator + ")";
|
|
||||||
|
|
||||||
return Parser.isMatch(pattern, textualDate.toLowerCase());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final String escapedPhrase = Pattern.quote(agoPhrase.toLowerCase());
|
||||||
|
final String escapedSeparator = patternsHolder.wordSeparator().equals(" ")
|
||||||
|
// From JDK8 → \h - Treat horizontal spaces as a normal one
|
||||||
|
// (non-breaking space, thin space, etc.)
|
||||||
|
// Also split the string on numbers to be able to parse strings like "2wk"
|
||||||
|
? "[ \\t\\xA0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000\\d]"
|
||||||
|
: Pattern.quote(patternsHolder.wordSeparator());
|
||||||
|
|
||||||
|
// (^|separator)pattern($|separator)
|
||||||
|
// Check if the pattern is surrounded by separators or start/end of the string.
|
||||||
|
final String pattern =
|
||||||
|
"(^|" + escapedSeparator + ")" + escapedPhrase + "($|" + escapedSeparator + ")";
|
||||||
|
|
||||||
|
return Parser.isMatch(pattern, textualDate.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateWrapper getResultFor(int timeAgoAmount, TimeAgoUnit timeAgoUnit) {
|
private DateWrapper getResultFor(final int timeAgoAmount, final ChronoUnit chronoUnit) {
|
||||||
final Calendar calendarTime = getNow();
|
OffsetDateTime offsetDateTime = now;
|
||||||
boolean isApproximation = false;
|
boolean isApproximation = false;
|
||||||
|
|
||||||
switch (timeAgoUnit) {
|
switch (chronoUnit) {
|
||||||
case SECONDS:
|
case SECONDS:
|
||||||
calendarTime.add(Calendar.SECOND, -timeAgoAmount);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MINUTES:
|
case MINUTES:
|
||||||
calendarTime.add(Calendar.MINUTE, -timeAgoAmount);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HOURS:
|
case HOURS:
|
||||||
calendarTime.add(Calendar.HOUR_OF_DAY, -timeAgoAmount);
|
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DAYS:
|
case DAYS:
|
||||||
calendarTime.add(Calendar.DAY_OF_MONTH, -timeAgoAmount);
|
|
||||||
isApproximation = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WEEKS:
|
case WEEKS:
|
||||||
calendarTime.add(Calendar.WEEK_OF_YEAR, -timeAgoAmount);
|
|
||||||
isApproximation = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MONTHS:
|
case MONTHS:
|
||||||
calendarTime.add(Calendar.MONTH, -timeAgoAmount);
|
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
|
||||||
isApproximation = true;
|
isApproximation = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case YEARS:
|
case YEARS:
|
||||||
calendarTime.add(Calendar.YEAR, -timeAgoAmount);
|
// minusDays is needed to prevent `PrettyTime` from showing '12 months ago'.
|
||||||
// Prevent `PrettyTime` from showing '12 months ago'.
|
offsetDateTime = offsetDateTime.minusYears(timeAgoAmount).minusDays(1);
|
||||||
calendarTime.add(Calendar.DAY_OF_MONTH, -1);
|
|
||||||
isApproximation = true;
|
isApproximation = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isApproximation) {
|
if (isApproximation) {
|
||||||
markApproximatedTime(calendarTime);
|
offsetDateTime = offsetDateTime.truncatedTo(ChronoUnit.HOURS);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DateWrapper(calendarTime, isApproximation);
|
return new DateWrapper(offsetDateTime, isApproximation);
|
||||||
}
|
|
||||||
|
|
||||||
private Calendar getNow() {
|
|
||||||
return (Calendar) consistentNow.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the time as approximated by setting minutes, seconds and milliseconds to 0.
|
|
||||||
*
|
|
||||||
* @param calendarTime Time to be marked as approximated
|
|
||||||
*/
|
|
||||||
private void markApproximatedTime(Calendar calendarTime) {
|
|
||||||
calendarTime.set(Calendar.MINUTE, 0);
|
|
||||||
calendarTime.set(Calendar.SECOND, 0);
|
|
||||||
calendarTime.set(Calendar.MILLISECOND, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,18 @@ import org.schabi.newpipe.extractor.timeago.PatternsManager;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public class TimeAgoPatternsManager {
|
public final class TimeAgoPatternsManager {
|
||||||
@Nullable
|
private TimeAgoPatternsManager() {
|
||||||
private static PatternsHolder getPatternsFor(@Nonnull Localization localization) {
|
|
||||||
return PatternsManager.getPatterns(localization.getLanguageCode(), localization.getCountryCode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static TimeAgoParser getTimeAgoParserFor(@Nonnull Localization localization) {
|
private static PatternsHolder getPatternsFor(@Nonnull final Localization localization) {
|
||||||
|
return PatternsManager.getPatterns(localization.getLanguageCode(),
|
||||||
|
localization.getCountryCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static TimeAgoParser getTimeAgoParserFor(@Nonnull final Localization localization) {
|
||||||
final PatternsHolder holder = getPatternsFor(localization);
|
final PatternsHolder holder = getPatternsFor(localization);
|
||||||
|
|
||||||
if (holder == null) {
|
if (holder == null) {
|
||||||
|
|
|
@ -1,23 +1,61 @@
|
||||||
package org.schabi.newpipe.extractor.playlist;
|
package org.schabi.newpipe.extractor.playlist;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
|
public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
|
||||||
|
|
||||||
public PlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
public PlaylistExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract String getThumbnailUrl() throws ParsingException;
|
|
||||||
public abstract String getBannerUrl() throws ParsingException;
|
|
||||||
|
|
||||||
public abstract String getUploaderUrl() throws ParsingException;
|
public abstract String getUploaderUrl() throws ParsingException;
|
||||||
public abstract String getUploaderName() throws ParsingException;
|
public abstract String getUploaderName() throws ParsingException;
|
||||||
public abstract String getUploaderAvatarUrl() throws ParsingException;
|
@Nonnull
|
||||||
|
public abstract List<Image> getUploaderAvatars() throws ParsingException;
|
||||||
|
public abstract boolean isUploaderVerified() throws ParsingException;
|
||||||
|
|
||||||
public abstract long getStreamCount() throws ParsingException;
|
public abstract long getStreamCount() throws ParsingException;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public abstract Description getDescription() throws ParsingException;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getBanners() throws ParsingException {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String getSubChannelName() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public String getSubChannelUrl() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getSubChannelAvatars() throws ParsingException {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
|
||||||
|
return PlaylistInfo.PlaylistType.NORMAL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,82 @@
|
||||||
package org.schabi.newpipe.extractor.playlist;
|
package org.schabi.newpipe.extractor.playlist;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PlaylistInfo extends ListInfo<StreamInfoItem> {
|
public final class PlaylistInfo extends ListInfo<StreamInfoItem> {
|
||||||
|
|
||||||
private PlaylistInfo(int serviceId, ListLinkHandler linkHandler, String name) throws ParsingException {
|
/**
|
||||||
|
* Mixes are handled as particular playlists in NewPipeExtractor. {@link PlaylistType#NORMAL} is
|
||||||
|
* for non-mixes, while other values are for the different types of mixes. The type of a mix
|
||||||
|
* depends on how its contents are autogenerated.
|
||||||
|
*/
|
||||||
|
public enum PlaylistType {
|
||||||
|
/**
|
||||||
|
* A normal playlist (not a mix)
|
||||||
|
*/
|
||||||
|
NORMAL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mix made only of streams related to a particular stream, for example YouTube mixes
|
||||||
|
*/
|
||||||
|
MIX_STREAM,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mix made only of music streams related to a particular stream, for example YouTube
|
||||||
|
* music mixes
|
||||||
|
*/
|
||||||
|
MIX_MUSIC,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mix made only of streams from (or related to) the same channel, for example YouTube
|
||||||
|
* channel mixes
|
||||||
|
*/
|
||||||
|
MIX_CHANNEL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mix made only of streams related to a particular (musical) genre, for example YouTube
|
||||||
|
* genre mixes
|
||||||
|
*/
|
||||||
|
MIX_GENRE,
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("RedundantThrows")
|
||||||
|
private PlaylistInfo(final int serviceId, final ListLinkHandler linkHandler, final String name)
|
||||||
|
throws ParsingException {
|
||||||
super(serviceId, linkHandler, name);
|
super(serviceId, linkHandler, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaylistInfo getInfo(String url) throws IOException, ExtractionException {
|
public static PlaylistInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlaylistInfo getInfo(StreamingService service, String url) throws IOException, ExtractionException {
|
public static PlaylistInfo getInfo(final StreamingService service, final String url)
|
||||||
PlaylistExtractor extractor = service.getPlaylistExtractor(url);
|
throws IOException, ExtractionException {
|
||||||
|
final PlaylistExtractor extractor = service.getPlaylistExtractor(url);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
return getInfo(extractor);
|
return getInfo(extractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfoItemsPage<StreamInfoItem> getMoreItems(StreamingService service,
|
public static InfoItemsPage<StreamInfoItem> getMoreItems(final StreamingService service,
|
||||||
String url,
|
final String url,
|
||||||
String pageUrl) throws IOException, ExtractionException {
|
final Page page)
|
||||||
return service.getPlaylistExtractor(url).getPage(pageUrl);
|
throws IOException, ExtractionException {
|
||||||
|
return service.getPlaylistExtractor(url).getPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,7 +84,8 @@ public class PlaylistInfo extends ListInfo<StreamInfoItem> {
|
||||||
*
|
*
|
||||||
* @param extractor an extractor where fetchPage() was already got called on.
|
* @param extractor an extractor where fetchPage() was already got called on.
|
||||||
*/
|
*/
|
||||||
public static PlaylistInfo getInfo(PlaylistExtractor extractor) throws ExtractionException {
|
public static PlaylistInfo getInfo(final PlaylistExtractor extractor)
|
||||||
|
throws ExtractionException {
|
||||||
|
|
||||||
final PlaylistInfo info = new PlaylistInfo(
|
final PlaylistInfo info = new PlaylistInfo(
|
||||||
extractor.getServiceId(),
|
extractor.getServiceId(),
|
||||||
|
@ -49,87 +93,122 @@ public class PlaylistInfo extends ListInfo<StreamInfoItem> {
|
||||||
extractor.getName());
|
extractor.getName());
|
||||||
// collect uploader extraction failures until we are sure this is not
|
// collect uploader extraction failures until we are sure this is not
|
||||||
// just a playlist without an uploader
|
// just a playlist without an uploader
|
||||||
List<Throwable> uploaderParsingErrors = new ArrayList<Throwable>(3);
|
final List<Throwable> uploaderParsingErrors = new ArrayList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setOriginalUrl(extractor.getOriginalUrl());
|
info.setOriginalUrl(extractor.getOriginalUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.setStreamCount(extractor.getStreamCount());
|
info.setStreamCount(extractor.getStreamCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.setThumbnailUrl(extractor.getThumbnailUrl());
|
info.setDescription(extractor.getDescription());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
info.setThumbnails(extractor.getThumbnails());
|
||||||
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.setUploaderUrl(extractor.getUploaderUrl());
|
info.setUploaderUrl(extractor.getUploaderUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.setUploaderUrl("");
|
|
||||||
uploaderParsingErrors.add(e);
|
uploaderParsingErrors.add(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.setUploaderName(extractor.getUploaderName());
|
info.setUploaderName(extractor.getUploaderName());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.setUploaderName("");
|
|
||||||
uploaderParsingErrors.add(e);
|
uploaderParsingErrors.add(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
|
info.setUploaderAvatars(extractor.getUploaderAvatars());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.setUploaderAvatarUrl("");
|
|
||||||
uploaderParsingErrors.add(e);
|
uploaderParsingErrors.add(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.setBannerUrl(extractor.getBannerUrl());
|
info.setSubChannelUrl(extractor.getSubChannelUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
uploaderParsingErrors.add(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
info.setSubChannelName(extractor.getSubChannelName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
uploaderParsingErrors.add(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
info.setSubChannelAvatars(extractor.getSubChannelAvatars());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
uploaderParsingErrors.add(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
info.setBanners(extractor.getBanners());
|
||||||
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
// do not fail if everything but the uploader infos could be collected
|
try {
|
||||||
if (uploaderParsingErrors.size() > 0 &&
|
info.setPlaylistType(extractor.getPlaylistType());
|
||||||
(!info.getErrors().isEmpty() || uploaderParsingErrors.size() < 3)) {
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not fail if everything but the uploader infos could be collected (TODO better comment)
|
||||||
|
if (!uploaderParsingErrors.isEmpty()
|
||||||
|
&& (!info.getErrors().isEmpty() || uploaderParsingErrors.size() < 3)) {
|
||||||
info.addAllErrors(uploaderParsingErrors);
|
info.addAllErrors(uploaderParsingErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
final InfoItemsPage<StreamInfoItem> itemsPage = ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
final InfoItemsPage<StreamInfoItem> itemsPage
|
||||||
|
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||||
info.setRelatedItems(itemsPage.getItems());
|
info.setRelatedItems(itemsPage.getItems());
|
||||||
info.setNextPageUrl(itemsPage.getNextPageUrl());
|
info.setNextPage(itemsPage.getNextPage());
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String thumbnailUrl;
|
private String uploaderUrl = "";
|
||||||
private String bannerUrl;
|
private String uploaderName = "";
|
||||||
private String uploaderUrl;
|
private String subChannelUrl;
|
||||||
private String uploaderName;
|
private String subChannelName;
|
||||||
private String uploaderAvatarUrl;
|
private Description description;
|
||||||
private long streamCount = 0;
|
@Nonnull
|
||||||
|
private List<Image> banners = List.of();
|
||||||
|
@Nonnull
|
||||||
|
private List<Image> subChannelAvatars = List.of();
|
||||||
|
@Nonnull
|
||||||
|
private List<Image> thumbnails = List.of();
|
||||||
|
@Nonnull
|
||||||
|
private List<Image> uploaderAvatars = List.of();
|
||||||
|
private long streamCount;
|
||||||
|
private PlaylistType playlistType;
|
||||||
|
|
||||||
public String getThumbnailUrl() {
|
@Nonnull
|
||||||
return thumbnailUrl;
|
public List<Image> getThumbnails() {
|
||||||
|
return thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThumbnailUrl(String thumbnailUrl) {
|
public void setThumbnails(@Nonnull final List<Image> thumbnails) {
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnails = thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBannerUrl() {
|
@Nonnull
|
||||||
return bannerUrl;
|
public List<Image> getBanners() {
|
||||||
|
return banners;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBannerUrl(String bannerUrl) {
|
public void setBanners(@Nonnull final List<Image> banners) {
|
||||||
this.bannerUrl = bannerUrl;
|
this.banners = banners;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUploaderUrl() {
|
public String getUploaderUrl() {
|
||||||
return uploaderUrl;
|
return uploaderUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploaderUrl(String uploaderUrl) {
|
public void setUploaderUrl(final String uploaderUrl) {
|
||||||
this.uploaderUrl = uploaderUrl;
|
this.uploaderUrl = uploaderUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,23 +216,65 @@ public class PlaylistInfo extends ListInfo<StreamInfoItem> {
|
||||||
return uploaderName;
|
return uploaderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploaderName(String uploaderName) {
|
public void setUploaderName(final String uploaderName) {
|
||||||
this.uploaderName = uploaderName;
|
this.uploaderName = uploaderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUploaderAvatarUrl() {
|
@Nonnull
|
||||||
return uploaderAvatarUrl;
|
public List<Image> getUploaderAvatars() {
|
||||||
|
return uploaderAvatars;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploaderAvatarUrl(String uploaderAvatarUrl) {
|
public void setUploaderAvatars(@Nonnull final List<Image> uploaderAvatars) {
|
||||||
this.uploaderAvatarUrl = uploaderAvatarUrl;
|
this.uploaderAvatars = uploaderAvatars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubChannelUrl() {
|
||||||
|
return subChannelUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChannelUrl(final String subChannelUrl) {
|
||||||
|
this.subChannelUrl = subChannelUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubChannelName() {
|
||||||
|
return subChannelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChannelName(final String subChannelName) {
|
||||||
|
this.subChannelName = subChannelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<Image> getSubChannelAvatars() {
|
||||||
|
return subChannelAvatars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChannelAvatars(@Nonnull final List<Image> subChannelAvatars) {
|
||||||
|
this.subChannelAvatars = subChannelAvatars;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getStreamCount() {
|
public long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamCount(long streamCount) {
|
public void setStreamCount(final long streamCount) {
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Description getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(final Description description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistType getPlaylistType() {
|
||||||
|
return playlistType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaylistType(final PlaylistType playlistType) {
|
||||||
|
this.playlistType = playlistType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
package org.schabi.newpipe.extractor.playlist;
|
package org.schabi.newpipe.extractor.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public class PlaylistInfoItem extends InfoItem {
|
public class PlaylistInfoItem extends InfoItem {
|
||||||
|
|
||||||
private String uploaderName;
|
private String uploaderName;
|
||||||
|
private String uploaderUrl;
|
||||||
|
private boolean uploaderVerified;
|
||||||
/**
|
/**
|
||||||
* How many streams this playlist have
|
* How many streams this playlist have
|
||||||
*/
|
*/
|
||||||
private long streamCount = 0;
|
private long streamCount = 0;
|
||||||
|
private Description description;
|
||||||
|
private PlaylistInfo.PlaylistType playlistType;
|
||||||
|
|
||||||
public PlaylistInfoItem(int serviceId, String url, String name) {
|
public PlaylistInfoItem(final int serviceId, final String url, final String name) {
|
||||||
super(InfoType.PLAYLIST, serviceId, url, name);
|
super(InfoType.PLAYLIST, serviceId, url, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,15 +25,48 @@ public class PlaylistInfoItem extends InfoItem {
|
||||||
return uploaderName;
|
return uploaderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUploaderName(String uploader_name) {
|
public void setUploaderName(final String uploaderName) {
|
||||||
this.uploaderName = uploader_name;
|
this.uploaderName = uploaderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return uploaderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploaderUrl(@Nullable final String uploaderUrl) {
|
||||||
|
this.uploaderUrl = uploaderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUploaderVerified() {
|
||||||
|
return uploaderVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploaderVerified(final boolean uploaderVerified) {
|
||||||
|
this.uploaderVerified = uploaderVerified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getStreamCount() {
|
public long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamCount(long stream_count) {
|
public void setStreamCount(final long streamCount) {
|
||||||
this.streamCount = stream_count;
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Description getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(final Description description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistInfo.PlaylistType getPlaylistType() {
|
||||||
|
return playlistType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaylistType(final PlaylistInfo.PlaylistType playlistType) {
|
||||||
|
this.playlistType = playlistType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,52 @@ package org.schabi.newpipe.extractor.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public interface PlaylistInfoItemExtractor extends InfoItemExtractor {
|
public interface PlaylistInfoItemExtractor extends InfoItemExtractor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the uploader name
|
* Get the uploader name
|
||||||
* @return the uploader name
|
* @return the uploader name
|
||||||
* @throws ParsingException
|
|
||||||
*/
|
*/
|
||||||
String getUploaderName() throws ParsingException;
|
String getUploaderName() throws ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the uploader url
|
||||||
|
* @return the uploader url
|
||||||
|
*/
|
||||||
|
String getUploaderUrl() throws ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the uploader is verified
|
||||||
|
* @return whether the uploader is verified
|
||||||
|
*/
|
||||||
|
boolean isUploaderVerified() throws ParsingException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the number of streams
|
* Get the number of streams
|
||||||
* @return the number of streams
|
* @return the number of streams
|
||||||
* @throws ParsingException
|
|
||||||
*/
|
*/
|
||||||
long getStreamCount() throws ParsingException;
|
long getStreamCount() throws ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the description of the playlist if there is any.
|
||||||
|
* Otherwise, an {@link Description#EMPTY_DESCRIPTION EMPTY_DESCRIPTION} is returned.
|
||||||
|
* @return the playlist's description
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
default Description getDescription() throws ParsingException {
|
||||||
|
return Description.EMPTY_DESCRIPTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the type of this playlist, see {@link PlaylistInfo.PlaylistType} for a description
|
||||||
|
* of types. If not overridden always returns {@link PlaylistInfo.PlaylistType#NORMAL}.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
default PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
|
||||||
|
return PlaylistInfo.PlaylistType.NORMAL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,34 +3,52 @@ package org.schabi.newpipe.extractor.playlist;
|
||||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
public class PlaylistInfoItemsCollector extends InfoItemsCollector<PlaylistInfoItem, PlaylistInfoItemExtractor> {
|
public class PlaylistInfoItemsCollector
|
||||||
|
extends InfoItemsCollector<PlaylistInfoItem, PlaylistInfoItemExtractor> {
|
||||||
|
|
||||||
public PlaylistInfoItemsCollector(int serviceId) {
|
public PlaylistInfoItemsCollector(final int serviceId) {
|
||||||
super(serviceId);
|
super(serviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlaylistInfoItem extract(PlaylistInfoItemExtractor extractor) throws ParsingException {
|
public PlaylistInfoItem extract(final PlaylistInfoItemExtractor extractor)
|
||||||
|
throws ParsingException {
|
||||||
String name = extractor.getName();
|
final PlaylistInfoItem resultItem = new PlaylistInfoItem(
|
||||||
int serviceId = getServiceId();
|
getServiceId(), extractor.getUrl(), extractor.getName());
|
||||||
String url = extractor.getUrl();
|
|
||||||
|
|
||||||
PlaylistInfoItem resultItem = new PlaylistInfoItem(serviceId, url, name);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resultItem.setUploaderName(extractor.getUploaderName());
|
resultItem.setUploaderName(extractor.getUploaderName());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
|
resultItem.setUploaderUrl(extractor.getUploaderUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resultItem.setUploaderVerified(extractor.isUploaderVerified());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resultItem.setThumbnails(extractor.getThumbnails());
|
||||||
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
resultItem.setStreamCount(extractor.getStreamCount());
|
resultItem.setStreamCount(extractor.getStreamCount());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resultItem.setDescription(extractor.getDescription());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resultItem.setPlaylistType(extractor.getPlaylistType());
|
||||||
|
} catch (final Exception e) {
|
||||||
addError(e);
|
addError(e);
|
||||||
}
|
}
|
||||||
return resultItem;
|
return resultItem;
|
||||||
|
|
|
@ -2,22 +2,24 @@ package org.schabi.newpipe.extractor.search;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class SearchExtractor extends ListExtractor<InfoItem> {
|
public abstract class SearchExtractor extends ListExtractor<InfoItem> {
|
||||||
|
|
||||||
public static class NothingFoundException extends ExtractionException {
|
public static class NothingFoundException extends ExtractionException {
|
||||||
public NothingFoundException(String message) {
|
public NothingFoundException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchExtractor(StreamingService service, SearchQueryHandler linkHandler) {
|
public SearchExtractor(final StreamingService service, final SearchQueryHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +27,18 @@ public abstract class SearchExtractor extends ListExtractor<InfoItem> {
|
||||||
return getLinkHandler().getSearchString();
|
return getLinkHandler().getSearchString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search suggestion provided by the service.
|
||||||
|
* <p>
|
||||||
|
* This method also returns the corrected query if
|
||||||
|
* {@link SearchExtractor#isCorrectedSearch()} is true.
|
||||||
|
*
|
||||||
|
* @return a suggestion to another query, the corrected query, or an empty String.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract String getSearchSuggestion() throws ParsingException;
|
public abstract String getSearchSuggestion() throws ParsingException;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public SearchQueryHandler getLinkHandler() {
|
public SearchQueryHandler getLinkHandler() {
|
||||||
return (SearchQueryHandler) super.getLinkHandler();
|
return (SearchQueryHandler) super.getLinkHandler();
|
||||||
|
@ -37,4 +49,24 @@ public abstract class SearchExtractor extends ListExtractor<InfoItem> {
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return getLinkHandler().getSearchString();
|
return getLinkHandler().getSearchString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell if the search was corrected by the service (if it's not exactly the search you typed).
|
||||||
|
* <p>
|
||||||
|
* Example: on YouTube, if you search for "pewdeipie",
|
||||||
|
* it will give you results for "pewdiepie", then isCorrectedSearch should return true.
|
||||||
|
*
|
||||||
|
* @return whether the results comes from a corrected query or not.
|
||||||
|
*/
|
||||||
|
public abstract boolean isCorrectedSearch() throws ParsingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta information about the search query.
|
||||||
|
* <p>
|
||||||
|
* Example: on YouTube, if you search for "Covid-19",
|
||||||
|
* there is a box with information from the WHO about Covid-19 and a link to the WHO's website.
|
||||||
|
* @return additional meta information about the search query
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public abstract List<MetaInfo> getMetaInfo() throws ParsingException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,34 +3,42 @@ package org.schabi.newpipe.extractor.search;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public class SearchInfo extends ListInfo<InfoItem> {
|
public class SearchInfo extends ListInfo<InfoItem> {
|
||||||
|
private final String searchString;
|
||||||
private String searchString;
|
|
||||||
private String searchSuggestion;
|
private String searchSuggestion;
|
||||||
|
private boolean isCorrectedSearch;
|
||||||
|
private List<MetaInfo> metaInfo = List.of();
|
||||||
|
|
||||||
public SearchInfo(int serviceId,
|
public SearchInfo(final int serviceId,
|
||||||
SearchQueryHandler qIHandler,
|
final SearchQueryHandler qIHandler,
|
||||||
String searchString) {
|
final String searchString) {
|
||||||
super(serviceId, qIHandler, "Search");
|
super(serviceId, qIHandler, "Search");
|
||||||
this.searchString = searchString;
|
this.searchString = searchString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static SearchInfo getInfo(StreamingService service, SearchQueryHandler searchQuery) throws ExtractionException, IOException {
|
public static SearchInfo getInfo(final StreamingService service,
|
||||||
SearchExtractor extractor = service.getSearchExtractor(searchQuery);
|
final SearchQueryHandler searchQuery)
|
||||||
|
throws ExtractionException, IOException {
|
||||||
|
final SearchExtractor extractor = service.getSearchExtractor(searchQuery);
|
||||||
extractor.fetchPage();
|
extractor.fetchPage();
|
||||||
return getInfo(extractor);
|
return getInfo(extractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SearchInfo getInfo(SearchExtractor extractor) throws ExtractionException, IOException {
|
public static SearchInfo getInfo(final SearchExtractor extractor)
|
||||||
|
throws ExtractionException, IOException {
|
||||||
final SearchInfo info = new SearchInfo(
|
final SearchInfo info = new SearchInfo(
|
||||||
extractor.getServiceId(),
|
extractor.getServiceId(),
|
||||||
extractor.getLinkHandler(),
|
extractor.getLinkHandler(),
|
||||||
|
@ -38,36 +46,68 @@ public class SearchInfo extends ListInfo<InfoItem> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info.setOriginalUrl(extractor.getOriginalUrl());
|
info.setOriginalUrl(extractor.getOriginalUrl());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
info.searchSuggestion = extractor.getSearchSuggestion();
|
info.setSearchSuggestion(extractor.getSearchSuggestion());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
info.setIsCorrectedSearch(extractor.isCorrectedSearch());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
info.addError(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
info.setMetaInfo(extractor.getMetaInfo());
|
||||||
|
} catch (final Exception e) {
|
||||||
info.addError(e);
|
info.addError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListExtractor.InfoItemsPage<InfoItem> page = ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
final ListExtractor.InfoItemsPage<InfoItem> page
|
||||||
|
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||||
info.setRelatedItems(page.getItems());
|
info.setRelatedItems(page.getItems());
|
||||||
info.setNextPageUrl(page.getNextPageUrl());
|
info.setNextPage(page.getNextPage());
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static ListExtractor.InfoItemsPage<InfoItem> getMoreItems(StreamingService service,
|
public static ListExtractor.InfoItemsPage<InfoItem> getMoreItems(final StreamingService service,
|
||||||
SearchQueryHandler query,
|
final SearchQueryHandler query,
|
||||||
String pageUrl)
|
final Page page)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
return service.getSearchExtractor(query).getPage(pageUrl);
|
return service.getSearchExtractor(query).getPage(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter
|
// Getter
|
||||||
public String getSearchString() {
|
public String getSearchString() {
|
||||||
return searchString;
|
return this.searchString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSearchSuggestion() {
|
public String getSearchSuggestion() {
|
||||||
return searchSuggestion;
|
return this.searchSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCorrectedSearch() {
|
||||||
|
return this.isCorrectedSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsCorrectedSearch(final boolean isCorrectedSearch) {
|
||||||
|
this.isCorrectedSearch = isCorrectedSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSearchSuggestion(final String searchSuggestion) {
|
||||||
|
this.searchSuggestion = searchSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<MetaInfo> getMetaInfo() {
|
||||||
|
return metaInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMetaInfo(@Nonnull final List<MetaInfo> metaInfo) {
|
||||||
|
this.metaInfo = metaInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||||
|
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.FEATURED_API_URL;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.KIOSK_FEATURED;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.KIOSK_RADIO;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.RADIO_API_URL;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampChannelExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampChannelTabExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampCommentsExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampPlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioStreamExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampSearchExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampSuggestionExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampChannelLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampChannelTabLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampCommentsLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampFeaturedLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampPlaylistLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampSearchQueryHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampStreamLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class BandcampService extends StreamingService {
|
||||||
|
|
||||||
|
public BandcampService(final int id) {
|
||||||
|
super(id, "Bandcamp", Arrays.asList(AUDIO, COMMENTS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinkHandlerFactory getStreamLHFactory() {
|
||||||
|
return BandcampStreamLinkHandlerFactory.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListLinkHandlerFactory getChannelLHFactory() {
|
||||||
|
return BandcampChannelLinkHandlerFactory.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListLinkHandlerFactory getChannelTabLHFactory() {
|
||||||
|
return BandcampChannelTabLinkHandlerFactory.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListLinkHandlerFactory getPlaylistLHFactory() {
|
||||||
|
return BandcampPlaylistLinkHandlerFactory.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchQueryHandlerFactory getSearchQHFactory() {
|
||||||
|
return BandcampSearchQueryHandlerFactory.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListLinkHandlerFactory getCommentsLHFactory() {
|
||||||
|
return BandcampCommentsLinkHandlerFactory.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchExtractor getSearchExtractor(final SearchQueryHandler queryHandler) {
|
||||||
|
return new BandcampSearchExtractor(this, queryHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SuggestionExtractor getSuggestionExtractor() {
|
||||||
|
return new BandcampSuggestionExtractor(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SubscriptionExtractor getSubscriptionExtractor() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KioskList getKioskList() throws ExtractionException {
|
||||||
|
final KioskList kioskList = new KioskList(this);
|
||||||
|
final ListLinkHandlerFactory h = BandcampFeaturedLinkHandlerFactory.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
kioskList.addKioskEntry(
|
||||||
|
(streamingService, url, kioskId) -> new BandcampFeaturedExtractor(
|
||||||
|
BandcampService.this,
|
||||||
|
h.fromUrl(FEATURED_API_URL),
|
||||||
|
kioskId
|
||||||
|
),
|
||||||
|
h,
|
||||||
|
KIOSK_FEATURED
|
||||||
|
);
|
||||||
|
|
||||||
|
kioskList.addKioskEntry(
|
||||||
|
(streamingService, url, kioskId) -> new BandcampRadioExtractor(
|
||||||
|
BandcampService.this,
|
||||||
|
h.fromUrl(RADIO_API_URL),
|
||||||
|
kioskId
|
||||||
|
),
|
||||||
|
h,
|
||||||
|
KIOSK_RADIO
|
||||||
|
);
|
||||||
|
|
||||||
|
kioskList.setDefaultKiosk(KIOSK_FEATURED);
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new ExtractionException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return kioskList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelExtractor getChannelExtractor(final ListLinkHandler linkHandler) {
|
||||||
|
return new BandcampChannelExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelTabExtractor getChannelTabExtractor(final ListLinkHandler linkHandler) {
|
||||||
|
if (linkHandler instanceof ReadyChannelTabListLinkHandler) {
|
||||||
|
return ((ReadyChannelTabListLinkHandler) linkHandler).getChannelTabExtractor(this);
|
||||||
|
} else {
|
||||||
|
return new BandcampChannelTabExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
|
||||||
|
return new BandcampPlaylistExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamExtractor getStreamExtractor(final LinkHandler linkHandler) {
|
||||||
|
if (BandcampExtractorHelper.isRadioUrl(linkHandler.getUrl())) {
|
||||||
|
return new BandcampRadioStreamExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
|
return new BandcampStreamExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommentsExtractor getCommentsExtractor(final ListLinkHandler linkHandler) {
|
||||||
|
return new BandcampCommentsExtractor(this, linkHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class BandcampAlbumInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||||
|
private final JsonObject albumInfoItem;
|
||||||
|
private final String uploaderUrl;
|
||||||
|
|
||||||
|
public BandcampAlbumInfoItemExtractor(final JsonObject albumInfoItem,
|
||||||
|
final String uploaderUrl) {
|
||||||
|
this.albumInfoItem = albumInfoItem;
|
||||||
|
this.uploaderUrl = uploaderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return albumInfoItem.getString("title");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() throws ParsingException {
|
||||||
|
return BandcampExtractorHelper.getStreamUrlFromIds(
|
||||||
|
albumInfoItem.getLong("band_id"),
|
||||||
|
albumInfoItem.getLong("item_id"),
|
||||||
|
albumInfoItem.getString("item_type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
return BandcampExtractorHelper.getImagesFromImageId(albumInfoItem.getLong("art_id"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() throws ParsingException {
|
||||||
|
return albumInfoItem.getString("band_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return uploaderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUploaderVerified() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() {
|
||||||
|
return ListExtractor.ITEM_COUNT_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN;
|
||||||
|
import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getArtistDetails;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.Image.ResolutionLevel;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampChannelTabLinkHandlerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class BandcampChannelExtractor extends ChannelExtractor {
|
||||||
|
|
||||||
|
private JsonObject channelInfo;
|
||||||
|
|
||||||
|
public BandcampChannelExtractor(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getAvatars() {
|
||||||
|
return getImagesFromImageId(channelInfo.getLong("bio_image_id"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getBanners() throws ParsingException {
|
||||||
|
/*
|
||||||
|
* Mobile API does not return the header or not the correct header.
|
||||||
|
* Therefore, we need to query the website
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
final String html = getDownloader()
|
||||||
|
.get(replaceHttpWithHttps(channelInfo.getString("bandcamp_url")))
|
||||||
|
.responseBody();
|
||||||
|
|
||||||
|
return Stream.of(Jsoup.parse(html).getElementById("customHeader"))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.flatMap(element -> element.getElementsByTag("img").stream())
|
||||||
|
.map(element -> element.attr("src"))
|
||||||
|
.filter(url -> !url.isEmpty())
|
||||||
|
.map(url -> new Image(
|
||||||
|
replaceHttpWithHttps(url), HEIGHT_UNKNOWN, WIDTH_UNKNOWN,
|
||||||
|
ResolutionLevel.UNKNOWN))
|
||||||
|
.collect(Collectors.toUnmodifiableList());
|
||||||
|
|
||||||
|
} catch (final IOException | ReCaptchaException e) {
|
||||||
|
throw new ParsingException("Could not download artist web site", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bandcamp discontinued their RSS feeds because it hadn't been used enough.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFeedUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSubscriberCount() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return channelInfo.getString("bio");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getParentChannelName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getParentChannelUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getParentChannelAvatars() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVerified() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<ListLinkHandler> getTabs() throws ParsingException {
|
||||||
|
final JsonArray discography = channelInfo.getArray("discography");
|
||||||
|
final TabExtractorBuilder builder = new TabExtractorBuilder(discography);
|
||||||
|
|
||||||
|
final List<ListLinkHandler> tabs = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean foundTrackItem = false;
|
||||||
|
boolean foundAlbumItem = false;
|
||||||
|
|
||||||
|
for (final Object discographyItem : discography) {
|
||||||
|
if (foundTrackItem && foundAlbumItem) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(discographyItem instanceof JsonObject)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject discographyJsonItem = (JsonObject) discographyItem;
|
||||||
|
final String itemType = discographyJsonItem.getString("item_type");
|
||||||
|
|
||||||
|
if (!foundTrackItem && "track".equals(itemType)) {
|
||||||
|
foundTrackItem = true;
|
||||||
|
tabs.add(new ReadyChannelTabListLinkHandler(getUrl()
|
||||||
|
+ BandcampChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.TRACKS),
|
||||||
|
getId(),
|
||||||
|
ChannelTabs.TRACKS,
|
||||||
|
builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundAlbumItem && "album".equals(itemType)) {
|
||||||
|
foundAlbumItem = true;
|
||||||
|
tabs.add(new ReadyChannelTabListLinkHandler(getUrl()
|
||||||
|
+ BandcampChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.ALBUMS),
|
||||||
|
getId(),
|
||||||
|
ChannelTabs.ALBUMS,
|
||||||
|
builder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.unmodifiableList(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
channelInfo = getArtistDetails(getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return channelInfo.getString("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TabExtractorBuilder
|
||||||
|
implements ReadyChannelTabListLinkHandler.ChannelTabExtractorBuilder {
|
||||||
|
private final JsonArray discography;
|
||||||
|
|
||||||
|
TabExtractorBuilder(final JsonArray discography) {
|
||||||
|
this.discography = discography;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public ChannelTabExtractor build(@Nonnull final StreamingService service,
|
||||||
|
@Nonnull final ListLinkHandler linkHandler) {
|
||||||
|
return BandcampChannelTabExtractor.fromDiscography(service, linkHandler, discography);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromSearchResult;
|
||||||
|
|
||||||
|
public class BandcampChannelInfoItemExtractor implements ChannelInfoItemExtractor {
|
||||||
|
|
||||||
|
private final Element resultInfo;
|
||||||
|
private final Element searchResult;
|
||||||
|
|
||||||
|
public BandcampChannelInfoItemExtractor(final Element searchResult) {
|
||||||
|
this.searchResult = searchResult;
|
||||||
|
resultInfo = searchResult.getElementsByClass("result-info").first();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return resultInfo.getElementsByClass("heading").text();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() throws ParsingException {
|
||||||
|
return resultInfo.getElementsByClass("itemurl").text();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
return getImagesFromSearchResult(searchResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return resultInfo.getElementsByClass("subhead").text();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSubscriberCount() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVerified() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampDiscographStreamInfoItemExtractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class BandcampChannelTabExtractor extends ChannelTabExtractor {
|
||||||
|
private JsonArray discography;
|
||||||
|
private final String filter;
|
||||||
|
|
||||||
|
public BandcampChannelTabExtractor(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
|
||||||
|
final String tab = linkHandler.getContentFilters().get(0);
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
filter = "track";
|
||||||
|
break;
|
||||||
|
case ChannelTabs.ALBUMS:
|
||||||
|
filter = "album";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported channel tab: " + tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BandcampChannelTabExtractor fromDiscography(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler,
|
||||||
|
final JsonArray discography) {
|
||||||
|
final BandcampChannelTabExtractor tabExtractor =
|
||||||
|
new BandcampChannelTabExtractor(service, linkHandler);
|
||||||
|
tabExtractor.discography = discography;
|
||||||
|
return tabExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader) throws ParsingException {
|
||||||
|
if (discography == null) {
|
||||||
|
discography = BandcampExtractorHelper.getArtistDetails(getId())
|
||||||
|
.getArray("discography");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||||
|
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
for (final Object discograph : discography) {
|
||||||
|
// A discograph is as an item appears in a discography
|
||||||
|
if (!(discograph instanceof JsonObject)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject discographJsonObject = (JsonObject) discograph;
|
||||||
|
final String itemType = discographJsonObject.getString("item_type", "");
|
||||||
|
|
||||||
|
if (!itemType.equals(filter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (itemType) {
|
||||||
|
case "track":
|
||||||
|
collector.commit(new BandcampDiscographStreamInfoItemExtractor(
|
||||||
|
discographJsonObject, getUrl()));
|
||||||
|
break;
|
||||||
|
case "album":
|
||||||
|
collector.commit(new BandcampAlbumInfoItemExtractor(
|
||||||
|
discographJsonObject, getUrl()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfoItemsPage<>(collector, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<InfoItem> getPage(final Page page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class BandcampCommentsExtractor extends CommentsExtractor {
|
||||||
|
|
||||||
|
private static final String REVIEWS_API_URL = BASE_API_URL + "/tralbumcollectors/2/reviews";
|
||||||
|
|
||||||
|
private Document document;
|
||||||
|
|
||||||
|
|
||||||
|
public BandcampCommentsExtractor(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
document = Jsoup.parse(downloader.get(getLinkHandler().getUrl()).responseBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<CommentsInfoItem> getInitialPage()
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
|
||||||
|
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
final JsonObject collectorsData = JsonUtils.toJsonObject(
|
||||||
|
document.getElementById("collectors-data").attr("data-blob"));
|
||||||
|
final JsonArray reviews = collectorsData.getArray("reviews");
|
||||||
|
|
||||||
|
for (final Object review : reviews) {
|
||||||
|
collector.commit(
|
||||||
|
new BandcampCommentsInfoItemExtractor((JsonObject) review, getUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collectorsData.getBoolean("more_reviews_available")) {
|
||||||
|
return new InfoItemsPage<>(collector, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String trackId = getTrackId();
|
||||||
|
final String token = getNextPageToken(reviews);
|
||||||
|
return new InfoItemsPage<>(collector, new Page(List.of(trackId, token)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
|
||||||
|
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
final List<String> pageIds = page.getIds();
|
||||||
|
final String trackId = pageIds.get(0);
|
||||||
|
final String token = pageIds.get(1);
|
||||||
|
final JsonObject reviewsData = fetchReviewsData(trackId, token);
|
||||||
|
final JsonArray reviews = reviewsData.getArray("results");
|
||||||
|
|
||||||
|
for (final Object review : reviews) {
|
||||||
|
collector.commit(
|
||||||
|
new BandcampCommentsInfoItemExtractor((JsonObject) review, getUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reviewsData.getBoolean("more_available")) {
|
||||||
|
return new InfoItemsPage<>(collector, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfoItemsPage<>(collector,
|
||||||
|
new Page(List.of(trackId, getNextPageToken(reviews))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject fetchReviewsData(final String trackId, final String token)
|
||||||
|
throws ParsingException {
|
||||||
|
try {
|
||||||
|
return JsonUtils.toJsonObject(getDownloader().postWithContentTypeJson(
|
||||||
|
REVIEWS_API_URL,
|
||||||
|
Collections.emptyMap(),
|
||||||
|
JsonWriter.string().object()
|
||||||
|
.value("tralbum_type", "t")
|
||||||
|
.value("tralbum_id", trackId)
|
||||||
|
.value("token", token)
|
||||||
|
.value("count", 7)
|
||||||
|
.array("exclude_fan_ids").end()
|
||||||
|
.end().done().getBytes(StandardCharsets.UTF_8)).responseBody());
|
||||||
|
} catch (final IOException | ReCaptchaException e) {
|
||||||
|
throw new ParsingException("Could not fetch reviews", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNextPageToken(final JsonArray reviews) throws ParsingException {
|
||||||
|
return reviews.stream()
|
||||||
|
.filter(JsonObject.class::isInstance)
|
||||||
|
.map(JsonObject.class::cast)
|
||||||
|
.map(review -> review.getString("token"))
|
||||||
|
.reduce((a, b) -> b) // keep only the last element
|
||||||
|
.orElseThrow(() -> new ParsingException("Could not get token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTrackId() throws ParsingException {
|
||||||
|
final JsonObject pageProperties = JsonUtils.toJsonObject(
|
||||||
|
document.selectFirst("meta[name=bc-page-properties]")
|
||||||
|
.attr("content"));
|
||||||
|
return Long.toString(pageProperties.getLong("item_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCommentsDisabled() throws ExtractionException {
|
||||||
|
return BandcampExtractorHelper.isRadioUrl(getUrl());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BandcampCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
|
||||||
|
|
||||||
|
private final JsonObject review;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
public BandcampCommentsInfoItemExtractor(final JsonObject review, final String url) {
|
||||||
|
this.review = review;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return getCommentText().getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
return getUploaderAvatars();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Description getCommentText() throws ParsingException {
|
||||||
|
return new Description(review.getString("why"), Description.PLAIN_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() throws ParsingException {
|
||||||
|
return review.getString("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getUploaderAvatars() {
|
||||||
|
return getImagesFromImageId(review.getLong("image_id"), false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,293 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.Image.ResolutionLevel;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
|
import org.schabi.newpipe.extractor.utils.ImageSuffix;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.DateTimeException;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN;
|
||||||
|
import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
|
||||||
|
|
||||||
|
public final class BandcampExtractorHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of image IDs which preserve aspect ratio with their theoretical dimension known.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Bandcamp images are not always squares, so images which preserve aspect ratio are only used.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* One of the direct consequences of this specificity is that only one dimension of images is
|
||||||
|
* known at time, depending of the image ID.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Note also that dimensions are only theoretical because if the image size is less than the
|
||||||
|
* dimensions of the image ID, it will be not upscaled but kept to its original size.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* IDs come from <a href="https://gist.github.com/f2k1de/06f5fd0ae9c919a7c3693a44ee522213">the
|
||||||
|
* GitHub Gist "Bandcamp File Format Parameters" by f2k1de</a>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private static final List<ImageSuffix> IMAGE_URL_SUFFIXES_AND_RESOLUTIONS = List.of(
|
||||||
|
// ID | HEIGHT | WIDTH
|
||||||
|
new ImageSuffix("10.jpg", HEIGHT_UNKNOWN, 1200, ResolutionLevel.HIGH),
|
||||||
|
new ImageSuffix("101.jpg", 90, WIDTH_UNKNOWN, ResolutionLevel.LOW),
|
||||||
|
new ImageSuffix("170.jpg", 422, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
// 180 returns the same image aspect ratio and size as 171
|
||||||
|
new ImageSuffix("171.jpg", 646, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
new ImageSuffix("20.jpg", HEIGHT_UNKNOWN, 1024, ResolutionLevel.HIGH),
|
||||||
|
// 203 returns the same image aspect ratio and size as 200
|
||||||
|
new ImageSuffix("200.jpg", 420, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
new ImageSuffix("201.jpg", 280, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
new ImageSuffix("202.jpg", 140, WIDTH_UNKNOWN, ResolutionLevel.LOW),
|
||||||
|
new ImageSuffix("204.jpg", 360, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
new ImageSuffix("205.jpg", 240, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
new ImageSuffix("206.jpg", 180, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM),
|
||||||
|
new ImageSuffix("207.jpg", 120, WIDTH_UNKNOWN, ResolutionLevel.LOW),
|
||||||
|
new ImageSuffix("43.jpg", 100, WIDTH_UNKNOWN, ResolutionLevel.LOW),
|
||||||
|
new ImageSuffix("44.jpg", 200, WIDTH_UNKNOWN, ResolutionLevel.MEDIUM));
|
||||||
|
|
||||||
|
private static final String IMAGE_URL_APPENDIX_AND_EXTENSION_REGEX = "_\\d+\\.\\w+";
|
||||||
|
private static final String IMAGES_DOMAIN_AND_PATH = "https://f4.bcbits.com/img/";
|
||||||
|
|
||||||
|
public static final String BASE_URL = "https://bandcamp.com";
|
||||||
|
public static final String BASE_API_URL = BASE_URL + "/api";
|
||||||
|
|
||||||
|
private BandcampExtractorHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate all these parameters together to the URL of the corresponding album or track
|
||||||
|
* using the mobile API
|
||||||
|
*/
|
||||||
|
public static String getStreamUrlFromIds(final long bandId,
|
||||||
|
final long itemId,
|
||||||
|
final String itemType) throws ParsingException {
|
||||||
|
try {
|
||||||
|
final String jsonString = NewPipe.getDownloader().get(
|
||||||
|
BASE_API_URL + "/mobile/22/tralbum_details?band_id=" + bandId
|
||||||
|
+ "&tralbum_id=" + itemId + "&tralbum_type=" + itemType.charAt(0))
|
||||||
|
.responseBody();
|
||||||
|
|
||||||
|
return replaceHttpWithHttps(JsonParser.object().from(jsonString)
|
||||||
|
.getString("bandcamp_url"));
|
||||||
|
|
||||||
|
} catch (final JsonParserException | ReCaptchaException | IOException e) {
|
||||||
|
throw new ParsingException("Ids could not be translated to URL", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch artist details from mobile endpoint.
|
||||||
|
* <a href="https://notabug.org/fynngodau/bandcampDirect/wiki/rewindBandcamp+%E2%80%93+Fetching+artist+details">
|
||||||
|
* More technical info.</a>
|
||||||
|
*/
|
||||||
|
public static JsonObject getArtistDetails(final String id) throws ParsingException {
|
||||||
|
try {
|
||||||
|
return JsonParser.object().from(NewPipe.getDownloader().postWithContentTypeJson(
|
||||||
|
BASE_API_URL + "/mobile/22/band_details",
|
||||||
|
Collections.emptyMap(),
|
||||||
|
JsonWriter.string()
|
||||||
|
.object()
|
||||||
|
.value("band_id", id)
|
||||||
|
.end()
|
||||||
|
.done()
|
||||||
|
.getBytes(StandardCharsets.UTF_8)).responseBody());
|
||||||
|
} catch (final IOException | ReCaptchaException | JsonParserException e) {
|
||||||
|
throw new ParsingException("Could not download band details", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an image url from an image ID.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The image ID {@code 10} was chosen because it provides images wide up to 1200px (when
|
||||||
|
* the original image width is more than or equal this resolution).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Other integer values are possible as well (e.g. 0 is a very large resolution, possibly the
|
||||||
|
* original); see {@link #IMAGE_URL_SUFFIXES_AND_RESOLUTIONS} for more details about image
|
||||||
|
* resolution IDs.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id the image ID
|
||||||
|
* @param isAlbum whether the image is the cover of an album or a track
|
||||||
|
* @return a URL of the image with this ID with a width up to 1200px
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static String getImageUrl(final long id, final boolean isAlbum) {
|
||||||
|
return IMAGES_DOMAIN_AND_PATH + (isAlbum ? 'a' : "") + id + "_10.jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return <code>true</code> if the given URL looks like it comes from a bandcamp custom domain
|
||||||
|
* or if it comes from <code>bandcamp.com</code> itself
|
||||||
|
*/
|
||||||
|
public static boolean isSupportedDomain(final String url) throws ParsingException {
|
||||||
|
|
||||||
|
// Accept all bandcamp.com URLs
|
||||||
|
if (url.toLowerCase().matches("https?://.+\\.bandcamp\\.com(/.*)?")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test other URLs for whether they contain a footer that links to bandcamp
|
||||||
|
return Jsoup.parse(NewPipe.getDownloader().get(url).responseBody())
|
||||||
|
.getElementById("pgFt")
|
||||||
|
.getElementById("pgFt-inner")
|
||||||
|
.getElementById("footer-logo-wrapper")
|
||||||
|
.getElementById("footer-logo")
|
||||||
|
.getElementsByClass("hiddenAccess")
|
||||||
|
.text().equals("Bandcamp");
|
||||||
|
} catch (final NullPointerException e) {
|
||||||
|
return false;
|
||||||
|
} catch (final IOException | ReCaptchaException e) {
|
||||||
|
throw new ParsingException("Could not determine whether URL is custom domain "
|
||||||
|
+ "(not available? network error?)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the URL points to a radio kiosk.
|
||||||
|
* @param url the URL to check
|
||||||
|
* @return true if the URL matches {@code https://bandcamp.com/?show=SHOW_ID}
|
||||||
|
*/
|
||||||
|
public static boolean isRadioUrl(final String url) {
|
||||||
|
return url.toLowerCase().matches("https?://bandcamp\\.com/\\?show=\\d+");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateWrapper parseDate(final String textDate) throws ParsingException {
|
||||||
|
try {
|
||||||
|
final ZonedDateTime zonedDateTime = ZonedDateTime.parse(textDate,
|
||||||
|
DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH));
|
||||||
|
return new DateWrapper(zonedDateTime.toOffsetDateTime(), false);
|
||||||
|
} catch (final DateTimeException e) {
|
||||||
|
throw new ParsingException("Could not parse date '" + textDate + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of images from a search result {@link Element}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method will call {@link #getImagesFromImageUrl(String)} using the first non null and
|
||||||
|
* non empty image URL found from the {@code src} attribute of {@code img} HTML elements, or an
|
||||||
|
* empty string if no valid image URL was found.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param searchResult a search result {@link Element}
|
||||||
|
* @return an unmodifiable list of {@link Image}s, which is never null but can be empty, in the
|
||||||
|
* case where no valid image URL was found
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static List<Image> getImagesFromSearchResult(@Nonnull final Element searchResult) {
|
||||||
|
return getImagesFromImageUrl(searchResult.getElementsByClass("art")
|
||||||
|
.stream()
|
||||||
|
.flatMap(element -> element.getElementsByTag("img").stream())
|
||||||
|
.map(element -> element.attr("src"))
|
||||||
|
.filter(imageUrl -> !isNullOrEmpty(imageUrl))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images which have resolutions preserving aspect ratio from an image URL.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method will remove the image ID and its extension from the end of the URL and then call
|
||||||
|
* {@link #getImagesFromImageBaseUrl(String)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param imageUrl the full URL of an image provided by Bandcamp, such as in its HTML code
|
||||||
|
* @return an unmodifiable list of {@link Image}s, which is never null but can be empty, in the
|
||||||
|
* case where the image URL has been not extracted (and so is null or empty)
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static List<Image> getImagesFromImageUrl(@Nullable final String imageUrl) {
|
||||||
|
if (isNullOrEmpty(imageUrl)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getImagesFromImageBaseUrl(
|
||||||
|
imageUrl.replaceFirst(IMAGE_URL_APPENDIX_AND_EXTENSION_REGEX, "_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images which have resolutions preserving aspect ratio from an image ID.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method will call {@link #getImagesFromImageBaseUrl(String)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id the id of an image provided by Bandcamp
|
||||||
|
* @param isAlbum whether the image is the cover of an album
|
||||||
|
* @return an unmodifiable list of {@link Image}s, which is never null but can be empty, in the
|
||||||
|
* case where the image ID has been not extracted (and so equal to 0)
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static List<Image> getImagesFromImageId(final long id, final boolean isAlbum) {
|
||||||
|
if (id == 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getImagesFromImageBaseUrl(IMAGES_DOMAIN_AND_PATH + (isAlbum ? 'a' : "") + id + "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images resolutions preserving aspect ratio from a base image URL.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Base image URLs are images containing the image path, a {@code a} letter if it comes from an
|
||||||
|
* album, its ID and an underscore.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Images resolutions returned are the ones of {@link #IMAGE_URL_SUFFIXES_AND_RESOLUTIONS}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param baseUrl the base URL of the image
|
||||||
|
* @return an unmodifiable and non-empty list of {@link Image}s
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
private static List<Image> getImagesFromImageBaseUrl(@Nonnull final String baseUrl) {
|
||||||
|
return IMAGE_URL_SUFFIXES_AND_RESOLUTIONS.stream()
|
||||||
|
.map(imageSuffix -> new Image(baseUrl + imageSuffix.getSuffix(),
|
||||||
|
imageSuffix.getHeight(), imageSuffix.getWidth(),
|
||||||
|
imageSuffix.getResolutionLevel()))
|
||||||
|
.collect(Collectors.toUnmodifiableList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||||
|
|
||||||
|
public class BandcampFeaturedExtractor extends KioskExtractor<PlaylistInfoItem> {
|
||||||
|
|
||||||
|
public static final String KIOSK_FEATURED = "Featured";
|
||||||
|
public static final String FEATURED_API_URL = BASE_API_URL + "/mobile/24/bootstrap_data";
|
||||||
|
public static final String MORE_FEATURED_API_URL
|
||||||
|
= BASE_API_URL + "/mobile/24/feed_older_logged_out";
|
||||||
|
|
||||||
|
private JsonObject json;
|
||||||
|
|
||||||
|
public BandcampFeaturedExtractor(final StreamingService streamingService,
|
||||||
|
final ListLinkHandler listLinkHandler,
|
||||||
|
final String kioskId) {
|
||||||
|
super(streamingService, listLinkHandler, kioskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
try {
|
||||||
|
json = JsonParser.object().from(getDownloader().postWithContentTypeJson(
|
||||||
|
FEATURED_API_URL,
|
||||||
|
Collections.emptyMap(),
|
||||||
|
"{\"platform\":\"\",\"version\":0}".getBytes(StandardCharsets.UTF_8))
|
||||||
|
.responseBody());
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ParsingException("Could not parse Bandcamp featured API response", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return KIOSK_FEATURED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<PlaylistInfoItem> getInitialPage()
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final JsonArray featuredStories = json.getObject("feed_content")
|
||||||
|
.getObject("stories")
|
||||||
|
.getArray("featured");
|
||||||
|
|
||||||
|
return extractItems(featuredStories);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InfoItemsPage<PlaylistInfoItem> extractItems(final JsonArray featuredStories) {
|
||||||
|
final PlaylistInfoItemsCollector c = new PlaylistInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
for (int i = 0; i < featuredStories.size(); i++) {
|
||||||
|
final JsonObject featuredStory = featuredStories.getObject(i);
|
||||||
|
|
||||||
|
if (featuredStory.isNull("album_title")) {
|
||||||
|
// Is not an album, ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
c.commit(new BandcampPlaylistInfoItemFeaturedExtractor(featuredStory));
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject lastFeaturedStory = featuredStories.getObject(featuredStories.size() - 1);
|
||||||
|
return new InfoItemsPage<>(c, getNextPageFrom(lastFeaturedStory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next Page can be generated from metadata of last featured story
|
||||||
|
*/
|
||||||
|
private Page getNextPageFrom(final JsonObject lastFeaturedStory) {
|
||||||
|
final long lastStoryDate = lastFeaturedStory.getLong("story_date");
|
||||||
|
final long lastStoryId = lastFeaturedStory.getLong("ntid");
|
||||||
|
final String lastStoryType = lastFeaturedStory.getString("story_type");
|
||||||
|
return new Page(
|
||||||
|
MORE_FEATURED_API_URL + "?story_groups=featured"
|
||||||
|
+ ':' + lastStoryDate + ':' + lastStoryType + ':' + lastStoryId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<PlaylistInfoItem> getPage(final Page page)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
|
||||||
|
final JsonObject response;
|
||||||
|
try {
|
||||||
|
response = JsonParser.object().from(
|
||||||
|
getDownloader().get(page.getUrl()).responseBody()
|
||||||
|
);
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ParsingException("Could not parse Bandcamp featured API response", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractItems(response.getObject("stories").getArray("featured"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor.getAlbumInfoJson;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.JsonUtils.getJsonData;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampPlaylistStreamInfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class BandcampPlaylistExtractor extends PlaylistExtractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An arbitrarily chosen number above which cover arts won't be fetched individually for each
|
||||||
|
* track; instead, it will be assumed that every track has the same cover art as the album,
|
||||||
|
* which is not always the case.
|
||||||
|
*/
|
||||||
|
private static final int MAXIMUM_INDIVIDUAL_COVER_ARTS = 10;
|
||||||
|
|
||||||
|
private Document document;
|
||||||
|
private JsonObject albumJson;
|
||||||
|
private JsonArray trackInfo;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public BandcampPlaylistExtractor(final StreamingService service,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final String html = downloader.get(getLinkHandler().getUrl()).responseBody();
|
||||||
|
document = Jsoup.parse(html);
|
||||||
|
albumJson = getAlbumInfoJson(html);
|
||||||
|
trackInfo = albumJson.getArray("trackinfo");
|
||||||
|
|
||||||
|
try {
|
||||||
|
name = getJsonData(html, "data-embed").getString("album_title");
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ParsingException("Faulty JSON; page likely does not contain album data", e);
|
||||||
|
} catch (final ArrayIndexOutOfBoundsException e) {
|
||||||
|
throw new ParsingException("JSON does not exist", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackInfo.isEmpty()) {
|
||||||
|
// Albums without trackInfo need to be purchased before they can be played
|
||||||
|
throw new PaidContentException("Album needs to be purchased");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
if (albumJson.isNull("art_id")) {
|
||||||
|
return List.of();
|
||||||
|
} else {
|
||||||
|
return getImagesFromImageId(albumJson.getLong("art_id"), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() throws ParsingException {
|
||||||
|
final String[] parts = getUrl().split("/");
|
||||||
|
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
|
||||||
|
return HTTPS + parts[2] + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
return albumJson.getString("artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getUploaderAvatars() {
|
||||||
|
return getImagesFromImageUrl(document.getElementsByClass("band-photo")
|
||||||
|
.stream()
|
||||||
|
.map(element -> element.attr("src"))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUploaderVerified() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() {
|
||||||
|
return trackInfo.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Description getDescription() throws ParsingException {
|
||||||
|
final Element tInfo = document.getElementById("trackInfo");
|
||||||
|
if (tInfo == null) {
|
||||||
|
throw new ParsingException("Could not find trackInfo in document");
|
||||||
|
}
|
||||||
|
final Elements about = tInfo.getElementsByClass("tralbum-about");
|
||||||
|
final Elements credits = tInfo.getElementsByClass("tralbum-credits");
|
||||||
|
final Element license = document.getElementById("license");
|
||||||
|
if (about.isEmpty() && credits.isEmpty() && license == null) {
|
||||||
|
return Description.EMPTY_DESCRIPTION;
|
||||||
|
}
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
if (!about.isEmpty()) {
|
||||||
|
sb.append(Objects.requireNonNull(about.first()).html());
|
||||||
|
}
|
||||||
|
if (!credits.isEmpty()) {
|
||||||
|
sb.append(Objects.requireNonNull(credits.first()).html());
|
||||||
|
}
|
||||||
|
if (license != null) {
|
||||||
|
sb.append(license.html());
|
||||||
|
}
|
||||||
|
return new Description(sb.toString(), Description.HTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
|
||||||
|
|
||||||
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
for (int i = 0; i < trackInfo.size(); i++) {
|
||||||
|
final JsonObject track = trackInfo.getObject(i);
|
||||||
|
|
||||||
|
if (trackInfo.size() < MAXIMUM_INDIVIDUAL_COVER_ARTS) {
|
||||||
|
// Load cover art of every track individually
|
||||||
|
collector.commit(new BandcampPlaylistStreamInfoItemExtractor(
|
||||||
|
track, getUploaderUrl(), getService()));
|
||||||
|
} else {
|
||||||
|
// Pretend every track has the same cover art as the album
|
||||||
|
collector.commit(new BandcampPlaylistStreamInfoItemExtractor(
|
||||||
|
track, getUploaderUrl(), getThumbnails()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfoItemsPage<>(collector, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromSearchResult;
|
||||||
|
|
||||||
|
public class BandcampPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||||
|
private final Element searchResult;
|
||||||
|
private final Element resultInfo;
|
||||||
|
|
||||||
|
public BandcampPlaylistInfoItemExtractor(@Nonnull final Element searchResult) {
|
||||||
|
this.searchResult = searchResult;
|
||||||
|
resultInfo = searchResult.getElementsByClass("result-info").first();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
return resultInfo.getElementsByClass("subhead").text()
|
||||||
|
.split(" by")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUploaderVerified() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() {
|
||||||
|
final String length = resultInfo.getElementsByClass("length").text();
|
||||||
|
return Integer.parseInt(length.split(" track")[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return resultInfo.getElementsByClass("heading").text();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() {
|
||||||
|
return resultInfo.getElementsByClass("itemurl").text();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() {
|
||||||
|
return getImagesFromSearchResult(searchResult);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
|
||||||
|
public class BandcampPlaylistInfoItemFeaturedExtractor implements PlaylistInfoItemExtractor {
|
||||||
|
|
||||||
|
private final JsonObject featuredStory;
|
||||||
|
|
||||||
|
public BandcampPlaylistInfoItemFeaturedExtractor(final JsonObject featuredStory) {
|
||||||
|
this.featuredStory = featuredStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
return featuredStory.getString("band_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUploaderVerified() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() {
|
||||||
|
return featuredStory.getInt("num_streamable_tracks");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return featuredStory.getString("album_title");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() {
|
||||||
|
return Utils.replaceHttpWithHttps(featuredStory.getString("item_url"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() {
|
||||||
|
return featuredStory.has("art_id")
|
||||||
|
? getImagesFromImageId(featuredStory.getLong("art_id"), true)
|
||||||
|
: getImagesFromImageId(featuredStory.getLong("item_art_id"), true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||||
|
|
||||||
|
public class BandcampRadioExtractor extends KioskExtractor<StreamInfoItem> {
|
||||||
|
|
||||||
|
public static final String KIOSK_RADIO = "Radio";
|
||||||
|
public static final String RADIO_API_URL = BASE_API_URL + "/bcweekly/1/list";
|
||||||
|
|
||||||
|
private JsonObject json = null;
|
||||||
|
|
||||||
|
public BandcampRadioExtractor(final StreamingService streamingService,
|
||||||
|
final ListLinkHandler linkHandler,
|
||||||
|
final String kioskId) {
|
||||||
|
super(streamingService, linkHandler, kioskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
try {
|
||||||
|
json = JsonParser.object().from(
|
||||||
|
getDownloader().get(RADIO_API_URL).responseBody());
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ExtractionException("Could not parse Bandcamp Radio API response", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return KIOSK_RADIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
||||||
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
final JsonArray radioShows = json.getArray("results");
|
||||||
|
|
||||||
|
for (int i = 0; i < radioShows.size(); i++) {
|
||||||
|
final JsonObject radioShow = radioShows.getObject(i);
|
||||||
|
collector.commit(new BandcampRadioInfoItemExtractor(radioShow));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfoItemsPage<>(collector, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.parseDate;
|
||||||
|
|
||||||
|
public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
|
||||||
|
|
||||||
|
private final JsonObject show;
|
||||||
|
|
||||||
|
public BandcampRadioInfoItemExtractor(final JsonObject radioShow) {
|
||||||
|
show = radioShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDuration() {
|
||||||
|
/* Duration is only present in the more detailed information that has to be queried
|
||||||
|
separately. Therefore, over 300 queries would be needed every time the kiosk is opened if we
|
||||||
|
were to display the real value. */
|
||||||
|
//return query(show.getInt("id")).getLong("audio_duration");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getTextualUploadDate() {
|
||||||
|
return show.getString("date");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public DateWrapper getUploadDate() throws ParsingException {
|
||||||
|
return parseDate(getTextualUploadDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return show.getString("subtitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() {
|
||||||
|
return BASE_URL + "/?show=" + show.getInt("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() {
|
||||||
|
return getImagesFromImageId(show.getLong("image_id"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return StreamType.AUDIO_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getViewCount() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() {
|
||||||
|
// JSON does not contain uploader name
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUploaderVerified() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAd() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.Image.ResolutionLevel;
|
||||||
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
||||||
|
|
||||||
|
private static final String OPUS_LO = "opus-lo";
|
||||||
|
private static final String MP3_128 = "mp3-128";
|
||||||
|
private JsonObject showInfo;
|
||||||
|
|
||||||
|
public BandcampRadioStreamExtractor(final StreamingService service,
|
||||||
|
final LinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonObject query(final int id) throws ParsingException {
|
||||||
|
try {
|
||||||
|
return JsonParser.object().from(NewPipe.getDownloader()
|
||||||
|
.get(BASE_API_URL + "/bcweekly/1/get?id=" + id).responseBody());
|
||||||
|
} catch (final IOException | ReCaptchaException | JsonParserException e) {
|
||||||
|
throw new ParsingException("could not get show data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
showInfo = query(Integer.parseInt(getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
/* Select "subtitle" and not "audio_title", as the latter would cause a lot of
|
||||||
|
* items to show the same title, e.g. "Bandcamp Weekly".
|
||||||
|
*/
|
||||||
|
return showInfo.getString("subtitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() throws ContentNotSupportedException {
|
||||||
|
throw new ContentNotSupportedException("Fan pages are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getUrl() throws ParsingException {
|
||||||
|
return getLinkHandler().getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() throws ParsingException {
|
||||||
|
return Jsoup.parse(showInfo.getString("image_caption")).getElementsByTag("a").stream()
|
||||||
|
.map(Element::text)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getTextualUploadDate() {
|
||||||
|
return showInfo.getString("published_date");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
return getImagesFromImageId(showInfo.getLong("show_image_id"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getUploaderAvatars() {
|
||||||
|
return Collections.singletonList(
|
||||||
|
new Image(BASE_URL + "/img/buttons/bandcamp-button-circle-whitecolor-512.png",
|
||||||
|
512, 512, ResolutionLevel.MEDIUM));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Description getDescription() {
|
||||||
|
return new Description(showInfo.getString("desc"), Description.PLAIN_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLength() {
|
||||||
|
return showInfo.getLong("audio_duration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AudioStream> getAudioStreams() {
|
||||||
|
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||||
|
final JsonObject streams = showInfo.getObject("audio_stream");
|
||||||
|
|
||||||
|
if (streams.has(MP3_128)) {
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
|
.setId(MP3_128)
|
||||||
|
.setContent(streams.getString(MP3_128), true)
|
||||||
|
.setMediaFormat(MediaFormat.MP3)
|
||||||
|
.setAverageBitrate(128)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.has(OPUS_LO)) {
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
|
.setId(OPUS_LO)
|
||||||
|
.setContent(streams.getString(OPUS_LO), true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(100).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<StreamSegment> getStreamSegments() throws ParsingException {
|
||||||
|
final JsonArray tracks = showInfo.getArray("tracks");
|
||||||
|
final List<StreamSegment> segments = new ArrayList<>(tracks.size());
|
||||||
|
for (final Object t : tracks) {
|
||||||
|
final JsonObject track = (JsonObject) t;
|
||||||
|
final StreamSegment segment = new StreamSegment(
|
||||||
|
track.getString("title"), track.getInt("timecode"));
|
||||||
|
// "track art" is the track's album cover
|
||||||
|
segment.setPreviewUrl(getImageUrl(track.getLong("track_art_id"), true));
|
||||||
|
segment.setChannelName(track.getString("artist"));
|
||||||
|
segments.add(segment);
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getLicence() {
|
||||||
|
// Contrary to other Bandcamp streams, radio streams don't have a license
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
// Contrary to other Bandcamp streams, radio streams don't have categories
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<String> getTags() {
|
||||||
|
// Contrary to other Bandcamp streams, radio streams don't have tags
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlaylistInfoItemsCollector getRelatedItems() {
|
||||||
|
// Contrary to other Bandcamp streams, radio streams don't have related items
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Created by Fynn Godau 2021, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts recommended albums from tracks' website
|
||||||
|
*/
|
||||||
|
public class BandcampRelatedPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||||
|
private final Element relatedAlbum;
|
||||||
|
|
||||||
|
public BandcampRelatedPlaylistInfoItemExtractor(@Nonnull final Element relatedAlbum) {
|
||||||
|
this.relatedAlbum = relatedAlbum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return relatedAlbum.getElementsByClass("release-title").text();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() throws ParsingException {
|
||||||
|
return relatedAlbum.getElementsByClass("album-link").attr("abs:href");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
return getImagesFromImageUrl(relatedAlbum.getElementsByClass("album-art").attr("src"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() throws ParsingException {
|
||||||
|
return relatedAlbum.getElementsByClass("by-artist").text().replace("by ", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() throws ParsingException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUploaderVerified() throws ParsingException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getStreamCount() throws ParsingException {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
|
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
|
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampSearchStreamInfoItemExtractor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class BandcampSearchExtractor extends SearchExtractor {
|
||||||
|
|
||||||
|
public BandcampSearchExtractor(final StreamingService service,
|
||||||
|
final SearchQueryHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSearchSuggestion() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCorrectedSearch() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<MetaInfo> getMetaInfo() throws ParsingException {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfoItemsPage<InfoItem> getPage(final Page page)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
||||||
|
final Document d = Jsoup.parse(getDownloader().get(page.getUrl()).responseBody());
|
||||||
|
|
||||||
|
for (final Element searchResult : d.getElementsByClass("searchresult")) {
|
||||||
|
final String type = searchResult.getElementsByClass("result-info").stream()
|
||||||
|
.flatMap(element -> element.getElementsByClass("itemtype").stream())
|
||||||
|
.map(Element::text)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("");
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "ARTIST":
|
||||||
|
collector.commit(new BandcampChannelInfoItemExtractor(searchResult));
|
||||||
|
break;
|
||||||
|
case "ALBUM":
|
||||||
|
collector.commit(new BandcampPlaylistInfoItemExtractor(searchResult));
|
||||||
|
break;
|
||||||
|
case "TRACK":
|
||||||
|
collector.commit(new BandcampSearchStreamInfoItemExtractor(searchResult, null));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// don't display fan results ("FAN") or other things
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count pages
|
||||||
|
final Elements pageLists = d.getElementsByClass("pagelist");
|
||||||
|
if (pageLists.isEmpty()) {
|
||||||
|
return new InfoItemsPage<>(collector, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Elements pages = pageLists.stream()
|
||||||
|
.map(element -> element.getElementsByTag("li"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseGet(Elements::new);
|
||||||
|
|
||||||
|
// Find current page
|
||||||
|
int currentPage = -1;
|
||||||
|
for (int i = 0; i < pages.size(); i++) {
|
||||||
|
final Element pageElement = pages.get(i);
|
||||||
|
if (!pageElement.getElementsByTag("span").isEmpty()) {
|
||||||
|
currentPage = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search results appear to be capped at six pages
|
||||||
|
assert pages.size() < 10;
|
||||||
|
|
||||||
|
String nextUrl = null;
|
||||||
|
if (currentPage < pages.size()) {
|
||||||
|
nextUrl = page.getUrl().substring(0, page.getUrl().length() - 1) + (currentPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfoItemsPage<>(collector, new Page(nextUrl));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||||
|
return getPage(new Page(getUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageId;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImagesFromImageUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.parseDate;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
|
private JsonObject albumJson;
|
||||||
|
private JsonObject current;
|
||||||
|
private Document document;
|
||||||
|
|
||||||
|
public BandcampStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
|
||||||
|
super(service, linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final String html = downloader.get(getLinkHandler().getUrl()).responseBody();
|
||||||
|
document = Jsoup.parse(html);
|
||||||
|
albumJson = getAlbumInfoJson(html);
|
||||||
|
current = albumJson.getObject("current");
|
||||||
|
|
||||||
|
if (albumJson.getArray("trackinfo").size() > 1) {
|
||||||
|
// In this case, we are actually viewing an album page!
|
||||||
|
throw new ExtractionException("Page is actually an album, not a track");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumJson.getArray("trackinfo").getObject(0).isNull("file")) {
|
||||||
|
throw new PaidContentException("This track is not available without being purchased");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the JSON that contains album's metadata from page
|
||||||
|
*
|
||||||
|
* @param html Website
|
||||||
|
* @return Album metadata JSON
|
||||||
|
* @throws ParsingException In case of a faulty website
|
||||||
|
*/
|
||||||
|
public static JsonObject getAlbumInfoJson(final String html) throws ParsingException {
|
||||||
|
try {
|
||||||
|
return JsonUtils.getJsonData(html, "data-tralbum");
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ParsingException("Faulty JSON; page likely does not contain album data", e);
|
||||||
|
} catch (final ArrayIndexOutOfBoundsException e) {
|
||||||
|
throw new ParsingException("JSON does not exist", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getName() throws ParsingException {
|
||||||
|
return current.getString("title");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getUploaderUrl() throws ParsingException {
|
||||||
|
final String[] parts = getUrl().split("/");
|
||||||
|
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
|
||||||
|
return HTTPS + parts[2] + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getUrl() throws ParsingException {
|
||||||
|
return replaceHttpWithHttps(albumJson.getString("url"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getUploaderName() throws ParsingException {
|
||||||
|
return albumJson.getString("artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getTextualUploadDate() {
|
||||||
|
return current.getString("publish_date");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public DateWrapper getUploadDate() throws ParsingException {
|
||||||
|
return parseDate(getTextualUploadDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getThumbnails() throws ParsingException {
|
||||||
|
if (albumJson.isNull("art_id")) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getImagesFromImageId(albumJson.getLong("art_id"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<Image> getUploaderAvatars() {
|
||||||
|
return getImagesFromImageUrl(document.getElementsByClass("band-photo")
|
||||||
|
.stream()
|
||||||
|
.map(element -> element.attr("src"))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Description getDescription() {
|
||||||
|
final String s = Utils.nonEmptyAndNullJoin("\n\n", current.getString("about"),
|
||||||
|
current.getString("lyrics"), current.getString("credits"));
|
||||||
|
return new Description(s, Description.PLAIN_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AudioStream> getAudioStreams() {
|
||||||
|
return Collections.singletonList(new AudioStream.Builder()
|
||||||
|
.setId("mp3-128")
|
||||||
|
.setContent(albumJson.getArray("trackinfo")
|
||||||
|
.getObject(0)
|
||||||
|
.getObject("file")
|
||||||
|
.getString("mp3-128"), true)
|
||||||
|
.setMediaFormat(MediaFormat.MP3)
|
||||||
|
.setAverageBitrate(128)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLength() throws ParsingException {
|
||||||
|
return (long) albumJson.getArray("trackinfo").getObject(0)
|
||||||
|
.getDouble("duration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<VideoStream> getVideoStreams() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<VideoStream> getVideoOnlyStreams() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return StreamType.AUDIO_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlaylistInfoItemsCollector getRelatedItems() {
|
||||||
|
final PlaylistInfoItemsCollector collector = new PlaylistInfoItemsCollector(getServiceId());
|
||||||
|
document.getElementsByClass("recommended-album")
|
||||||
|
.stream()
|
||||||
|
.map(BandcampRelatedPlaylistInfoItemExtractor::new)
|
||||||
|
.forEach(collector::commit);
|
||||||
|
|
||||||
|
return collector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
// Get first tag from html, which is the artist's Genre
|
||||||
|
return document.getElementsByClass("tralbum-tags").stream()
|
||||||
|
.flatMap(element -> element.getElementsByClass("tag").stream())
|
||||||
|
.map(Element::text)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getLicence() {
|
||||||
|
/*
|
||||||
|
Tests resulted in this mapping of ints to licence:
|
||||||
|
https://cloud.disroot.org/s/ZTWBxbQ9fKRmRWJ/preview (screenshot from a Bandcamp artist's
|
||||||
|
account)
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch (current.getInt("license_type")) {
|
||||||
|
case 1:
|
||||||
|
return "All rights reserved ©";
|
||||||
|
case 2:
|
||||||
|
return "CC BY-NC-ND 3.0";
|
||||||
|
case 3:
|
||||||
|
return "CC BY-NC-SA 3.0";
|
||||||
|
case 4:
|
||||||
|
return "CC BY-NC 3.0";
|
||||||
|
case 5:
|
||||||
|
return "CC BY-ND 3.0";
|
||||||
|
case 6:
|
||||||
|
return "CC BY 3.0";
|
||||||
|
case 8:
|
||||||
|
return "CC BY-SA 3.0";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public List<String> getTags() {
|
||||||
|
return document.getElementsByAttributeValue("itemprop", "keywords")
|
||||||
|
.stream()
|
||||||
|
.map(Element::text)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||||
|
|
||||||
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class BandcampSuggestionExtractor extends SuggestionExtractor {
|
||||||
|
|
||||||
|
private static final String AUTOCOMPLETE_URL = BASE_API_URL + "/fuzzysearch/1/autocomplete?q=";
|
||||||
|
public BandcampSuggestionExtractor(final StreamingService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggestionList(final String query) throws IOException, ExtractionException {
|
||||||
|
final Downloader downloader = NewPipe.getDownloader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final JsonObject fuzzyResults = JsonParser.object().from(downloader
|
||||||
|
.get(AUTOCOMPLETE_URL + Utils.encodeUrlUtf8(query)).responseBody());
|
||||||
|
|
||||||
|
return fuzzyResults.getObject("auto").getArray("results").stream()
|
||||||
|
.filter(JsonObject.class::isInstance)
|
||||||
|
.map(JsonObject.class::cast)
|
||||||
|
.map(jsonObject -> jsonObject.getString("name"))
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue