1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-14 02:32:40 +00:00

Compare commits

...

179 Commits

Author SHA1 Message Date
Christian Schabesberger
6f015349e8 moved on to v0.9.1 2017-04-08 17:17:58 +02:00
Christian Schabesberger
a37802c2b9 add prefered video format 2017-04-08 17:17:11 +02:00
Florian
4fa3baf5e1 Translated using Weblate (French)
Currently translated at 99.3% (158 of 159 strings)
2017-04-07 12:45:03 +02:00
Oscar Hemelaar
ef8c2c81d5 Translated using Weblate (Italian)
Currently translated at 98.7% (157 of 159 strings)
2017-04-07 03:45:31 +02:00
Mladen Pejaković
8c80d8c457 Translated using Weblate (Serbian)
Currently translated at 99.3% (158 of 159 strings)
2017-04-07 00:46:25 +02:00
Matej U
1596872c54 Translated using Weblate (Slovenian)
Currently translated at 100.0% (159 of 159 strings)
2017-04-05 14:47:22 +02:00
Oscar Hemelaar
da8873fa78 Translated using Weblate (French)
Currently translated at 99.3% (158 of 159 strings)
2017-04-05 12:14:32 +02:00
Oscar Hemelaar
ecd8439b3f Translated using Weblate (French)
Currently translated at 98.1% (156 of 159 strings)

if it's for a video restart button, it's "Relancer"
2017-04-05 02:53:08 +02:00
Oscar Hemelaar
5d178532ac Translated using Weblate (French)
Currently translated at 97.4% (155 of 159 strings)
2017-04-05 02:52:24 +02:00
Oscar Hemelaar
08e40a013d Translated using Weblate (French)
Currently translated at 96.8% (154 of 159 strings)

or "OK" ?
2017-04-05 02:50:49 +02:00
Oscar Hemelaar
c136f7363c Translated using Weblate (French)
Currently translated at 96.2% (153 of 159 strings)
2017-04-05 02:50:26 +02:00
Florian
a60f10d739 Translated using Weblate (French)
Currently translated at 90.5% (144 of 159 strings)
2017-04-04 21:45:44 +02:00
naofum
ae46afcb42 Translated using Weblate (Japanese)
Currently translated at 100.0% (159 of 159 strings)
2017-04-04 16:15:27 +02:00
mueller-ma
bffb9f6800 Translated using Weblate (German)
Currently translated at 95.5% (152 of 159 strings)
2017-04-03 09:29:35 +02:00
Tobias Groza
79aa9ad04b Translated using Weblate (German)
Currently translated at 94.9% (151 of 159 strings)
2017-04-03 09:26:41 +02:00
ff5714f04a Translated using Weblate (French)
Currently translated at 89.9% (143 of 159 strings)
2017-04-02 18:59:25 +02:00
zmni
ce499a9766 Translated using Weblate (Indonesian)
Currently translated at 100.0% (159 of 159 strings)
2017-04-02 16:49:33 +02:00
Weblate
d3bb8b7651 Merge remote-tracking branch 'origin/master' 2017-04-02 16:48:37 +02:00
Freddy Morán Jr
6d9c23c4cb Translated using Weblate (Spanish)
Currently translated at 100.0% (158 of 158 strings)
2017-04-02 16:48:36 +02:00
Nathan Follens
b95a9332a9 Translated using Weblate (Dutch)
Currently translated at 100.0% (158 of 158 strings)
2017-04-02 16:48:35 +02:00
zmni
609715eb5c Translated using Weblate (Indonesian)
Currently translated at 100.0% (158 of 158 strings)
2017-04-02 16:48:33 +02:00
Christian Schabesberger
a1266c919c Merge branch 'fix-next-video' of git://github.com/mauriciocolli/NewPipe into strfx 2017-04-02 15:56:41 +02:00
Christian Schabesberger
a1925a0302 Merge branch 'master' of github.com:theScrabi/NewPipe 2017-04-02 15:48:53 +02:00
Christian Schabesberger
a7a4c03372 update features list, and moved on to v0.9.0 2017-04-02 15:47:21 +02:00
Christian Schabesberger
37201600e0 Merge pull request #494 from mueller-ma/patch-1
Fix list in readme
2017-04-02 15:44:14 +02:00
mueller-ma
a94f40ed62 Fix list in readme 2017-04-02 14:33:52 +02:00
naofum
0b08cf8c76 Translated using Weblate (Japanese)
Currently translated at 100.0% (158 of 158 strings)
2017-04-01 13:36:04 +02:00
Mauricio Colli
33e29be7db Fix next video and refactor
- Refactor VideoItemDetailActivity, StreamExtractorWorker
- Remove redundant styles
- Change dimensions
- Nicer animation/transitions
2017-03-31 20:39:54 -03:00
Freddy Morán Jr
bd1c7851c7 Translated using Weblate (Portuguese)
Currently translated at 100.0% (158 of 158 strings)
2017-04-01 01:02:49 +02:00
Freddy Morán Jr
82faea5965 Translated using Weblate (Spanish)
Currently translated at 100.0% (158 of 158 strings)
2017-04-01 00:51:34 +02:00
Weblate
0bbbfd3217 Merge remote-tracking branch 'origin/master' 2017-04-01 00:44:52 +02:00
Freddy Morán Jr
fb578ecda8 Translated using Weblate (Spanish)
Currently translated at 100.0% (156 of 156 strings)
2017-04-01 00:44:41 +02:00
Christian Schabesberger
e804647a65 make exoplayer default player 2017-03-29 09:17:03 +02:00
Christian Schabesberger
c3c3a94593 Merge branch 'feature-popup-fullscreen' of git://github.com/mauriciocolli/NewPipe into toogle 2017-03-29 08:48:00 +02:00
Mauricio Colli
9d55569f80 Fix keep screen on 2017-03-28 09:12:03 -03:00
Christian Schabesberger
5dd8271c15 Merge pull request #489 from mauriciocolli/master
Fix travis and some clean-up
2017-03-27 23:53:04 +02:00
Mauricio Colli
7a4a54c3ea Fix travis
- Remove duplicate of AndroidManifest
- Remove some non-translatable strings from "ar" translation, and general clean-up of other
2017-03-27 16:34:37 -03:00
Mauricio Colli
7c9078a625 Fix non-commited file 2017-03-27 11:26:36 -03:00
Mauricio Colli
71ae342f52 Implement screen orientation toggle 2017-03-27 10:12:22 -03:00
Mauricio Colli
b43c56085d Implement fullscreen and quality selector
- Implement cache
- Abstract player
- Quality selector
- Fullscreen switcher
- Change some drawables
2017-03-27 01:08:16 -03:00
Christian Schabesberger
83d2ab95e0 Merge branch 'master' of github.com:TeamNewPipe/NewPipe 2017-03-26 17:29:25 +02:00
Christian Schabesberger
20a8d7372c fix broken overlay on .svg icon 2017-03-26 17:29:16 +02:00
Weblate
c36ba88db7 Merge remote-tracking branch 'origin/master' 2017-03-25 14:05:16 +01:00
Nathan Follens
4aa23023ee Translated using Weblate (Dutch)
Currently translated at 100.0% (156 of 156 strings)
2017-03-25 14:05:13 +01:00
Christian Schabesberger
8735cf931a update to new backend with playlist support 2017-03-22 20:14:56 +01:00
Weblate
2473069326 Merge remote-tracking branch 'origin/master' 2017-03-21 09:44:34 +01:00
vesp
03bab57a97 Translated using Weblate (Czech)
Currently translated at 99.3% (155 of 156 strings)
2017-03-21 09:44:32 +01:00
Christian Schabesberger
e3baf69533 fix channel rotation problem 2017-03-17 20:35:22 +01:00
Christian Schabesberger
461f747af1 add IOException 2017-03-17 20:18:44 +01:00
Christian Schabesberger
028872a7d8 Merge pull request #483 from mauriciocolli/patch-2
Fix bug when fetching unavailable content
2017-03-17 17:10:03 +01:00
Christian Schabesberger
a1483b6c55 Merge pull request #481 from mauriciocolli/patch-1
Handle embed links
2017-03-17 17:09:13 +01:00
mauriciocolli
e406ba094c Improve bug detection
- Show a message with the appropriate error (network error)
2017-03-17 12:09:20 -03:00
mauriciocolli
c100d15ba8 Fix bug when fetching unavailable content
- Fix #482
- When opening a invalid/deleted/unavailable video, the popup was just printing the error, now it shows a message to the user and exits
2017-03-17 11:50:27 -03:00
mauriciocolli
b4ea592638 Handle embed links
- Fix #480
- Add /embed/ to intent filter, as the app already handles these type of links internally
2017-03-17 00:27:59 -03:00
Sérgio Marques
16b757d9a3 Translated using Weblate (Portuguese)
Currently translated at 100.0% (156 of 156 strings)
2017-03-17 00:46:17 +01:00
Sérgio Marques
2aa801a392 Translated using Weblate (Portuguese)
Currently translated at 100.0% (156 of 156 strings)
2017-03-14 23:24:24 +01:00
Matej U
b838344526 Translated using Weblate (Slovenian)
Currently translated at 100.0% (156 of 156 strings)
2017-03-13 10:30:41 +01:00
zmni
233a3df222 Translated using Weblate (Indonesian)
Currently translated at 100.0% (156 of 156 strings)
2017-03-11 13:27:00 +01:00
naofum
da6661b1ea Translated using Weblate (Japanese)
Currently translated at 100.0% (156 of 156 strings)
2017-03-11 12:37:49 +01:00
Mladen Pejaković
c6e120fc51 Translated using Weblate (Serbian)
Currently translated at 100.0% (156 of 156 strings)
2017-03-10 23:09:42 +01:00
Marian Hanzel
c416a1254d Translated using Weblate (Slovak)
Currently translated at 100.0% (156 of 156 strings)
2017-03-10 23:07:10 +01:00
Weblate
2aa4f6ddda Merge remote-tracking branch 'origin/master' 2017-03-10 21:45:11 +01:00
Enrico Monese
129597023d Translated using Weblate (Italian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-10 21:45:10 +01:00
vesp
02aed86b7e Translated using Weblate (Czech)
Currently translated at 98.7% (152 of 154 strings)
2017-03-10 21:45:10 +01:00
Tobias Groza
e44f4b5823 Translated using Weblate (German)
Currently translated at 98.7% (152 of 154 strings)
2017-03-10 21:45:08 +01:00
Christian Schabesberger
2f0c0f0fc2 Merge branch 'master' of github.com:TeamNewPipe/NewPipe 2017-03-09 16:21:36 +01:00
Christian Schabesberger
e5ce3f3007 moved on to version 0.8.12 2017-03-09 16:21:20 +01:00
Christian Schabesberger
095a2be748 change popuplayer class description 2017-03-09 16:14:49 +01:00
Christian Schabesberger
c75fe88757 Merge branch 'feature-popup' of git://github.com/mauriciocolli/NewPipe 2017-03-09 16:08:33 +01:00
Christian Schabesberger
a8ff4b0744 Fix backbutton behaviour
- close on MainActivity when back pressed
- clear NavStack on rotation
2017-03-09 16:01:09 +01:00
Christian Schabesberger
c02c511e31 Update .travis.yml
update build-tools version for travis
2017-03-09 15:41:10 +01:00
Mauricio Colli
b9550fb528 Merge remote-tracking branch 'upstream/master' into feature-popup 2017-03-09 04:49:15 -03:00
Mauricio Colli
a37d8f083a Implement popup mode
- Add icons replay, fast_forward
- Add strings
- Add menu entry
- Add as option to open link directly to popup mode
2017-03-09 04:42:40 -03:00
Mauricio Colli
abff1f537b Add ExoMedia and remove ExoPlayer
- Remove ExoPlayer files
- Added ExoMedia library
2017-03-09 01:44:00 -03:00
Enrico Monese
483fde5e42 Translated using Weblate (Italian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-08 23:52:45 +01:00
Weblate
761b7ea57b Merge remote-tracking branch 'origin/master' 2017-03-07 18:45:34 +01:00
zmni
af92631a0c Translated using Weblate (Indonesian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-07 18:45:29 +01:00
Christian Schabesberger
ffe832d061 update gradle verson 2017-03-07 14:14:02 +01:00
Christian Schabesberger
a08cbfcef1 Merge branch 'master' of github.com:TeamNewPipe/NewPipe 2017-03-06 15:36:13 +01:00
Christian Schabesberger
d2d6ac1bf8 add CCC slides 2017-03-06 15:36:04 +01:00
Weblate
615ffca64b Merge remote-tracking branch 'origin/master' 2017-03-05 16:35:55 +01:00
Tobias Groza
2fcf6197c5 Translated using Weblate (German)
Currently translated at 97.4% (150 of 154 strings)
2017-03-05 16:35:54 +01:00
zmni
22d31ae14f Translated using Weblate (Indonesian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-05 16:35:52 +01:00
Christian Schabesberger
8c786e121b get down to support lib 25.1.0 2017-03-04 13:30:21 +01:00
Christian Schabesberger
99d2a33c8c reset support lib to 25.1.1
25.2.0 is not yet supported by fdroid
2017-03-04 13:23:54 +01:00
Marian Hanzel
010a24db3f Translated using Weblate (Slovak)
Currently translated at 100.0% (154 of 154 strings)
2017-03-04 10:57:28 +01:00
Weblate
9408da453b Merge remote-tracking branch 'origin/master' 2017-03-02 15:26:33 +01:00
Jose Maeso
dbda4202fd Translated using Weblate (Spanish)
Currently translated at 98.7% (152 of 154 strings)
2017-03-02 15:26:32 +01:00
Mladen Pejaković
5776e91459 Translated using Weblate (Serbian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-02 15:26:32 +01:00
Matej U
664d7b69b1 Translated using Weblate (Slovenian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-02 15:26:30 +01:00
Christian Schabesberger
6b9140c60d make extractor sub use https 2017-03-01 19:25:19 +01:00
Christian Schabesberger
383857d110 add extractor submodule 2017-03-01 19:08:12 +01:00
Christian Schabesberger
cffd049c8a remove extractor 2017-03-01 19:06:27 +01:00
Christian Schabesberger
ecabc65e94 Merge branch 'master' of github.com:TeamNewPipe/NewPipe 2017-03-01 18:03:50 +01:00
Christian Schabesberger
780f1d5da3 made extractor systemindipendent again 2017-03-01 18:03:36 +01:00
naofum
e2d0fc34a3 Translated using Weblate (Japanese)
Currently translated at 100.0% (154 of 154 strings)
2017-03-01 17:41:48 +01:00
Christian Schabesberger
3404231457 get rid of xul xml parser 2017-03-01 16:40:04 +01:00
Mladen Pejaković
b17928570d Translated using Weblate (Serbian)
Currently translated at 100.0% (154 of 154 strings)
2017-03-01 14:15:48 +01:00
Weblate
8ed86261ef Merge remote-tracking branch 'origin/master' 2017-03-01 12:45:58 +01:00
RACER
a313b91a0a Translated using Weblate (Japanese)
Currently translated at 100.0% (149 of 149 strings)
2017-03-01 12:45:45 +01:00
Christian Schabesberger
7b6dae20be add loading footer to search fragment 2017-02-28 13:24:07 +01:00
Christian Schabesberger
552c70bb0d add adblocker for youtube red stuff 2017-02-28 13:05:20 +01:00
Christian Schabesberger
8f734737f0 fix bug and add footer cycle to channel 2017-02-28 12:13:29 +01:00
Christian Schabesberger
800e7bcb7a moved on to v0.8.11 2017-02-27 21:37:22 +01:00
Christian Schabesberger
2002234d86 fix channel load more videos error 2017-02-27 21:31:20 +01:00
Christian Schabesberger
5923663e08 add routed_intent activity 2017-02-27 21:14:03 +01:00
Christian Schabesberger
4cdf20ab8c fix differences 2017-02-27 19:21:41 +01:00
Christian Schabesberger
af65a1cfef Merge branch 'feature-theme-change' of git://github.com/mauriciocolli/NewPipe into conf 2017-02-27 19:04:38 +01:00
Christian Schabesberger
927057ab83 Merge pull request #465 from mauriciocolli/fix-travis
Fix travis and YoutubeStreamUrlIdHandler
2017-02-27 19:03:28 +01:00
Christian Schabesberger
ffbc001ad5 fix navstack channel problem 2017-02-27 19:00:06 +01:00
Christian Schabesberger
f5625a1151 fix dashmpd bug 2017-02-27 17:57:16 +01:00
Christian Schabesberger
ce2ceb8a1b fix layout 2017-02-27 16:38:01 +01:00
Christian Schabesberger
c14771534f redesign channel activity 2017-02-27 15:58:09 +01:00
59436419
553cec16d5 Removed old icons 2017-02-27 17:30:47 +05:30
59436419
d17496f720 Improved code for changing theme 2017-02-27 17:25:15 +05:30
Christian Schabesberger
89e70626eb update support framework 2017-02-26 21:38:02 +01:00
Mauricio Colli
2ccae841d6 Change variable names
For a better understanding of what is going on
2017-02-26 07:47:13 -03:00
Mauricio Colli
07f6d0f149 Fix bug lower case id
This method was passing the lowercase url
2017-02-25 18:15:50 -03:00
Mauricio Colli
319d769233 Change video Id
The previous wasn't working
2017-02-25 18:14:32 -03:00
Mauricio Colli
6ec393699e Check if selected theme it's not the current 2017-02-25 16:04:10 -03:00
Mauricio Colli
f8d9e0fa60 Implement restart dialog 2017-02-25 15:36:31 -03:00
Mauricio Colli
50ed962a82 Add necessary strings 2017-02-25 15:34:45 -03:00
Christian Schabesberger
8654705e9b Change T to K 2017-02-22 12:12:39 +01:00
Weblate
b79ed8185f Merge remote-tracking branch 'origin/master' 2017-02-21 17:48:35 +01:00
Rom1
0d6c67f64f Translated using Weblate (French)
Currently translated at 96.6% (144 of 149 strings)
2017-02-21 17:48:34 +01:00
Marian Hanzel
e97a6569a6 Translated using Weblate (Slovak)
Currently translated at 100.0% (149 of 149 strings)
2017-02-21 17:48:32 +01:00
Christian Schabesberger
d0d41c6b16 move on to v0.8.10 2017-02-19 16:09:39 +01:00
Christian Schabesberger
f7a531e71b Merge branch 'fix_nav' 2017-02-19 16:09:02 +01:00
Christian Schabesberger
c28fddc4dd did some finetuning 2017-02-19 16:07:45 +01:00
zmni
b6ea10fc73 Translated using Weblate (Indonesian)
Currently translated at 100.0% (149 of 149 strings)
2017-02-19 15:45:16 +01:00
Gian Maria Viglianti
320eb44061 Translated using Weblate (Italian)
Currently translated at 100.0% (149 of 149 strings)
2017-02-19 00:54:12 +01:00
Christian Schabesberger
7a6b5dd5b7 add initial support for NavStack 2017-02-18 21:59:48 +01:00
naofum
460653ed16 Translated using Weblate (Japanese)
Currently translated at 100.0% (149 of 149 strings)
2017-02-17 16:18:49 +01:00
Mladen Pejaković
b6fda788c5 Translated using Weblate (Serbian)
Currently translated at 100.0% (149 of 149 strings)
2017-02-16 22:12:17 +01:00
Matej U
da4b9306fa Translated using Weblate (Slovenian)
Currently translated at 100.0% (149 of 149 strings)
2017-02-16 22:00:25 +01:00
Weblate
d76c02cbf4 Merge remote-tracking branch 'origin/master' 2017-02-16 21:44:49 +01:00
nonamebg
4d5466b5cd Translated using Weblate (English)
Currently translated at 100.0% (141 of 141 strings)
2017-02-16 21:44:40 +01:00
Christian Schabesberger
e9c20ac8b0 add search channels to available features 2017-02-16 13:57:48 +01:00
Christian Schabesberger
961820a250 Merge branch 'search_channel' 2017-02-16 13:33:46 +01:00
Christian Schabesberger
4cc2976061 add unit test for new channel search 2017-02-16 00:46:15 +01:00
Christian Schabesberger
477f182b43 convert android tests to junit tests 2017-02-16 00:17:43 +01:00
Christian Schabesberger
a5252bb765 add search filter menu 2017-02-15 15:21:36 +01:00
Christian Schabesberger
3f0078f38a git channel item running 2017-02-15 12:59:36 +01:00
Christian Schabesberger
91434dd2ac setup core for search channel support 2017-02-13 00:55:11 +01:00
Christian Schabesberger
f29bd0a6e7 fix actionbar icon theming 2017-02-12 16:45:01 +01:00
Christian Schabesberger
7186f58374 remove StreamInfoItemSearchCollector 2017-02-12 14:22:39 +01:00
Christian Schabesberger
5c8bcf15ba make colector hirarchicall
remove broken commit()
2017-02-12 13:39:39 +01:00
Christian Schabesberger
130757ee99 Update CONTRIBUTING.md 2017-02-12 00:16:41 +01:00
Christian Schabesberger
5020a4f9dc add mailinglist to contributing 2017-02-11 22:26:00 +01:00
Christian Schabesberger
ef15902ec4 add InfoItem 2017-02-11 21:33:01 +01:00
zmni
ed8d13f837 Translated using Weblate (Indonesian)
Currently translated at 100.0% (141 of 141 strings)
2017-02-11 19:20:01 +01:00
ksyko
c2fcae7c43 Merge pull request #1 from TeamNewPipe/master
0.8.9 update
2017-02-11 23:21:58 +05:30
aladar42
27f2c65e6d Translated using Weblate (Czech)
Currently translated at 100.0% (141 of 141 strings)
2017-02-10 18:44:38 +01:00
Weblate
0b3428ede9 Merge remote-tracking branch 'origin/master' 2017-02-08 17:13:21 +01:00
aladar42
3e0102ad2a Translated using Weblate (Czech)
Currently translated at 100.0% (141 of 141 strings)
2017-02-08 17:13:19 +01:00
Christian Schabesberger
c42002ccc5 removed download notice again 2017-02-06 00:04:20 +01:00
Christian Schabesberger
e8101d5d91 change download link 2017-02-05 23:56:26 +01:00
Christian Schabesberger
ecdf4ad502 add np download to readme 2017-02-05 21:09:36 +01:00
Matej U
af760f9cdc Translated using Weblate (Slovenian)
Currently translated at 100.0% (141 of 141 strings)
2017-02-05 20:16:33 +01:00
Weblate
b3491da49f Merge remote-tracking branch 'origin/master' 2017-02-05 12:46:26 +01:00
Snob Malkowitch
c322feeaff Translated using Weblate (Russian)
Currently translated at 94.3% (134 of 142 strings)
2017-02-05 12:46:18 +01:00
Christian Schabesberger
b26f46a063 Merge pull request #439 from marcobiscaro2112/master
Adding brazilian portuguese translation
2017-02-03 12:37:22 +01:00
Christian Schabesberger
9f0944dc5a Merge pull request #441 from k3b/master
fix NullPointerExcpeption when opening "Downloads" with android-api < 23
2017-02-03 12:35:52 +01:00
k3b
e869098434 fix NullPointerExcpeption when opening "Downloads" with android-api < 23 2017-02-02 01:58:47 +01:00
Weblate
4baa23af5f Merge remote-tracking branch 'origin/master' 2017-02-01 20:58:21 +01:00
aladar42
78fc5bbbd5 Translated using Weblate (Czech)
Currently translated at 97.1% (138 of 142 strings)
2017-02-01 20:58:20 +01:00
Wiredframe
a69784d168 Translated using Weblate (German)
Currently translated at 100.0% (142 of 142 strings)
2017-02-01 20:58:11 +01:00
Marco Biscaro
d48a7f1a18 Adding brazilian portuguese translation
(Also set settings_keys as untranslatable)
2017-02-01 12:32:06 -02:00
Christian Schabesberger
082c6128ad moved on to 0.8.9 2017-01-31 18:28:47 +01:00
Christian Schabesberger
7c9771873b Merge branch 'master' of git://github.com/ksyko/NewPipe into jf 2017-01-31 18:25:30 +01:00
Christian Schabesberger
7257cdacc8 fix background button problem 2017-01-31 18:23:22 +01:00
Christian Schabesberger
140b480f82 url signature fix
* fixed stacktrace
* changed player url
* took regex fix from youtube-dl

final fix taken from youtube-dl
2017-01-31 18:06:44 +01:00
Christian Schabesberger
77db2f8a48 update support Framework 2017-01-31 10:58:53 +01:00
naofum
22bc81b0eb Translated using Weblate (Japanese)
Currently translated at 100.0% (142 of 142 strings)
2017-01-27 15:50:44 +01:00
Mladen Pejaković
b52b3d4be0 Translated using Weblate (Serbian)
Currently translated at 100.0% (142 of 142 strings)
2017-01-26 18:49:26 +01:00
Nathan Follens
f845098b42 Translated using Weblate (Dutch)
Currently translated at 100.0% (142 of 142 strings)
2017-01-26 16:48:57 +01:00
Weblate
0f7397e3b8 Merge remote-tracking branch 'origin/master' 2017-01-26 15:46:09 +01:00
Shaye Faletha
0b9d99fdc9 Translated using Weblate (Polish)
Currently translated at 99.2% (140 of 141 strings)
2017-01-26 15:46:06 +01:00
59436419
9a1da5cc75 Made changes for ICS and JB 2017-01-26 12:07:41 +05:30
230 changed files with 7981 additions and 9844 deletions

View File

@@ -13,6 +13,9 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
* Check if this issue/feature is already fixed/implemented in the repository
* If you are an android/java developer you are always welcome to fix/implement an issue/a feature yourself
## Bugfixing
* If you want to help NewPipe getting bug free, you can send me a mail to tnp@newpipe.schabi.org to let me know that you intent to help, and than register at our [sentry](https://support.schabi.org) setup.
## Translation
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
@@ -31,6 +34,6 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
## Communication
* I hereby declare our Slack channel as dead!!! There are no plans on building a new chat, but if there is interest on creating one and keeping it alive, I'd be pleased to create one again.
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
* If you want to get in contact with me or one of our other contributors you can send me an email at tnp(at)schabi.org
* Feel free to post suggestions, changes, ideas etc!

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "app/src/main/java/org/schabi/newpipe/extractor"]
path = app/src/main/java/org/schabi/newpipe/extractor
url = https://github.com/TeamNewPipe/NewPipeExtractor.git

View File

@@ -5,35 +5,15 @@ android:
components:
# The BuildTools version used by NewPipe
- tools
- build-tools-23.0.3
- build-tools-25.0.0
# The SDK version used to compile NewPipe
- android-25
# Additional components
- extra-android-support
- extra-android-m2repository
- extra-google-m2repository
# Emulators
- sys-img-armeabi-v7a-android-21
- sys-img-armeabi-v7a-android-19
- sys-img-armeabi-v7a-android-15
env:
global:
- ADB_INSTALL_TIMEOUT=8 # minutes (2 by default)
- GRADLE_OPTS=-Xmx512m # give gradle more memory since it seem to fail otherwise
matrix:
- ANDROID_TARGET=android-21 ANDROID_ABI=armeabi-v7a
before_script:
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
- emulator -avd test -no-skin -no-audio -no-window &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script: ./gradlew --info build connectedCheck
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
licenses:
- '.+'

Binary file not shown.

View File

@@ -37,24 +37,27 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
* Listen to YouTube videos (experimental)
* Select the streaming player to watch the video with
* Download videos
* Download audio only
* Download audio only
* Open a video in Kodi
* Show Next/Related videos
* Search YouTube in a specific language
* Watch age restricted material
* Display general information about channels
* Search channels
* Watch videos from a channel
* Orbot/Tor support (not yet directly)
### Coming Features
* Orbot/Tor support
* Bookmarks
* View history
* Search history
* Search channels
* Subscribe to channels
* Watch videos from a channel
* Search/Watch Playlists
* Queeing videos
* Subtitles support
* 1080p support
* livestream support
* ... and many more
### Multiservice support

View File

@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '23.0.3'
buildToolsVersion '25.0.0'
defaultConfig {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 25
versionCode 22
versionName "0.8.8"
versionCode 28
versionName "0.9.1"
}
buildTypes {
release {
@@ -32,6 +32,9 @@ android {
dependencies {
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile 'org.json:json:20160810'
compile 'com.android.support:appcompat-v7:25.1.0'
compile 'com.android.support:support-v4:25.1.0'
compile 'com.android.support:design:25.1.0'
@@ -42,10 +45,8 @@ dependencies {
compile 'de.hdodenhof:circleimageview:2.0.0'
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'com.github.nirhart:parallaxscroll:1.0'
compile 'com.google.android.exoplayer:exoplayer:r1.5.5'
compile 'com.google.code.gson:gson:2.4'
compile 'com.nononsenseapps:filepicker:3.0.0'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
compile 'ch.acra:acra:4.9.0'
compile 'com.google.android.exoplayer:exoplayer:r2.3.1'
}

View File

@@ -1,52 +0,0 @@
package org.schabi.newpipe.extractor.youtube;
import android.test.AndroidTestCase;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 11.03.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeStreamExtractorLiveStreamTest.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeStreamExtractorLiveStreamTest extends AndroidTestCase {
private StreamExtractor extractor;
public void setUp() throws IOException, ExtractionException {
//todo: make the extractor not throw over a livestream
/*
NewPipe.init(Downloader.getInstance());
extractor = NewPipe.getService("Youtube")
.getExtractorInstance("https://www.youtube.com/watch?v=J0s6NjqdjLE", Downloader.getInstance());
*/
}
public void testStreamType() throws ParsingException {
assertTrue(true);
// assertTrue(extractor.getStreamType() == AbstractVideoInfo.StreamType.LIVE_STREAM);
}
}

View File

@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:name=".App"
@@ -16,7 +17,6 @@
android:logo="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
@@ -34,53 +34,6 @@
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="www.youtube.com" />
<data android:pathPrefix="/v/" />
<data android:pathPrefix="/watch" />
<data android:pathPrefix="/attribution_link" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtu.be" />
<data android:pathPrefix="/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vnd.youtube" />
<data android:scheme="vnd.youtube.launch" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity
android:name=".player.PlayVideoActivity"
@@ -98,24 +51,7 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@style/PlayerTheme">
<intent-filter>
<action android:name="org.schabi.newpipe.exoplayer.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="content" />
<data android:scheme="asset" />
<data android:scheme="file" />
</intent-filter>
</activity>
<service
android:name=".player.BackgroundPlayer"
android:exported="false"
android:label="@string/background_player_name" />
android:theme="@style/PlayerTheme"/>
<activity
android:name=".settings.SettingsActivity"
@@ -149,15 +85,11 @@
<activity
android:name="com.nononsenseapps.filepicker.FilePickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme"
android:launchMode="singleTop">
</activity>
android:launchMode="singleTop"
android:theme="@style/FilePickerTheme"/>
<activity
android:name=".ChannelActivity"
android:label="@string/title_activity_channel"
android:theme="@style/AppTheme.NoActionBar" />
android:launchMode="singleTask" />
<activity
android:name=".ReCaptchaActivity"
android:label="@string/reCaptchaActivity" />
@@ -169,8 +101,126 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
android:resource="@xml/provider_paths" />
</provider>
<activity android:name=".RouterActivity"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="www.youtube.com" />
<!-- video prefix -->
<data android:pathPrefix="/v/" />
<data android:pathPrefix="/embed/" />
<data android:pathPrefix="/watch" />
<data android:pathPrefix="/attribution_link" />
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtu.be" />
<data android:pathPrefix="/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vnd.youtube" />
<data android:scheme="vnd.youtube.launch" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name=".PopupActivity"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/popup_mode_share_menu_title">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="www.youtube.com" />
<!-- video prefix -->
<data android:pathPrefix="/v/" />
<data android:pathPrefix="/embed/" />
<data android:pathPrefix="/watch" />
<data android:pathPrefix="/attribution_link" />
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtu.be" />
<data android:pathPrefix="/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vnd.youtube" />
<data android:scheme="vnd.youtube.launch" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<service android:name=".player.PopupVideoPlayer"/>
</application>
</manifest>
</manifest>

View File

@@ -4,23 +4,22 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.detail.VideoItemDetailActivity;
import org.schabi.newpipe.detail.VideoItemDetailFragment;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
@@ -30,9 +29,14 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.NavStack;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import java.util.Objects;
import static android.os.Build.VERSION.SDK_INT;
/**
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@@ -53,15 +57,9 @@ import java.util.Objects;
*/
public class ChannelActivity extends AppCompatActivity {
private static final String TAG = ChannelActivity.class.toString();
private View rootView = null;
// intent const
public static final String CHANNEL_URL = "channel_url";
public static final String SERVICE_ID = "service_id";
private int serviceId = -1;
private String channelUrl = "";
private int pageNumber = 0;
@@ -71,34 +69,43 @@ public class ChannelActivity extends AppCompatActivity {
private ImageLoader imageLoader = ImageLoader.getInstance();
private InfoListAdapter infoListAdapter = null;
private String subS = "";
ProgressBar progressBar = null;
ImageView channelBanner = null;
ImageView avatarView = null;
TextView titleView = null;
TextView subscirberView = null;
Button subscriberButton = null;
View subscriberLayout = null;
View header = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme_NoActionBar);
}
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, true);
setContentView(R.layout.activity_channel);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
rootView = findViewById(R.id.rootView);
setSupportActionBar(toolbar);
Intent i = getIntent();
channelUrl = i.getStringExtra(CHANNEL_URL);
serviceId = i.getIntExtra(SERVICE_ID, -1);
rootView = findViewById(android.R.id.content);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowTitleEnabled(true);
infoListAdapter = new InfoListAdapter(this, rootView);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.channel_streams_view);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
header = getLayoutInflater().inflate(R.layout.channel_header, recyclerView, false);
infoListAdapter.setHeader(header);
infoListAdapter.setFooter(
getLayoutInflater().inflate(R.layout.pignate_footer, recyclerView, false));
recyclerView.setAdapter(infoListAdapter);
infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
infoListAdapter.setOnStreamInfoItemSelectedListener(
new InfoItemBuilder.OnInfoItemSelectedListener() {
@Override
public void selected(String url) {
Intent detailIntent = new Intent(ChannelActivity.this, VideoItemDetailActivity.class);
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
detailIntent.putExtra(
VideoItemDetailFragment.STREAMING_SERVICE, serviceId);
startActivity(detailIntent);
public void selected(String url, int serviceId) {
NavStack.getInstance()
.openDetailActivity(ChannelActivity.this, url, serviceId);
}
});
@@ -125,39 +132,80 @@ public class ChannelActivity extends AppCompatActivity {
}
});
subS = getString(R.string.subscriber);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
channelBanner = (ImageView) header.findViewById(R.id.channel_banner_image);
avatarView = (ImageView) header.findViewById(R.id.channel_avatar_view);
titleView = (TextView) header.findViewById(R.id.channel_title_view);
subscirberView = (TextView) header.findViewById(R.id.channel_subscriber_view);
subscriberButton = (Button) header.findViewById(R.id.channel_subscribe_button);
subscriberLayout = header.findViewById(R.id.channel_subscriber_layout);
if(savedInstanceState == null) {
handleIntent(getIntent());
} else {
channelUrl = savedInstanceState.getString(NavStack.URL);
serviceId = savedInstanceState.getInt(NavStack.SERVICE_ID);
NavStack.getInstance()
.restoreSavedInstanceState(savedInstanceState);
handleIntent(getIntent());
}
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent i) {
channelUrl = i.getStringExtra(NavStack.URL);
serviceId = i.getIntExtra(NavStack.SERVICE_ID, -1);
requestData(false);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(NavStack.URL, channelUrl);
outState.putInt(NavStack.SERVICE_ID, serviceId);
NavStack.getInstance()
.onSaveInstanceState(outState);
}
private void updateUi(final ChannelInfo info) {
CollapsingToolbarLayout ctl = (CollapsingToolbarLayout) findViewById(R.id.channel_toolbar_layout);
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
ImageView channelBanner = (ImageView) findViewById(R.id.channel_banner_image);
final FloatingActionButton feedButton = (FloatingActionButton) findViewById(R.id.channel_rss_fab);
ImageView avatarView = (ImageView) findViewById(R.id.channel_avatar_view);
ImageView haloView = (ImageView) findViewById(R.id.channel_avatar_halo);
findViewById(R.id.channel_header_layout).setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
if(info.channel_name != null && !info.channel_name.isEmpty()) {
ctl.setTitle(info.channel_name);
getSupportActionBar().setTitle(info.channel_name);
titleView.setText(info.channel_name);
}
if(info.banner_url != null && !info.banner_url.isEmpty()) {
imageLoader.displayImage(info.banner_url, channelBanner,
new ImageErrorLoadingListener(this, rootView ,info.service_id));
new ImageErrorLoadingListener(this, rootView ,info.service_id));
}
if(info.avatar_url != null && !info.avatar_url.isEmpty()) {
avatarView.setVisibility(View.VISIBLE);
haloView.setVisibility(View.VISIBLE);
imageLoader.displayImage(info.avatar_url, avatarView,
new ImageErrorLoadingListener(this, rootView ,info.service_id));
}
if(info.subscriberCount != -1) {
subscirberView.setText(buildSubscriberString(info.subscriberCount));
}
if((info.feed_url != null && !info.feed_url.isEmpty()) ||
(info.subscriberCount != -1)) {
subscriberLayout.setVisibility(View.VISIBLE);
}
if(info.feed_url != null && !info.feed_url.isEmpty()) {
feedButton.setOnClickListener(new View.OnClickListener() {
subscriberButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, info.feed_url);
@@ -166,12 +214,13 @@ public class ChannelActivity extends AppCompatActivity {
}
});
} else {
feedButton.setVisibility(View.GONE);
subscriberButton.setVisibility(View.INVISIBLE);
}
}
private void addVideos(final ChannelInfo info) {
infoListAdapter.addStreamItemList(info.related_streams);
infoListAdapter.addInfoItemList(info.related_streams);
}
private void postNewErrorToast(Handler h, final int stringResource) {
@@ -187,6 +236,22 @@ public class ChannelActivity extends AppCompatActivity {
private void requestData(final boolean onlyVideos) {
// start processing
isLoading = true;
if(!onlyVideos) {
//delete already displayed content
progressBar.setVisibility(View.VISIBLE);
infoListAdapter.clearSteamItemList();
pageNumber = 0;
subscriberLayout.setVisibility(View.GONE);
titleView.setText("");
getSupportActionBar().setTitle("");
if (SDK_INT >= 21) {
channelBanner.setImageDrawable(getDrawable(R.drawable.channel_banner));
avatarView.setImageDrawable(getDrawable(R.drawable.buddy));
}
infoListAdapter.showFooter(false);
}
Thread channelExtractorThread = new Thread(new Runnable() {
Handler h = new Handler();
@@ -207,8 +272,12 @@ public class ChannelActivity extends AppCompatActivity {
isLoading = false;
if(!onlyVideos) {
updateUi(info);
infoListAdapter.showFooter(true);
}
hasNextPage = info.hasNextPage;
if(!hasNextPage) {
infoListAdapter.showFooter(false);
}
addVideos(info);
}
});
@@ -232,7 +301,7 @@ public class ChannelActivity extends AppCompatActivity {
postNewErrorToast(h, R.string.network_error);
ioe.printStackTrace();
} catch(ParsingException pe) {
ErrorActivity.reportError(h, ChannelActivity.this, pe, VideoItemDetailFragment.class, null,
ErrorActivity.reportError(h, ChannelActivity.this, pe, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
service.getServiceInfo().name, channelUrl, R.string.parsing_error));
h.post(new Runnable() {
@@ -243,9 +312,13 @@ public class ChannelActivity extends AppCompatActivity {
});
pe.printStackTrace();
} catch(ExtractionException ex) {
ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailFragment.class, null,
String name = "none";
if(service != null) {
name = service.getServiceInfo().name;
}
ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
service.getServiceInfo().name, channelUrl, R.string.parsing_error));
name, channelUrl, R.string.parsing_error));
h.post(new Runnable() {
@Override
public void run() {
@@ -254,7 +327,7 @@ public class ChannelActivity extends AppCompatActivity {
});
ex.printStackTrace();
} catch(Exception e) {
ErrorActivity.reportError(h, ChannelActivity.this, e, VideoItemDetailFragment.class, null,
ErrorActivity.reportError(h, ChannelActivity.this, e, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
service.getServiceInfo().name, channelUrl, R.string.general_error));
h.post(new Runnable() {
@@ -271,4 +344,64 @@ public class ChannelActivity extends AppCompatActivity {
channelExtractorThread.start();
}
@Override
public void onBackPressed() {
try {
NavStack.getInstance()
.navBack(this);
} catch (Exception e) {
ErrorActivity.reportUiError(this, e);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_channel, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch(item.getItemId()) {
case R.id.action_settings: {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
case R.id.menu_item_openInBrowser: {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(channelUrl));
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
}
case R.id.menu_item_share:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
intent.setType("text/plain");
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
case android.R.id.home:
NavStack.getInstance().openMainActivity(this);
default:
return super.onOptionsItemSelected(item);
}
}
private String buildSubscriberString(long count) {
String out = "";
if(count >= 1000000000){
out += Long.toString((count/1000000000)%1000)+".";
}
if(count>=1000000){
out += Long.toString((count/1000000)%1000) + ".";
}
if(count>=1000){
out += Long.toString((count/1000)%1000)+".";
}
out += Long.toString(count%1000) + " " + subS;
return out;
}
}

View File

@@ -2,15 +2,17 @@ package org.schabi.newpipe;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.NavUtils;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
/**
* Created by Christian Schabesberger on 02.08.16.
@@ -32,13 +34,15 @@ import org.schabi.newpipe.util.PermissionHelper;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class MainActivity extends ThemableActivity {
public class MainActivity extends AppCompatActivity {
private Fragment mainFragment = null;
private static final String TAG = MainActivity.class.toString();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, true);
setContentView(R.layout.activity_main);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mainFragment = getSupportFragmentManager()
@@ -49,7 +53,6 @@ public class MainActivity extends ThemableActivity {
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
return true;
}
@@ -58,7 +61,7 @@ public class MainActivity extends ThemableActivity {
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch(id) {
switch (id) {
case android.R.id.home: {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -71,10 +74,10 @@ public class MainActivity extends ThemableActivity {
return true;
}
case R.id.action_show_downloads: {
if(!PermissionHelper.checkStoragePermissions(this)) {
if (!PermissionHelper.checkStoragePermissions(this)) {
return false;
}
Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class);
Intent intent = new Intent(this, DownloadActivity.class);
startActivity(intent);
return true;
}

View File

@@ -0,0 +1,136 @@
package org.schabi.newpipe;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.util.NavStack;
import org.schabi.newpipe.util.PermissionHelper;
import java.util.Collection;
import java.util.HashSet;
/**
* This activity is thought to open video streams form an external app using the popup playser.
*/
public class PopupActivity extends Activity {
private static final String TAG = RouterActivity.class.toString();
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleIntent(getIntent());
finish();
}
private static String removeHeadingGibberish(final String input) {
int start = 0;
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
if (!input.substring(i, i + 1).matches("\\p{L}")) {
start = i + 1;
break;
}
}
return input.substring(start, input.length());
}
private static String trim(final String input) {
if (input == null || input.length() < 1) {
return input;
} else {
String output = input;
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(1);
}
while (output.length() > 0
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(0, output.length() - 1);
}
return output;
}
}
/**
* Retrieves all Strings which look remotely like URLs from a text.
* Used if NewPipe was called through share menu.
*
* @param sharedText text to scan for URLs.
* @return potential URLs
*/
private String[] getUris(final String sharedText) {
final Collection<String> result = new HashSet<>();
if (sharedText != null) {
final String[] array = sharedText.split("\\p{Space}");
for (String s : array) {
s = trim(s);
if (s.length() != 0) {
if (s.matches(".+://.+")) {
result.add(removeHeadingGibberish(s));
} else if (s.matches(".+\\..+")) {
result.add("http://" + s);
}
}
}
}
return result.toArray(new String[result.size()]);
}
private void handleIntent(Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !PermissionHelper.checkSystemAlertWindowPermission(this)) {
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
return;
}
String videoUrl = "";
StreamingService service = null;
// first gather data and find service
if (intent.getData() != null) {
// this means the video was called though another app
videoUrl = intent.getData().toString();
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
//this means that vidoe was called through share menu
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
videoUrl = getUris(extraText)[0];
}
service = NewPipe.getServiceByUrl(videoUrl);
if (service == null) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
.show();
return;
} else {
Intent callIntent = new Intent();
switch (service.getLinkTypeByUrl(videoUrl)) {
case STREAM:
callIntent.setClass(this, PopupVideoPlayer.class);
break;
case PLAYLIST:
Log.e(TAG, "NOT YET DEFINED");
break;
default:
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
return;
}
callIntent.putExtra(NavStack.URL, videoUrl);
callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId());
startService(callIntent);
}
}
}

View File

@@ -0,0 +1,156 @@
package org.schabi.newpipe;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.detail.VideoItemDetailActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.util.NavStack;
import java.util.Collection;
import java.util.HashSet;
/**
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* RouterActivity .java is part of NewPipe.
*
* OpenHitboxStreams 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.
*
* OpenHitboxStreams 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 OpenHitboxStreams. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This Acitivty is designed to route share/open intents to the specified service, and
* to the part of the service which can handle the url.
*/
public class RouterActivity extends Activity {
private static final String TAG = RouterActivity.class.toString();
/**
* Removes invisible separators (\p{Z}) and punctuation characters including
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
* more details.
*/
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleIntent(getIntent());
finish();
}
private static String removeHeadingGibberish(final String input) {
int start = 0;
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
if (!input.substring(i, i + 1).matches("\\p{L}")) {
start = i + 1;
break;
}
}
return input.substring(start, input.length());
}
private static String trim(final String input) {
if (input == null || input.length() < 1) {
return input;
} else {
String output = input;
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(1);
}
while (output.length() > 0
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(0, output.length() - 1);
}
return output;
}
}
/**
* Retrieves all Strings which look remotely like URLs from a text.
* Used if NewPipe was called through share menu.
*
* @param sharedText text to scan for URLs.
* @return potential URLs
*/
private String[] getUris(final String sharedText) {
final Collection<String> result = new HashSet<>();
if (sharedText != null) {
final String[] array = sharedText.split("\\p{Space}");
for (String s : array) {
s = trim(s);
if (s.length() != 0) {
if (s.matches(".+://.+")) {
result.add(removeHeadingGibberish(s));
} else if (s.matches(".+\\..+")) {
result.add("http://" + s);
}
}
}
}
return result.toArray(new String[result.size()]);
}
private void handleIntent(Intent intent) {
String videoUrl = "";
StreamingService service = null;
// first gather data and find service
if (intent.getData() != null) {
// this means the video was called though another app
videoUrl = intent.getData().toString();
} else if(intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
//this means that vidoe was called through share menu
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
videoUrl = getUris(extraText)[0];
}
service = NewPipe.getServiceByUrl(videoUrl);
if(service == null) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
.show();
return;
} else {
Intent callIntent = new Intent();
switch(service.getLinkTypeByUrl(videoUrl)) {
case CHANNEL:
callIntent.setClass(this, ChannelActivity.class);
break;
case STREAM:
callIntent.setClass(this, VideoItemDetailActivity.class);
callIntent.putExtra(VideoItemDetailActivity.AUTO_PLAY,
PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(
getString(R.string.autoplay_through_intent_key), false));
break;
case PLAYLIST:
Log.e(TAG, "NOT YET DEFINED");
break;
default:
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
.show();
return;
}
callIntent.putExtra(NavStack.URL, videoUrl);
callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId());
startActivity(callIntent);
}
}
}

View File

@@ -4,17 +4,24 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import java.util.Objects;
import static org.schabi.newpipe.R.attr.theme;
public class ThemableActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
if (PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)).
equals(getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme);
}
}
@Override
protected void onResume() {
super.onResume();
if (PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)).
equals(getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme);
}
}

View File

@@ -53,6 +53,7 @@ class ActionBarHandler {
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
private OnActionListener onShareListener;
private OnActionListener onOpenInBrowserListener;
private OnActionListener onOpenInPopupListener;
private OnActionListener onDownloadListener;
private OnActionListener onPlayWithKodiListener;
private OnActionListener onPlayAudioListener;
@@ -118,15 +119,32 @@ class ActionBarHandler {
.getString(activity.getString(R.string.default_resolution_key),
activity.getString(R.string.default_resolution_value));
String preferedFormat = defaultPreferences
.getString(activity.getString(R.string.preferred_video_format_key),
activity.getString(R.string.preferred_video_format_default));
// first try to find the one with the right resolution
int selectedFormat = 0;
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream item = videoStreams.get(i);
if (defaultResolution.equals(item.resolution)) {
return i;
selectedFormat = i;
}
}
// than try to find the one with the right resolution and format
for (int i = 0; i < videoStreams.size(); i++) {
VideoStream item = videoStreams.get(i);
if (defaultResolution.equals(item.resolution)
&& preferedFormat.equals(MediaFormat.getNameById(item.format))) {
selectedFormat = i;
}
}
// this is actually an error,
// but maybe there is really no stream fitting to the default value.
return 0;
return selectedFormat;
}
public void setupMenu(Menu menu, MenuInflater inflater) {
@@ -190,6 +208,12 @@ class ActionBarHandler {
activity.startActivity(intent);
return true;
}
case R.id.menu_item_popup: {
if(onOpenInPopupListener != null) {
onOpenInPopupListener.onActionSelected(selectedVideoStream);
}
return true;
}
default:
Log.e(TAG, "Menu Item not known");
}
@@ -208,6 +232,10 @@ class ActionBarHandler {
onOpenInBrowserListener = listener;
}
public void setOnOpenInPopupListener(OnActionListener listener) {
onOpenInPopupListener = listener;
}
public void setOnDownloadListener(OnActionListener listener) {
onDownloadListener = listener;
}

View File

@@ -0,0 +1,250 @@
package org.schabi.newpipe.detail;
import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.report.ErrorActivity;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Extract {@link StreamInfo} with {@link StreamExtractor} from the given url of the given service
*/
@SuppressWarnings("WeakerAccess")
public class StreamExtractorWorker extends Thread {
private static final String TAG = "StreamExtractorWorker";
private Activity activity;
private final String videoUrl;
private final int serviceId;
private OnStreamInfoReceivedListener callback;
private final AtomicBoolean isRunning = new AtomicBoolean(false);
private final Handler handler = new Handler();
public interface OnStreamInfoReceivedListener {
void onReceive(StreamInfo info);
void onError(int messageId);
void onReCaptchaException();
void onBlockedByGemaError();
void onContentErrorWithMessage(int messageId);
void onContentError();
}
public StreamExtractorWorker(Activity activity, String videoUrl, int serviceId, OnStreamInfoReceivedListener callback) {
this.serviceId = serviceId;
this.videoUrl = videoUrl;
this.activity = activity;
this.callback = callback;
}
/**
* Returns a new instance <b>already</b> started of {@link StreamExtractorWorker}.<br>
* The caller is responsible to check if {@link StreamExtractorWorker#isRunning()}, or {@link StreamExtractorWorker#cancel()} it
*
* @param serviceId id of the request service
* @param url videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k)
* @param activity activity for error reporting purposes
* @param callback listener that will be called-back when events occur (check {@link OnStreamInfoReceivedListener})
* @return new instance already started of {@link StreamExtractorWorker}
*/
public static StreamExtractorWorker startExtractorThread(int serviceId, String url, Activity activity, OnStreamInfoReceivedListener callback) {
StreamExtractorWorker extractorThread = getExtractorThread(serviceId, url, activity, callback);
extractorThread.start();
return extractorThread;
}
/**
* Returns a new instance of {@link StreamExtractorWorker}.<br>
* The caller is responsible to check if {@link StreamExtractorWorker#isRunning()}, or {@link StreamExtractorWorker#cancel()}
* when it doesn't need it anymore
* <p>
* <b>Note:</b> this instance is <b>not</b> started yet
*
* @param serviceId id of the request service
* @param url videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k)
* @param activity activity for error reporting purposes
* @param callback listener that will be called-back when events occur (check {@link OnStreamInfoReceivedListener})
* @return instance of {@link StreamExtractorWorker}
*/
public static StreamExtractorWorker getExtractorThread(int serviceId, String url, Activity activity, OnStreamInfoReceivedListener callback) {
return new StreamExtractorWorker(activity, url, serviceId, callback);
}
@Override
//Just ignore the errors for now
@SuppressWarnings("ConstantConditions")
public void run() {
// TODO: Improve error checking
// and this method in general
StreamInfo streamInfo = null;
StreamingService service;
try {
service = NewPipe.getService(serviceId);
} catch (Exception e) {
e.printStackTrace();
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
"", videoUrl, R.string.could_not_get_stream));
return;
}
try {
isRunning.set(true);
StreamExtractor streamExtractor = service.getExtractorInstance(videoUrl);
streamInfo = StreamInfo.getVideoInfo(streamExtractor);
final StreamInfo info = streamInfo;
if (callback != null) handler.post(new Runnable() {
@Override
public void run() {
callback.onReceive(info);
}
});
isRunning.set(false);
// look for errors during extraction
// this if statement only covers extra information.
// if these are not available or caused an error, they are just not available
// but don't render the stream information unusalbe.
if (streamInfo != null && !streamInfo.errors.isEmpty()) {
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
for (Throwable e : streamInfo.errors) {
e.printStackTrace();
Log.e(TAG, "------");
}
View rootView = activity != null ? activity.findViewById(R.id.video_item_detail) : null;
ErrorActivity.reportError(handler, activity,
streamInfo.errors, null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, 0 /* no message for the user */));
}
// These errors render the stream information unusable.
} catch (ReCaptchaException e) {
if (callback != null) handler.post(new Runnable() {
@Override
public void run() {
callback.onReCaptchaException();
}
});
} catch (IOException e) {
if (callback != null) handler.post(new Runnable() {
@Override
public void run() {
callback.onError(R.string.network_error);
}
});
if (callback != null) e.printStackTrace();
} catch (YoutubeStreamExtractor.DecryptException de) {
// custom service related exceptions
ErrorActivity.reportError(handler, activity, de, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.youtube_signature_decryption_error));
handler.post(new Runnable() {
@Override
public void run() {
activity.finish();
}
});
de.printStackTrace();
} catch (YoutubeStreamExtractor.GemaException ge) {
if (callback != null) handler.post(new Runnable() {
@Override
public void run() {
callback.onBlockedByGemaError();
}
});
} catch (YoutubeStreamExtractor.LiveStreamException e) {
if (callback != null) handler.post(new Runnable() {
@Override
public void run() {
callback.onContentErrorWithMessage(R.string.live_streams_not_supported);
}
});
}
// ----------------------------------------
catch (StreamExtractor.ContentNotAvailableException e) {
if (callback != null) handler.post(new Runnable() {
@Override
public void run() {
callback.onContentError();
}
});
e.printStackTrace();
} catch (StreamInfo.StreamExctractException e) {
if (!streamInfo.errors.isEmpty()) {
// !!! if this case ever kicks in someone gets kicked out !!!
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
} else {
ErrorActivity.reportError(handler, activity, streamInfo.errors, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
}
handler.post(new Runnable() {
@Override
public void run() {
activity.finish();
}
});
e.printStackTrace();
} catch (ParsingException e) {
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.parsing_error));
handler.post(new Runnable() {
@Override
public void run() {
activity.finish();
}
});
e.printStackTrace();
} catch (Exception e) {
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.general_error));
handler.post(new Runnable() {
@Override
public void run() {
activity.finish();
}
});
e.printStackTrace();
}
}
/**
* Return true if the extraction is not completed yet
*
* @return the value of the AtomicBoolean {@link #isRunning}
*/
public boolean isRunning() {
return isRunning.get();
}
/**
* Cancel this ExtractorThread, setting the callback to null, the AtomicBoolean {@link #isRunning} to false and interrupt this thread.
* <p>
* <b>Note:</b> Any I/O that is active in the moment that this method is called will be canceled and a Exception will be thrown, because of the {@link #interrupt()}.<br>
* This is useful when you don't want the resulting {@link StreamInfo} anymore, but don't want to waste bandwidth, otherwise it'd run till it receives the StreamInfo.
*/
public void cancel() {
this.callback = null;
this.isRunning.set(false);
this.interrupt();
}
}

View File

@@ -1,227 +0,0 @@
package org.schabi.newpipe.detail;
import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 02.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoWorker.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class StreamInfoWorker {
private static final String TAG = StreamInfoWorker.class.toString();
public interface OnStreamInfoReceivedListener {
void onReceive(StreamInfo info);
void onError(int messageId);
void onReCaptchaException();
void onBlockedByGemaError();
void onContentErrorWithMessage(int messageId);
void onContentError();
}
private class StreamExtractorRunnable implements Runnable {
private final Handler h = new Handler();
private StreamExtractor streamExtractor;
private final int serviceId;
private final String videoUrl;
private Activity a;
public StreamExtractorRunnable(Activity a, String videoUrl, int serviceId) {
this.serviceId = serviceId;
this.videoUrl = videoUrl;
this.a = a;
}
@Override
public void run() {
StreamInfo streamInfo = null;
StreamingService service = null;
try {
service = NewPipe.getService(serviceId);
} catch (Exception e) {
e.printStackTrace();
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
"", videoUrl, R.string.could_not_get_stream));
return;
}
try {
streamExtractor = service.getExtractorInstance(videoUrl);
streamInfo = StreamInfo.getVideoInfo(streamExtractor);
final StreamInfo info = streamInfo;
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener.onReceive(info);
}
});
// look for errors during extraction
// this if statement only covers extra information.
// if these are not available or caused an error, they are just not available
// but don't render the stream information unusalbe.
if(streamInfo != null &&
!streamInfo.errors.isEmpty()) {
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
for (Throwable e : streamInfo.errors) {
e.printStackTrace();
Log.e(TAG, "------");
}
View rootView = a != null ? a.findViewById(R.id.video_item_detail) : null;
ErrorActivity.reportError(h, a,
streamInfo.errors, null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, 0 /* no message for the user */));
}
// These errors render the stream information unusable.
} catch (ReCaptchaException e) {
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener.onReCaptchaException();
}
});
} catch (IOException e) {
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener.onError(R.string.network_error);
}
});
e.printStackTrace();
} catch (YoutubeStreamExtractor.DecryptException de) {
// custom service related exceptions
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener.onError(R.string.youtube_signature_decryption_error);
}
});
de.printStackTrace();
} catch (YoutubeStreamExtractor.GemaException ge) {
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener.onBlockedByGemaError();
}
});
} catch(YoutubeStreamExtractor.LiveStreamException e) {
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener
.onContentErrorWithMessage(R.string.live_streams_not_supported);
}
});
}
// ----------------------------------------
catch(StreamExtractor.ContentNotAvailableException e) {
h.post(new Runnable() {
@Override
public void run() {
onStreamInfoReceivedListener
.onContentError();
}
});
e.printStackTrace();
} catch(StreamInfo.StreamExctractException e) {
if(!streamInfo.errors.isEmpty()) {
// !!! if this case ever kicks in someone gets kicked out !!!
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
} else {
ErrorActivity.reportError(h, a, streamInfo.errors, VideoItemDetailFragment.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
}
h.post(new Runnable() {
@Override
public void run() {
a.finish();
}
});
e.printStackTrace();
} catch (ParsingException e) {
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.parsing_error));
h.post(new Runnable() {
@Override
public void run() {
a.finish();
}
});
e.printStackTrace();
} catch(Exception e) {
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
service.getServiceInfo().name, videoUrl, R.string.general_error));
h.post(new Runnable() {
@Override
public void run() {
a.finish();
}
});
e.printStackTrace();
}
}
}
private static StreamInfoWorker streamInfoWorker = null;
private StreamExtractorRunnable runnable = null;
private OnStreamInfoReceivedListener onStreamInfoReceivedListener = null;
private StreamInfoWorker() {
}
public static StreamInfoWorker getInstance() {
return streamInfoWorker == null ? (streamInfoWorker = new StreamInfoWorker()) : streamInfoWorker;
}
public void search(int serviceId, String url, Activity a) {
runnable = new StreamExtractorRunnable(a, url, serviceId);
Thread thread = new Thread(runnable);
thread.start();
}
public void setOnStreamInfoReceivedListener(
OnStreamInfoReceivedListener onStreamInfoReceivedListener) {
this.onStreamInfoReceivedListener = onStreamInfoReceivedListener;
}
}

View File

@@ -1,832 +0,0 @@
package org.schabi.newpipe.detail;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer.util.Util;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.ChannelActivity;
import org.schabi.newpipe.ImageErrorLoadingListener;
import org.schabi.newpipe.Localization;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.ExoPlayerActivity;
import org.schabi.newpipe.player.PlayVideoActivity;
import org.schabi.newpipe.report.ErrorActivity;
import java.util.Vector;
import org.schabi.newpipe.util.PermissionHelper;
import static android.app.Activity.RESULT_OK;
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
/**
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* VideoItemDetailFragment.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class VideoItemDetailFragment extends Fragment {
private static final String TAG = VideoItemDetailFragment.class.toString();
private static final String KORE_PACKET = "org.xbmc.kore";
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
public static final String VIDEO_URL = "video_url";
public static final String STREAMING_SERVICE = "streaming_service";
public static final String AUTO_PLAY = "auto_play";
private AppCompatActivity activity;
private ActionBarHandler actionBarHandler;
private ProgressBar progressBar;
private int streamingServiceId = -1;
private boolean autoPlayEnabled;
private boolean showNextStreamItem;
private View thumbnailWindowLayout;
//this only remains due to downwards compatibility
private FloatingActionButton playVideoButton;
private final Point initialThumbnailPos = new Point(0, 0);
private View rootView = null;
private Bitmap streamThumbnail = null;
private ImageLoader imageLoader = ImageLoader.getInstance();
private DisplayImageOptions displayImageOptions =
new DisplayImageOptions.Builder().cacheInMemory(true).build();
private InfoItemBuilder infoItemBuilder = null;
public interface OnInvokeCreateOptionsMenuListener {
void createOptionsMenu();
}
private OnInvokeCreateOptionsMenuListener onInvokeCreateOptionsMenuListener;
private void updateInfo(final StreamInfo info) {
Activity a = getActivity();
RelativeLayout textContentLayout =
(RelativeLayout) activity.findViewById(R.id.detail_text_content_layout);
final TextView videoTitleView =
(TextView) activity.findViewById(R.id.detail_video_title_view);
TextView uploaderView = (TextView) activity.findViewById(R.id.detail_uploader_view);
TextView viewCountView = (TextView) activity.findViewById(R.id.detail_view_count_view);
TextView thumbsUpView = (TextView) activity.findViewById(R.id.detail_thumbs_up_count_view);
TextView thumbsDownView =
(TextView) activity.findViewById(R.id.detail_thumbs_down_count_view);
TextView uploadDateView = (TextView) activity.findViewById(R.id.detail_upload_date_view);
TextView descriptionView = (TextView) activity.findViewById(R.id.detail_description_view);
RecyclerView nextStreamView =
(RecyclerView) activity.findViewById(R.id.detail_next_stream_content);
RelativeLayout nextVideoRootFrame =
(RelativeLayout) activity.findViewById(R.id.detail_next_stream_root_layout);
TextView similarTitle = (TextView) activity.findViewById(R.id.detail_similar_title);
Button backgroundButton = (Button)
activity.findViewById(R.id.detail_stream_thumbnail_window_background_button);
View topView = activity.findViewById(R.id.detailTopView);
Button channelButton = (Button) activity.findViewById(R.id.channel_button);
// prevents a crash if the activity/fragment was already left when the response came
if(channelButton != null) {
progressBar.setVisibility(View.GONE);
if (info.next_video != null) {
// todo: activate this function or remove it
nextStreamView.setVisibility(View.GONE);
} else {
nextStreamView.setVisibility(View.GONE);
activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE);
}
textContentLayout.setVisibility(View.VISIBLE);
if (android.os.Build.VERSION.SDK_INT < 18) {
playVideoButton.setVisibility(View.VISIBLE);
} else {
ImageView playArrowView = (ImageView) activity.findViewById(R.id.play_arrow_view);
playArrowView.setVisibility(View.VISIBLE);
}
if (!showNextStreamItem) {
nextVideoRootFrame.setVisibility(View.GONE);
similarTitle.setVisibility(View.GONE);
}
videoTitleView.setText(info.title);
topView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == android.view.MotionEvent.ACTION_UP) {
ImageView arrow = (ImageView) activity.findViewById(R.id.toggle_description_view);
View extra = activity.findViewById(R.id.detailExtraView);
if (extra.getVisibility() == View.VISIBLE) {
extra.setVisibility(View.GONE);
arrow.setImageResource(R.drawable.arrow_down);
} else {
extra.setVisibility(View.VISIBLE);
arrow.setImageResource(R.drawable.arrow_up);
}
}
return true;
}
});
// Since newpipe is designed to work even if certain information is not available,
// the UI has to react on missing information.
videoTitleView.setText(info.title);
if (!info.uploader.isEmpty()) {
uploaderView.setText(info.uploader);
} else {
activity.findViewById(R.id.detail_uploader_view).setVisibility(View.GONE);
}
if (info.view_count >= 0) {
viewCountView.setText(Localization.localizeViewCount(info.view_count, a));
} else {
viewCountView.setVisibility(View.GONE);
}
if (info.dislike_count >= 0) {
thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, a));
} else {
thumbsDownView.setVisibility(View.INVISIBLE);
activity.findViewById(R.id.detail_thumbs_down_count_view).setVisibility(View.GONE);
}
if (info.like_count >= 0) {
thumbsUpView.setText(Localization.localizeNumber(info.like_count, a));
} else {
thumbsUpView.setVisibility(View.GONE);
activity.findViewById(R.id.detail_thumbs_up_img_view).setVisibility(View.GONE);
thumbsDownView.setVisibility(View.GONE);
activity.findViewById(R.id.detail_thumbs_down_img_view).setVisibility(View.GONE);
}
if (!info.upload_date.isEmpty()) {
uploadDateView.setText(Localization.localizeDate(info.upload_date, a));
} else {
uploadDateView.setVisibility(View.GONE);
}
if (!info.description.isEmpty()) {
descriptionView.setText(Html.fromHtml(info.description));
} else {
descriptionView.setVisibility(View.GONE);
}
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
// parse streams
Vector<VideoStream> streamsToUse = new Vector<>();
for (VideoStream i : info.video_streams) {
if (useStream(i, streamsToUse)) {
streamsToUse.add(i);
}
}
textContentLayout.setVisibility(View.VISIBLE);
if (info.next_video == null) {
activity.findViewById(R.id.detail_next_stream_title).setVisibility(View.GONE);
}
if (info.related_streams != null && !info.related_streams.isEmpty()) {
initSimilarVideos(info);
} else {
activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE);
activity.findViewById(R.id.similar_streams_view).setVisibility(View.GONE);
}
setupActionBarHandler(info);
if (autoPlayEnabled) {
playVideo(info);
}
if (android.os.Build.VERSION.SDK_INT < 18) {
playVideoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
playVideo(info);
}
});
}
backgroundButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
playVideo(info);
}
});
if (info.channel_url != null && info.channel_url != "") {
channelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent i = new Intent(activity, ChannelActivity.class);
i.putExtra(ChannelActivity.CHANNEL_URL, info.channel_url);
i.putExtra(ChannelActivity.SERVICE_ID, info.service_id);
startActivity(i);
}
});
} else {
channelButton.setVisibility(Button.GONE);
}
initThumbnailViews(info);
}
}
private void initThumbnailViews(final StreamInfo info) {
ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
ImageView uploaderThumb
= (ImageView) activity.findViewById(R.id.detail_uploader_thumbnail_view);
if (info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
imageLoader.displayImage(info.thumbnail_url, videoThumbnailView,
displayImageOptions, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
ErrorActivity.reportError(getActivity(),
failReason.getCause(), null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE,
NewPipe.getNameOfService(info.service_id), imageUri,
R.string.could_not_load_thumbnails));
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
streamThumbnail = loadedImage;
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
}
});
} else {
videoThumbnailView.setImageResource(R.drawable.dummy_thumbnail_dark);
}
if (info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) {
imageLoader.displayImage(info.uploader_thumbnail_url,
uploaderThumb, displayImageOptions,
new ImageErrorLoadingListener(activity, rootView, info.service_id));
}
}
private void setupActionBarHandler(final StreamInfo info) {
actionBarHandler.setupStreamList(info.video_streams);
actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() {
@Override
public void onActionSelected(int selectedStreamId) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url);
intent.setType("text/plain");
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
}
});
actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() {
@Override
public void onActionSelected(int selectedStreamId) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(info.webpage_url));
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser)));
}
});
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
@Override
public void onActionSelected(int selectedStreamId) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(KORE_PACKET);
intent.setData(Uri.parse(info.webpage_url.replace("https", "http")));
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(activity.getString(R.string.fdroid_kore_url)));
activity.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
}
});
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
@Override
public void onActionSelected(int selectedStreamId) {
if(!PermissionHelper.checkStoragePermissions(getActivity())) {
return;
}
try {
Bundle args = new Bundle();
// Sometimes it may be that some information is not available due to changes fo the
// website which was crawled. Then the ui has to understand this and act right.
if (info.audio_streams != null) {
AudioStream audioStream =
info.audio_streams.get(getPreferredAudioStreamId(info));
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
args.putString(DownloadDialog.AUDIO_URL, audioStream.url);
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
}
if (info.video_streams != null) {
VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format);
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url);
}
args.putString(DownloadDialog.TITLE, info.title);
DownloadDialog downloadDialog = DownloadDialog.newInstance(args);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
});
if (info.audio_streams == null) {
actionBarHandler.showAudioAction(false);
} else {
actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() {
@Override
public void onActionSelected(int selectedStreamId) {
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
Intent intent;
AudioStream audioStream =
info.audio_streams.get(getPreferredAudioStreamId(info));
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
//internal music player: explicit intent
if (!BackgroundPlayer.isRunning && streamThumbnail != null) {
ActivityCommunicator.getCommunicator()
.backgroundPlayerThumbnail = streamThumbnail;
intent = new Intent(activity, BackgroundPlayer.class);
intent.setAction(Intent.ACTION_VIEW);
Log.i(TAG, "audioStream is null:" + (audioStream == null));
Log.i(TAG, "audioStream.url is null:" + (audioStream.url == null));
intent.setDataAndType(Uri.parse(audioStream.url),
MediaFormat.getMimeById(audioStream.format));
intent.putExtra(BackgroundPlayer.TITLE, info.title);
intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url);
intent.putExtra(BackgroundPlayer.SERVICE_ID, streamingServiceId);
intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader);
activity.startService(intent);
}
} else {
intent = new Intent();
try {
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(audioStream.url),
MediaFormat.getMimeById(audioStream.format));
intent.putExtra(Intent.EXTRA_TITLE, info.title);
intent.putExtra("title", info.title);
// HERE !!!
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.no_player_found)
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
activity.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.i(TAG, "You unlocked a secret unicorn.");
}
});
builder.create().show();
Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:");
e.printStackTrace();
}
}
}
});
}
}
private int getPreferredAudioStreamId(final StreamInfo info) {
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity())
.getString(activity.getString(R.string.default_audio_format_key), "webm");
int preferredFormat = MediaFormat.WEBMA.id;
switch(preferredFormatString) {
case "webm":
preferredFormat = MediaFormat.WEBMA.id;
break;
case "m4a":
preferredFormat = MediaFormat.M4A.id;
break;
default:
break;
}
for(int i = 0; i < info.audio_streams.size(); i++) {
if(info.audio_streams.get(i).format == preferredFormat) {
return i;
}
}
//todo: make this a proper error
Log.e(TAG, "FAILED to set audioStream value!");
return 0;
}
private void initSimilarVideos(final StreamInfo info) {
LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similar_streams_view);
for (final StreamPreviewInfo item : info.related_streams) {
similarLayout.addView(infoItemBuilder.buildView(similarLayout, item));
}
infoItemBuilder.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
@Override
public void selected(String url) {
openStreamUrl(url);
}
});
}
private void onErrorBlockedByGema() {
Button backgroundButton = (Button)
activity.findViewById(R.id.detail_stream_thumbnail_window_background_button);
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
progressBar.setVisibility(View.GONE);
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
getResources(), R.drawable.gruese_die_gema));
backgroundButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(activity.getString(R.string.c3s_url)));
activity.startActivity(intent);
}
});
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
}
private void onNotSpecifiedContentError() {
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
progressBar.setVisibility(View.GONE);
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
getResources(), R.drawable.not_available_monkey));
Toast.makeText(activity, R.string.content_not_available, Toast.LENGTH_LONG)
.show();
}
private void onNotSpecifiedContentErrorWithMessage(int resourceId) {
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
progressBar.setVisibility(View.GONE);
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
getResources(), R.drawable.not_available_monkey));
Toast.makeText(activity, resourceId, Toast.LENGTH_LONG)
.show();
}
private boolean useStream(VideoStream stream, Vector<VideoStream> streams) {
for(VideoStream i : streams) {
if(i.resolution.equals(stream.resolution)) {
return false;
}
}
return true;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = (AppCompatActivity) getActivity();
showNextStreamItem = PreferenceManager.getDefaultSharedPreferences(getActivity())
.getBoolean(activity.getString(R.string.show_next_video_key), true);
StreamInfoWorker siw = StreamInfoWorker.getInstance();
siw.setOnStreamInfoReceivedListener(new StreamInfoWorker.OnStreamInfoReceivedListener() {
@Override
public void onReceive(StreamInfo info) {
updateInfo(info);
}
@Override
public void onError(int messageId) {
postNewErrorToast(messageId);
}
@Override
public void onReCaptchaException() {
Toast.makeText(getActivity(), R.string.recaptcha_request_toast,
Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
startActivityForResult(
new Intent(getActivity(), ReCaptchaActivity.class),
RECAPTCHA_REQUEST);
}
@Override
public void onBlockedByGemaError() {
onErrorBlockedByGema();
}
@Override
public void onContentErrorWithMessage(int messageId) {
onNotSpecifiedContentErrorWithMessage(messageId);
}
@Override
public void onContentError() {
onNotSpecifiedContentError();
}
});
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false);
progressBar = (ProgressBar) rootView.findViewById(R.id.detail_progress_bar);
actionBarHandler = new ActionBarHandler(activity);
actionBarHandler.setupNavMenu(activity);
if(onInvokeCreateOptionsMenuListener != null) {
onInvokeCreateOptionsMenuListener.createOptionsMenu();
}
return rootView;
}
@Override
public void onStart() {
super.onStart();
Activity a = getActivity();
infoItemBuilder = new InfoItemBuilder(a, a.findViewById(android.R.id.content));
if (android.os.Build.VERSION.SDK_INT < 18) {
playVideoButton = (FloatingActionButton) a.findViewById(R.id.play_video_button);
}
thumbnailWindowLayout = a.findViewById(R.id.detail_stream_thumbnail_window_layout);
Button backgroundButton = (Button)
a.findViewById(R.id.detail_stream_thumbnail_window_background_button);
// Sometimes when this fragment is not visible it still gets initiated
// then we must not try to access objects of this fragment.
// Otherwise the applications would crash.
if(backgroundButton != null) {
streamingServiceId = getArguments().getInt(STREAMING_SERVICE);
String videoUrl = getArguments().getString(VIDEO_URL);
StreamInfoWorker siw = StreamInfoWorker.getInstance();
siw.search(streamingServiceId, videoUrl, getActivity());
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
if(Build.VERSION.SDK_INT >= 18) {
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
// This is used to synchronize the thumbnailWindowButton and the playVideoButton
// inside the ScrollView with the actual size of the thumbnail.
//todo: onLayoutChage sometimes not triggered
// background buttons area seem to overlap the thumbnail view
// So although you just clicked slightly beneath the thumbnail the action still
// gets triggered.
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
RelativeLayout.LayoutParams newWindowLayoutParams =
(RelativeLayout.LayoutParams) thumbnailWindowLayout.getLayoutParams();
newWindowLayoutParams.height = bottom - top;
thumbnailWindowLayout.setLayoutParams(newWindowLayoutParams);
//noinspection SuspiciousNameCombination
initialThumbnailPos.set(top, left);
}
});
}
}
}
public void playVideo(final StreamInfo info) {
// ----------- THE MAGIC MOMENT ---------------
VideoStream selectedVideoStream =
info.video_streams.get(actionBarHandler.getSelectedVideoStream());
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_video_player_key), false)) {
// External Player
Intent intent = new Intent();
try {
intent.setAction(Intent.ACTION_VIEW)
.setDataAndType(Uri.parse(selectedVideoStream.url),
MediaFormat.getMimeById(selectedVideoStream.format))
.putExtra(Intent.EXTRA_TITLE, info.title)
.putExtra("title", info.title);
activity.startActivity(intent); // HERE !!!
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.no_player_found)
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent()
.setAction(Intent.ACTION_VIEW)
.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
activity.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
} else {
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
// exo player
if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) {
// try dash
Intent intent = new Intent(activity, ExoPlayerActivity.class)
.setData(Uri.parse(info.dashMpdUrl))
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
startActivity(intent);
} else if((info.audio_streams != null && !info.audio_streams.isEmpty()) &&
(info.video_only_streams != null && !info.video_only_streams.isEmpty())) {
// try smooth streaming
} else {
//default streaming
Intent intent = new Intent(activity, ExoPlayerActivity.class)
.setDataAndType(Uri.parse(selectedVideoStream.url),
MediaFormat.getMimeById(selectedVideoStream.format))
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER);
activity.startActivity(intent); // HERE !!!
}
//-------------
} else {
// Internal Player
Intent intent = new Intent(activity, PlayVideoActivity.class)
.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title)
.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url)
.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url)
.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
activity.startActivity(intent); //also HERE !!!
}
}
// --------------------------------------------
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
actionBarHandler.setupMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
return actionBarHandler.onItemSelected(item);
}
public void setOnInvokeCreateOptionsMenuListener(OnInvokeCreateOptionsMenuListener listener) {
this.onInvokeCreateOptionsMenuListener = listener;
}
private void postNewErrorToast(final int stringResource) {
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
stringResource, Toast.LENGTH_LONG).show();
}
private void openStreamUrl(String url) {
Intent detailIntent = new Intent(activity, VideoItemDetailActivity.class);
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
detailIntent.putExtra(
VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
activity.startActivity(detailIntent);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case RECAPTCHA_REQUEST:
if (resultCode == RESULT_OK) {
String videoUrl = getArguments().getString(VIDEO_URL);
StreamInfoWorker siw = StreamInfoWorker.getInstance();
siw.search(streamingServiceId, videoUrl, getActivity());
} else {
Log.d(TAG, "ReCaptcha failed");
}
break;
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
break;
}
}
}

View File

@@ -3,18 +3,15 @@ package org.schabi.newpipe.download;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.FragmentTransaction;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -28,32 +25,28 @@ import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.File;
import java.util.Vector;
import us.shandian.giga.get.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.fragment.AllMissionsFragment;
import us.shandian.giga.ui.fragment.MissionsFragment;
import us.shandian.giga.util.CrashHandler;
import us.shandian.giga.util.Utility;
public class DownloadActivity extends ThemableActivity implements AdapterView.OnItemClickListener{
public class DownloadActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
public static final String INTENT_DOWNLOAD = "us.shandian.giga.intent.DOWNLOAD";
public static final String INTENT_LIST = "us.shandian.giga.intent.LIST";
private static final String TAG = DownloadActivity.class.toString();
public static final String THREADS = "threads";
private static final String TAG = DownloadActivity.class.toString();
private MissionsFragment mFragment;
@@ -72,9 +65,9 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
startService(i);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, true);
setContentView(R.layout.activity_downloader);
//noinspection ConstantConditions
// its ok if this fails, we will catch that error later, and send it as report
@@ -242,9 +235,7 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
switch (id) {
case android.R.id.home: {
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);
onBackPressed();
return true;
}
case R.id.action_settings: {
@@ -252,14 +243,6 @@ public class DownloadActivity extends ThemableActivity implements AdapterView.On
startActivity(intent);
return true;
}
case R.id.action_report_error: {
ErrorActivity.reportError(DownloadActivity.this, new Vector<Throwable>(),
null, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.USER_REPORT,
null,
"user_report", R.string.user_report));
return true;
}
default:
return super.onOptionsItemSelected(item);
}

View File

@@ -1,41 +0,0 @@
package org.schabi.newpipe.extractor;
/**
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* AbstractStreamInfo.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**Common properties between StreamInfo and StreamPreviewInfo.*/
public abstract class AbstractStreamInfo {
public static enum StreamType {
NONE, // placeholder to check if stream type was checked or not
VIDEO_STREAM,
AUDIO_STREAM,
LIVE_STREAM,
AUDIO_LIVE_STREAM,
FILE
}
public StreamType stream_type;
public int service_id = -1;
public String id = "";
public String title = "";
public String uploader = "";
public String thumbnail_url = "";
public String webpage_url = "";
public String upload_date = "";
public long view_count = -1;
}

View File

@@ -1,113 +0,0 @@
package org.schabi.newpipe.extractor;
import android.util.Xml;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.xmlpull.v1.XmlPullParser;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Vector;
/**
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* DashMpdParser.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class DashMpdParser {
private DashMpdParser() {
}
static class DashMpdParsingException extends ParsingException {
DashMpdParsingException(String message, Exception e) {
super(message, e);
}
}
public static List<AudioStream> getAudioStreams(String dashManifestUrl)
throws DashMpdParsingException, ReCaptchaException {
String dashDoc;
Downloader downloader = NewPipe.getDownloader();
try {
dashDoc = downloader.download(dashManifestUrl);
} catch(IOException ioe) {
throw new DashMpdParsingException("Could not get dash mpd: " + dashManifestUrl, ioe);
} catch (ReCaptchaException e) {
throw new ReCaptchaException("reCaptcha Challenge needed");
}
Vector<AudioStream> audioStreams = new Vector<>();
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(dashDoc));
String tagName = "";
String currentMimeType = "";
int currentBandwidth = -1;
int currentSamplingRate = -1;
boolean currentTagIsBaseUrl = false;
for(int eventType = parser.getEventType();
eventType != XmlPullParser.END_DOCUMENT;
eventType = parser.next() ) {
switch(eventType) {
case XmlPullParser.START_TAG:
tagName = parser.getName();
if(tagName.equals("AdaptationSet")) {
currentMimeType = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "mimeType");
} else if(tagName.equals("Representation") && currentMimeType.contains("audio")) {
currentBandwidth = Integer.parseInt(
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "bandwidth"));
currentSamplingRate = Integer.parseInt(
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "audioSamplingRate"));
} else if(tagName.equals("BaseURL")) {
currentTagIsBaseUrl = true;
}
break;
case XmlPullParser.TEXT:
// actual stream tag
if(currentTagIsBaseUrl &&
(currentMimeType.contains("audio"))) {
int format = -1;
if(currentMimeType.equals(MediaFormat.WEBMA.mimeType)) {
format = MediaFormat.WEBMA.id;
} else if(currentMimeType.equals(MediaFormat.M4A.mimeType)) {
format = MediaFormat.M4A.id;
}
audioStreams.add(new AudioStream(parser.getText(),
format, currentBandwidth, currentSamplingRate));
}
break;
case XmlPullParser.END_TAG:
if(tagName.equals("AdaptationSet")) {
currentMimeType = "";
} else if(tagName.equals("BaseURL")) {
currentTagIsBaseUrl = false;
}
break;
}
}
} catch(Exception e) {
throw new DashMpdParsingException("Could not parse Dash mpd", e);
}
return audioStreams;
}
}

View File

@@ -1,52 +0,0 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.IOException;
import java.util.Map;
/**
* Created by Christian Schabesberger on 28.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* Downloader.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public interface Downloader {
/**Download the text file at the supplied URL as in download(String),
* but set the HTTP header field "Accept-Language" to the supplied string.
* @param siteUrl the URL of the text file to return the contents of
* @param language the language (usually a 2-character code) to set as the preferred language
* @return the contents of the specified text file
* @throws IOException*/
String download(String siteUrl, String language) throws IOException, ReCaptchaException;
/**Download the text file at the supplied URL as in download(String),
* but set the HTTP header field "Accept-Language" to the supplied string.
* @param siteUrl the URL of the text file to return the contents of
* @param customProperties set request header properties
* @return the contents of the specified text file
* @throws IOException*/
String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException;
/**Download (via HTTP) the text file located at the supplied URL, and return its contents.
* Primarily intended for downloading web pages.
* @param siteUrl the URL of the text file to download
* @return the contents of the specified text file
* @throws IOException*/
String download(String siteUrl) throws IOException, ReCaptchaException;
}

View File

@@ -1,83 +0,0 @@
package org.schabi.newpipe.extractor;
/**
* Created by Adam Howard on 08/11/15.
*
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
* and Adam Howard <achdisposable1@gmail.com> 2015
*
* MediaFormat.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**Static data about various media formats support by Newpipe, eg mime type, extension*/
public enum MediaFormat {
//video and audio combined formats
// id name suffix mime type
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
v3GPP (0x1, "3GPP", "3gp", "video/3gpp"),
WEBM (0x2, "WebM", "webm", "video/webm"),
// audio formats
M4A (0x3, "m4a", "m4a", "audio/mp4"),
WEBMA (0x4, "WebM", "webm", "audio/webm");
public final int id;
@SuppressWarnings("WeakerAccess")
public final String name;
@SuppressWarnings("WeakerAccess")
public final String suffix;
public final String mimeType;
MediaFormat(int id, String name, String suffix, String mimeType) {
this.id = id;
this.name = name;
this.suffix = suffix;
this.mimeType = mimeType;
}
/**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.
* @return the friendly name of the MediaFormat associated with this ids,
* or an empty String if none match it.*/
public static String getNameById(int ident) {
for (MediaFormat vf : MediaFormat.values()) {
if(vf.id == ident) return vf.name;
}
return "";
}
/**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.
* @return the file extension of the MediaFormat associated with this ids,
* or an empty String if none match it.*/
public static String getSuffixById(int ident) {
for (MediaFormat vf : MediaFormat.values()) {
if(vf.id == ident) return vf.suffix;
}
return "";
}
/**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.
* @return the MIME type of the MediaFormat associated with this ids,
* or an empty String if none match it.*/
public static String getMimeById(int ident) {
for (MediaFormat vf : MediaFormat.values()) {
if(vf.id == ident) return vf.mimeType;
}
return "";
}
}

View File

@@ -1,82 +0,0 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
/**
* Created by Christian Schabesberger on 23.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* NewPipe.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**Provides access to the video streaming services supported by NewPipe.
* Currently only Youtube until the API becomes more stable.*/
@SuppressWarnings("ALL")
public class NewPipe {
private NewPipe() {
}
private static final String TAG = NewPipe.class.toString();
private static final StreamingService[] serviceList = {
new YoutubeService(0)
};
private static Downloader downloader = null;
public static StreamingService[] getServices() {
return serviceList;
}
public static StreamingService getService(int serviceId)throws ExtractionException {
for(StreamingService s : serviceList) {
if(s.getServiceId() == serviceId) {
return s;
}
}
throw new ExtractionException("Service not known: " + Integer.toString(serviceId));
}
public static StreamingService getService(String serviceName) throws ExtractionException {
return serviceList[getIdOfService(serviceName)];
}
public static String getNameOfService(int id) {
try {
return getService(id).getServiceInfo().name;
} catch (Exception e) {
System.err.println("Service id not known");
e.printStackTrace();
return "";
}
}
public static int getIdOfService(String serviceName) throws ExtractionException {
for(int i = 0; i < serviceList.length; i++) {
if(serviceList[i].getServiceInfo().name.equals(serviceName)) {
return i;
}
}
throw new ExtractionException("Error: Service " + serviceName + " not known.");
}
public static void init(Downloader d) {
downloader = d;
}
public static Downloader getDownloader() {
return downloader;
}
}

View File

@@ -1,69 +0,0 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* Parser.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/** avoid using regex !!! */
public class Parser {
private Parser() {
}
public static class RegexException extends ParsingException {
public RegexException(String message) {
super(message);
}
}
public static String matchGroup1(String pattern, String input) throws RegexException {
Pattern pat = Pattern.compile(pattern);
Matcher mat = pat.matcher(input);
boolean foundMatch = mat.find();
if (foundMatch) {
return mat.group(1);
}
else {
//Log.e(TAG, "failed to find pattern \""+pattern+"\" inside of \""+input+"\"");
throw new RegexException("failed to find pattern \""+pattern+" inside of "+input+"\"");
}
}
public static Map<String, String> compatParseMap(final String input) throws UnsupportedEncodingException {
Map<String, String> map = new HashMap<>();
for(String arg : input.split("&")) {
String[] splitArg = arg.split("=");
if(splitArg.length > 1) {
map.put(splitArg[0], URLDecoder.decode(splitArg[1], "UTF-8"));
} else {
map.put(splitArg[0], "");
}
}
return map;
}
}

View File

@@ -1,56 +0,0 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SuggestionExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 23.08.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamingService.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class StreamingService {
public class ServiceInfo {
public String name = "";
}
private int serviceId;
public StreamingService(int id) {
serviceId = id;
}
public abstract ServiceInfo getServiceInfo();
public abstract StreamExtractor getExtractorInstance(String url)
throws IOException, ExtractionException;
public abstract SearchEngine getSearchEngineInstance();
public abstract UrlIdHandler getUrlIdHandlerInstance();
public abstract UrlIdHandler getChannelUrlIdHandlerInstance();
public abstract ChannelExtractor getChannelExtractorInstance(String url, int page)
throws ExtractionException, IOException;
public abstract SuggestionExtractor getSuggestionExtractorInstance();
public final int getServiceId() {
return serviceId;
}
}

View File

@@ -1,34 +0,0 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
/**
* Created by Christian Schabesberger on 26.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* UrlIdHandler.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public interface UrlIdHandler {
String getUrl(String videoId);
String getId(String siteUrl) throws ParsingException;
String cleanUrl(String siteUrl) throws ParsingException;
/**When a VIEW_ACTION is caught this function will test if the url delivered within the calling
Intent was meant to be watched with this Service.
Return false if this service shall not allow to be called through ACTIONs.*/
boolean acceptUrl(String videoUrl);
}

View File

@@ -1,61 +0,0 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoCollector;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 25.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class ChannelExtractor {
private int serviceId;
private String url;
private UrlIdHandler urlIdHandler;
private StreamPreviewInfoCollector previewInfoCollector;
private int page = -1;
public ChannelExtractor(UrlIdHandler urlIdHandler, String url, int page, int serviceId)
throws ExtractionException, IOException {
this.url = url;
this.page = page;
this.serviceId = serviceId;
this.urlIdHandler = urlIdHandler;
previewInfoCollector = new StreamPreviewInfoCollector(urlIdHandler, serviceId);
}
public String getUrl() { return url; }
public UrlIdHandler getUrlIdHandler() { return urlIdHandler; }
public StreamPreviewInfoCollector getStreamPreviewInfoCollector() {
return previewInfoCollector;
}
public abstract String getChannelName() throws ParsingException;
public abstract String getAvatarUrl() throws ParsingException;
public abstract String getBannerUrl() throws ParsingException;
public abstract String getFeedUrl() throws ParsingException;
public abstract StreamPreviewInfoCollector getStreams() throws ParsingException;
public abstract boolean hasNextPage() throws ParsingException;
public int getServiceId() {
return serviceId;
}
}

View File

@@ -1,81 +0,0 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoCollector;
import java.util.List;
import java.util.Vector;
/**
* Created by Christian Schabesberger on 31.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfo.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ChannelInfo {
public void addException(Exception e) {
errors.add(e);
}
public static ChannelInfo getInfo(ChannelExtractor extractor)
throws ParsingException {
ChannelInfo info = new ChannelInfo();
// importand data
info.service_id = extractor.getServiceId();
info.channel_name = extractor.getChannelName();
info.hasNextPage = extractor.hasNextPage();
try {
info.avatar_url = extractor.getAvatarUrl();
} catch (Exception e) {
info.errors.add(e);
}
try {
info.banner_url = extractor.getBannerUrl();
} catch (Exception e) {
info.errors.add(e);
}
try {
info.feed_url = extractor.getFeedUrl();
} catch(Exception e) {
info.errors.add(e);
}
try {
StreamPreviewInfoCollector c = extractor.getStreams();
info.related_streams = c.getItemList();
info.errors.addAll(c.getErrors());
} catch(Exception e) {
info.errors.add(e);
}
return info;
}
public int service_id = -1;
public String channel_name = "";
public String avatar_url = "";
public String banner_url = "";
public String feed_url = "";
public List<StreamPreviewInfo> related_streams = null;
public boolean hasNextPage = false;
public List<Throwable> errors = new Vector<>();
}

View File

@@ -1,33 +0,0 @@
package org.schabi.newpipe.extractor.exceptions;
/**
* Created by Christian Schabesberger on 30.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ExtractionException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExtractionException extends Exception {
public ExtractionException(String message) {
super(message);
}
public ExtractionException(Throwable cause) {
super(cause);
}
public ExtractionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,30 +0,0 @@
package org.schabi.newpipe.extractor.exceptions;
/**
* Created by Christian Schabesberger on 12.09.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* FoundAdException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class FoundAdException extends ParsingException {
public FoundAdException(String message) {
super(message);
}
public FoundAdException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,31 +0,0 @@
package org.schabi.newpipe.extractor.exceptions;
/**
* Created by Christian Schabesberger on 31.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ParsingException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ParsingException extends ExtractionException {
public ParsingException(String message) {
super(message);
}
public ParsingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,27 +0,0 @@
package org.schabi.newpipe.extractor.exceptions;
/**
* Created by beneth <bmauduit@beneth.fr> on 07.12.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ReCaptchaException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ReCaptchaException extends ExtractionException {
public ReCaptchaException(String message) {
super(message);
}
}

View File

@@ -1,49 +0,0 @@
package org.schabi.newpipe.extractor.search;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 10.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* SearchEngine.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class SearchEngine {
public static class NothingFoundException extends ExtractionException {
public NothingFoundException(String message) {
super(message);
}
}
private StreamPreviewInfoSearchCollector collector;
public SearchEngine(UrlIdHandler urlIdHandler, int serviceId) {
collector = new StreamPreviewInfoSearchCollector(urlIdHandler, serviceId);
}
protected StreamPreviewInfoSearchCollector getStreamPreviewInfoSearchCollector() {
return collector;
}
//Result search(String query, int page);
public abstract StreamPreviewInfoSearchCollector search(
String query, int page, String contentCountry)
throws ExtractionException, IOException;
}

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe.extractor.search;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import java.io.IOException;
import java.util.List;
import java.util.Vector;
/**
* Created by Christian Schabesberger on 29.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* SearchResult.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class SearchResult {
public static SearchResult getSearchResult(SearchEngine engine, String query,
int page, String languageCode)
throws ExtractionException, IOException {
SearchResult result = engine.search(query, page, languageCode).getSearchResult();
if(result.resultList.isEmpty()) {
if(result.suggestion.isEmpty()) {
throw new ExtractionException("Empty result despite no error");
} else {
// This is used as a fallback. Do not relay on it !!!
throw new SearchEngine.NothingFoundException(result.suggestion);
}
}
return result;
}
public String suggestion = "";
public List<StreamPreviewInfo> resultList = new Vector<>();
public List<Throwable> errors = new Vector<>();
}

View File

@@ -1,45 +0,0 @@
package org.schabi.newpipe.extractor.search;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoCollector;
/**
* Created by Christian Schabesberger on 11.05.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamPreviewInfoSearchCollector.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class StreamPreviewInfoSearchCollector extends StreamPreviewInfoCollector {
private String suggestion = "";
public StreamPreviewInfoSearchCollector(UrlIdHandler handler, int serviceId) {
super(handler, serviceId);
}
public void setSuggestion(String suggestion) {
this.suggestion = suggestion;
}
public SearchResult getSearchResult() {
SearchResult result = new SearchResult();
result.suggestion = suggestion;
result.errors = getErrors();
result.resultList = getItemList();
return result;
}
}

View File

@@ -1,43 +0,0 @@
package org.schabi.newpipe.extractor.search;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.io.IOException;
import java.util.List;
/**
* Created by Christian Schabesberger on 28.09.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* SuggestionExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class SuggestionExtractor {
private int serviceId;
public SuggestionExtractor(int serviceId) {
this.serviceId = serviceId;
}
public abstract List<String> suggestionList(
String query,String contentCountry)
throws ExtractionException, IOException;
public int getServiceId() {
return serviceId;
}
}

View File

@@ -1,339 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoCollector;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoExtractor;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 25.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeChannelExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeChannelExtractor extends ChannelExtractor {
private static final String TAG = YoutubeChannelExtractor.class.toString();
// private CSSOMParser cssParser = new CSSOMParser(new SACParserCSS3());
private Document doc = null;
private boolean isAjaxPage = false;
private static String userUrl = "";
private static String channelName = "";
private static String avatarUrl = "";
private static String bannerUrl = "";
private static String feedUrl = "";
// the fist page is html all other pages are ajax. Every new page can be requested by sending
// this request url.
private static String nextPageUrl = "";
public YoutubeChannelExtractor(UrlIdHandler urlIdHandler, String url, int page, int serviceId)
throws ExtractionException, IOException {
super(urlIdHandler, url, page, serviceId);
Downloader downloader = NewPipe.getDownloader();
url = urlIdHandler.cleanUrl(url) ; //+ "/video?veiw=0&flow=list&sort=dd";
if(page == 0) {
if (isUserUrl(url)) {
userUrl = url;
} else {
// we first need to get the user url. Otherwise we can't find videos
String channelPageContent = downloader.download(url);
Document channelDoc = Jsoup.parse(channelPageContent, url);
userUrl = getUserUrl(channelDoc);
}
userUrl = userUrl + "/videos?veiw=0&flow=list&sort=dd&live_view=10000";
String pageContent = downloader.download(userUrl);
doc = Jsoup.parse(pageContent, userUrl);
nextPageUrl = getNextPageUrl(doc);
isAjaxPage = false;
} else {
String ajaxDataRaw = downloader.download(nextPageUrl);
JSONObject ajaxData;
try {
ajaxData = new JSONObject(ajaxDataRaw);
String htmlDataRaw = ajaxData.getString("content_html");
doc = Jsoup.parse(htmlDataRaw, nextPageUrl);
String nextPageHtmlDataRaw = ajaxData.getString("load_more_widget_html");
if(!nextPageHtmlDataRaw.isEmpty()) {
Document nextPageData = Jsoup.parse(nextPageHtmlDataRaw, nextPageUrl);
nextPageUrl = getNextPageUrl(nextPageData);
} else {
nextPageUrl = "";
}
} catch (JSONException e) {
throw new ParsingException("Could not parse json data for next page", e);
}
isAjaxPage = true;
}
}
@Override
public String getChannelName() throws ParsingException {
try {
if(!isAjaxPage) {
channelName = doc.select("span[class=\"qualified-channel-title-text\"]").first()
.select("a").first().text();
}
return channelName;
} catch(Exception e) {
throw new ParsingException("Could not get channel name");
}
}
@Override
public String getAvatarUrl() throws ParsingException {
try {
if(!isAjaxPage) {
avatarUrl = doc.select("img[class=\"channel-header-profile-image\"]")
.first().attr("abs:src");
}
return avatarUrl;
} catch(Exception e) {
throw new ParsingException("Could not get avatar", e);
}
}
@Override
public String getBannerUrl() throws ParsingException {
try {
if(!isAjaxPage) {
Element el = doc.select("div[id=\"gh-banner\"]").first().select("style").first();
String cssContent = el.html();
String url = "https:" + Parser.matchGroup1("url\\(([^)]+)\\)", cssContent);
if (url.contains("s.ytimg.com")) {
bannerUrl = null;
} else {
bannerUrl = url;
}
}
return bannerUrl;
} catch(Exception e) {
throw new ParsingException("Could not get Banner", e);
}
}
@Override
public StreamPreviewInfoCollector getStreams() throws ParsingException {
StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector();
Element ul = null;
if(isAjaxPage) {
ul = doc.select("body").first();
} else {
ul = doc.select("ul[id=\"browse-items-primary\"]").first();
}
for(final Element li : ul.children()) {
if (li.select("div[class=\"feed-item-dismissable\"]").first() != null) {
collector.commit(new StreamPreviewInfoExtractor() {
@Override
public AbstractStreamInfo.StreamType getStreamType() throws ParsingException {
return AbstractStreamInfo.StreamType.VIDEO_STREAM;
}
@Override
public String getWebPageUrl() throws ParsingException {
try {
Element el = li.select("div[class=\"feed-item-dismissable\"]").first();
Element dl = el.select("h3").first().select("a").first();
return dl.attr("abs:href");
} catch (Exception e) {
throw new ParsingException("Could not get web page url for the video", e);
}
}
@Override
public String getTitle() throws ParsingException {
try {
Element el = li.select("div[class=\"feed-item-dismissable\"]").first();
Element dl = el.select("h3").first().select("a").first();
return dl.text();
} catch (Exception e) {
throw new ParsingException("Could not get title", e);
}
}
@Override
public int getDuration() throws ParsingException {
try {
return YoutubeParsingHelper.parseDurationString(
li.select("span[class=\"video-time\"]").first().text());
} catch(Exception e) {
if(isLiveStream(li)) {
// -1 for no duration
return -1;
} else {
throw new ParsingException("Could not get Duration: " + getTitle(), e);
}
}
}
@Override
public String getUploader() throws ParsingException {
return getChannelName();
}
@Override
public String getUploadDate() throws ParsingException {
try {
return li.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").first()
.text();
} catch(Exception e) {
throw new ParsingException("Could not get uplaod date", e);
}
}
@Override
public long getViewCount() throws ParsingException {
String output;
String input;
try {
input = li.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").get(1)
.text();
} catch (IndexOutOfBoundsException e) {
if(isLiveStream(li)) {
// -1 for no view count
return -1;
} else {
throw new ParsingException(
"Could not parse yt-lockup-meta although available: " + getTitle(), e);
}
}
output = Parser.matchGroup1("([0-9,\\. ]*)", input)
.replace(" ", "")
.replace(".", "")
.replace(",", "");
try {
return Long.parseLong(output);
} catch (NumberFormatException e) {
// if this happens the video probably has no views
if(!input.isEmpty()) {
return 0;
} else {
throw new ParsingException("Could not handle input: " + input, e);
}
}
}
@Override
public String getThumbnailUrl() throws ParsingException {
try {
String url;
Element te = li.select("span[class=\"yt-thumb-clip\"]").first()
.select("img").first();
url = te.attr("abs:src");
// Sometimes youtube sends links to gif files which somehow seem to not exist
// anymore. Items with such gif also offer a secondary image source. So we are going
// to use that if we've caught such an item.
if (url.contains(".gif")) {
url = te.attr("abs:data-thumb");
}
return url;
} catch (Exception e) {
throw new ParsingException("Could not get thumbnail url", e);
}
}
private boolean isLiveStream(Element item) {
Element bla = item.select("span[class*=\"yt-badge-live\"]").first();
if(bla == null) {
// sometimes livestreams dont have badges but sill are live streams
// if video time is not available we most likly have an offline livestream
if(item.select("span[class*=\"video-time\"]").first() == null) {
return true;
}
}
return bla != null;
}
});
}
}
return collector;
}
@Override
public String getFeedUrl() throws ParsingException {
try {
if(userUrl.contains("channel")) {
//channels don't have feeds in youtube, only user can provide such
return "";
}
if(!isAjaxPage) {
feedUrl = doc.select("link[title=\"RSS\"]").first().attr("abs:href");
}
return feedUrl;
} catch(Exception e) {
throw new ParsingException("Could not get feed url", e);
}
}
@Override
public boolean hasNextPage() throws ParsingException {
return !nextPageUrl.isEmpty();
}
private String getUserUrl(Document d) throws ParsingException {
return d.select("span[class=\"qualified-channel-title-text\"]").first()
.select("a").first().attr("abs:href");
}
private boolean isUserUrl(String url) throws ParsingException {
return url.contains("/user/");
}
private String getNextPageUrl(Document d) throws ParsingException {
try {
Element button = d.select("button[class*=\"yt-uix-load-more\"]").first();
if(button != null) {
return button.attr("abs:data-uix-load-more-href");
} else {
// sometimes channels are simply so small, they don't have a second/next4q page
return "";
}
} catch(Exception e) {
throw new ParsingException("could not load next page url", e);
}
}
}

View File

@@ -1,47 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
/**
* Created by Christian Schabesberger on 25.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeChannelUrlIdHandler.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeChannelUrlIdHandler implements UrlIdHandler {
public String getUrl(String channelId) {
return "https://www.youtube.com/" + channelId;
}
public String getId(String siteUrl) throws ParsingException {
return Parser.matchGroup1("/(user/[A-Za-z0-9_-]*|channel/[A-Za-z0-9_-]*)", siteUrl);
}
public String cleanUrl(String siteUrl) throws ParsingException {
return getUrl(getId(siteUrl));
}
public boolean acceptUrl(String videoUrl) {
return (videoUrl.contains("youtube") ||
videoUrl.contains("youtu.be")) &&
( videoUrl.contains("/user/") ||
videoUrl.contains("/channel/"));
}
}

View File

@@ -1,66 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
/**
* Created by Christian Schabesberger on 02.03.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeParsingHelper.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeParsingHelper {
private YoutubeParsingHelper() {
}
public static int parseDurationString(String input)
throws ParsingException, NumberFormatException {
String[] splitInput = input.split(":");
String days = "0";
String hours = "0";
String minutes = "0";
String seconds;
switch(splitInput.length) {
case 4:
days = splitInput[0];
hours = splitInput[1];
minutes = splitInput[2];
seconds = splitInput[3];
break;
case 3:
hours = splitInput[0];
minutes = splitInput[1];
seconds = splitInput[2];
break;
case 2:
minutes = splitInput[0];
seconds = splitInput[1];
break;
case 1:
seconds = splitInput[0];
break;
default:
throw new ParsingException("Error duration string with unknown format: " + input);
}
return ((((Integer.parseInt(days) * 24)
+ Integer.parseInt(hours) * 60)
+ Integer.parseInt(minutes)) * 60)
+ Integer.parseInt(seconds);
}
}

View File

@@ -1,113 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.StreamPreviewInfoSearchCollector;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoExtractor;
import java.net.URLEncoder;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 09.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeSearchEngine.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeSearchEngine extends SearchEngine {
private static final String TAG = YoutubeSearchEngine.class.toString();
public static final String CHARSET_UTF_8 = "UTF-8";
public YoutubeSearchEngine(UrlIdHandler urlIdHandler, int serviceId) {
super(urlIdHandler, serviceId);
}
@Override
public StreamPreviewInfoSearchCollector search(String query, int page, String languageCode)
throws IOException, ExtractionException {
StreamPreviewInfoSearchCollector collector = getStreamPreviewInfoSearchCollector();
Downloader downloader = NewPipe.getDownloader();
String url = "https://www.youtube.com/results"
+ "?search_query=" + URLEncoder.encode(query, CHARSET_UTF_8)
+ "&page=" + Integer.toString(page + 1)
+ "&filters=" + "video";
String site;
//String url = builder.build().toString();
//if we've been passed a valid language code, append it to the URL
if(!languageCode.isEmpty()) {
//assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode);
site = downloader.download(url, languageCode);
}
else {
site = downloader.download(url);
}
Document doc = Jsoup.parse(site, url);
Element list = doc.select("ol[class=\"item-section\"]").first();
for (Element item : list.children()) {
/* First we need to determine which kind of item we are working with.
Youtube depicts five different kinds of items on its search result page. These are
regular videos, playlists, channels, two types of video suggestions, and a "no video
found" item. Since we only want videos, we need to filter out all the others.
An example for this can be seen here:
https://www.youtube.com/results?search_query=asdf&page=1
We already applied a filter to the url, so we don't need to care about channels and
playlists now.
*/
Element el;
// both types of spell correction item
if ((el = item.select("div[class*=\"spell-correction\"]").first()) != null) {
collector.setSuggestion(el.select("a").first().text());
if(list.children().size() == 1) {
throw new NothingFoundException("Did you mean: " + el.select("a").first().text());
}
// search message item
} else if ((el = item.select("div[class*=\"search-message\"]").first()) != null) {
//result.errorMessage = el.text();
throw new NothingFoundException(el.text());
// video item type
} else if ((el = item.select("div[class*=\"yt-lockup-video\"").first()) != null) {
collector.commit(extractPreviewInfo(el));
} else {
//noinspection ConstantConditions
collector.addError(new Exception("unexpected element found:\"" + el + "\""));
}
}
return collector;
}
private StreamPreviewInfoExtractor extractPreviewInfo(final Element item) {
return new YoutubeStreamPreviewInfoExtractor(item);
}
}

View File

@@ -1,82 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SuggestionExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 23.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeService.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeService extends StreamingService {
public YoutubeService(int id) {
super(id);
}
@Override
public ServiceInfo getServiceInfo() {
ServiceInfo serviceInfo = new ServiceInfo();
serviceInfo.name = "Youtube";
return serviceInfo;
}
@Override
public StreamExtractor getExtractorInstance(String url)
throws ExtractionException, IOException {
UrlIdHandler urlIdHandler = YoutubeStreamUrlIdHandler.getInstance();
if(urlIdHandler.acceptUrl(url)) {
return new YoutubeStreamExtractor(urlIdHandler, url, getServiceId());
}
else {
throw new IllegalArgumentException("supplied String is not a valid Youtube URL");
}
}
@Override
public SearchEngine getSearchEngineInstance() {
return new YoutubeSearchEngine(getUrlIdHandlerInstance(), getServiceId());
}
@Override
public UrlIdHandler getUrlIdHandlerInstance() {
return YoutubeStreamUrlIdHandler.getInstance();
}
@Override
public UrlIdHandler getChannelUrlIdHandlerInstance() {
return new YoutubeChannelUrlIdHandler();
}
@Override
public ChannelExtractor getChannelExtractorInstance(String url, int page)
throws ExtractionException, IOException {
return new YoutubeChannelExtractor(getChannelUrlIdHandlerInstance(), url, page, getServiceId());
}
@Override
public SuggestionExtractor getSuggestionExtractorInstance() {
return new YoutubeSuggestionExtractor(getServiceId());
}
}

View File

@@ -1,852 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptableObject;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
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.stream_info.AudioStream;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoCollector;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoExtractor;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Christian Schabesberger on 06.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeStreamExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeStreamExtractor extends StreamExtractor {
public static final String URL_ENCODED_FMT_STREAM_MAP = "url_encoded_fmt_stream_map";
public static final String HTTPS = "https:";
public static final String CONTENT = "content";
public static final String REGEX_INT = "[^\\d]";
// exceptions
public class DecryptException extends ParsingException {
DecryptException(String message, Throwable cause) {
super(message, cause);
}
}
// special content not available exceptions
public class GemaException extends ContentNotAvailableException {
GemaException(String message) {
super(message);
}
}
public class LiveStreamException extends ContentNotAvailableException {
LiveStreamException(String message) {
super(message);
}
}
// ----------------
// Sometimes if the html page of youtube is already downloaded, youtube web page will internally
// download the /get_video_info page. Since a certain date dashmpd url is only available over
// this /get_video_info page, so we always need to download this one to.
// %%video_id%% will be replaced by the actual video id
// $$el_type$$ will be replaced by the actual el_type (se the declarations below)
private static final String GET_VIDEO_INFO_URL =
"https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en";
// eltype is necessary for the url above
private static final String EL_INFO = "el=info";
public enum ItagType {
AUDIO,
VIDEO,
VIDEO_ONLY
}
private static class ItagItem {
public ItagItem(int id, ItagType type, MediaFormat format, String res, int fps) {
this.id = id;
this.itagType = type;
this.mediaFormatId = format.id;
this.resolutionString = res;
this.fps = fps;
}
public ItagItem(int id, ItagType type, MediaFormat format, int samplingRate, int bandWidth) {
this.id = id;
this.itagType = type;
this.mediaFormatId = format.id;
this.samplingRate = samplingRate;
this.bandWidth = bandWidth;
}
public int id;
public ItagType itagType;
public int mediaFormatId;
public String resolutionString;
public int fps = -1;
public int samplingRate = -1;
public int bandWidth = -1;
}
private static final ItagItem[] itagList = {
// video streams
// id, ItagType, MediaFormat, Resolution, fps
new ItagItem(17, ItagType.VIDEO, MediaFormat.v3GPP, "144p", 12),
new ItagItem(18, ItagType.VIDEO, MediaFormat.MPEG_4, "360p", 24),
new ItagItem(22, ItagType.VIDEO, MediaFormat.MPEG_4, "720p", 24),
new ItagItem(36, ItagType.VIDEO, MediaFormat.v3GPP, "240p", 24),
new ItagItem(37, ItagType.VIDEO, MediaFormat.MPEG_4, "1080p", 24),
new ItagItem(38, ItagType.VIDEO, MediaFormat.MPEG_4, "1080p", 24),
new ItagItem(43, ItagType.VIDEO, MediaFormat.WEBM, "360p", 24),
new ItagItem(44, ItagType.VIDEO, MediaFormat.WEBM, "480p", 24),
new ItagItem(45, ItagType.VIDEO, MediaFormat.WEBM, "720p", 24),
new ItagItem(46, ItagType.VIDEO, MediaFormat.WEBM, "1080p", 24),
// audio streams
// id, ItagType, MediaFormat, samplingR, bandwidth
new ItagItem(249, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0), // bandwith/samplingR 0 because not known
new ItagItem(250, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0),
new ItagItem(171, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0),
new ItagItem(140, ItagType.AUDIO, MediaFormat.M4A, 0, 0),
new ItagItem(251, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0),
// video only streams
new ItagItem(160, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "144p", 24),
new ItagItem(133, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "240p", 24),
new ItagItem(134, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "360p", 24),
new ItagItem(135, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "480p", 24),
new ItagItem(136, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "720p", 24),
new ItagItem(137, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "1080p", 24),
};
/**These lists only contain itag formats that are supported by the common Android Video player.
However if you are looking for a list showing all itag formats, look at
https://github.com/rg3/youtube-dl/issues/1687 */
public static boolean itagIsSupported(int itag) {
for(ItagItem item : itagList) {
if(itag == item.id) {
return true;
}
}
return false;
}
public static ItagItem getItagItem(int itag) throws ParsingException {
for(ItagItem item : itagList) {
if(itag == item.id) {
return item;
}
}
throw new ParsingException("itag=" + Integer.toString(itag) + " not supported");
}
private static final String TAG = YoutubeStreamExtractor.class.toString();
private final Document doc;
private JSONObject playerArgs;
private boolean isAgeRestricted;
private Map<String, String> videoInfoPage;
// static values
private static final String DECRYPTION_FUNC_NAME="decrypt";
// cached values
private static volatile String decryptionCode = "";
UrlIdHandler urlidhandler = YoutubeStreamUrlIdHandler.getInstance();
String pageUrl = "";
public YoutubeStreamExtractor(UrlIdHandler urlIdHandler, String pageUrl, int serviceId)
throws ExtractionException, IOException {
super(urlIdHandler, pageUrl, serviceId);
//most common videoInfo fields are now set in our superclass, for all services
this.pageUrl = pageUrl;
Downloader downloader = NewPipe.getDownloader();
String pageContent = downloader.download(urlidhandler.cleanUrl(pageUrl));
doc = Jsoup.parse(pageContent, pageUrl);
JSONObject ytPlayerConfig;
String playerUrl;
// Check if the video is age restricted
if (pageContent.contains("<meta property=\"og:restrictions:age")) {
String videoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
urlidhandler.getId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
String videoInfoPageString = downloader.download(videoInfoUrl);
videoInfoPage = Parser.compatParseMap(videoInfoPageString);
playerUrl = getPlayerUrlFromRestrictedVideo(pageUrl);
isAgeRestricted = true;
} else {
ytPlayerConfig = getPlayerConfig(pageContent);
playerArgs = getPlayerArgs(ytPlayerConfig);
playerUrl = getPlayerUrl(ytPlayerConfig);
isAgeRestricted = false;
}
if(decryptionCode.isEmpty()) {
decryptionCode = loadDecryptionCode(playerUrl);
}
}
private JSONObject getPlayerConfig(String pageContent) throws ParsingException {
try {
String ytPlayerConfigRaw =
Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent);
return new JSONObject(ytPlayerConfigRaw);
} catch (Parser.RegexException e) {
String errorReason = findErrorReason(doc);
switch(errorReason) {
case "GEMA":
throw new GemaException(errorReason);
case "":
throw new ContentNotAvailableException("Content not available: player config empty", e);
default:
throw new ContentNotAvailableException("Content not available", e);
}
} catch (JSONException e) {
throw new ParsingException("Could not parse yt player config", e);
}
}
private JSONObject getPlayerArgs(JSONObject playerConfig) throws ParsingException {
JSONObject playerArgs;
//attempt to load the youtube js player JSON arguments
boolean isLiveStream = false; //used to determine if this is a livestream or not
try {
playerArgs = playerConfig.getJSONObject("args");
// check if we have a live stream. We need to filter it, since its not yet supported.
if((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
|| (playerArgs.get(URL_ENCODED_FMT_STREAM_MAP).toString().isEmpty())) {
isLiveStream = true;
}
} catch (JSONException e) {
throw new ParsingException("Could not parse yt player config", e);
}
if (isLiveStream) {
throw new LiveStreamException("This is a Life stream. Can't use those right now.");
}
return playerArgs;
}
private String getPlayerUrl(JSONObject playerConfig) throws ParsingException {
try {
// The Youtube service needs to be initialized by downloading the
// js-Youtube-player. This is done in order to get the algorithm
// for decrypting cryptic signatures inside certain stream urls.
String playerUrl = "";
JSONObject ytAssets = playerConfig.getJSONObject("assets");
playerUrl = ytAssets.getString("js");
if (playerUrl.startsWith("//")) {
playerUrl = HTTPS + playerUrl;
}
return playerUrl;
} catch (JSONException e) {
throw new ParsingException(
"Could not load decryption code for the Youtube service.", e);
}
}
private String getPlayerUrlFromRestrictedVideo(String pageUrl) throws ParsingException, ReCaptchaException {
try {
Downloader downloader = NewPipe.getDownloader();
String playerUrl = "";
String videoId = urlidhandler.getId(pageUrl);
String embedUrl = "https://www.youtube.com/embed/" + videoId;
String embedPageContent = downloader.download(embedUrl);
//todo: find out if this can be reapaced by Parser.matchGroup1()
Pattern assetsPattern = Pattern.compile("\"assets\":.+?\"js\":\\s*(\"[^\"]+\")");
Matcher patternMatcher = assetsPattern.matcher(embedPageContent);
while (patternMatcher.find()) {
playerUrl = patternMatcher.group(1);
}
playerUrl = playerUrl.replace("\\", "").replace("\"", "");
if (playerUrl.startsWith("//")) {
playerUrl = HTTPS + playerUrl;
}
return playerUrl;
} catch (IOException e) {
throw new ParsingException(
"Could load decryption code form restricted video for the Youtube service.", e);
} catch (ReCaptchaException e) {
throw new ReCaptchaException("reCaptcha Challenge requested");
}
}
@Override
public String getTitle() throws ParsingException {
try {
if (playerArgs == null) {
return videoInfoPage.get("title");
}
//json player args method
return playerArgs.getString("title");
} catch(JSONException je) {//html <meta> method
je.printStackTrace();
System.err.println("failed to load title from JSON args; trying to extract it from HTML");
try { // fall through to fall-back
return doc.select("meta[name=title]").attr(CONTENT);
} catch (Exception e) {
throw new ParsingException("failed permanently to load title.", e);
}
}
}
@Override
public String getDescription() throws ParsingException {
try {
return doc.select("p[id=\"eow-description\"]").first().html();
} catch (Exception e) {//todo: add fallback method <-- there is no ... as long as i know
throw new ParsingException("failed to load description.", e);
}
}
@Override
public String getUploader() throws ParsingException {
try {
if (playerArgs == null) {
return videoInfoPage.get("author");
}
//json player args method
return playerArgs.getString("author");
} catch(JSONException je) {
je.printStackTrace();
System.err.println(
"failed to load uploader name from JSON args; trying to extract it from HTML");
} try {//fall through to fallback HTML method
return doc.select("div.yt-user-info").first().text();
} catch (Exception e) {
throw new ParsingException("failed permanently to load uploader name.", e);
}
}
@Override
public int getLength() throws ParsingException {
try {
if (playerArgs == null) {
return Integer.valueOf(videoInfoPage.get("length_seconds"));
}
return playerArgs.getInt("length_seconds");
} catch (JSONException e) {//todo: find fallback method
throw new ParsingException("failed to load video duration from JSON args", e);
}
}
@Override
public long getViewCount() throws ParsingException {
try {
String viewCountString = doc.select("meta[itemprop=interactionCount]").attr(CONTENT);
return Long.parseLong(viewCountString);
} catch (Exception e) {//todo: find fallback method
throw new ParsingException("failed to get number of views", e);
}
}
@Override
public String getUploadDate() throws ParsingException {
try {
return doc.select("meta[itemprop=datePublished]").attr(CONTENT);
} catch (Exception e) {//todo: add fallback method
throw new ParsingException("failed to get upload date.", e);
}
}
@Override
public String getThumbnailUrl() throws ParsingException {
//first attempt getting a small image version
//in the html extracting part we try to get a thumbnail with a higher resolution
// Try to get high resolution thumbnail if it fails use low res from the player instead
try {
return doc.select("link[itemprop=\"thumbnailUrl\"]").first().attr("abs:href");
} catch(Exception e) {
System.err.println("Could not find high res Thumbnail. Using low res instead");
}
try { //fall through to fallback
return playerArgs.getString("thumbnail_url");
} catch (JSONException je) {
throw new ParsingException(
"failed to extract thumbnail URL from JSON args; trying to extract it from HTML", je);
} catch (NullPointerException ne) {
// Get from the video info page instead
return videoInfoPage.get("thumbnail_url");
}
}
@Override
public String getUploaderThumbnailUrl() throws ParsingException {
try {
return doc.select("a[class*=\"yt-user-photo\"]").first()
.select("img").first()
.attr("abs:data-thumb");
} catch (Exception e) {//todo: add fallback method
throw new ParsingException("failed to get uploader thumbnail URL.", e);
}
}
@Override
public String getDashMpdUrl() throws ParsingException {
/*
try {
String dashManifestUrl = videoInfoPage.get("dashmpd");
if(!dashManifestUrl.contains("/signature/")) {
String encryptedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifestUrl);
String decryptedSig;
decryptedSig = decryptSignature(encryptedSig, decryptionCode);
dashManifestUrl = dashManifestUrl.replace("/s/" + encryptedSig, "/signature/" + decryptedSig);
}
return dashManifestUrl;
} catch (Exception e) {
throw new ParsingException(
"Could not get \"dashmpd\" maybe VideoInfoPage is broken.", e);
}
*/
return "";
}
@Override
public List<AudioStream> getAudioStreams() throws ParsingException {
Vector<AudioStream> audioStreams = new Vector<>();
try{
String encodedUrlMap;
// playerArgs could be null if the video is age restricted
if (playerArgs == null) {
encodedUrlMap = videoInfoPage.get("adaptive_fmts");
} else {
encodedUrlMap = playerArgs.getString("adaptive_fmts");
}
for(String url_data_str : encodedUrlMap.split(",")) {
// This loop iterates through multiple streams, therefor tags
// is related to one and the same stream at a time.
Map<String, String> tags = Parser.compatParseMap(
org.jsoup.parser.Parser.unescapeEntities(url_data_str, true));
int itag = Integer.parseInt(tags.get("itag"));
if (itagIsSupported(itag)) {
ItagItem itagItem = getItagItem(itag);
if (itagItem.itagType == ItagType.AUDIO) {
String streamUrl = tags.get("url");
// if video has a signature: decrypt it and add it to the url
if (tags.get("s") != null) {
streamUrl = streamUrl + "&signature="
+ decryptSignature(tags.get("s"), decryptionCode);
}
audioStreams.add(new AudioStream(streamUrl,
itagItem.mediaFormatId,
itagItem.bandWidth,
itagItem.samplingRate));
}
}
}
} catch (Exception e) {
throw new ParsingException("Could not get audiostreams", e);
}
return audioStreams;
}
@Override
public List<VideoStream> getVideoStreams() throws ParsingException {
Vector<VideoStream> videoStreams = new Vector<>();
try{
String encodedUrlMap;
// playerArgs could be null if the video is age restricted
if (playerArgs == null) {
encodedUrlMap = videoInfoPage.get(URL_ENCODED_FMT_STREAM_MAP);
} else {
encodedUrlMap = playerArgs.getString(URL_ENCODED_FMT_STREAM_MAP);
}
for(String url_data_str : encodedUrlMap.split(",")) {
try {
// This loop iterates through multiple streams, therefor tags
// is related to one and the same stream at a time.
Map<String, String> tags = Parser.compatParseMap(
org.jsoup.parser.Parser.unescapeEntities(url_data_str, true));
int itag = Integer.parseInt(tags.get("itag"));
if (itagIsSupported(itag)) {
ItagItem itagItem = getItagItem(itag);
if(itagItem.itagType == ItagType.VIDEO) {
String streamUrl = tags.get("url");
// if video has a signature: decrypt it and add it to the url
if (tags.get("s") != null) {
streamUrl = streamUrl + "&signature="
+ decryptSignature(tags.get("s"), decryptionCode);
}
videoStreams.add(new VideoStream(
streamUrl,
itagItem.mediaFormatId,
itagItem.resolutionString));
}
}
} catch (Exception e) {
//todo: dont log throw an error
System.err.println("Could not get Video stream.");
e.printStackTrace();
}
}
} catch (Exception e) {
throw new ParsingException("Failed to get video streams", e);
}
if(videoStreams.isEmpty()) {
throw new ParsingException("Failed to get any video stream");
}
return videoStreams;
}
@Override
public List<VideoStream> getVideoOnlyStreams() throws ParsingException {
return null;
}
/**Attempts to parse (and return) the offset to start playing the video from.
* @return the offset (in seconds), or 0 if no timestamp is found.*/
@Override
public int getTimeStamp() throws ParsingException {
String timeStamp;
try {
timeStamp = Parser.matchGroup1("((#|&|\\?)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)", pageUrl);
} catch (Parser.RegexException e) {
// catch this instantly since an url does not necessarily have to have a time stamp
// -2 because well the testing system will then know its the regex that failed :/
// not good i know
return -2;
}
if(!timeStamp.isEmpty()) {
try {
String secondsString = "";
String minutesString = "";
String hoursString = "";
try {
secondsString = Parser.matchGroup1("(\\d{1,3})s", timeStamp);
minutesString = Parser.matchGroup1("(\\d{1,3})m", timeStamp);
hoursString = Parser.matchGroup1("(\\d{1,3})h", timeStamp);
} catch (Exception e) {
//it could be that time is given in another method
if (secondsString.isEmpty() //if nothing was got,
&& minutesString.isEmpty()//treat as unlabelled seconds
&& hoursString.isEmpty()) {
secondsString = Parser.matchGroup1("t=(\\d+)", timeStamp);
}
}
int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString);
int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString);
int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString);
//don't trust BODMAS!
return seconds + (60 * minutes) + (3600 * hours);
//Log.d(TAG, "derived timestamp value:"+ret);
//the ordering varies internationally
} catch (ParsingException e) {
throw new ParsingException("Could not get timestamp.", e);
}
} else {
return 0;
}
}
@Override
public int getAgeLimit() throws ParsingException {
if (!isAgeRestricted) {
return 0;
}
try {
return Integer.valueOf(doc.head()
.getElementsByAttributeValue("property", "og:restrictions:age")
.attr(CONTENT).replace("+", ""));
} catch (Exception e) {
throw new ParsingException("Could not get age restriction");
}
}
@Override
public String getAverageRating() throws ParsingException {
try {
if (playerArgs == null) {
return videoInfoPage.get("avg_rating");
}
return playerArgs.getString("avg_rating");
} catch (JSONException e) {
throw new ParsingException("Could not get Average rating", e);
}
}
@Override
public int getLikeCount() throws ParsingException {
String likesString = "";
try {
Element button = doc.select("button.like-button-renderer-like-button").first();
try {
likesString = button.select("span.yt-uix-button-content").first().text();
} catch (NullPointerException e) {
//if this ckicks in our button has no content and thefore likes/dislikes are disabled
return -1;
}
return Integer.parseInt(likesString.replaceAll(REGEX_INT, ""));
} catch (NumberFormatException nfe) {
throw new ParsingException(
"failed to parse likesString \"" + likesString + "\" as integers", nfe);
} catch (Exception e) {
throw new ParsingException("Could not get like count", e);
}
}
@Override
public int getDislikeCount() throws ParsingException {
String dislikesString = "";
try {
Element button = doc.select("button.like-button-renderer-dislike-button").first();
try {
dislikesString = button.select("span.yt-uix-button-content").first().text();
} catch (NullPointerException e) {
//if this kicks in our button has no content and therefore likes/dislikes are disabled
return -1;
}
return Integer.parseInt(dislikesString.replaceAll(REGEX_INT, ""));
} catch(NumberFormatException nfe) {
throw new ParsingException(
"failed to parse dislikesString \"" + dislikesString + "\" as integers", nfe);
} catch(Exception e) {
throw new ParsingException("Could not get dislike count", e);
}
}
@Override
public StreamPreviewInfoExtractor getNextVideo() throws ParsingException {
try {
return extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first()
.select("li").first());
} catch(Exception e) {
throw new ParsingException("Could not get next video", e);
}
}
@Override
public StreamPreviewInfoCollector getRelatedVideos() throws ParsingException {
try {
StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector();
Element ul = doc.select("ul[id=\"watch-related\"]").first();
if(ul != null) {
for (Element li : ul.children()) {
// first check if we have a playlist. If so leave them out
if (li.select("a[class*=\"content-link\"]").first() != null) {
collector.commit(extractVideoPreviewInfo(li));
}
}
}
return collector;
} catch(Exception e) {
throw new ParsingException("Could not get related videos", e);
}
}
@Override
public String getPageUrl() {
return pageUrl;
}
@Override
public String getChannelUrl() throws ParsingException {
try {
return doc.select("div[class=\"yt-user-info\"]").first().children()
.select("a").first().attr("abs:href");
} catch(Exception e) {
throw new ParsingException("Could not get channel link", e);
}
}
@Override
public StreamInfo.StreamType getStreamType() throws ParsingException {
//todo: if implementing livestream support this value should be generated dynamically
return StreamInfo.StreamType.VIDEO_STREAM;
}
/**Provides information about links to other videos on the video page, such as related videos.
* This is encapsulated in a StreamPreviewInfo object,
* which is a subset of the fields in a full StreamInfo.*/
private StreamPreviewInfoExtractor extractVideoPreviewInfo(final Element li) {
return new StreamPreviewInfoExtractor() {
@Override
public AbstractStreamInfo.StreamType getStreamType() throws ParsingException {
return null;
}
@Override
public String getWebPageUrl() throws ParsingException {
return li.select("a.content-link").first().attr("abs:href");
}
@Override
public String getTitle() throws ParsingException {
//todo: check NullPointerException causing
return li.select("span.title").first().text();
//this page causes the NullPointerException, after finding it by searching for "tjvg":
//https://www.youtube.com/watch?v=Uqg0aEhLFAg
}
@Override
public int getDuration() throws ParsingException {
return YoutubeParsingHelper.parseDurationString(
li.select("span.video-time").first().text());
}
@Override
public String getUploader() throws ParsingException {
return li.select("span.g-hovercard").first().text();
}
@Override
public String getUploadDate() throws ParsingException {
return null;
}
@Override
public long getViewCount() throws ParsingException {
//this line is unused
//String views = li.select("span.view-count").first().text();
//Log.i(TAG, "title:"+info.title);
//Log.i(TAG, "view count:"+views);
try {
return Long.parseLong(li.select("span.view-count")
.first().text().replaceAll(REGEX_INT, ""));
} catch (Exception e) {
//related videos sometimes have no view count
return 0;
}
}
@Override
public String getThumbnailUrl() throws ParsingException {
Element img = li.select("img").first();
String thumbnailUrl = img.attr("abs:src");
// Sometimes youtube sends links to gif files which somehow seem to not exist
// anymore. Items with such gif also offer a secondary image source. So we are going
// to use that if we caught such an item.
if (thumbnailUrl.contains(".gif")) {
thumbnailUrl = img.attr("data-thumb");
}
if (thumbnailUrl.startsWith("//")) {
thumbnailUrl = HTTPS + thumbnailUrl;
}
return thumbnailUrl;
}
};
}
private String loadDecryptionCode(String playerUrl) throws DecryptException {
String decryptionFuncName;
String decryptionFunc;
String helperObjectName;
String helperObject;
String callerFunc = "function " + DECRYPTION_FUNC_NAME + "(a){return %%(a);}";
String decryptionCode;
try {
Downloader downloader = NewPipe.getDownloader();
String playerCode = downloader.download(playerUrl);
decryptionFuncName =
Parser.matchGroup1("\\.sig\\|\\|([a-zA-Z0-9$]+)\\(", playerCode);
String functionPattern = "("
+ decryptionFuncName.replace("$", "\\$")
+ "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})";
decryptionFunc = "var " + Parser.matchGroup1(functionPattern, playerCode) + ";";
helperObjectName = Parser
.matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(", decryptionFunc);
String helperPattern = "(var "
+ helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)";
helperObject = Parser.matchGroup1(helperPattern, playerCode);
callerFunc = callerFunc.replace("%%", decryptionFuncName);
decryptionCode = helperObject + decryptionFunc + callerFunc;
} catch(IOException ioe) {
throw new DecryptException("Could not load decrypt function", ioe);
} catch(Exception e) {
throw new DecryptException("Could not parse decrypt function ", e);
}
return decryptionCode;
}
private String decryptSignature(String encryptedSig, String decryptionCode)
throws DecryptException{
Context context = Context.enter();
context.setOptimizationLevel(-1);
Object result = null;
try {
ScriptableObject scope = context.initStandardObjects();
context.evaluateString(scope, decryptionCode, "decryptionCode", 1, null);
Function decryptionFunc = (Function) scope.get("decrypt", scope);
result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig});
} catch (Exception e) {
throw new DecryptException("could not get decrypt signature", e);
} finally {
Context.exit();
}
return result == null ? "" : result.toString();
}
private String findErrorReason(Document doc) {
String errorMessage = doc.select("h1[id=\"unavailable-message\"]").first().text();
if(errorMessage.contains("GEMA")) {
// Gema sometimes blocks youtube music content in germany:
// https://www.gema.de/en/
// Detailed description:
// https://en.wikipedia.org/wiki/GEMA_%28German_organization%29
return "GEMA";
}
return "";
}
}

View File

@@ -1,169 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfoExtractor;
/**
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeStreamPreviewInfoExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeStreamPreviewInfoExtractor implements StreamPreviewInfoExtractor {
private final Element item;
public YoutubeStreamPreviewInfoExtractor(Element item) {
this.item = item;
}
@Override
public String getWebPageUrl() throws ParsingException {
try {
Element el = item.select("div[class*=\"yt-lockup-video\"").first();
Element dl = el.select("h3").first().select("a").first();
return dl.attr("abs:href");
} catch (Exception e) {
throw new ParsingException("Could not get web page url for the video", e);
}
}
@Override
public String getTitle() throws ParsingException {
try {
Element el = item.select("div[class*=\"yt-lockup-video\"").first();
Element dl = el.select("h3").first().select("a").first();
return dl.text();
} catch (Exception e) {
throw new ParsingException("Could not get title", e);
}
}
@Override
public int getDuration() throws ParsingException {
try {
return YoutubeParsingHelper.parseDurationString(
item.select("span[class=\"video-time\"]").first().text());
} catch(Exception e) {
if(isLiveStream(item)) {
// -1 for no duration
return -1;
} else {
throw new ParsingException("Could not get Duration: " + getTitle(), e);
}
}
}
@Override
public String getUploader() throws ParsingException {
try {
return item.select("div[class=\"yt-lockup-byline\"]").first()
.select("a").first()
.text();
} catch (Exception e) {
throw new ParsingException("Could not get uploader", e);
}
}
@Override
public String getUploadDate() throws ParsingException {
try {
return item.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").first()
.text();
} catch(Exception e) {
throw new ParsingException("Could not get uplaod date", e);
}
}
@Override
public long getViewCount() throws ParsingException {
String output;
String input;
try {
input = item.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").get(1)
.text();
} catch (IndexOutOfBoundsException e) {
if(isLiveStream(item)) {
// -1 for no view count
return -1;
} else {
throw new ParsingException(
"Could not parse yt-lockup-meta although available: " + getTitle(), e);
}
}
output = Parser.matchGroup1("([0-9,\\. ]*)", input)
.replace(" ", "")
.replace(".", "")
.replace(",", "");
try {
return Long.parseLong(output);
} catch (NumberFormatException e) {
// if this happens the video probably has no views
if(!input.isEmpty()) {
return 0;
} else {
throw new ParsingException("Could not handle input: " + input, e);
}
}
}
@Override
public String getThumbnailUrl() throws ParsingException {
try {
String url;
Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first()
.select("img").first();
url = te.attr("abs:src");
// Sometimes youtube sends links to gif files which somehow seem to not exist
// anymore. Items with such gif also offer a secondary image source. So we are going
// to use that if we've caught such an item.
if (url.contains(".gif")) {
url = te.attr("abs:data-thumb");
}
return url;
} catch (Exception e) {
throw new ParsingException("Could not get thumbnail url", e);
}
}
@Override
public AbstractStreamInfo.StreamType getStreamType() {
if(isLiveStream(item)) {
return AbstractStreamInfo.StreamType.LIVE_STREAM;
} else {
return AbstractStreamInfo.StreamType.VIDEO_STREAM;
}
}
private boolean isLiveStream(Element item) {
Element bla = item.select("span[class*=\"yt-badge-live\"]").first();
if(bla == null) {
// sometimes livestreams dont have badges but sill are live streams
// if video time is not available we most likly have an offline livestream
if(item.select("span[class*=\"video-time\"]").first() == null) {
return true;
}
}
return bla != null;
}
}

View File

@@ -1,156 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import android.support.annotation.NonNull;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.regex.Matcher;
/**
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeStreamUrlIdHandler.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeStreamUrlIdHandler implements UrlIdHandler {
private static final YoutubeStreamUrlIdHandler instance = new YoutubeStreamUrlIdHandler();
private static final String ID_PATTERN = "([\\-a-zA-Z0-9_]{11})";
private YoutubeStreamUrlIdHandler() {}
public static YoutubeStreamUrlIdHandler getInstance() {
return instance;
}
@Override
public String getUrl(String videoId) {
return "https://www.youtube.com/watch?v=" + videoId;
}
@Override
public String getId(String url) throws ParsingException, IllegalArgumentException {
if(url.isEmpty()) {
throw new IllegalArgumentException("The url parameter should not be empty");
}
String id;
String lowercaseUrl = url.toLowerCase();
if(lowercaseUrl.contains("youtube")) {
if (url.contains("attribution_link")) {
try {
String escapedQuery = Parser.matchGroup1("u=(.[^&|$]*)", url);
String query = URLDecoder.decode(escapedQuery, "UTF-8");
id = Parser.matchGroup1("v=" + ID_PATTERN, query);
} catch (UnsupportedEncodingException uee) {
throw new ParsingException("Could not parse attribution_link", uee);
}
} else if(lowercaseUrl.contains("youtube.com/shared?ci=")) {
return getRealIdFromSharedLink(url);
} else if (url.contains("vnd.youtube")) {
id = Parser.matchGroup1(ID_PATTERN, url);
} else if (url.contains("embed")) {
id = Parser.matchGroup1("embed/" + ID_PATTERN, url);
} else if(url.contains("googleads")) {
throw new FoundAdException("Error found add: " + url);
} else {
id = Parser.matchGroup1("[?&]v=" + ID_PATTERN, url);
}
}
else if(lowercaseUrl.contains("youtu.be")) {
if(url.contains("v=")) {
id = Parser.matchGroup1("v=" + ID_PATTERN, url);
} else {
id = Parser.matchGroup1("[Yy][Oo][Uu][Tt][Uu]\\.[Bb][Ee]/" + ID_PATTERN, url);
}
}
else {
throw new ParsingException("Error no suitable url: " + url);
}
if(!id.isEmpty()){
return id;
} else {
throw new ParsingException("Error could not parse url: " + url);
}
}
/**
* Get the real url from a shared uri.
*
* Shared URI's look like this:
* <pre>
* * https://www.youtube.com/shared?ci=PJICrTByb3E
* * vnd.youtube://www.youtube.com/shared?ci=PJICrTByb3E&feature=twitter-deep-link
* </pre>
* @param url The shared url
* @return the id of the stream
* @throws ParsingException
*/
private @NonNull String getRealIdFromSharedLink(String url) throws ParsingException {
URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
throw new ParsingException("Invalid shared link", e);
}
String sharedId = getSharedId(uri);
Downloader downloader = NewPipe.getDownloader();
String content;
try {
content = downloader.download("https://www.youtube.com/shared?ci=" + sharedId);
} catch (IOException | ReCaptchaException e) {
throw new ParsingException("Unable to resolve shared link", e);
}
// is this bad? is this fragile?:
String realId = Parser.matchGroup1("rel=\"shortlink\" href=\"https://youtu.be/" + ID_PATTERN, content);
if(sharedId.equals(realId)) {
throw new ParsingException("Got same id for as shared id: " + sharedId);
}
return realId;
}
private @NonNull String getSharedId(URI uri) throws ParsingException {
if (!"/shared".equals(uri.getPath())) {
throw new ParsingException("Not a shared link: " + uri.toString() + " (path != " + uri.getPath() + ")");
}
return Parser.matchGroup1("ci=" + ID_PATTERN, uri.getQuery());
}
public String cleanUrl(String complexUrl) throws ParsingException {
return getUrl(getId(complexUrl));
}
@Override
public boolean acceptUrl(String videoUrl) {
videoUrl = videoUrl.toLowerCase();
return videoUrl.contains("youtube") ||
videoUrl.contains("youtu.be");
}
}

View File

@@ -1,100 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SuggestionExtractor;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* Created by Christian Schabesberger on 28.09.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeSuggestionExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeSuggestionExtractor extends SuggestionExtractor {
public static final String CHARSET_UTF_8 = "UTF-8";
public YoutubeSuggestionExtractor(int serviceId) {
super(serviceId);
}
@Override
public List<String> suggestionList(
String query, String contentCountry)
throws ExtractionException, IOException {
List<String> suggestions = new ArrayList<>();
Downloader dl = NewPipe.getDownloader();
String url = "https://suggestqueries.google.com/complete/search"
+ "?client=" + ""
+ "&output=" + "toolbar"
+ "&ds=" + "yt"
+ "&hl=" + URLEncoder.encode(contentCountry, CHARSET_UTF_8)
+ "&q=" + URLEncoder.encode(query, CHARSET_UTF_8);
String response = dl.download(url);
//TODO: Parse xml data using Jsoup not done
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder;
org.w3c.dom.Document doc = null;
try {
dBuilder = dbFactory.newDocumentBuilder();
doc = dBuilder.parse(new InputSource(
new ByteArrayInputStream(response.getBytes(CHARSET_UTF_8))));
doc.getDocumentElement().normalize();
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new ParsingException("Could not parse document.");
}
try {
NodeList nList = doc.getElementsByTagName("CompleteSuggestion");
for (int temp = 0; temp < nList.getLength(); temp++) {
NodeList nList1 = doc.getElementsByTagName("suggestion");
Node nNode1 = nList1.item(temp);
if (nNode1.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1;
suggestions.add(eElement.getAttribute("data"));
}
}
return suggestions;
} catch(Exception e) {
throw new ParsingException("Could not get suggestions form document.", e);
}
}
}

View File

@@ -1,46 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
/**
* Created by Christian Schabesberger on 04.03.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* AudioStream.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class AudioStream {
public String url = "";
public int format = -1;
public int bandwidth = -1;
public int sampling_rate = -1;
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
this.url = url; this.format = format;
this.bandwidth = bandwidth; this.sampling_rate = samplingRate;
}
// reveals whether two streams are the same, but have different urls
public boolean equalStats(AudioStream cmp) {
return format == cmp.format
&& bandwidth == cmp.bandwidth
&& sampling_rate == cmp.sampling_rate;
}
// reveals whether two streams are equal
public boolean equals(AudioStream cmp) {
return cmp != null && equalStats(cmp)
&& url == cmp.url;
}
}

View File

@@ -1,104 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
/**
* Created by Christian Schabesberger on 10.08.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.List;
/**Scrapes information from a video streaming service (eg, YouTube).*/
@SuppressWarnings("ALL")
public abstract class StreamExtractor {
private int serviceId;
private String url;
private UrlIdHandler urlIdHandler;
private StreamPreviewInfoCollector previewInfoCollector;
public class ExtractorInitException extends ExtractionException {
public ExtractorInitException(String message) {
super(message);
}
public ExtractorInitException(Throwable cause) {
super(cause);
}
public ExtractorInitException(String message, Throwable cause) {
super(message, cause);
}
}
public class ContentNotAvailableException extends ParsingException {
public ContentNotAvailableException(String message) {
super(message);
}
public ContentNotAvailableException(String message, Throwable cause) {
super(message, cause);
}
}
public StreamExtractor(UrlIdHandler urlIdHandler, String url, int serviceId) {
this.serviceId = serviceId;
this.urlIdHandler = urlIdHandler;
previewInfoCollector = new StreamPreviewInfoCollector(urlIdHandler, serviceId);
}
protected StreamPreviewInfoCollector getStreamPreviewInfoCollector() {
return previewInfoCollector;
}
public String getUrl() {
return url;
}
public UrlIdHandler getUrlIdHandler() {
return urlIdHandler;
}
public abstract int getTimeStamp() throws ParsingException;
public abstract String getTitle() throws ParsingException;
public abstract String getDescription() throws ParsingException;
public abstract String getUploader() throws ParsingException;
public abstract String getChannelUrl() throws ParsingException;
public abstract int getLength() throws ParsingException;
public abstract long getViewCount() throws ParsingException;
public abstract String getUploadDate() throws ParsingException;
public abstract String getThumbnailUrl() throws ParsingException;
public abstract String getUploaderThumbnailUrl() throws ParsingException;
public abstract List<AudioStream> getAudioStreams() throws ParsingException;
public abstract List<VideoStream> getVideoStreams() throws ParsingException;
public abstract List<VideoStream> getVideoOnlyStreams() throws ParsingException;
public abstract String getDashMpdUrl() throws ParsingException;
public abstract int getAgeLimit() throws ParsingException;
public abstract String getAverageRating() throws ParsingException;
public abstract int getLikeCount() throws ParsingException;
public abstract int getDislikeCount() throws ParsingException;
public abstract StreamPreviewInfoExtractor getNextVideo() throws ParsingException;
public abstract StreamPreviewInfoCollector getRelatedVideos() throws ParsingException;
public abstract String getPageUrl();
public abstract StreamInfo.StreamType getStreamType() throws ParsingException;
public int getServiceId() {
return serviceId;
}
}

View File

@@ -1,293 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.DashMpdParser;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.io.IOException;
import java.util.List;
import java.util.Vector;
/**
* Created by Christian Schabesberger on 26.08.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfo.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**Info object for opened videos, ie the video ready to play.*/
@SuppressWarnings("ALL")
public class StreamInfo extends AbstractStreamInfo {
public static class StreamExctractException extends ExtractionException {
StreamExctractException(String message) {
super(message);
}
}
public StreamInfo() {}
/**Creates a new StreamInfo object from an existing AbstractVideoInfo.
* All the shared properties are copied to the new StreamInfo.*/
@SuppressWarnings("WeakerAccess")
public StreamInfo(AbstractStreamInfo avi) {
this.id = avi.id;
this.title = avi.title;
this.uploader = avi.uploader;
this.thumbnail_url = avi.thumbnail_url;
this.webpage_url = avi.webpage_url;
this.upload_date = avi.upload_date;
this.upload_date = avi.upload_date;
this.view_count = avi.view_count;
//todo: better than this
if(avi instanceof StreamPreviewInfo) {
//shitty String to convert code
/*
String dur = ((StreamPreviewInfo)avi).duration;
int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":")));
int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length()));
*/
this.duration = ((StreamPreviewInfo)avi).duration;
}
}
public void addException(Exception e) {
errors.add(e);
}
/**Fills out the video info fields which are common to all services.
* Probably needs to be overridden by subclasses*/
public static StreamInfo getVideoInfo(StreamExtractor extractor)
throws ExtractionException, IOException {
StreamInfo streamInfo = new StreamInfo();
streamInfo = extractImportantData(streamInfo, extractor);
streamInfo = extractStreams(streamInfo, extractor);
streamInfo = extractOptionalData(streamInfo, extractor);
return streamInfo;
}
private static StreamInfo extractImportantData(
StreamInfo streamInfo, StreamExtractor extractor)
throws ExtractionException, IOException {
/* ---- important data, withoug the video can't be displayed goes here: ---- */
// if one of these is not available an exception is meant to be thrown directly into the frontend.
UrlIdHandler uiconv = extractor.getUrlIdHandler();
streamInfo.service_id = extractor.getServiceId();
streamInfo.webpage_url = extractor.getPageUrl();
streamInfo.stream_type = extractor.getStreamType();
streamInfo.id = uiconv.getId(extractor.getPageUrl());
streamInfo.title = extractor.getTitle();
streamInfo.age_limit = extractor.getAgeLimit();
if((streamInfo.stream_type == StreamType.NONE)
|| (streamInfo.webpage_url == null || streamInfo.webpage_url.isEmpty())
|| (streamInfo.id == null || streamInfo.id.isEmpty())
|| (streamInfo.title == null /* streamInfo.title can be empty of course */)
|| (streamInfo.age_limit == -1)) {
throw new ExtractionException("Some importand stream information was not given.");
}
return streamInfo;
}
private static StreamInfo extractStreams(
StreamInfo streamInfo, StreamExtractor extractor)
throws ExtractionException, IOException {
/* ---- stream extraction goes here ---- */
// At least one type of stream has to be available,
// otherwise an exception will be thrown directly into the frontend.
try {
streamInfo.dashMpdUrl = extractor.getDashMpdUrl();
} catch(Exception e) {
streamInfo.addException(new ExtractionException("Couldn't get Dash manifest", e));
}
/* Load and extract audio */
try {
streamInfo.audio_streams = extractor.getAudioStreams();
} catch(Exception e) {
streamInfo.addException(new ExtractionException("Couldn't get audio streams", e));
}
// also try to get streams from the dashMpd
if(streamInfo.dashMpdUrl != null && !streamInfo.dashMpdUrl.isEmpty()) {
if(streamInfo.audio_streams == null) {
streamInfo.audio_streams = new Vector<>();
}
//todo: make this quick and dirty solution a real fallback
// same as the quick and dirty above
try {
streamInfo.audio_streams.addAll(
DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl));
} catch(Exception e) {
streamInfo.addException(
new ExtractionException("Couldn't get audio streams from dash mpd", e));
}
}
/* Extract video stream url*/
try {
streamInfo.video_streams = extractor.getVideoStreams();
} catch (Exception e) {
streamInfo.addException(
new ExtractionException("Couldn't get video streams", e));
}
/* Extract video only stream url*/
try {
streamInfo.video_only_streams = extractor.getVideoOnlyStreams();
} catch(Exception e) {
streamInfo.addException(
new ExtractionException("Couldn't get video only streams", e));
}
// either dash_mpd audio_only or video has to be available, otherwise we didn't get a stream,
// and therefore failed. (Since video_only_streams are just optional they don't caunt).
if((streamInfo.video_streams == null || streamInfo.video_streams.isEmpty())
&& (streamInfo.audio_streams == null || streamInfo.audio_streams.isEmpty())
&& (streamInfo.dashMpdUrl == null || streamInfo.dashMpdUrl.isEmpty())) {
throw new StreamExctractException(
"Could not get any stream. See error variable to get further details.");
}
return streamInfo;
}
private static StreamInfo extractOptionalData(
StreamInfo streamInfo, StreamExtractor extractor) {
/* ---- optional data goes here: ---- */
// If one of these fails, the frontend needs to handle that they are not available.
// Exceptions are therefore not thrown into the frontend, but stored into the error List,
// so the frontend can afterwards check where errors happened.
try {
streamInfo.thumbnail_url = extractor.getThumbnailUrl();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.duration = extractor.getLength();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.uploader = extractor.getUploader();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.channel_url = extractor.getChannelUrl();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.description = extractor.getDescription();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.view_count = extractor.getViewCount();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.upload_date = extractor.getUploadDate();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.start_position = extractor.getTimeStamp();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.average_rating = extractor.getAverageRating();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.like_count = extractor.getLikeCount();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
streamInfo.dislike_count = extractor.getDislikeCount();
} catch(Exception e) {
streamInfo.addException(e);
}
try {
// get next video
if(streamInfo.next_video != null)
{
StreamPreviewInfoCollector c = new StreamPreviewInfoCollector(
extractor.getUrlIdHandler(), extractor.getServiceId());
StreamPreviewInfoExtractor nextVideo = extractor.getNextVideo();
c.commit(nextVideo);
if(c.getItemList().size() != 0) {
streamInfo.next_video = c.getItemList().get(0);
}
streamInfo.errors.addAll(c.getErrors());
}
}
catch(Exception e) {
streamInfo.addException(e);
}
try {
// get related videos
StreamPreviewInfoCollector c = extractor.getRelatedVideos();
streamInfo.related_streams = c.getItemList();
streamInfo.errors.addAll(c.getErrors());
} catch(Exception e) {
streamInfo.addException(e);
}
return streamInfo;
}
public String uploader_thumbnail_url = "";
public String channel_url = "";
public String description = "";
public List<VideoStream> video_streams = null;
public List<AudioStream> audio_streams = null;
public List<VideoStream> video_only_streams = null;
// video streams provided by the dash mpd do not need to be provided as VideoStream.
// Later on this will also aplly to audio streams. Since dash mpd is standarized,
// crawling such a file is not service dependent. Therefore getting audio only streams by yust
// providing the dash mpd fille will be possible in the future.
public String dashMpdUrl = "";
public int duration = -1;
public int age_limit = -1;
public int like_count = -1;
public int dislike_count = -1;
public String average_rating = "";
public StreamPreviewInfo next_video = null;
public List<StreamPreviewInfo> related_streams = null;
//in seconds. some metadata is not passed using a StreamInfo object!
public int start_position = 0;
public List<Throwable> errors = new Vector<>();
}

View File

@@ -1,28 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
/**
* Created by Christian Schabesberger on 26.08.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamPreviewInfo.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import org.schabi.newpipe.extractor.AbstractStreamInfo;
/**Info object for previews of unopened videos, eg search results, related videos*/
public class StreamPreviewInfo extends AbstractStreamInfo {
public int duration;
}

View File

@@ -1,102 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamUrlIdHandler;
import java.util.List;
import java.util.Vector;
/**
* Created by Christian Schabesberger on 28.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamPreviewInfoCollector.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class StreamPreviewInfoCollector {
private List<StreamPreviewInfo> itemList = new Vector<>();
private List<Throwable> errors = new Vector<>();
private UrlIdHandler urlIdHandler;
private int serviceId = -1;
public StreamPreviewInfoCollector(UrlIdHandler handler, int serviceId) {
urlIdHandler = handler;
this.serviceId = serviceId;
}
public List<StreamPreviewInfo> getItemList() {
return itemList;
}
public List<Throwable> getErrors() {
return errors;
}
public void addError(Exception e) {
errors.add(e);
}
public void commit(StreamPreviewInfoExtractor extractor) throws ParsingException {
try {
StreamPreviewInfo resultItem = new StreamPreviewInfo();
// importand information
resultItem.service_id = serviceId;
resultItem.webpage_url = extractor.getWebPageUrl();
if (urlIdHandler == null) {
throw new ParsingException("Error: UrlIdHandler not set");
} else if(!resultItem.webpage_url.isEmpty()) {
resultItem.id = NewPipe.getService(serviceId).getUrlIdHandlerInstance().getId(resultItem.webpage_url);
}
resultItem.title = extractor.getTitle();
resultItem.stream_type = extractor.getStreamType();
// optional information
try {
resultItem.duration = extractor.getDuration();
} catch (Exception e) {
addError(e);
}
try {
resultItem.uploader = extractor.getUploader();
} catch (Exception e) {
addError(e);
}
try {
resultItem.upload_date = extractor.getUploadDate();
} catch (Exception e) {
addError(e);
}
try {
resultItem.view_count = extractor.getViewCount();
} catch (Exception e) {
addError(e);
}
try {
resultItem.thumbnail_url = extractor.getThumbnailUrl();
} catch (Exception e) {
addError(e);
}
itemList.add(resultItem);
} catch(FoundAdException ae) {
System.out.println("AD_WARNING: " + ae.getMessage());
} catch (Exception e) {
addError(e);
}
}
}

View File

@@ -1,35 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
/**
* Created by Christian Schabesberger on 28.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamPreviewInfoExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public interface StreamPreviewInfoExtractor {
AbstractStreamInfo.StreamType getStreamType() throws ParsingException;
String getWebPageUrl() throws ParsingException;
String getTitle() throws ParsingException;
int getDuration() throws ParsingException;
String getUploader() throws ParsingException;
String getUploadDate() throws ParsingException;
long getViewCount() throws ParsingException;
String getThumbnailUrl() throws ParsingException;
}

View File

@@ -1,44 +0,0 @@
package org.schabi.newpipe.extractor.stream_info;
/**
* Created by Christian Schabesberger on 04.03.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* VideoStream.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class VideoStream {
//url of the stream
public String url = "";
public int format = -1;
public String resolution = "";
public VideoStream(String url, int format, String res) {
this.url = url; this.format = format; resolution = res;
}
// reveals wether two streams are the same, but have diferent urls
public boolean equalStats(VideoStream cmp) {
return format == cmp.format
&& resolution == cmp.resolution;
}
// revelas wether two streams are equal
public boolean equals(VideoStream cmp) {
return cmp != null && equalStats(cmp)
&& url == cmp.url;
}
}

View File

@@ -0,0 +1,54 @@
package org.schabi.newpipe.info_list;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import de.hdodenhof.circleimageview.CircleImageView;
/**
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ChannelInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
public final TextView itemChannelTitleView;
public final TextView itemSubscriberCountView;
public final TextView itemVideoCountView;
public final TextView itemChannelDescriptionView;
public final Button itemButton;
ChannelInfoItemHolder(View v) {
super(v);
itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView);
itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView);
itemSubscriberCountView = (TextView) v.findViewById(R.id.itemSubscriberCountView);
itemVideoCountView = (TextView) v.findViewById(R.id.itemVideoCountView);
itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView);
itemButton = (Button) v.findViewById(R.id.item_button);
}
@Override
public InfoItem.InfoType infoType() {
return InfoItem.InfoType.CHANNEL;
}
}

View File

@@ -1,9 +1,11 @@
package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
@@ -11,7 +13,9 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.ImageErrorLoadingListener;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
/**
* Created by Christian Schabesberger on 26.09.16.
@@ -35,8 +39,17 @@ import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
public class InfoItemBuilder {
public interface OnItemSelectedListener {
void selected(String url);
final String viewsS;
final String videosS;
final String subsS;
final String thousand;
final String million;
final String billion;
private static final String TAG = InfoItemBuilder.class.toString();
public interface OnInfoItemSelectedListener {
void selected(String url, int serviceId);
}
private Activity activity = null;
@@ -44,18 +57,75 @@ public class InfoItemBuilder {
private ImageLoader imageLoader = ImageLoader.getInstance();
private DisplayImageOptions displayImageOptions =
new DisplayImageOptions.Builder().cacheInMemory(true).build();
private OnItemSelectedListener onItemSelectedListener;
private OnInfoItemSelectedListener onStreamInfoItemSelectedListener;
private OnInfoItemSelectedListener onChannelInfoItemSelectedListener;
public InfoItemBuilder(Activity a, View rootView) {
activity = a;
this.rootView = rootView;
viewsS = a.getString(R.string.views);
videosS = a.getString(R.string.videos);
subsS = a.getString(R.string.subscriber);
thousand = a.getString(R.string.short_thousand);
million = a.getString(R.string.short_million);
billion = a.getString(R.string.short_billion);
}
public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
this.onItemSelectedListener = onItemSelectedListener;
public void setOnStreamInfoItemSelectedListener(
OnInfoItemSelectedListener listener) {
this.onStreamInfoItemSelectedListener = listener;
}
public void buildByHolder(InfoItemHolder holder, final StreamPreviewInfo info) {
public void setOnChannelInfoItemSelectedListener(
OnInfoItemSelectedListener listener) {
this.onChannelInfoItemSelectedListener = listener;
}
public void buildByHolder(InfoItemHolder holder, final InfoItem i) {
if(i.infoType() != holder.infoType())
return;
switch(i.infoType()) {
case STREAM:
buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i);
break;
case CHANNEL:
buildChannelInfoItem((ChannelInfoItemHolder) holder, (ChannelInfoItem) i);
break;
case PLAYLIST:
Log.e(TAG, "Not yet implemented");
break;
default:
Log.e(TAG, "Trollolo");
}
}
public View buildView(ViewGroup parent, final InfoItem info) {
View itemView = null;
InfoItemHolder holder = null;
switch(info.infoType()) {
case STREAM:
itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.stream_item, parent, false);
holder = new StreamInfoItemHolder(itemView);
break;
case CHANNEL:
itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.channel_item, parent, false);
holder = new ChannelInfoItemHolder(itemView);
break;
case PLAYLIST:
Log.e(TAG, "Not yet implemented");
default:
Log.e(TAG, "Trollolo");
}
buildByHolder(holder, info);
return itemView;
}
private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) {
if(info.infoType() != InfoItem.InfoType.STREAM) {
Log.e("InfoItemBuilder", "Info type not yet supported");
}
// fill holder with information
holder.itemVideoTitleView.setText(info.title);
if(info.uploader != null && !info.uploader.isEmpty()) {
@@ -92,29 +162,55 @@ public class InfoItemBuilder {
holder.itemButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onItemSelectedListener.selected(info.webpage_url);
onStreamInfoItemSelectedListener.selected(info.webpage_url, info.service_id);
}
});
}
public View buildView(ViewGroup parent, final StreamPreviewInfo info) {
View streamPreviewView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.video_item, parent, false);
InfoItemHolder holder = new InfoItemHolder(streamPreviewView);
buildByHolder(holder, info);
return streamPreviewView;
private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) {
holder.itemChannelTitleView.setText(info.getTitle());
holder.itemSubscriberCountView.setText(shortSubscriber(info.subscriberCount) + "");
holder.itemVideoCountView.setText(info.videoAmount + " " + videosS);
holder.itemChannelDescriptionView.setText(info.description);
holder.itemThumbnailView.setImageResource(R.drawable.buddy_channel_item);
if(info.thumbnailUrl != null && !info.thumbnailUrl.isEmpty()) {
imageLoader.displayImage(info.thumbnailUrl,
holder.itemThumbnailView,
displayImageOptions,
new ImageErrorLoadingListener(activity, rootView, info.serviceId));
}
holder.itemButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onChannelInfoItemSelectedListener.selected(info.getLink(), info.serviceId);
}
});
}
public static String shortViewCount(Long viewCount){
public String shortViewCount(Long viewCount){
if(viewCount >= 1000000000){
return Long.toString(viewCount/1000000000)+"B views";
return Long.toString(viewCount/1000000000)+ billion + " " + viewsS;
}else if(viewCount>=1000000){
return Long.toString(viewCount/1000000)+"M views";
return Long.toString(viewCount/1000000)+ million + " " + viewsS;
}else if(viewCount>=1000){
return Long.toString(viewCount/1000)+"K views";
return Long.toString(viewCount/1000)+ thousand + " " + viewsS;
}else {
return Long.toString(viewCount)+" views";
return Long.toString(viewCount)+ " " + viewsS;
}
}
public String shortSubscriber(Long count){
if(count >= 1000000000){
return Long.toString(count/1000000000)+ billion + " " + subsS;
}else if(count>=1000000){
return Long.toString(count/1000000)+ million + " " + subsS;
}else if(count>=1000){
return Long.toString(count/1000)+ thousand + " " + subsS;
}else {
return Long.toString(count)+ " " + subsS;
}
}

View File

@@ -2,14 +2,11 @@ package org.schabi.newpipe.info_list;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
/**
* Created by Christian Schabesberger on 01.08.16.
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemHolder.java is part of NewPipe.
@@ -28,25 +25,9 @@ import org.schabi.newpipe.R;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class InfoItemHolder extends RecyclerView.ViewHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView,
itemUploaderView,
itemDurationView,
itemUploadDateView,
itemViewCountView;
public final Button itemButton;
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
public InfoItemHolder(View v) {
super(v);
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView);
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
itemButton = (Button) v.findViewById(R.id.item_button);
}
public abstract InfoItem.InfoType infoType();
}

View File

@@ -2,12 +2,13 @@ package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import org.schabi.newpipe.extractor.InfoItem;
import java.util.List;
import java.util.Vector;
@@ -32,48 +33,131 @@ import java.util.Vector;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class InfoListAdapter extends RecyclerView.Adapter<InfoItemHolder> {
public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = InfoListAdapter.class.toString();
private final InfoItemBuilder infoItemBuilder;
private final List<StreamPreviewInfo> streamList;
private final List<InfoItem> infoItemList;
private boolean showFooter = false;
private View header = null;
private View footer = null;
public class HFHolder extends RecyclerView.ViewHolder {
public HFHolder(View v) {
super(v);
view = v;
}
public View view;
}
public void showFooter(boolean show) {
showFooter = show;
notifyDataSetChanged();
}
public InfoListAdapter(Activity a, View rootView) {
infoItemBuilder = new InfoItemBuilder(a, rootView);
streamList = new Vector<>();
infoItemList = new Vector<>();
}
public void setOnItemSelectedListener
(InfoItemBuilder.OnItemSelectedListener onItemSelectedListener) {
infoItemBuilder.setOnItemSelectedListener(onItemSelectedListener);
public void setOnStreamInfoItemSelectedListener
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
infoItemBuilder.setOnStreamInfoItemSelectedListener(listener);
}
public void addStreamItemList(List<StreamPreviewInfo> videos) {
if(videos!= null) {
streamList.addAll(videos);
public void setOnChannelInfoItemSelectedListener
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
infoItemBuilder.setOnChannelInfoItemSelectedListener(listener);
}
public void addInfoItemList(List<InfoItem> data) {
if(data != null) {
infoItemList.addAll(data);
notifyDataSetChanged();
}
}
public void clearSteamItemList() {
streamList.clear();
infoItemList.clear();
notifyDataSetChanged();
}
public void setHeader(View header) {
this.header = header;
notifyDataSetChanged();
}
public void setFooter(View view) {
this.footer = view;
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return streamList.size();
int cound = infoItemList.size();
if(header != null) cound++;
if(footer != null && showFooter) cound++;
return cound;
}
// don't ask why we have to do that this way... it's android accept it -.-
@Override
public int getItemViewType(int position) {
if(header != null && position == 0) {
return 0;
} else if(header != null) {
position--;
}
if(footer != null && position == infoItemList.size() && showFooter) {
return 1;
}
switch(infoItemList.get(position).infoType()) {
case STREAM:
return 2;
case CHANNEL:
return 3;
case PLAYLIST:
return 4;
default:
Log.e(TAG, "Trollolo");
return -1;
}
}
@Override
public InfoItemHolder onCreateViewHolder(ViewGroup parent, int i) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.video_item, parent, false);
return new InfoItemHolder(itemView);
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) {
case 0:
return new HFHolder(header);
case 1:
return new HFHolder(footer);
case 2:
return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.stream_item, parent, false));
case 3:
return new ChannelInfoItemHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.channel_item, parent, false));
case 4:
Log.e(TAG, "Playlist is not yet implemented");
return null;
default:
Log.e(TAG, "Trollolo");
return null;
}
}
@Override
public void onBindViewHolder(InfoItemHolder holder, int i) {
infoItemBuilder.buildByHolder(holder, streamList.get(i));
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
//god damen f*** ANDROID SH**
if(holder instanceof InfoItemHolder) {
if(header != null) {
i--;
}
infoItemBuilder.buildByHolder((InfoItemHolder) holder, infoItemList.get(i));
} else if(holder instanceof HFHolder && i == 0 && header != null) {
((HFHolder) holder).view = header;
} else if(holder instanceof HFHolder && i == infoItemList.size() && footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
}

View File

@@ -0,0 +1,58 @@
package org.schabi.newpipe.info_list;
import android.icu.text.IDNA;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
/**
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class StreamInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView,
itemUploaderView,
itemDurationView,
itemUploadDateView,
itemViewCountView;
public final Button itemButton;
public StreamInfoItemHolder(View v) {
super(v);
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView);
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
itemButton = (Button) v.findViewById(R.id.item_button);
}
@Override
public InfoItem.InfoType infoType() {
return InfoItem.InfoType.STREAM;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.detail.VideoItemDetailActivity;
import org.schabi.newpipe.detail.VideoItemDetailFragment;
import org.schabi.newpipe.util.NavStack;
import java.io.IOException;
import java.util.Arrays;
@@ -342,7 +342,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
/*
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
(R.drawable.ic_pause_white_24dp, "Pause", playPI).build();
(R.drawable.ic_pause_white, "Pause", playPI).build();
*/
PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID,
@@ -355,8 +355,8 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
//build intent to return to video, on tapping notification
Intent openDetailViewIntent = new Intent(getApplicationContext(),
VideoItemDetailActivity.class);
openDetailViewIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, serviceId);
openDetailViewIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webUrl);
openDetailViewIntent.putExtra(NavStack.SERVICE_ID, serviceId);
openDetailViewIntent.putExtra(NavStack.URL, webUrl);
openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID,
openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -464,7 +464,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
RemoteViews views = getContentView(), bigViews = getBigContentView();
int imageSrc;
if(isPlaying) {
imageSrc = R.drawable.ic_pause_white_24dp;
imageSrc = R.drawable.ic_pause_white;
} else {
imageSrc = R.drawable.ic_play_circle_filled_white_24dp;
}

View File

@@ -5,9 +5,8 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.media.MediaPlayer;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -28,7 +27,6 @@ import android.widget.MediaController;
import android.widget.ProgressBar;
import android.widget.VideoView;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
/**

View File

@@ -0,0 +1,617 @@
package org.schabi.newpipe.player;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupMenu;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.detail.VideoItemDetailActivity;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.util.NavStack;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import java.util.ArrayList;
/**
* Service Popup Player implementing AbstractPlayer
*
* @author mauriciocolli
*/
public class PopupVideoPlayer extends Service {
private static final String TAG = ".PopupVideoPlayer";
private static final boolean DEBUG = AbstractPlayer.DEBUG;
private static final int NOTIFICATION_ID = 40028922;
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE";
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL";
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT";
private BroadcastReceiver broadcastReceiver;
private WindowManager windowManager;
private WindowManager.LayoutParams windowLayoutParams;
private GestureDetector gestureDetector;
private float screenWidth, screenHeight;
private float popupWidth, popupHeight;
private float currentPopupHeight = 110.0f * Resources.getSystem().getDisplayMetrics().density;
//private float minimumHeight = 100; // TODO: Use it when implementing the resize of the popup
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private ImageLoader imageLoader = ImageLoader.getInstance();
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
private AbstractPlayerImpl playerImpl;
/*//////////////////////////////////////////////////////////////////////////
// Service LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
initReceiver();
playerImpl = new AbstractPlayerImpl();
ThemeHelper.setTheme(this, false);
}
@Override
@SuppressWarnings("unchecked")
public int onStartCommand(final Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
if (playerImpl.getPlayer() == null) initPopup();
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
if (imageLoader != null) imageLoader.clearMemoryCache();
if (intent.getStringExtra(NavStack.URL) != null) {
playerImpl.setStartedFromNewPipe(false);
Thread fetcher = new Thread(new FetcherRunnable(intent));
fetcher.start();
} else {
playerImpl.setStartedFromNewPipe(true);
playerImpl.handleIntent(intent);
}
return START_NOT_STICKY;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
updateScreenSize();
}
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy() called");
stopForeground(true);
if (playerImpl != null) {
playerImpl.destroy();
if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView());
}
if (imageLoader != null) imageLoader.clearMemoryCache();
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
private void initReceiver() {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]");
switch (intent.getAction()) {
case ACTION_CLOSE:
onVideoClose();
break;
case ACTION_PLAY_PAUSE:
playerImpl.onVideoPlayPause();
break;
case ACTION_OPEN_DETAIL:
onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl());
break;
case ACTION_REPEAT:
playerImpl.onRepeatClicked();
break;
case AbstractPlayer.ACTION_UPDATE_THUMB:
playerImpl.onUpdateThumbnail(intent);
break;
}
}
};
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_CLOSE);
intentFilter.addAction(ACTION_PLAY_PAUSE);
intentFilter.addAction(ACTION_OPEN_DETAIL);
intentFilter.addAction(ACTION_REPEAT);
intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB);
registerReceiver(broadcastReceiver, intentFilter);
}
@SuppressLint("RtlHardcoded")
private void initPopup() {
if (DEBUG) Log.d(TAG, "initPopup() called");
View rootView = View.inflate(this, R.layout.player_popup, null);
playerImpl.setup(rootView);
updateScreenSize();
windowLayoutParams = new WindowManager.LayoutParams(
(int) getMinimumVideoWidth(currentPopupHeight), (int) currentPopupHeight,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
gestureDetector = new GestureDetector(this, listener);
gestureDetector.setIsLongpressEnabled(false);
rootView.setOnTouchListener(listener);
playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height);
windowManager.addView(rootView, windowLayoutParams);
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification);
if (playerImpl.getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail());
else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName());
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
switch (playerImpl.getCurrentRepeatMode()) {
case REPEAT_DISABLED:
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
break;
case REPEAT_ONE:
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
break;
case REPEAT_ALL:
// Waiting :)
break;
}
return new NotificationCompat.Builder(this)
.setOngoing(true)
.setSmallIcon(R.drawable.ic_play_arrow_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContent(notRemoteView);
}
/**
* Updates the notification, and the play/pause button in it.
* Used for changes on the remoteView
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
private void updateNotification(int drawableId) {
if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
if (notBuilder == null || notRemoteView == null) return;
if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
}
/*//////////////////////////////////////////////////////////////////////////
// Misc
//////////////////////////////////////////////////////////////////////////*/
public void onVideoClose() {
if (DEBUG) Log.d(TAG, "onVideoClose() called");
stopSelf();
}
public void onOpenDetail(Context context, String videoUrl) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, VideoItemDetailActivity.class);
i.putExtra(NavStack.SERVICE_ID, 0)
.putExtra(NavStack.URL, videoUrl)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private float getMinimumVideoWidth(float height) {
float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width);
return width;
}
private void updateScreenSize() {
DisplayMetrics metrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;
screenHeight = metrics.heightPixels;
if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight);
}
///////////////////////////////////////////////////////////////////////////
private class AbstractPlayerImpl extends AbstractPlayer {
AbstractPlayerImpl() {
super("AbstractPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this);
}
@Override
public void playVideo(Uri videoURI, boolean autoPlay) {
super.playVideo(videoURI, autoPlay);
windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight);
windowManager.updateViewLayout(getRootView(), windowLayoutParams);
notBuilder = createNotification();
startForeground(NOTIFICATION_ID, notBuilder.build());
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
}
@Override
public void onFullScreenButtonClicked() {
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
Intent intent;
//if (getSharedPreferences().getBoolean(getResources().getString(R.string.use_exoplayer_key), false)) {
// TODO: Remove this check when ExoPlayer is the default
// For now just disable the non-exoplayer player
//noinspection ConstantConditions,ConstantIfStatement
if (true) {
intent = new Intent(PopupVideoPlayer.this, ExoPlayerActivity.class)
.putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle())
.putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl())
.putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName())
.putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream())
.putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList())
.putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition()));
if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(AbstractPlayer.STARTED_FROM_NEWPIPE, false);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} else {
intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class)
.putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle())
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedStreamUri().toString())
.putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl())
.putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
stopSelf();
}
@Override
public void onRepeatClicked() {
super.onRepeatClicked();
switch (getCurrentRepeatMode()) {
case REPEAT_DISABLED:
// Drawable didn't work on low API :/
//notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white);
// Set the icon to 30% opacity - 255 (max) * .3
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
break;
case REPEAT_ONE:
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
break;
case REPEAT_ALL:
// Waiting :)
break;
}
updateNotification(-1);
}
@Override
public void onUpdateThumbnail(Intent intent) {
super.onUpdateThumbnail(intent);
if (getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, getVideoThumbnail());
updateNotification(-1);
}
@Override
public void onDismiss(PopupMenu menu) {
super.onDismiss(menu);
if (isPlaying()) animateView(getControlsRoot(), false, 500, 0);
}
@Override
public void onError() {
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show();
stopSelf();
}
/*//////////////////////////////////////////////////////////////////////////
// States
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoading() {
super.onLoading();
updateNotification(R.drawable.ic_play_arrow_white);
}
@Override
public void onPlaying() {
super.onPlaying();
updateNotification(R.drawable.ic_pause_white);
}
@Override
public void onBuffering() {
super.onBuffering();
updateNotification(R.drawable.ic_play_arrow_white);
}
@Override
public void onPaused() {
super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white);
showAndAnimateControl(R.drawable.ic_play_arrow_white, false);
}
@Override
public void onPausedSeek() {
super.onPausedSeek();
updateNotification(R.drawable.ic_play_arrow_white);
}
@Override
public void onCompleted() {
super.onCompleted();
updateNotification(R.drawable.ic_replay_white);
showAndAnimateControl(R.drawable.ic_replay_white, false);
}
}
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
private int initialPopupX, initialPopupY;
private boolean isMoving;
@Override
public boolean onDoubleTap(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (!playerImpl.isPlaying()) return false;
if (e.getX() > popupWidth / 2) playerImpl.onFastForward();
else playerImpl.onFastRewind();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl.getPlayer() == null) return false;
playerImpl.onVideoPlayPause();
return true;
}
@Override
public boolean onDown(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
initialPopupX = windowLayoutParams.x;
initialPopupY = windowLayoutParams.y;
popupWidth = playerImpl.getRootView().getWidth();
popupHeight = playerImpl.getRootView().getHeight();
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f) playerImpl.animateView(playerImpl.getControlsRoot(), true, 30, 0);
isMoving = true;
float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX);
float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY);
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
else if (posX < 0) posX = 0;
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
else if (posY < 0) posY = 0;
windowLayoutParams.x = (int) posX;
windowLayoutParams.y = (int) posY;
//noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " +
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]" +
", posXy = [" + posX + ", " + posY + "]" +
", popupWh rootView.get wh = [" + popupWidth + " x " + popupHeight + "]");
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
return true;
}
private void onScrollEnd() {
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) {
playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME);
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP && isMoving) {
isMoving = false;
onScrollEnd();
}
return true;
}
}
/**
* Fetcher used if open by a link out of NewPipe
*/
private class FetcherRunnable implements Runnable {
private final Intent intent;
private final Handler mainHandler;
private final boolean printStreams = true;
FetcherRunnable(Intent intent) {
this.intent = intent;
this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper());
}
@Override
public void run() {
StreamExtractor streamExtractor;
try {
StreamingService service = NewPipe.getService(0);
if (service == null) return;
streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL));
StreamInfo info = StreamInfo.getVideoInfo(streamExtractor);
String defaultResolution = playerImpl.getSharedPreferences().getString(
getResources().getString(R.string.default_resolution_key),
getResources().getString(R.string.default_resolution_value));
VideoStream chosen = null, secondary = null, fallback = null;
playerImpl.setVideoStreamsList(info.video_streams instanceof ArrayList
? (ArrayList<VideoStream>) info.video_streams
: new ArrayList<>(info.video_streams));
for (VideoStream item : info.video_streams) {
if (DEBUG && printStreams) {
Log.d(TAG, "FetcherRunnable.StreamExtractor: current Item"
+ ", item.resolution = " + item.resolution
+ ", item.format = " + item.format
+ ", item.url = " + item.url);
}
if (defaultResolution.equals(item.resolution)) {
if (item.format == MediaFormat.MPEG_4.id) {
chosen = item;
if (DEBUG) Log.d(TAG, "FetcherRunnable.StreamExtractor: CHOSEN item, item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url);
} else if (item.format == 2) secondary = item;
else fallback = item;
}
}
int selectedIndexStream;
if (chosen != null) selectedIndexStream = info.video_streams.indexOf(chosen);
else if (secondary != null) selectedIndexStream = info.video_streams.indexOf(secondary);
else if (fallback != null) selectedIndexStream = info.video_streams.indexOf(fallback);
else selectedIndexStream = 0;
playerImpl.setSelectedIndexStream(selectedIndexStream);
if (DEBUG && printStreams) Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + chosen
+ "\n, secondary = " + secondary
+ "\n, fallback = " + fallback
+ "\n, info.video_streams.get(0).url = " + info.video_streams.get(0).url);
playerImpl.setVideoUrl(info.webpage_url);
playerImpl.setVideoTitle(info.title);
playerImpl.setChannelName(info.uploader);
if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000);
else playerImpl.setVideoStartPos(-1);
mainHandler.post(new Runnable() {
@Override
public void run() {
playerImpl.playVideo(playerImpl.getSelectedStreamUri(), true);
}
});
imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) {
mainHandler.post(new Runnable() {
@Override
public void run() {
playerImpl.setVideoThumbnail(loadedImage);
if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
updateNotification(-1);
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage;
}
});
}
});
} catch (IOException ie) {
if (DEBUG) ie.printStackTrace();
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(PopupVideoPlayer.this, R.string.network_error, Toast.LENGTH_SHORT).show();
}
});
stopSelf();
} catch (Exception e) {
if (DEBUG) e.printStackTrace();
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(PopupVideoPlayer.this, R.string.content_not_available, Toast.LENGTH_SHORT).show();
}
});
stopSelf();
}
}
}
}

View File

@@ -0,0 +1,19 @@
package org.schabi.newpipe.player;
public interface StateInterface {
int STATE_LOADING = 123;
int STATE_PLAYING = 124;
int STATE_BUFFERING = 125;
int STATE_PAUSED = 126;
int STATE_PAUSED_SEEK = 127;
int STATE_COMPLETED = 128;
void changeState(int state);
void onLoading();
void onPlaying();
void onBuffering();
void onPaused();
void onPausedSeek();
void onCompleted();
}

View File

@@ -1,268 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.DefaultDashTrackSelector;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.UtcTimingElement;
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback;
import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler;
import android.util.Log;
import java.io.IOException;
/**
* A {@link RendererBuilder} for DASH.
*/
public class DashRendererBuilder implements RendererBuilder {
private static final String TAG = "DashRendererBuilder";
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 54;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private static final int SECURITY_LEVEL_UNKNOWN = -1;
private static final int SECURITY_LEVEL_1 = 1;
private static final int SECURITY_LEVEL_3 = 3;
private final Context context;
private final String userAgent;
private final String url;
private final MediaDrmCallback drmCallback;
private AsyncRendererBuilder currentAsyncBuilder;
public DashRendererBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback) {
this.context = context;
this.userAgent = userAgent;
this.url = url;
this.drmCallback = drmCallback;
}
@Override
public void buildRenderers(NPExoPlayer player) {
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
currentAsyncBuilder.init();
}
@Override
public void cancel() {
if (currentAsyncBuilder != null) {
currentAsyncBuilder.cancel();
currentAsyncBuilder = null;
}
}
private static final class AsyncRendererBuilder
implements ManifestFetcher.ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
private final Context context;
private final String userAgent;
private final MediaDrmCallback drmCallback;
private final NPExoPlayer player;
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final UriDataSource manifestDataSource;
private boolean canceled;
private MediaPresentationDescription manifest;
private long elapsedRealtimeOffset;
public AsyncRendererBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback, NPExoPlayer player) {
this.context = context;
this.userAgent = userAgent;
this.drmCallback = drmCallback;
this.player = player;
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
manifestDataSource = new DefaultUriDataSource(context, userAgent);
manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
}
public void init() {
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
public void cancel() {
canceled = true;
}
@Override
public void onSingleManifest(MediaPresentationDescription manifest) {
if (canceled) {
return;
}
this.manifest = manifest;
if (manifest.dynamic && manifest.utcTiming != null) {
UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming,
manifestFetcher.getManifestLoadCompleteTimestamp(), this);
} else {
buildRenderers();
}
}
@Override
public void onSingleManifestError(IOException e) {
if (canceled) {
return;
}
player.onRenderersError(e);
}
@Override
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
if (canceled) {
return;
}
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
buildRenderers();
}
@Override
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
if (canceled) {
return;
}
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
// Be optimistic and continue in the hope that the device clock is correct.
buildRenderers();
}
private void buildRenderers() {
Period period = manifest.getPeriod(0);
Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
boolean hasContentProtection = false;
for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) {
hasContentProtection |= adaptationSet.hasContentProtection();
}
}
// Check drm support if necessary.
boolean filterHdContent = false;
StreamingDrmSessionManager drmSessionManager = null;
if (hasContentProtection) {
if (Util.SDK_INT < 18) {
player.onRenderersError(
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
return;
}
try {
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1;
} catch (UnsupportedDrmException e) {
player.onRenderersError(e);
return;
}
}
// Build the video renderer.
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent),
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS,
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_VIDEO);
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
drmSessionManager, true, mainHandler, player, 50);
// Build the audio renderer.
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher,
DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS,
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO);
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_AUDIO);
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
// Build the text renderer.
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource textChunkSource = new DashChunkSource(manifestFetcher,
DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS,
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT);
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_TEXT);
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
mainHandler.getLooper());
// Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
}
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
.equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
}
}
}

View File

@@ -1,214 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.util.VerboseLogUtil;
import android.media.MediaCodec.CryptoException;
import android.os.SystemClock;
import android.util.Log;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Logs player events using {@link Log}.
*/
public class EventLogger implements NPExoPlayer.Listener, NPExoPlayer.InfoListener,
NPExoPlayer.InternalErrorListener {
private static final String TAG = "EventLogger";
private static final NumberFormat TIME_FORMAT;
static {
TIME_FORMAT = NumberFormat.getInstance(Locale.US);
TIME_FORMAT.setMinimumFractionDigits(2);
TIME_FORMAT.setMaximumFractionDigits(2);
}
private long sessionStartTimeMs;
private long[] loadStartTimeMs;
private long[] availableRangeValuesUs;
public EventLogger() {
loadStartTimeMs = new long[NPExoPlayer.RENDERER_COUNT];
}
public void startSession() {
sessionStartTimeMs = SystemClock.elapsedRealtime();
Log.d(TAG, "start [0]");
}
public void endSession() {
Log.d(TAG, "end [" + getSessionTimeString() + "]");
}
// NPExoPlayer.Listener
@Override
public void onStateChanged(boolean playWhenReady, int state) {
Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
+ getStateString(state) + "]");
}
@Override
public void onError(Exception e) {
Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees
+ ", " + pixelWidthHeightRatio + "]");
}
// NPExoPlayer.InfoListener
@Override
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", "
+ getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
}
@Override
public void onDroppedFrames(int count, long elapsed) {
Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
}
@Override
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs) {
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) {
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
+ ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
}
}
@Override
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
if (VerboseLogUtil.isTagEnabled(TAG)) {
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime
+ "]");
}
}
@Override
public void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs) {
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
+ Integer.toString(trigger) + "]");
}
@Override
public void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs) {
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
+ Integer.toString(trigger) + "]");
}
// NPExoPlayer.InternalErrorListener
@Override
public void onLoadError(int sourceId, IOException e) {
printInternalError("loadError", e);
}
@Override
public void onRendererInitializationError(Exception e) {
printInternalError("rendererInitError", e);
}
@Override
public void onDrmSessionManagerError(Exception e) {
printInternalError("drmSessionManagerError", e);
}
@Override
public void onDecoderInitializationError(DecoderInitializationException e) {
printInternalError("decoderInitializationError", e);
}
@Override
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
printInternalError("audioTrackInitializationError", e);
}
@Override
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
printInternalError("audioTrackWriteError", e);
}
@Override
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
+ elapsedSinceLastFeedMs + "]", null);
}
@Override
public void onCryptoError(CryptoException e) {
printInternalError("cryptoError", e);
}
@Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
}
@Override
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0]
+ ", " + availableRangeValuesUs[1] + "]");
}
private void printInternalError(String type, Exception e) {
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
}
private String getStateString(int state) {
switch (state) {
case ExoPlayer.STATE_BUFFERING:
return "B";
case ExoPlayer.STATE_ENDED:
return "E";
case ExoPlayer.STATE_IDLE:
return "I";
case ExoPlayer.STATE_PREPARING:
return "P";
case ExoPlayer.STATE_READY:
return "R";
default:
return "?";
}
}
private String getSessionTimeString() {
return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs);
}
private String getTimeString(long timeMs) {
return TIME_FORMAT.format((timeMs) / 1000f);
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.net.Uri;
/**
* A {@link RendererBuilder} for streams that can be read using an {@link Extractor}.
*/
public class ExtractorRendererBuilder implements RendererBuilder {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int BUFFER_SEGMENT_COUNT = 256;
private final Context context;
private final String userAgent;
private final Uri uri;
public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) {
this.context = context;
this.userAgent = userAgent;
this.uri = uri;
}
@Override
public void buildRenderers(NPExoPlayer player) {
Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
// Build the video and audio renderers.
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(),
null);
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
player.getMainHandler(), player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player,
player.getMainHandler().getLooper());
// Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
}
@Override
public void cancel() {
// Do nothing.
}
}

View File

@@ -1,180 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsMasterPlaylist;
import com.google.android.exoplayer.hls.HlsPlaylist;
import com.google.android.exoplayer.hls.HlsPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
import com.google.android.exoplayer.metadata.Id3Parser;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler;
import java.io.IOException;
import java.util.Map;
/**
* A {@link RendererBuilder} for HLS.
*/
public class HlsRendererBuilder implements RendererBuilder {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int MAIN_BUFFER_SEGMENTS = 256;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private final Context context;
private final String userAgent;
private final String url;
private AsyncRendererBuilder currentAsyncBuilder;
public HlsRendererBuilder(Context context, String userAgent, String url) {
this.context = context;
this.userAgent = userAgent;
this.url = url;
}
@Override
public void buildRenderers(NPExoPlayer player) {
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, player);
currentAsyncBuilder.init();
}
@Override
public void cancel() {
if (currentAsyncBuilder != null) {
currentAsyncBuilder.cancel();
currentAsyncBuilder = null;
}
}
private static final class AsyncRendererBuilder implements ManifestCallback<HlsPlaylist> {
private final Context context;
private final String userAgent;
private final String url;
private final NPExoPlayer player;
private final ManifestFetcher<HlsPlaylist> playlistFetcher;
private boolean canceled;
public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) {
this.context = context;
this.userAgent = userAgent;
this.url = url;
this.player = player;
HlsPlaylistParser parser = new HlsPlaylistParser();
playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent),
parser);
}
public void init() {
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
public void cancel() {
canceled = true;
}
@Override
public void onSingleManifestError(IOException e) {
if (canceled) {
return;
}
player.onRenderersError(e);
}
@Override
public void onSingleManifest(HlsPlaylist manifest) {
if (canceled) {
return;
}
Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
// Build the video/audio/metadata renderers.
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
5000, mainHandler, player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
// Build the text renderer, preferring Webvtt where available.
boolean preferWebvtt = false;
if (manifest instanceof HlsMasterPlaylist) {
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
}
TrackRenderer textRenderer;
if (preferWebvtt) {
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper());
} else {
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
}
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer;
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
}
}
}

View File

@@ -1,599 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.DummyTrackRenderer;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.util.DebugTextViewHelper;
import com.google.android.exoplayer.util.PlayerControl;
import android.media.MediaCodec.CryptoException;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
* with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
* SmoothStreaming and so on).
*/
public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {
/**
* Builds renderers for the player.
*/
public interface RendererBuilder {
/**
* Builds renderers for playback.
*
* @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers}
* should be invoked once the renderers have been built. If building fails,
* {@link NPExoPlayer#onRenderersError} should be invoked.
*/
void buildRenderers(NPExoPlayer player);
/**
* Cancels the current build operation, if there is one. Else does nothing.
* <p>
* A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or
* {@link NPExoPlayer#onRenderersError} on the player, which may have been released.
*/
void cancel();
}
/**
* A listener for core events.
*/
public interface Listener {
void onStateChanged(boolean playWhenReady, int playbackState);
void onError(Exception e);
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio);
}
/**
* A listener for internal errors.
* <p>
* These errors are not visible to the user, and hence this listener is provided for
* informational purposes only. Note however that an internal error may cause a fatal
* error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
* will be invoked.
*/
public interface InternalErrorListener {
void onRendererInitializationError(Exception e);
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
void onAudioTrackWriteError(AudioTrack.WriteException e);
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
void onDecoderInitializationError(DecoderInitializationException e);
void onCryptoError(CryptoException e);
void onLoadError(int sourceId, IOException e);
void onDrmSessionManagerError(Exception e);
}
/**
* A listener for debugging information.
*/
public interface InfoListener {
void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs);
void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs);
void onDroppedFrames(int count, long elapsed);
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs);
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
void onAvailableRangeChanged(int sourceId, TimeRange availableRange);
}
/**
* A listener for receiving notifications of timed text.
*/
public interface CaptionListener {
void onCues(List<Cue> cues);
}
/**
* A listener for receiving ID3 metadata parsed from the media stream.
*/
public interface Id3MetadataListener {
void onId3Metadata(Map<String, Object> metadata);
}
// Constants pulled into this class for convenience.
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
public static final int STATE_READY = ExoPlayer.STATE_READY;
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
public static final int RENDERER_COUNT = 4;
public static final int TYPE_VIDEO = 0;
public static final int TYPE_AUDIO = 1;
public static final int TYPE_TEXT = 2;
public static final int TYPE_METADATA = 3;
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
private static final int RENDERER_BUILDING_STATE_BUILT = 3;
private final RendererBuilder rendererBuilder;
private final ExoPlayer player;
private final PlayerControl playerControl;
private final Handler mainHandler;
private final CopyOnWriteArrayList<Listener> listeners;
private int rendererBuildingState;
private int lastReportedPlaybackState;
private boolean lastReportedPlayWhenReady;
private Surface surface;
private TrackRenderer videoRenderer;
private CodecCounters codecCounters;
private Format videoFormat;
private int videoTrackToRestore;
private BandwidthMeter bandwidthMeter;
private boolean backgrounded;
private CaptionListener captionListener;
private Id3MetadataListener id3MetadataListener;
private InternalErrorListener internalErrorListener;
private InfoListener infoListener;
public NPExoPlayer(RendererBuilder rendererBuilder) {
this.rendererBuilder = rendererBuilder;
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
player.addListener(this);
playerControl = new PlayerControl(player);
mainHandler = new Handler();
listeners = new CopyOnWriteArrayList<>();
lastReportedPlaybackState = STATE_IDLE;
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
// Disable text initially.
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
}
public PlayerControl getPlayerControl() {
return playerControl;
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public void setInternalErrorListener(InternalErrorListener listener) {
internalErrorListener = listener;
}
public void setInfoListener(InfoListener listener) {
infoListener = listener;
}
public void setCaptionListener(CaptionListener listener) {
captionListener = listener;
}
public void setMetadataListener(Id3MetadataListener listener) {
id3MetadataListener = listener;
}
public void setSurface(Surface surface) {
this.surface = surface;
pushSurface(false);
}
public Surface getSurface() {
return surface;
}
public void blockingClearSurface() {
surface = null;
pushSurface(true);
}
public int getTrackCount(int type) {
return player.getTrackCount(type);
}
public MediaFormat getTrackFormat(int type, int index) {
return player.getTrackFormat(type, index);
}
public int getSelectedTrack(int type) {
return player.getSelectedTrack(type);
}
public void setSelectedTrack(int type, int index) {
player.setSelectedTrack(type, index);
if (type == TYPE_TEXT && index < 0 && captionListener != null) {
captionListener.onCues(Collections.<Cue>emptyList());
}
}
public boolean getBackgrounded() {
return backgrounded;
}
public void setBackgrounded(boolean backgrounded) {
if (this.backgrounded == backgrounded) {
return;
}
this.backgrounded = backgrounded;
if (backgrounded) {
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
blockingClearSurface();
} else {
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
}
}
public void prepare() {
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
player.stop();
}
rendererBuilder.cancel();
videoFormat = null;
videoRenderer = null;
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
maybeReportPlayerState();
rendererBuilder.buildRenderers(this);
}
/**
* Invoked with the results from a {@link RendererBuilder}.
*
* @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual
* element may be null if there do not exist tracks of the corresponding type.
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
*/
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
for (int i = 0; i < RENDERER_COUNT; i++) {
if (renderers[i] == null) {
// Convert a null renderer to a dummy renderer.
renderers[i] = new DummyTrackRenderer();
}
}
// Complete preparation.
this.videoRenderer = renderers[TYPE_VIDEO];
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
this.bandwidthMeter = bandwidthMeter;
pushSurface(false);
player.prepare(renderers);
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
}
/**
* Invoked if a {@link RendererBuilder} encounters an error.
*
* @param e Describes the error.
*/
/* package */ void onRenderersError(Exception e) {
if (internalErrorListener != null) {
internalErrorListener.onRendererInitializationError(e);
}
for (Listener listener : listeners) {
listener.onError(e);
}
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
maybeReportPlayerState();
}
public void setPlayWhenReady(boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);
}
public void seekTo(long positionMs) {
player.seekTo(positionMs);
}
public void release() {
rendererBuilder.cancel();
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
surface = null;
player.release();
}
public int getPlaybackState() {
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
return STATE_PREPARING;
}
int playerState = player.getPlaybackState();
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
// This is an edge case where the renderers are built, but are still being passed to the
// player's playback thread.
return STATE_PREPARING;
}
return playerState;
}
@Override
public Format getFormat() {
return videoFormat;
}
@Override
public BandwidthMeter getBandwidthMeter() {
return bandwidthMeter;
}
@Override
public CodecCounters getCodecCounters() {
return codecCounters;
}
@Override
public long getCurrentPosition() {
return player.getCurrentPosition();
}
public long getDuration() {
return player.getDuration();
}
public int getBufferedPercentage() {
return player.getBufferedPercentage();
}
public boolean getPlayWhenReady() {
return player.getPlayWhenReady();
}
/* package */ Looper getPlaybackLooper() {
return player.getPlaybackLooper();
}
/* package */ Handler getMainHandler() {
return mainHandler;
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int state) {
maybeReportPlayerState();
}
@Override
public void onPlayerError(ExoPlaybackException exception) {
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
for (Listener listener : listeners) {
listener.onError(exception);
}
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
for (Listener listener : listeners) {
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
}
}
@Override
public void onDroppedFrames(int count, long elapsed) {
if (infoListener != null) {
infoListener.onDroppedFrames(count, elapsed);
}
}
@Override
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
if (infoListener != null) {
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
}
}
@Override
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
long mediaTimeMs) {
if (infoListener == null) {
return;
}
if (sourceId == TYPE_VIDEO) {
videoFormat = format;
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
} else if (sourceId == TYPE_AUDIO) {
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
}
}
@Override
public void onDrmKeysLoaded() {
// Do nothing.
}
@Override
public void onDrmSessionManagerError(Exception e) {
if (internalErrorListener != null) {
internalErrorListener.onDrmSessionManagerError(e);
}
}
@Override
public void onDecoderInitializationError(DecoderInitializationException e) {
if (internalErrorListener != null) {
internalErrorListener.onDecoderInitializationError(e);
}
}
@Override
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackInitializationError(e);
}
}
@Override
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackWriteError(e);
}
}
@Override
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
@Override
public void onCryptoError(CryptoException e) {
if (internalErrorListener != null) {
internalErrorListener.onCryptoError(e);
}
}
@Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
if (infoListener != null) {
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
}
}
@Override
public void onLoadError(int sourceId, IOException e) {
if (internalErrorListener != null) {
internalErrorListener.onLoadError(sourceId, e);
}
}
@Override
public void onCues(List<Cue> cues) {
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
captionListener.onCues(cues);
}
}
@Override
public void onMetadata(Map<String, Object> metadata) {
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
id3MetadataListener.onId3Metadata(metadata);
}
}
@Override
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
if (infoListener != null) {
infoListener.onAvailableRangeChanged(sourceId, availableRange);
}
}
@Override
public void onPlayWhenReadyCommitted() {
// Do nothing.
}
@Override
public void onDrawnToSurface(Surface surface) {
// Do nothing.
}
@Override
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs) {
if (infoListener != null) {
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
mediaEndTimeMs);
}
}
@Override
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
if (infoListener != null) {
infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
}
}
@Override
public void onLoadCanceled(int sourceId, long bytesLoaded) {
// Do nothing.
}
@Override
public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
// Do nothing.
}
private void maybeReportPlayerState() {
boolean playWhenReady = player.getPlayWhenReady();
int playbackState = getPlaybackState();
if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
for (Listener listener : listeners) {
listener.onStateChanged(playWhenReady, playbackState);
}
lastReportedPlayWhenReady = playWhenReady;
lastReportedPlaybackState = playbackState;
}
}
private void pushSurface(boolean blockForSurfacePush) {
if (videoRenderer == null) {
return;
}
if (blockForSurfacePush) {
player.blockingSendMessage(
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
} else {
player.sendMessage(
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
}
}
}

View File

@@ -1,206 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler;
import java.io.IOException;
/**
* A {@link RendererBuilder} for SmoothStreaming.
*/
public class SmoothStreamingRendererBuilder implements RendererBuilder {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 54;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final Context context;
private final String userAgent;
private final String url;
private final MediaDrmCallback drmCallback;
private AsyncRendererBuilder currentAsyncBuilder;
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback) {
this.context = context;
this.userAgent = userAgent;
this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest";
this.drmCallback = drmCallback;
}
@Override
public void buildRenderers(NPExoPlayer player) {
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
currentAsyncBuilder.init();
}
@Override
public void cancel() {
if (currentAsyncBuilder != null) {
currentAsyncBuilder.cancel();
currentAsyncBuilder = null;
}
}
private static final class AsyncRendererBuilder
implements ManifestFetcher.ManifestCallback<SmoothStreamingManifest> {
private final Context context;
private final String userAgent;
private final MediaDrmCallback drmCallback;
private final NPExoPlayer player;
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
private boolean canceled;
public AsyncRendererBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback, NPExoPlayer player) {
this.context = context;
this.userAgent = userAgent;
this.drmCallback = drmCallback;
this.player = player;
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null),
parser);
}
public void init() {
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
public void cancel() {
canceled = true;
}
@Override
public void onSingleManifestError(IOException exception) {
if (canceled) {
return;
}
player.onRenderersError(exception);
}
@Override
public void onSingleManifest(SmoothStreamingManifest manifest) {
if (canceled) {
return;
}
Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
// Check drm support if necessary.
DrmSessionManager drmSessionManager = null;
if (manifest.protectionElement != null) {
if (Util.SDK_INT < 18) {
player.onRenderersError(
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
return;
}
try {
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
} catch (UnsupportedDrmException e) {
player.onRenderersError(e);
return;
}
}
// Build the video renderer.
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false),
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_VIDEO);
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
drmSessionManager, true, mainHandler, player, 50);
// Build the audio renderer.
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
DefaultSmoothStreamingTrackSelector.newAudioInstance(),
audioDataSource, null, LIVE_EDGE_LATENCY_MS);
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_AUDIO);
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
// Build the text renderer.
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
DefaultSmoothStreamingTrackSelector.newTextInstance(),
textDataSource, null, LIVE_EDGE_LATENCY_MS);
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_TEXT);
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
mainHandler.getLooper());
// Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
}
}
}

View File

@@ -1,5 +1,3 @@
package org.schabi.newpipe.report;
import android.app.Activity;
@@ -8,14 +6,15 @@ import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.design.widget.Snackbar;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@@ -34,8 +33,8 @@ import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -65,65 +64,12 @@ import java.util.Vector;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ErrorActivity extends ThemableActivity {
public static class ErrorInfo implements Parcelable {
public int userAction;
public String request;
public String serviceName;
public int message;
public static ErrorInfo make(int userAction, String serviceName, String request, int message) {
ErrorInfo info = new ErrorInfo();
info.userAction = userAction;
info.serviceName = serviceName;
info.request = request;
info.message = message;
return info;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.userAction);
dest.writeString(this.request);
dest.writeString(this.serviceName);
dest.writeInt(this.message);
}
public ErrorInfo() {
}
protected ErrorInfo(Parcel in) {
this.userAction = in.readInt();
this.request = in.readString();
this.serviceName = in.readString();
this.message = in.readInt();
}
public static final Parcelable.Creator<ErrorInfo> CREATOR = new Parcelable.Creator<ErrorInfo>() {
@Override
public ErrorInfo createFromParcel(Parcel source) {
return new ErrorInfo(source);
}
@Override
public ErrorInfo[] newArray(int size) {
return new ErrorInfo[size];
}
};
}
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_LIST = "error_list";
// MESSAGE ID
public static final int SEARCHED = 0;
public static final int REQUESTED_STREAM = 1;
@@ -133,7 +79,6 @@ public class ErrorActivity extends ThemableActivity {
public static final int LOAD_IMAGE = 5;
public static final int UI_ERROR = 6;
public static final int REQUESTED_CHANNEL = 7;
// MESSAGE STRING
public static final String SEARCHED_STRING = "searched";
public static final String REQUESTED_STREAM_STRING = "requested stream";
@@ -143,17 +88,14 @@ public class ErrorActivity extends ThemableActivity {
public static final String LOAD_IMAGE_STRING = "load image";
public static final String UI_ERROR_STRING = "ui error";
public static final String REQUESTED_CHANNEL_STRING = "requested channel";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME;
Thread globIpRangeThread;
private String[] errorList;
private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp;
private String globIpRange;
Thread globIpRangeThread;
// views
private TextView errorView;
private EditText userCommentBox;
@@ -161,6 +103,10 @@ public class ErrorActivity extends ThemableActivity {
private TextView infoView;
private TextView errorMessageView;
public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UI_ERROR, "none", "", R.string.app_ui_crash));
}
public static void reportError(final Context context, final List<Throwable> el,
final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) {
@@ -238,9 +184,26 @@ public class ErrorActivity extends ThemableActivity {
context.startActivity(intent);
}
private static String getStackTrace(final Throwable throwable) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true);
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
}
// errorList to StringList
private static String[] elToSl(List<Throwable> stackTraces) {
String[] out = new String[stackTraces.size()];
for (int i = 0; i < stackTraces.size(); i++) {
out[i] = getStackTrace(stackTraces.get(i));
}
return out;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, true);
setContentView(R.layout.activity_error);
Intent intent = getIntent();
@@ -297,6 +260,11 @@ public class ErrorActivity extends ThemableActivity {
}
errorView.setText(formErrorText(errorList));
//print stack trace once again for debugging:
for(String e : errorList) {
Log.e(TAG, e);
}
}
@Override
@@ -325,13 +293,6 @@ public class ErrorActivity extends ThemableActivity {
return false;
}
private static String getStackTrace(final Throwable throwable) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true);
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
}
private String formErrorText(String[] el) {
String text = "";
if(el != null) {
@@ -468,6 +429,56 @@ public class ErrorActivity extends ThemableActivity {
return df.format(new Date());
}
public static class ErrorInfo implements Parcelable {
public static final Parcelable.Creator<ErrorInfo> CREATOR = new Parcelable.Creator<ErrorInfo>() {
@Override
public ErrorInfo createFromParcel(Parcel source) {
return new ErrorInfo(source);
}
@Override
public ErrorInfo[] newArray(int size) {
return new ErrorInfo[size];
}
};
public int userAction;
public String request;
public String serviceName;
public int message;
public ErrorInfo() {
}
protected ErrorInfo(Parcel in) {
this.userAction = in.readInt();
this.request = in.readString();
this.serviceName = in.readString();
this.message = in.readInt();
}
public static ErrorInfo make(int userAction, String serviceName, String request, int message) {
ErrorInfo info = new ErrorInfo();
info.userAction = userAction;
info.serviceName = serviceName;
info.request = request;
info.message = message;
return info;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.userAction);
dest.writeString(this.request);
dest.writeString(this.serviceName);
dest.writeInt(this.message);
}
}
private class IpRagneRequester implements Runnable {
Handler h = new Handler();
public void run() {
@@ -487,8 +498,6 @@ public class ErrorActivity extends ThemableActivity {
}
}
private class IpRageReturnRunnable implements Runnable {
String ipRange;
public IpRageReturnRunnable(String ipRange) {
@@ -504,13 +513,4 @@ public class ErrorActivity extends ThemableActivity {
}
}
}
// errorList to StringList
private static String[] elToSl(List<Throwable> stackTraces) {
String[] out = new String[stackTraces.size()];
for(int i = 0; i < stackTraces.size(); i++) {
out[i] = getStackTrace(stackTraces.get(i));
}
return out;
}
}

View File

@@ -19,15 +19,17 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.ProgressBar;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.detail.VideoItemDetailActivity;
import org.schabi.newpipe.detail.VideoItemDetailFragment;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavStack;
import java.util.EnumSet;
import static android.app.Activity.RESULT_OK;
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
@@ -56,6 +58,9 @@ public class SearchInfoItemFragment extends Fragment {
private static final String TAG = SearchInfoItemFragment.class.toString();
private EnumSet<SearchEngine.Filter> filter =
EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM);
/**
* Listener for search queries
*/
@@ -166,7 +171,7 @@ public class SearchInfoItemFragment extends Fragment {
sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() {
@Override
public void onResult(SearchResult result) {
infoListAdapter.addStreamItemList(result.resultList);
infoListAdapter.addInfoItemList(result.resultList);
setDoneLoading();
}
@@ -197,7 +202,6 @@ public class SearchInfoItemFragment extends Fragment {
RECAPTCHA_REQUEST);
}
});
}
@Override
@@ -213,10 +217,21 @@ public class SearchInfoItemFragment extends Fragment {
infoListAdapter = new InfoListAdapter(getActivity(),
getActivity().findViewById(android.R.id.content));
infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
infoListAdapter.setFooter(inflater.inflate(R.layout.pignate_footer, recyclerView, false));
infoListAdapter.showFooter(false);
infoListAdapter.setOnStreamInfoItemSelectedListener(
new InfoItemBuilder.OnInfoItemSelectedListener() {
@Override
public void selected(String url) {
startDetailActivity(url);
public void selected(String url, int serviceId) {
NavStack.getInstance()
.openDetailActivity(getContext(), url, serviceId);
}
});
infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
@Override
public void selected(String url, int serviceId) {
NavStack.getInstance()
.openChannelActivity(getContext(), url, serviceId);
}
});
recyclerView.setAdapter(infoListAdapter);
@@ -243,13 +258,6 @@ public class SearchInfoItemFragment extends Fragment {
return view;
}
private void startDetailActivity(String url) {
Intent i = new Intent(getActivity(), VideoItemDetailActivity.class);
i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
i.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
getActivity().startActivity(i);
}
@Override
public void onStart() {
super.onStart();
@@ -275,6 +283,32 @@ public class SearchInfoItemFragment extends Fragment {
setupSearchView(searchView);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_filter_all:
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL));
return true;
case R.id.menu_filter_video:
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM));
return true;
case R.id.menu_filter_channel:
changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL));
return true;
default:
return false;
}
}
private void changeFilter(MenuItem item, EnumSet<SearchEngine.Filter> filter) {
this.filter = filter;
item.setChecked(true);
if(searchQuery != null && !searchQuery.isEmpty()) {
Log.d(TAG, "Fuck+ " + searchQuery);
search(searchQuery);
}
}
private void setupSearchView(SearchView searchView) {
suggestionListAdapter = new SuggestionListAdapter(getActivity());
searchView.setSuggestionsAdapter(suggestionListAdapter);
@@ -288,6 +322,7 @@ public class SearchInfoItemFragment extends Fragment {
private void search(String query) {
infoListAdapter.clearSteamItemList();
infoListAdapter.showFooter(false);
pageNumber = 0;
searchQuery = query;
search(query, pageNumber);
@@ -298,12 +333,17 @@ public class SearchInfoItemFragment extends Fragment {
private void search(String query, int page) {
isLoading = true;
SearchWorker sw = SearchWorker.getInstance();
sw.search(streamingServiceId, query, page, getActivity());
sw.search(streamingServiceId,
query,
page,
getActivity(),
filter);
}
private void setDoneLoading() {
this.isLoading = false;
loadingIndicator.setVisibility(View.GONE);
infoListAdapter.showFooter(true);
}
/**

View File

@@ -16,6 +16,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import java.io.IOException;
import java.util.EnumSet;
/**
* Created by Christian Schabesberger on 02.08.16.
@@ -67,14 +68,21 @@ public class SearchWorker {
public static final String YOUTUBE = "Youtube";
private final String query;
private final int page;
private final EnumSet<SearchEngine.Filter> filter;
final Handler h = new Handler();
private volatile boolean runs = true;
private Activity a = null;
private int serviceId = -1;
public SearchRunnable(int serviceId, String query, int page, Activity activity, int requestId) {
public SearchRunnable(int serviceId,
String query,
int page,
EnumSet<SearchEngine.Filter> filter,
Activity activity,
int requestId) {
this.serviceId = serviceId;
this.query = query;
this.page = page;
this.filter = filter;
this.a = activity;
}
void terminate() {
@@ -102,13 +110,14 @@ public class SearchWorker {
String searchLanguage = sp.getString(searchLanguageKey,
a.getString(R.string.default_language_value));
result = SearchResult
.getSearchResult(engine, query, page, searchLanguage);
.getSearchResult(engine, query, page, searchLanguage, filter);
if(runs) {
h.post(new ResultRunnable(result, requestId));
}
// look for errors during extraction
// soft errors:
View rootView = a.findViewById(android.R.id.content);
if(result != null &&
!result.errors.isEmpty()) {
Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:");
@@ -117,11 +126,17 @@ public class SearchWorker {
Log.e(TAG, "------");
}
View rootView = a.findViewById(android.R.id.content);
ErrorActivity.reportError(h, a, result.errors, null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
serviceName, query, R.string.light_parsing_error));
if(result.resultList.isEmpty()&& !result.errors.isEmpty()) {
// if it compleatly failes dont show snackbar, instead show error directlry
ErrorActivity.reportError(h, a, result.errors, null, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
serviceName, query, R.string.parsing_error));
} else {
// if it partly show snackbar
ErrorActivity.reportError(h, a, result.errors, null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
serviceName, query, R.string.light_parsing_error));
}
}
// hard errors:
} catch (ReCaptchaException e) {
@@ -180,11 +195,15 @@ public class SearchWorker {
}
public void search(int serviceId, String query, int page, Activity a) {
public void search(int serviceId,
String query,
int page,
Activity a,
EnumSet<SearchEngine.Filter> filter) {
if(runnable != null) {
terminate();
}
runnable = new SearchRunnable(serviceId, query, page, a, requestId);
runnable = new SearchRunnable(serviceId, query, page, filter, a, requestId);
Thread thread = new Thread(runnable);
thread.start();
}

View File

@@ -8,7 +8,7 @@ import android.widget.Toast;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.search.SuggestionExtractor;
import org.schabi.newpipe.extractor.SuggestionExtractor;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R;

View File

@@ -5,7 +5,6 @@ import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
@@ -16,8 +15,7 @@ import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import java.util.Objects;
import org.schabi.newpipe.util.ThemeHelper;
/**
@@ -41,15 +39,16 @@ import java.util.Objects;
*/
public class SettingsActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate = null;
SettingsFragment f = new SettingsFragment();
private AppCompatDelegate mDelegate = null;
public static void initSettings(Context context) {
NewPipeSettings.initSettings(context);
}
@Override
protected void onCreate(Bundle savedInstanceBundle) {
if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme);
}
ThemeHelper.setTheme(this, true);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceBundle);
super.onCreate(savedInstanceBundle);
@@ -155,8 +154,4 @@ public class SettingsActivity extends PreferenceActivity {
}
return true;
}
public static void initSettings(Context context) {
NewPipeSettings.initSettings(context);
}
}

View File

@@ -1,27 +1,28 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.app.ListActivity;
import android.content.ClipData;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v7.app.AlertDialog;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import java.util.Objects;
import java.util.List;
import info.guardianproject.netcipher.proxy.OrbotHelper;
@@ -48,20 +49,19 @@ import info.guardianproject.netcipher.proxy.OrbotHelper;
public class SettingsFragment extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener
{
public static final int REQUEST_INSTALL_ORBOT = 0x1234;
SharedPreferences.OnSharedPreferenceChangeListener prefListener;
// get keys
String DEFAULT_RESOLUTION_PREFERENCE;
String PREFERRED_VIDEO_FORMAT_PREFERENCE;
String DEFAULT_AUDIO_FORMAT_PREFERENCE;
String SEARCH_LANGUAGE_PREFERENCE;
String DOWNLOAD_PATH_PREFERENCE;
String DOWNLOAD_PATH_AUDIO_PREFERENCE;
String USE_TOR_KEY;
String THEME;
public static final int REQUEST_INSTALL_ORBOT = 0x1234;
private ListPreference defaultResolutionPreference;
private ListPreference preferredVideoFormatPreference;
private ListPreference defaultAudioFormatPreference;
private ListPreference searchLanguagePreference;
private Preference downloadPathPreference;
@@ -80,6 +80,7 @@ public class SettingsFragment extends PreferenceFragment
// get keys
DEFAULT_RESOLUTION_PREFERENCE = getString(R.string.default_resolution_key);
PREFERRED_VIDEO_FORMAT_PREFERENCE = getString(R.string.preferred_video_format_key);
DEFAULT_AUDIO_FORMAT_PREFERENCE = getString(R.string.default_audio_format_key);
SEARCH_LANGUAGE_PREFERENCE = getString(R.string.search_language_key);
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
@@ -90,6 +91,8 @@ public class SettingsFragment extends PreferenceFragment
// get pref objects
defaultResolutionPreference =
(ListPreference) findPreference(DEFAULT_RESOLUTION_PREFERENCE);
preferredVideoFormatPreference =
(ListPreference) findPreference(PREFERRED_VIDEO_FORMAT_PREFERENCE);
defaultAudioFormatPreference =
(ListPreference) findPreference(DEFAULT_AUDIO_FORMAT_PREFERENCE);
searchLanguagePreference =
@@ -98,6 +101,8 @@ public class SettingsFragment extends PreferenceFragment
downloadPathAudioPreference = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
themePreference = findPreference(THEME);
final String currentTheme = defaultPreferences.getString(THEME, "Light");
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
@@ -139,8 +144,27 @@ public class SettingsFragment extends PreferenceFragment
}
else if (key == THEME)
{
String theme = sharedPreferences.getString(THEME, "Light");
themePreference.setSummary(theme);
String selectedTheme = sharedPreferences.getString(THEME, "Light");
themePreference.setSummary(selectedTheme);
if(!selectedTheme.equals(currentTheme)) { // If it's not the current theme
new AlertDialog.Builder(activity)
.setTitle(R.string.restart_title)
.setMessage(R.string.msg_restart)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intentToMain = new Intent(activity, MainActivity.class);
intentToMain.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
activity.startActivity(intentToMain);
activity.finish();
Runtime.getRuntime().exit(0);
}
})
.setNegativeButton(R.string.later, null)
.create().show();
}
}
updateSummary();
}
@@ -236,6 +260,9 @@ public class SettingsFragment extends PreferenceFragment
defaultResolutionPreference.setSummary(
defaultPreferences.getString(DEFAULT_RESOLUTION_PREFERENCE,
getString(R.string.default_resolution_value)));
preferredVideoFormatPreference.setSummary(
defaultPreferences.getString(PREFERRED_VIDEO_FORMAT_PREFERENCE,
getString(R.string.preferred_video_format_default)));
defaultAudioFormatPreference.setSummary(
defaultPreferences.getString(DEFAULT_AUDIO_FORMAT_PREFERENCE,
getString(R.string.default_audio_format_value)));

View File

@@ -0,0 +1,148 @@
package org.schabi.newpipe.util;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import org.schabi.newpipe.ChannelActivity;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.detail.VideoItemDetailActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import java.util.ArrayList;
import java.util.Stack;
/**
* Created by Christian Schabesberger on 16.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* NavStack.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* class helps to navigate within the app
* IMPORTAND: the top of the stack is the current activity !!!
*/
public class NavStack {
private static final String TAG = NavStack.class.toString();
public static final String SERVICE_ID = "service_id";
public static final String URL = "url";
private static final String NAV_STACK="nav_stack";
private enum ActivityId {
CHANNEL,
DETAIL
}
private class NavEntry {
public NavEntry(String url, int serviceId) {
this.url = url;
this.serviceId = serviceId;
}
public String url;
public int serviceId;
}
private static NavStack instance = new NavStack();
private Stack<NavEntry> stack = new Stack<NavEntry>();
private NavStack() {
}
public static NavStack getInstance() {
return instance;
}
public void navBack(Activity activity) throws Exception {
if(stack.size() == 0) { // if stack is already empty here, activity was probably called
// from another app
activity.finish();
return;
}
stack.pop(); // remove curent activty, since we dont want to return to itself
if (stack.size() == 0) {
openMainActivity(activity); // if no more page is on the stack this means we are home
return;
}
NavEntry entry = stack.pop(); // this element will reapear, since by calling the old page
// this element will be pushed on top again
try {
StreamingService service = NewPipe.getService(entry.serviceId);
switch (service.getLinkTypeByUrl(entry.url)) {
case STREAM:
openDetailActivity(activity, entry.url, entry.serviceId);
break;
case CHANNEL:
openChannelActivity(activity, entry.url, entry.serviceId);
break;
case NONE:
throw new Exception("Url not known to service. service="
+ Integer.toString(entry.serviceId) + " url=" + entry.url);
default:
openMainActivity(activity);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void openChannelActivity(Context context, String url, int serviceId) {
openActivity(context, url, serviceId, ChannelActivity.class);
}
public void openDetailActivity(Context context, String url, int serviceId) {
openActivity(context, url, serviceId, VideoItemDetailActivity.class);
}
private void openActivity(Context context, String url, int serviceId, Class acitivtyClass) {
//if last element has the same url do not push to stack again
if(stack.isEmpty() || !stack.peek().url.equals(url)) {
stack.push(new NavEntry(url, serviceId));
}
Intent i = new Intent(context, acitivtyClass);
i.putExtra(SERVICE_ID, serviceId);
i.putExtra(URL, url);
context.startActivity(i);
}
public void openMainActivity(Activity a) {
stack.clear();
Intent i = new Intent(a, MainActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(a, i);
}
public void onSaveInstanceState(Bundle state) {
ArrayList<String> sa = new ArrayList<>();
for(NavEntry entry : stack) {
sa.add(entry.url);
}
state.putStringArrayList(NAV_STACK, sa);
}
public void restoreSavedInstanceState(Bundle state) {
ArrayList<String> sa = state.getStringArrayList(NAV_STACK);
stack.clear();
for(String url : sa) {
stack.push(new NavEntry(url, NewPipe.getServiceByUrl(url).getServiceId()));
}
}
}

View File

@@ -2,8 +2,12 @@ package org.schabi.newpipe.util;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.RequiresApi;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
@@ -11,7 +15,7 @@ import android.support.v4.content.ContextCompat;
public class PermissionHelper {
public static final int PERMISSION_WRITE_STORAGE = 778;
public static final int PERMISSION_READ_STORAGE = 777;
public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779;
public static boolean checkStoragePermissions(Activity activity) {
@@ -65,4 +69,27 @@ public class PermissionHelper {
}
return true;
}
/**
* In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted.
* <p>
* On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest),
* on > 23, however, it have to start a activity asking the user if he agree.
* <p>
* This method just return if canDraw over other apps, if it doesn't, try to get the permission,
* it does not get the result of the startActivityForResult, if the user accept, the next time that he tries to open
* it will return true.
*
* @param activity context to startActivityForResult
* @return returns {@link Settings#canDrawOverlays(Context)}
**/
@RequiresApi(api = Build.VERSION_CODES.M)
public static boolean checkSystemAlertWindowPermission(Activity activity) {
if (!Settings.canDrawOverlays(activity)) {
Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(i, PERMISSION_SYSTEM_ALERT_WINDOW);
return false;
}else return true;
}
}

View File

@@ -0,0 +1,34 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.preference.PreferenceManager;
import org.schabi.newpipe.R;
public class ThemeHelper {
/**
* Apply the selected theme (on NewPipe settings) in the context
*
* @param context context that the theme will be applied
* @param useActionbarTheme whether to use an action bar theme or not
*/
public static void setTheme(Context context, boolean useActionbarTheme) {
String themeKey = context.getString(R.string.theme_key);
String darkTheme = context.getResources().getString(R.string.dark_theme_title);
String blackTheme = context.getResources().getString(R.string.black_theme_title);
String sp = PreferenceManager.getDefaultSharedPreferences(context)
.getString(themeKey, context.getResources().getString(R.string.light_theme_title));
if (useActionbarTheme) {
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme);
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme);
else context.setTheme(R.style.AppTheme);
} else {
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme_NoActionBar);
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme_NoActionBar);
else context.setTheme(R.style.AppTheme_NoActionBar);
}
}
}

View File

@@ -84,9 +84,23 @@ public abstract class MissionsFragment extends Fragment
return v;
}
@Override
/** Added in API level 23. */
@Override
public void onAttach(Context activity) {
super.onAttach(activity);
// Bug: in api< 23 this is never called
// so mActivity=null
// so app crashes with nullpointer exception
mActivity = activity;
}
/** deprecated in API level 23,
* but must remain to allow compatibility with api<23 */
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = activity;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View File

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

View File

Before

Width:  |  Height:  |  Size: 92 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

View File

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

View File

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

View File

Before

Width:  |  Height:  |  Size: 90 B

After

Width:  |  Height:  |  Size: 90 B

Some files were not shown because too many files have changed in this diff Show More