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

Compare commits

...

253 Commits

Author SHA1 Message Date
Matej U
a1a12ca9f7 Translated using Weblate (Slovenian)
Currently translated at 100.0% (68 of 68 strings)
2016-02-19 15:46:49 +01:00
Christian Schabesberger
5a594173fa Merge pull request #193 from satiricon/fix-autoplay
Fix ArrayIndexOutOfBoundsException when using autoplay
2016-02-19 15:14:06 +01:00
Christian Schabesberger
1e989945b9 moved on to 0.7.5 2016-02-19 14:40:49 +01:00
Christian Schabesberger
679bef849c Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-19 14:40:20 +01:00
Christian Schabesberger
1c17be9760 made new notification layout available for older platforms 2016-02-19 13:22:49 +01:00
naofum
6def0e3115 Translated using Weblate (Japanese)
Currently translated at 100.0% (68 of 68 strings)
2016-02-19 12:51:38 +01:00
David
fd3436d5c0 Fix ArrayIndexOutOfBoundsException when using autoplay 2016-02-18 13:40:26 -03:00
Christian Schabesberger
a94f9fd3e5 made the ui react on missing information 2016-02-18 13:49:01 +01:00
Christian Schabesberger
77850464d4 renamed crawler into extractor 2016-02-18 11:50:22 +01:00
Christian Schabesberger
3e9edba189 fix broken unit test 2016-02-18 11:47:21 +01:00
Christian Schabesberger
3d168542fe add arabic translation 2016-02-18 00:00:47 +01:00
Christian Schabesberger
0de00e26d8 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-17 23:55:32 +01:00
Christian Schabesberger
f0705c612e removed play fab button 2016-02-17 23:55:19 +01:00
Christian Schabesberger
648b9b5d02 made the ui more accommodating for not available audio streams 2016-02-17 21:39:41 +01:00
Christian Schabesberger
b15a0b92f9 add support for attribution_link links 2016-02-17 20:29:31 +01:00
Christian Schabesberger
80482c0578 made settings_keys unsupportable 2016-02-17 19:23:23 +01:00
Weblate
16701923a1 Merge remote-tracking branch 'origin/master' 2016-02-17 19:21:20 +01:00
kamadi
67cee06523 Translated using Weblate (Russian)
Currently translated at 80.5% (54 of 67 strings)
2016-02-17 19:21:20 +01:00
Omar Kharrab
685dab39af Translated using Weblate (French)
Currently translated at 97.0% (65 of 67 strings)
2016-02-17 19:21:19 +01:00
David Duong
0ff9555cc4 Translated using Weblate (Czech)
Currently translated at 100.0% (67 of 67 strings)
2016-02-17 18:44:14 +01:00
Robin van der Vliet
4f808dd17d Translated using Weblate (Dutch)
Currently translated at 65.6% (44 of 67 strings)
2016-02-17 00:44:16 +01:00
vadin
69d71dac36 Translated using Weblate (Spanish)
Currently translated at 100.0% (67 of 67 strings)
2016-02-16 23:18:33 +01:00
Jossi Wolf
4880319991 Translated using Weblate (German)
Currently translated at 98.5% (66 of 67 strings)
2016-02-16 21:44:52 +01:00
Robin van der Vliet
13b9850b39 Translated using Weblate (Esperanto)
Currently translated at 100.0% (67 of 67 strings)

Imperative because it is a command to the user.
2016-02-16 17:29:05 +01:00
Robin van der Vliet
366f645eda Translated using Weblate (Esperanto)
Currently translated at 97.0% (65 of 67 strings)

Translated "media center" just as "application", if somebody has a better translation please change it.
2016-02-16 17:28:40 +01:00
Robin van der Vliet
dcf7190c90 Translated using Weblate (Esperanto)
Currently translated at 91.0% (61 of 67 strings)

elŝutujo = container for downloads
2016-02-16 17:22:13 +01:00
Robin van der Vliet
ae6647276c Translated using Weblate (Esperanto)
Currently translated at 86.5% (58 of 67 strings)

A bit long
2016-02-16 17:14:35 +01:00
Robin van der Vliet
460ff37f7a Translated using Weblate (Esperanto)
Currently translated at 79.1% (53 of 67 strings)

A bit long because
2016-02-16 17:05:14 +01:00
Robin van der Vliet
e7ef913416 Translated using Weblate (Esperanto)
Currently translated at 77.6% (52 of 67 strings)

Added "the app" because the accusative cannot really go on the word "Kore"
2016-02-16 17:03:53 +01:00
Robin van der Vliet
c149050a3a Translated using Weblate (Esperanto)
Currently translated at 71.6% (48 of 67 strings)

Added "the app" because the accusative cannot really go on the word "VLC"
2016-02-16 16:59:18 +01:00
Robin van der Vliet
a4e5ca54db Translated using Weblate (Esperanto)
Currently translated at 70.1% (47 of 67 strings)

Intent untranslated because there is not really a common translation. If somebody has a better translation, please change it.
2016-02-16 16:58:13 +01:00
Robin van der Vliet
cb81fd3353 Translated using Weblate (Esperanto)
Currently translated at 68.6% (46 of 67 strings)

Added "the program" because the accusative cannot really go on the word "Tor"
2016-02-16 16:57:28 +01:00
David Duong
352a883b5d Translated using Weblate (Czech)
Currently translated at 100.0% (67 of 67 strings)
2016-02-15 18:04:52 +01:00
David Duong
5247b22ef4 Translated using Weblate (Czech)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-15 17:33:44 +01:00
Robin van der Vliet
1bf00e80b0 Translated using Weblate (Esperanto)
Currently translated at 67.1% (45 of 67 strings)
2016-02-14 23:57:46 +01:00
Robin van der Vliet
6331d9b7a4 Translated using Weblate (Esperanto)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-14 23:44:51 +01:00
Marian Hanzel
0deb0ad851 Translated using Weblate (Slovak)
Currently translated at 100.0% (67 of 67 strings)
2016-02-14 21:45:11 +01:00
Matej U
3b42041c4e Translated using Weblate (Slovenian)
Currently translated at 100.0% (67 of 67 strings)
2016-02-14 21:23:03 +01:00
Benedikt Freisen
d8987aa94a Translated using Weblate (German)
Currently translated at 98.5% (66 of 67 strings)

Where readable and unambiguous, the compound-noun-spelling without hyphen is usually preferred.
2016-02-14 21:06:39 +01:00
Mladen Pejaković
19ed16efc6 Translated using Weblate (Serbian)
Currently translated at 100.0% (67 of 67 strings)
2016-02-14 11:33:57 +01:00
Benedikt Freisen
0e32d70aef Translated using Weblate (German)
Currently translated at 98.5% (66 of 67 strings)
2016-02-12 22:59:12 +01:00
Marian Hanzel
5b71f279fd Translated using Weblate (Slovak)
Currently translated at 100.0% (67 of 67 strings)
2016-02-12 19:59:06 +01:00
Marian Hanzel
6d170ffb16 Translated using Weblate (Slovak)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-12 19:29:38 +01:00
Christian Schabesberger
72d90374a0 moved on to 0.7.4 2016-02-12 15:32:16 +01:00
Christian Schabesberger
fc8160acda fixed autoplay bug 2016-02-12 15:29:11 +01:00
Christian Schabesberger
751ffb9de9 quick and dirty solution for download/audio bug
Youtube has changed (again) this time it was a bit harder,
therfor the changes are deeper. May be still a bit unstable.
2016-02-12 01:29:14 +01:00
Christian Schabesberger
60d636940d set orbot/tor support into experimental status 2016-02-10 12:09:43 +01:00
Christian Schabesberger
7a5cca519a Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-08 18:46:56 +01:00
Christian Schabesberger
1e93d06a25 restructured ActionbarHandler 2016-02-08 18:46:42 +01:00
naofum
4341f8aaec Translated using Weblate (Japanese)
Currently translated at 100.0% (67 of 67 strings)
2016-02-08 12:44:42 +01:00
Licaon Kter
46022d60f3 Translated using Weblate (Romanian)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-07 22:16:35 +01:00
Sérgio Marques
80ddc76926 Translated using Weblate (Portuguese)
Currently translated at 100.0% (67 of 67 strings)
2016-02-06 23:07:05 +01:00
naofum
bd999b4b0a Translated using Weblate (Japanese)
Currently translated at 100.0% (67 of 67 strings)
2016-02-06 11:42:08 +01:00
Christian Schabesberger
4cdc387338 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-05 17:14:18 +01:00
Christian Schabesberger
df258a0003 Merge branch 'algoprog-master' 2016-02-05 17:10:00 +01:00
Christian Schabesberger
7cc1c0fbdd merge code, and adjust code 2016-02-05 17:09:29 +01:00
Mladen Pejaković
42644c956b Translated using Weblate (Serbian)
Currently translated at 100.0% (67 of 67 strings)
2016-02-05 14:50:14 +01:00
Weblate
00eaedcbfa Merge remote-tracking branch 'origin/master' 2016-02-05 14:38:53 +01:00
Chris Samarinas
1ff5a97c85 Translated using Weblate (Greek)
Currently translated at 100.0% (61 of 61 strings)
2016-02-05 14:38:52 +01:00
Christian Schabesberger
241414f81b Merge pull request #167 from theScrabi/crawlerRefactor
Crawler refactor
2016-02-05 14:38:48 +01:00
Christian Schabesberger
1bf046a8ba made all arrays into lists 2016-02-05 14:09:04 +01:00
Christian Schabesberger
61471fdd3c renamed UrlIdHandler into VideoUrlIdHandler 2016-02-05 13:34:44 +01:00
Allan Nordhøy
a0524fb136 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (61 of 61 strings)
2016-02-05 01:55:35 +01:00
Allan Nordhøy
fa39389a64 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-05 01:38:27 +01:00
Chris Samarinas
697a24e699 Translated using Weblate (Greek)
Currently translated at 100.0% (61 of 61 strings)
2016-02-04 06:10:42 +01:00
Chris Samarinas
cf3bb87a54 Translated using Weblate (Greek)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-04 05:46:47 +01:00
Christian Schabesberger
14fb7d8a7a fixed failing tests 2016-02-02 19:01:53 +01:00
Christian Schabesberger
d097363b24 restructure parser 2016-02-02 18:43:20 +01:00
Christian Schabesberger
bad576c23d got rid of getVideoInfo() in youtube crawler 2016-02-02 14:06:09 +01:00
Weblate
8d9e4e7442 Merge remote-tracking branch 'origin/master' 2016-02-01 13:44:46 +01:00
Christian Schabesberger
431d03c63f Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-01 13:46:01 +01:00
Christian Schabesberger
24321731d9 Merge branch 'iit2014086-master' 2016-02-01 13:45:44 +01:00
Christian Schabesberger
fddcade1fb removed unnececeary call of setVolumeControlStream() 2016-02-01 13:45:22 +01:00
theshayy
a0aa7dcdc1 Translated using Weblate (French)
Currently translated at 100.0% (61 of 61 strings)
2016-02-01 13:44:46 +01:00
Teja Vojjala
d3e9f354b3 Update SettingsActivity.java 2016-02-01 15:00:08 +05:30
Teja Vojjala
e958334406 Update VideoItemListActivity.java 2016-02-01 14:29:49 +05:30
Teja Vojjala
a42029970d Update VideoItemDetailActivity.java 2016-02-01 14:29:28 +05:30
Teja Vojjala
2108e09e13 Update PlayVideoActivity.java 2016-02-01 14:28:38 +05:30
Teja Vojjala
8c010093e8 Update SettingsActivity.java 2016-02-01 14:27:24 +05:30
Teja Vojjala
adc98be441 Update PanicResponderActivity.java 2016-02-01 14:25:23 +05:30
Christian Schabesberger
fb942912db disable gema test agian 2016-01-31 20:16:21 +01:00
Christian Schabesberger
7f12b58722 use java error system in the crawler 2016-01-31 19:57:30 +01:00
Éfrit
05d58f8f98 Translated using Weblate (French)
Currently translated at 100.0% (61 of 61 strings)
2016-01-31 12:49:00 +01:00
Christian Schabesberger
46c2db310a add suport for dash 2016-01-30 00:22:16 +01:00
Teja Vojjala
607408b9b9 Update PanicResponderActivity.java 2016-01-29 20:21:44 +05:30
Teja Vojjala
b81e8cb81a Update SettingsActivity.java 2016-01-29 20:20:55 +05:30
Teja Vojjala
44ef04d90a Update PlayVideoActivity.java 2016-01-29 20:19:09 +05:30
Teja Vojjala
3e9d84b109 Merge pull request #2 from iit2014086/iit2014086-patch-2
Update VideoItemDetailActivity.java
2016-01-29 20:17:24 +05:30
Teja Vojjala
aa2d8d4833 Update VideoItemDetailActivity.java 2016-01-29 20:16:57 +05:30
Teja Vojjala
55f63b948b Merge pull request #1 from iit2014086/iit2014086-patch-1
Update VideoItemListActivity.java
2016-01-29 20:09:13 +05:30
Teja Vojjala
cece543d6b Update VideoItemListActivity.java 2016-01-29 20:07:15 +05:30
Christian Schabesberger
9204a89319 fiexed some licence headers 2016-01-28 23:27:16 +01:00
Christian Schabesberger
f8ed96bb25 renamed services into crawer 2016-01-28 21:34:35 +01:00
Christian Schabesberger
54d318bf04 detatch android related downloader from crawler 2016-01-28 21:21:19 +01:00
Christian Schabesberger
f152d66cd8 fixed some bugs 2016-01-28 12:10:50 +01:00
Sérgio Marques
576786c751 Translated using Weblate (Portuguese)
Currently translated at 100.0% (61 of 61 strings)
2016-01-19 00:22:40 +01:00
halcyonest
a6b7cd3202 Translated using Weblate (Korean)
Currently translated at 100.0% (61 of 61 strings)
2016-01-16 12:44:51 +01:00
Yoo
ea9caaaaf0 Translated using Weblate (Hungarian)
Currently translated at 96.7% (59 of 61 strings)
2016-01-14 21:44:37 +01:00
Matej U
22d8f434be Translated using Weblate (Slovenian)
Currently translated at 100.0% (61 of 61 strings)
2016-01-14 15:45:12 +01:00
halcyonest
8360c10141 Translated using Weblate (Korean)
Currently translated at 100.0% (61 of 61 strings)
2016-01-12 10:14:37 +01:00
Matej U
750dc6f2cc Translated using Weblate (Slovenian)
Currently translated at 100.0% (61 of 61 strings)
2016-01-10 14:22:05 +01:00
riotism
47b556544d Translated using Weblate (Chinese (Taiwan))
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 23:06:53 +01:00
riotism
f6cee739a6 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 23:02:34 +01:00
riotism
d1afe0028c Translated using Weblate (Chinese (China))
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 22:59:42 +01:00
M2ck
a4cace1ef7 Translated using Weblate (French)
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 20:55:00 +01:00
red
707ac8be27 Translated using Weblate (French)
Currently translated at 81.9% (50 of 61 strings)
2016-01-09 20:37:38 +01:00
Weblate
2f2254966a Merge remote-tracking branch 'origin/master' 2016-01-09 15:04:05 +01:00
Christian Schabesberger
eb57675f55 Translated using Weblate (German)
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 15:04:05 +01:00
Christian Schabesberger
dde93c5ccf Translated using Weblate (English)
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 15:04:05 +01:00
Christian Schabesberger
886b5959f7 Merge pull request #149 from narutosanjiv/master
ProgressBar must stop incase of network failure
2016-01-09 15:04:02 +01:00
naofum
dc44546ac7 Translated using Weblate (Japanese)
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 13:38:56 +01:00
Sanjiv Jha
42c6698732 ProgressBar must stop incase of network failure in videolist & detail page 2016-01-09 16:39:31 +05:30
Mladen Pejaković
bd014a107f Translated using Weblate (Serbian)
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 02:36:00 +01:00
Christian Schabesberger
3a7298bb99 Translated using Weblate (German)
Currently translated at 100.0% (61 of 61 strings)
2016-01-09 00:09:55 +01:00
Christian Schabesberger
aa06e3490d Merge branch 'master' of github.com:theScrabi/NewPipe 2016-01-08 22:22:55 +01:00
Christian Schabesberger
ece889b36b moved on to 0.7.3 2016-01-08 22:22:39 +01:00
Christian Schabesberger
9848f19ce6 Merge github.com:k3b/NewPipe into k3bMaster 2016-01-08 22:04:42 +01:00
Weblate
e59b087057 Merge remote-tracking branch 'origin/master' 2016-01-08 22:00:27 +01:00
Greg
5b451d1ac7 Translated using Weblate (Spanish)
Currently translated at 64.2% (36 of 56 strings)
2016-01-08 22:00:27 +01:00
Greg
f050c05b3c Translated using Weblate (Russian)
Currently translated at 98.2% (55 of 56 strings)
2016-01-08 22:00:27 +01:00
Sérgio Marques
06b8edefbf Translated using Weblate (Portuguese)
Currently translated at 100.0% (56 of 56 strings)
2016-01-08 22:00:26 +01:00
Greg
f3a2a28398 Translated using Weblate (German)
Currently translated at 91.0% (51 of 56 strings)
2016-01-08 22:00:26 +01:00
Greg
9b0a1fc2ec Translated using Weblate (English)
Currently translated at 100.0% (56 of 56 strings)
2016-01-08 22:00:26 +01:00
riotism
c87ab234eb Translated using Weblate (Chinese (Taiwan))
Currently translated at 100.0% (56 of 56 strings)
2016-01-08 22:00:25 +01:00
riotism
68468756a8 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (56 of 56 strings)
2016-01-08 22:00:24 +01:00
riotism
9016df0195 Translated using Weblate (Chinese (China))
Currently translated at 100.0% (56 of 56 strings)
2016-01-08 22:00:24 +01:00
Christian Schabesberger
7acece9705 Merge pull request #145 from 42SK/master
Fixed #143
2016-01-08 22:00:21 +01:00
red
d1896c23c0 Translated using Weblate (French)
Currently translated at 100.0% (56 of 56 strings)
2016-01-08 14:04:29 +01:00
riotism
dae19f03e0 Translated using Weblate (Chinese (China))
Currently translated at 100.0% (56 of 56 strings)
2016-01-07 23:07:42 +01:00
riotism
9dafccf0f7 Translated using Weblate (Chinese (China))
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-01-07 22:46:44 +01:00
riotism
5f270c41ae Translated using Weblate (Chinese (Taiwan))
Currently translated at 100.0% (56 of 56 strings)
2016-01-07 22:46:12 +01:00
riotism
f9f80e9003 Translated using Weblate (Chinese (Taiwan))
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-01-07 22:08:02 +01:00
riotism
c5063d4269 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (56 of 56 strings)
2016-01-07 22:06:57 +01:00
riotism
ccbe18ec1c Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-01-07 21:21:58 +01:00
k3b
321a8a8b25 Translated using Weblate (German)
Currently translated at 91.0% (51 of 56 strings)
2016-01-07 20:20:57 +01:00
k3b
77a9560376 #143 #44 #42 #22: Fixed some download problems with invalid directories or filenames. Added user Feedback. Different settings for audio and video download dir. 2016-01-07 14:32:16 +01:00
k3b
058a039a82 #143 #44 #42 #22: Fixed some download problems with invalid directories or filenames. Added user Feedback. Different settings for audio and video download dir. 2016-01-07 14:22:55 +01:00
naofum
7c744703e4 Translated using Weblate (Japanese)
Currently translated at 100.0% (56 of 56 strings)
2016-01-07 14:15:56 +01:00
42SK
a934cbb085 Fixed #143 2016-01-07 13:28:17 +01:00
Sérgio Marques
cf4158c0d0 Translated using Weblate (Portuguese)
Currently translated at 100.0% (56 of 56 strings)
2016-01-07 00:48:13 +01:00
M2ck
dc56eab9b6 Translated using Weblate (French)
Currently translated at 100.0% (56 of 56 strings)
2016-01-06 16:47:54 +01:00
Weblate
37d1f59132 Merge remote-tracking branch 'origin/master' 2016-01-06 15:13:03 +01:00
Christian Schabesberger
ab0ce55411 merge unitTesting 2016-01-06 15:13:55 +01:00
Matej U
c1d66596d1 Translated using Weblate (Slovenian)
Currently translated at 100.0% (56 of 56 strings)
2016-01-06 15:13:03 +01:00
Mladen Pejaković
cbfccdf0d3 Translated using Weblate (Serbian)
Currently translated at 100.0% (56 of 56 strings)
2016-01-06 15:13:02 +01:00
Christian Schabesberger
9362037177 remove theme option since it's not yet working 2016-01-06 15:00:33 +01:00
Matej U
e25c93bae2 Translated using Weblate (Slovenian)
Currently translated at 100.0% (56 of 56 strings)
2016-01-06 09:28:10 +01:00
Mladen Pejaković
367c434010 Translated using Weblate (Serbian)
Currently translated at 100.0% (56 of 56 strings)
2016-01-05 23:54:30 +01:00
chschtsch
02d8463e15 rename resource 2016-01-06 00:02:10 +03:00
Christian Schabesberger
9d5a1d5c43 removed unnececeary comment 2016-01-05 21:54:40 +01:00
Christian Schabesberger
c8d94f541f resolved merge conflict 2016-01-05 21:50:25 +01:00
Christian Schabesberger
27d06eaa6b removed hardcoded string, and add licece to some files. 2016-01-05 21:41:55 +01:00
Christian Schabesberger
7f32857e00 buxfix 2016-01-05 21:23:16 +01:00
chschtsch
2f060f0f52 merging with eb0df2b 2016-01-05 23:16:50 +03:00
chschtsch
f89d405226 merging with eb0df2b 2016-01-05 23:13:52 +03:00
chschtsch
fd4459e570 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-01-05 23:12:22 +03:00
Christian Schabesberger
eb0df2b101 set download path on startup 2016-01-05 21:11:15 +01:00
chschtsch
6c178cfb7e add back missing like button margin 2016-01-05 23:09:15 +03:00
chschtsch
8ced68430d update resources names to match naming convention & cleanup & start working on themes 2016-01-05 22:56:40 +03:00
Aitor Beriain
0aade598ff Translated using Weblate (Basque)
Currently translated at 100.0% (53 of 53 strings)
2016-01-04 20:02:01 +01:00
Aitor Beriain
95949fd1ab Translated using Weblate (Basque)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-01-04 19:35:11 +01:00
naofum
1a56382112 Translated using Weblate (Japanese)
Currently translated at 100.0% (53 of 53 strings)
2016-01-04 11:51:32 +01:00
Adam Howard
d610e4b19b minor code tweaks to BackgroundPlayer 2016-01-04 01:51:24 +00:00
Adam Howard
fc44d9e36e Merge branch 'master' of github.com:theScrabi/NewPipe 2016-01-04 01:15:33 +00:00
Adam Howard
c07686576a possible fix for expandedView bug in BackgroundPlayer 2016-01-04 01:15:13 +00:00
Weblate
c2400aea4d Merge remote-tracking branch 'origin/master' 2016-01-04 00:11:51 +01:00
Benedikt Geißler
fb4bf0dde4 Translated using Weblate (German)
Currently translated at 98.1% (52 of 53 strings)
2016-01-04 00:11:51 +01:00
Christian Schabesberger
e4f638d1ce Merge branch 'master' of github.com:theScrabi/NewPipe 2016-01-04 00:11:00 +01:00
Christian Schabesberger
5c492c01a1 adjusted orbot support and moved on to 0.7.2 2016-01-04 00:10:51 +01:00
M2ck
f451e11f82 Translated using Weblate (French)
Currently translated at 100.0% (53 of 53 strings)
2016-01-03 20:55:44 +01:00
Adam Howard
95b73f35f7 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-01-03 19:44:13 +00:00
Adam Howard
58147e9e12 removed commented code in BackgroundPlayer 2016-01-03 19:44:04 +00:00
Christian Schabesberger
a8830e2ede preference screen previews set values 2016-01-03 19:55:04 +01:00
Mladen Pejaković
9804bb95cc Translated using Weblate (Serbian)
Currently translated at 100.0% (53 of 53 strings)
2016-01-03 19:12:16 +01:00
Christian Schabesberger
0da1aef763 Merge pull request #136 from eighthave/tor-support-for-all-except-streaming
Tor support for all except streaming
2016-01-03 17:40:02 +01:00
M2ck
0a334804a3 Translated using Weblate (French)
Currently translated at 100.0% (51 of 51 strings)
2016-01-03 16:56:51 +01:00
naofum
94d2f03e9b Translated using Weblate (Japanese)
Currently translated at 100.0% (51 of 51 strings)
2016-01-03 10:31:40 +01:00
Hans-Christoph Steiner
9127f7f0c2 make progress notification for Tor downloader (closes #39) 2016-01-03 00:04:55 +01:00
Hans-Christoph Steiner
0bb0226bc2 download files via Tor when Tor is enabled
DownloadManager does not let you set its proxy or change how it connects to
the internet.  So we have to make a custom one, unfortunately.  This is a
very basic downloader with none of the special sauce that makes the
built-in DownloadManager handy.
2016-01-02 22:47:21 +01:00
Hans-Christoph Steiner
b3a1a5dcc2 Android provides global vars for the actual download directories 2016-01-02 21:53:48 +01:00
Hans-Christoph Steiner
984dd1cc25 checking on "Use Tor" when Orbot is not installed starts install
If the user turns on "Use Tor" and they are missing Orbot, bring them to
the screen to install Tor.
2016-01-02 21:21:34 +01:00
Hans-Christoph Steiner
5663e543a4 whenever an Activity resumes and tor is enabled, request it start
This makes sure that Orbot is running when the user expects it to be. If
NewPipe is configured to use Tor, then going to a NewPipe screen should
ensure Tor is running.
2016-01-02 21:21:34 +01:00
Hans-Christoph Steiner
d3879a0398 setup Tor at app start, and config immediately when pref is changed
This adds an Application subclass to get the onCreate() method, which is
called once at the first start up of the app, before any Activity starts.
Tor is configured there to ensure it is setup before anything happens.

This also moves the "Use Tor" pref listener to a more appropriate place.
2016-01-02 21:21:34 +01:00
Hans-Christoph Steiner
6bd2468d44 if Orbot is installed, then default to using Tor
If the user has not changed the "Use Tor" preference, then the default
should be to use Tor if Orbot is installed. The user can still override it
by going an unchecking "Use Tor".
2016-01-02 21:21:34 +01:00
Hans-Christoph Steiner
e63d43151b add a title plus summary to "Use Tor" preference 2016-01-02 21:21:33 +01:00
Hans-Christoph Steiner
0265da4ae6 use HttpsURLConnections since youtube.com always uses HTTPS
This helps enforce that the connection is encrypted. If for whatever reason
an unencrypted connection is created, an Exception will be thrown.
2016-01-02 21:21:28 +01:00
GDR!
ef255d12ae Test tor code 2016-01-02 20:22:05 +01:00
Matej U
eeb612f9a2 Translated using Weblate (Slovenian)
Currently translated at 100.0% (51 of 51 strings)
2016-01-02 19:48:51 +01:00
Mladen Pejaković
dfcb4edb81 Translated using Weblate (Serbian)
Currently translated at 100.0% (51 of 51 strings)
2016-01-02 18:04:01 +01:00
Christian Schabesberger
d3500e9036 removed onion routing from readme 2016-01-02 17:41:33 +01:00
Christian Schabesberger
adcb8c6469 add c3s thumbnail and moved on to 0.7.1 2016-01-02 17:40:58 +01:00
Christian Schabesberger
592eee7d3d Merge pull request #134 from eighthave/intent-filters-and-other-fixes
Intent filters and other fixes
2016-01-02 16:18:56 +01:00
Christian Schabesberger
7dadb2b26c fixed close notification problem 2016-01-02 16:08:18 +01:00
Hans-Christoph Steiner
7cbb135f28 include Tibetan as a language option
The Tibetan alphabet was only recently included on Android, so the language
name needs to also have the English there.  Otherwise it'll appear blank
on devices without Tibetan.
2016-01-02 12:15:56 +01:00
Hans-Christoph Steiner
966ac0673c gradle.properties is only commented out defaults, so remove from git
For anyone who tweaks this file for local settings, it becomes painful
to have it committed in git because those changes which are only
relevant to the local setup will show up in git as changed.
2016-01-02 01:35:29 +01:00
Hans-Christoph Steiner
d715eae0d1 route video downloads to "Movies" and audio to "Music"
use the standard Android folders when downloading files.
2016-01-02 01:34:18 +01:00
Hans-Christoph Steiner
ccdd13d136 youtube URLs can also come from media searches and NFC sends 2016-01-02 00:15:27 +01:00
Hans-Christoph Steiner
efe5de4c75 support youtube's custom URL schemes (vnd.youtube: and vnd.youtube.launch:) 2016-01-02 00:15:27 +01:00
Hans-Christoph Steiner
2a93e9bd2e precisely target these URLs https://www.youtube.com/watch?v=mS1gstS6YS8
These URLs have a Path that always starts with "/watch" so no need for a
pattern.  Also, everything after the "?" is considered the "Query String",
not the Path.  Anything after a "#" is the "Feature String".  The path
matching in IntentFilters only see the Path, and nothing from the "Query
String" or "Feature String".

these are the available kinds of URLs:
https://developer.apple.com/library/ios/featuredarticles/iPhoneURLScheme_Reference/YouTubeLinks/YouTubeLinks.html
2016-01-02 00:15:27 +01:00
Hans-Christoph Steiner
28dd53ae50 support another youtube URL format: https://www.youtube.com/v/mS1gstS6YS8
https://developer.apple.com/library/ios/featuredarticles/iPhoneURLScheme_Reference/YouTubeLinks/YouTubeLinks.html
2016-01-02 00:15:27 +01:00
Hans-Christoph Steiner
3c1e64d8dc simplify youtube URL IntentFilters
Each <data> elements applies to the whole IntentFilter, so there is no need
to declare the host, scheme, etc. multiple times within a single
IntentFilter.

Also, pathPrefix="/" will match all paths, so it is unnecessary.
2016-01-02 00:15:27 +01:00
Hans-Christoph Steiner
4fe3cb2bca use symlinks to provide alternate folders for Hebrew and Indonesian
These two languages must be included twice (iw/he and id/in)

For a full discussion of why, see:
https://gitlab.com/fdroid/fdroidclient/issues/139
https://stackoverflow.com/questions/5074769/cyanogenmod-translate-a-project/8470980#8470980
https://stackoverflow.com/questions/8393771/android-not-using-finding-my-hebrew-localization

I included a blank placeholder file for Indonesian, it can be simply
replaced by the real one, whenever that comes along.
2016-01-02 00:15:27 +01:00
Hans-Christoph Steiner
b31490c4e3 make all strings translatable 2016-01-02 00:14:35 +01:00
Hans-Christoph Steiner
5533f6ba86 use the standard "Movies" folder for downloads
On all of the devices that I've checked, there is a folder called "Movies"
on the SD Card by default.  NewPipe should use that standard location
since it is always downloading movies :).  People can always change that
via the preferences.

Also, this makes the defaults the same when creating the dir and when
setting the destination URL.
2016-01-02 00:14:11 +01:00
Weblate
8aa5f87a1c Merge remote-tracking branch 'origin/master' 2016-01-01 15:47:36 +01:00
Greg
6deb674377 Translated using Weblate (Hebrew)
Currently translated at 16.6% (8 of 48 strings)
2016-01-01 15:47:36 +01:00
Christian Schabesberger
12d1d998a3 Merge pull request #133 from eighthave/panic-button-support
allow "panic button" apps to wipe activity
2016-01-01 15:47:32 +01:00
Christian Schabesberger
d90162d06f ugly workaround for GEMA test failing 2016-01-01 15:43:06 +01:00
Christian Schabesberger
97c924341c inital YoutubeExtractor test 2016-01-01 15:26:03 +01:00
Hans-Christoph Steiner
e91fc225e1 after receiving panic trigger, quit remove from history
This makes the app fully exit, and removes it from the Recent Apps listing
with the goal of hiding whatever the user was currently watching, and/or
searching for.

PanicKit provides a common framework for creating "panic button" apps that
can trigger actions in "panic responder" apps.  In this case, the response
is to lock the app, if it has been configured to do so

https://dev.guardianproject.info/projects/panic/wiki
2015-12-31 21:53:14 +01:00
M2ck
43149fd832 Translated using Weblate (French)
Currently translated at 100.0% (48 of 48 strings)
2015-12-31 10:27:52 +01:00
Greg
78df579703 Translated using Weblate (Hebrew)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2015-12-29 21:45:44 +01:00
Greg
f61b915894 Translated using Weblate (Russian)
Currently translated at 100.0% (48 of 48 strings)
2015-12-29 21:45:08 +01:00
Christian Schabesberger
cd3f405bff slightly improved YoutubeSearchEngineTest 2015-12-29 21:41:10 +01:00
Christian Schabesberger
7cfdca7a81 remove failing test again 2015-12-29 21:28:27 +01:00
Christian Schabesberger
216063dba8 test if CI failes on failing test 2015-12-29 21:16:18 +01:00
Christian Schabesberger
b647bacd72 add testcase for YoutubeSearchEngine 2015-12-29 21:05:02 +01:00
Christian Schabesberger
4f77937e3e Merge pull request #131 from chschtsch/refactor
refactor localization
2015-12-29 19:14:55 +01:00
chschtsch
48e299b2ac code cleanup 2015-12-29 17:53:24 +03:00
chschtsch
40f00af196 refactor localization 2015-12-29 17:35:51 +03:00
Christian Schabesberger
bd6cc22e63 small hacky error handling refactoring, and add link to c3s 2015-12-28 00:32:38 +01:00
naofum
8760792426 Translated using Weblate (Japanese)
Currently translated at 100.0% (48 of 48 strings)
2015-12-26 12:33:33 +01:00
Christian Schabesberger
870b0bf7aa Merge pull request #127 from 912d/download-directory
Added visible notification after succesful download
2015-12-25 21:27:45 +01:00
Jacek Musiał
afd0bd4318 Added visible notification after succesful downloads
After succesful download, notification will stay in notification bar. I
used "VISIBILITY_VISIBLE_NOTIFY_COMPLETED"  key.
2015-12-25 19:28:09 +01:00
Jacek Musiał
f829ac1d34 Merge remote-tracking branch 'refs/remotes/theScrabi/master' into download-directory 2015-12-25 19:25:13 +01:00
Christian Schabesberger
86a0177855 Merge pull request #126 from Adam77Root/master
Fix view count parsing for large numbers
2015-12-25 16:02:10 +01:00
Adam77Root
718d4fd0bd Fix view count parsing for large numbers 2015-12-25 15:03:11 +01:00
Mladen Pejaković
365137c32b Translated using Weblate (Serbian)
Currently translated at 100.0% (48 of 48 strings)
2015-12-25 00:22:26 +01:00
Christian Schabesberger
e83ca0dfda some improvements for background player 2015-12-25 00:09:35 +01:00
Christian Schabesberger
6a9f6ef651 Merge pull request #123 from 912d/download-directory
Added check if directory exist and try create it
2015-12-23 23:20:42 +01:00
Jacek Musiał
99122ccc03 added check if directory exist and try create it
Also I added new variable `'final File dir`' with value of key
"download_path_preference" or externalStorageDirectory.
Firstly I check if dir exits, then eventually try to create it and next
- download file.
2015-12-23 17:52:01 +01:00
Christian Schabesberger
97923697e1 fixed small writing erros in the README 2015-12-23 14:31:57 +01:00
Greg
68888b15e0 Merge pull request #121 from chschtsch/master
fix sw600dp-land layout problem (#120)
2015-12-23 13:53:42 +03:00
Greg
f48b26067b Update README.md 2015-12-23 11:31:14 +03:00
Greg
4bf2d5837d Update and rename contributing.md to CONTRIBUTING.md 2015-12-23 11:31:11 +03:00
Weblate
4eb2d09c75 Merge remote-tracking branch 'origin/master' 2015-12-22 19:39:49 +01:00
Jack Musial
a146c1c4b6 Translated using Weblate (Polish)
Currently translated at 100.0% (47 of 47 strings)
2015-12-22 19:39:48 +01:00
Sorry Keep Me Anonymous
2f1ea9aa5d Translated using Weblate (Dutch)
Currently translated at 97.8% (46 of 47 strings)
2015-12-22 19:39:48 +01:00
Greg
a90da62deb Merge pull request #122 from rrooij/contributing_guidelines
Add contribution guidelines
2015-12-22 21:39:45 +03:00
Jack Musial
c1c3fbdf26 Translated using Weblate (Polish)
Currently translated at 100.0% (47 of 47 strings)
2015-12-22 18:50:33 +01:00
rrooij
e07a824d82 Add contribution guidelines
Contribution guidelines are a helpful way to inform new potential
contributors about the way in which they can help with the project.

It's also a helpful way to enforce some consistent coding style and
quality among the contributions.
2015-12-22 16:14:56 +01:00
chschtsch
2aff660a5b fix sw600dp-land layout problem (#120) 2015-12-22 15:11:28 +03:00
Weblate
abfcbe6f0e Merge remote-tracking branch 'origin/master' 2015-12-21 22:04:25 +01:00
Matej U
28bf72ed75 Translated using Weblate (Slovenian)
Currently translated at 100.0% (47 of 47 strings)
2015-12-21 22:04:25 +01:00
naofum
21b054d4ca Translated using Weblate (Japanese)
Currently translated at 100.0% (47 of 47 strings)
2015-12-21 22:04:25 +01:00
Greg
34f115b322 remove broken screenshot links 2015-12-22 00:04:22 +03:00
naofum
8b67354076 Translated using Weblate (Japanese)
Currently translated at 100.0% (47 of 47 strings)
2015-12-21 16:20:39 +01:00
M2ck
b62e0a8b40 Translated using Weblate (French)
Currently translated at 100.0% (47 of 47 strings)
2015-12-21 14:20:07 +01:00
Matej U
f46d5376fe Translated using Weblate (Slovenian)
Currently translated at 100.0% (47 of 47 strings)
2015-12-21 13:39:59 +01:00
Matej U
dc3640578f Translated using Weblate (Slovenian)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2015-12-21 13:07:59 +01:00
Sérgio Marques
0a43494de5 Translated using Weblate (Portuguese)
Currently translated at 100.0% (47 of 47 strings)
2015-12-20 22:46:22 +01:00
Sérgio Marques
2544e45d2d Translated using Weblate (Portuguese)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2015-12-20 22:37:30 +01:00
Mladen Pejaković
4a53e9e018 Translated using Weblate (Serbian)
Currently translated at 100.0% (47 of 47 strings)
2015-12-20 22:21:12 +01:00
131 changed files with 6619 additions and 3048 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@
/app/app.iml
/.idea
/*.iml
gradle.properties

33
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,33 @@
#Contribution
This document contains guidelines on making contributions to NewPipe.
## Programming
* Follow the [Google Style Guidelines](https://google.github.io/styleguide/javaguide.html)
* Make a new feature on a separate branch, not on the master branch
* Make a [pull request](https://github.com/theScrabi/NewPipe/pulls) if you're done with your changes
* When submitting changes, you agree that your code will be GPLv3 licensed
## Commit messages
* The subject line of your commit message shouldn't be longer than 72 characters
* Try to keep each line of your commit message 72 characters to ensure proper
compatibility with all git tools
* [This guide](http://chris.beams.io/posts/git-commit/) goes more in depth on what makes a good commit message
## Translation
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
## Issue reporting
* Search the [existing issues](https://github.com/theScrabi/NewPipe/issues) first to make sure your issue hasn't been reported before
* Check if this issue is already fixed in the repository
* When making bug reports, be sure to tell which version of NewPipe you are using and the steps to reproduce the problem
* Please include a log if you can
## Communication
* For the time being, [Slack](http://invite.chschtsch.ml/) is being used for project communication
* Feel free to post suggestions, changes, ideas etc!

View File

@@ -18,10 +18,6 @@ Project status:
[<img src="screenshots/screenshot_3.png" width=150>](screenshots/screenshot_3.png)
[<img src="screenshots/screenshot_4.png" width=150>](screenshots/screenshot_4.png)
[<img src="screenshots/screenshot_5.png" width=150>](screenshots/screenshot_5.png)
[<img src="screenshots/screenshot_6.png" width=250>](screenshots/screenshot_6.png)
[![Screenshot 1](assets/screenshot_1.png)](assets/screenshot_1.png)
[![Screenshot 2](assets/screenshot_2.png)](assets/screenshot_2.png)
## Description
@@ -39,6 +35,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
* Open a video in Kodi
* Show Next/Related videos
* Search YouTube in a specific language
* Orbot/Tor support (no streaming yet, experimental)
### Coming Features
@@ -60,7 +57,7 @@ Although NewPipe only supports YouTube at the moment, it's designed to support m
Whether you have ideas, translation, design changes, code cleaning, or real heavy code changes, help is always welcome.
The more is done the better it gets!
Join our [Slack group](http://invite.chschtsch.ml/) if you like to get involved.
If you'd like to get involved, check our [contribution notes](CONTRIBUTING.md).
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
buildToolsVersion '23.0.2'
defaultConfig {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 23
versionCode 9
versionName "0.7.0"
versionCode 14
versionName "0.7.5"
}
buildTypes {
release {
@@ -17,13 +17,17 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
dependencies {
@@ -31,8 +35,11 @@ dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:support-v4:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.android.support:cardview-v7:23.1.1'
compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'org.jsoup:jsoup:1.8.3'
compile 'org.mozilla:rhino:1.7.7'
compile 'info.guardianproject.netcipher:netcipher:1.2'
compile 'de.hdodenhof:circleimageview:2.0.0'
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'com.github.nirhart:parallaxscroll:1.0'
}

View File

@@ -0,0 +1,93 @@
package org.schabi.newpipe.services.youtube;
import android.test.AndroidTestCase;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import org.schabi.newpipe.extractor.SearchEngine;
import org.schabi.newpipe.extractor.services.youtube.YoutubeSearchEngine;
import org.schabi.newpipe.Downloader;
import java.util.ArrayList;
/**
* Created by the-scrabi on 29.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeSearchEngineTest.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 YoutubeSearchEngineTest extends AndroidTestCase {
private SearchEngine.Result result;
private ArrayList<String> suggestionReply;
@Override
public void setUp() throws Exception{
super.setUp();
SearchEngine engine = new YoutubeSearchEngine();
result = engine.search("https://www.youtube.com/results?search_query=bla",
0, "de", new Downloader());
suggestionReply = engine.suggestionList("hello", new Downloader());
}
public void testIfNoErrorOccur() {
assertEquals(result.errorMessage, "");
}
public void testIfListIsNotEmpty() {
assertEquals(result.resultList.size() > 0, true);
}
public void testItemsHaveTitle() {
for(VideoPreviewInfo i : result.resultList) {
assertEquals(i.title.isEmpty(), false);
}
}
public void testItemsHaveUploader() {
for(VideoPreviewInfo i : result.resultList) {
assertEquals(i.uploader.isEmpty(), false);
}
}
public void testItemsHaveRightDuration() {
for(VideoPreviewInfo i : result.resultList) {
assertTrue(i.duration, i.duration.contains(":"));
}
}
public void testItemsHaveRightThumbnail() {
for (VideoPreviewInfo i : result.resultList) {
assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://"));
}
}
public void testItemsHaveRightVideoUrl() {
for (VideoPreviewInfo i : result.resultList) {
assertTrue(i.webpage_url, i.webpage_url.contains("https://"));
}
}
public void testIfSuggestionsAreReplied() {
assertEquals(suggestionReply.size() > 0, true);
}
public void testIfSuggestionsAreValid() {
for(String s : suggestionReply) {
assertTrue(s, !s.isEmpty());
}
}
}

View File

@@ -0,0 +1,109 @@
package org.schabi.newpipe.services.youtube;
import android.test.AndroidTestCase;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.extractor.ExctractionException;
import org.schabi.newpipe.extractor.ParsingException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.VideoInfo;
import java.io.IOException;
/**
* Created by the-scrabi on 30.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeVideoExtractorDefault.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 YoutubeStreamExtractorDefaultTest extends AndroidTestCase {
private YoutubeStreamExtractor extractor;
public void setUp() throws IOException, ExctractionException {
/* some anonymus video test
extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=FmG385_uUys",
new Downloader()); */
/* some vevo video (suggested to test against) */
extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=YQHsXMglC9A",
new Downloader());
}
public void testGetInvalidTimeStamp() throws ParsingException {
assertTrue(Integer.toString(extractor.getTimeStamp()),
extractor.getTimeStamp() <= 0);
}
public void testGetValidTimeStamp() throws ExctractionException, IOException {
YoutubeStreamExtractor extractor =
new YoutubeStreamExtractor("https://youtu.be/FmG385_uUys?t=174", new Downloader());
assertTrue(Integer.toString(extractor.getTimeStamp()),
extractor.getTimeStamp() == 174);
}
public void testGetTitle() throws ParsingException {
assertTrue(!extractor.getTitle().isEmpty());
}
public void testGetDescription() throws ParsingException {
assertTrue(extractor.getDescription() != null);
}
public void testGetUploader() throws ParsingException {
assertTrue(!extractor.getUploader().isEmpty());
}
public void testGetLength() throws ParsingException {
assertTrue(extractor.getLength() > 0);
}
public void testGetViews() throws ParsingException {
assertTrue(extractor.getLength() > 0);
}
public void testGetUploadDate() throws ParsingException {
assertTrue(extractor.getUploadDate().length() > 0);
}
public void testGetThumbnailUrl() throws ParsingException {
assertTrue(extractor.getThumbnailUrl(),
extractor.getThumbnailUrl().contains("https://"));
}
public void testGetUploaderThumbnailUrl() throws ParsingException {
assertTrue(extractor.getUploaderThumbnailUrl(),
extractor.getUploaderThumbnailUrl().contains("https://"));
}
public void testGetAudioStreams() throws ParsingException {
assertTrue(!extractor.getAudioStreams().isEmpty());
}
public void testGetVideoStreams() throws ParsingException {
for(VideoInfo.VideoStream s : extractor.getVideoStreams()) {
assertTrue(s.url,
s.url.contains("https://"));
assertTrue(s.resolution.length() > 0);
assertTrue(Integer.toString(s.format),
0 <= s.format && s.format <= 4);
}
}
public void testGetDashMpd() throws ParsingException {
assertTrue(extractor.getDashMpdUrl(),
extractor.getDashMpdUrl() != null || !extractor.getDashMpdUrl().isEmpty());
}
}

View File

@@ -0,0 +1,49 @@
package org.schabi.newpipe.services.youtube;
import android.test.AndroidTestCase;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.extractor.ExctractionException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import java.io.IOException;
/**
* Created by the-scrabi on 30.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeVideoExtractorGema.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/>.
*/
// This class only works in Germany.
public class YoutubeStreamExtractorGemaTest extends AndroidTestCase {
// Deaktivate this Test Case bevore uploading it githup, otherwise CI will fail.
private static final boolean testActive = false;
public void testGemaError() throws IOException, ExctractionException {
if(testActive) {
try {
new YoutubeStreamExtractor("https://www.youtube.com/watch?v=3O1_3zBUKM8",
new Downloader());
assertTrue("Gema exception not thrown", false);
} catch(YoutubeStreamExtractor.GemaException ge) {
assertTrue(true);
}
}
}
}

View File

@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:logo="@mipmap/ic_launcher"
@@ -15,7 +16,8 @@
tools:ignore="AllowBackup">
<activity
android:name=".VideoItemListActivity"
android:label="@string/app_name">
android:label="@string/app_name"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -25,47 +27,53 @@
<activity
android:name=".VideoItemDetailActivity"
android:label="@string/title_videoitem_detail"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:configChanges="orientation|screenSize"
android:screenOrientation="portrait">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".VideoItemListActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="youtube.com"
android:scheme="http"
android:pathPattern="/?*#*/*watch"/>
<data
android:host="youtube.com"
android:scheme="https"
android:pathPattern="/?*#*/*watch"/>
<data
android:host="www.youtube.com"
android:scheme="http"
android:pathPattern="/?*#*/*watch"/>
<data
android:host="www.youtube.com"
android:scheme="https"
android:pathPattern="/?*#*/*watch"/>
<data
android:host="m.youtube.com"
android:scheme="http"
android:pathPattern="/?*#*/*watch"/>
<data
android:host="m.youtube.com"
android:scheme="https"
android:pathPattern="/?*#*/*watch"/>
<data
android:host="youtu.be"
android:scheme="https"
android:pathPrefix="/"/>
<data
android:host="youtu.be"
android:scheme="http"
android:pathPrefix="/"/>
<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>
</activity>
<activity android:name=".PlayVideoActivity"
@@ -74,15 +82,26 @@
android:parentActivityName=".VideoItemDetailActivity"
tools:ignore="UnusedAttribute">
</activity>
<!--TODO: make label a translatable string -->
<service
android:name=".BackgroundPlayer"
android:label="NewPipe Background Player"
android:exported="false" >
</service>
android:label="@string/background_player_name"
android:exported="false" />
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings" >
android:label="@string/settings_activity_title" >
</activity>
<activity
android:name=".PanicResponderActivity"
android:launchMode="singleInstance"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ExitActivity"
android:theme="@android:style/Theme.NoDisplay" />
</application>
</manifest>

View File

@@ -1,13 +1,9 @@
package org.schabi.newpipe;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
@@ -15,6 +11,11 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.VideoInfo;
import java.util.List;
/**
* Created by Christian Schabesberger on 18.08.15.
*
@@ -38,25 +39,26 @@ import android.widget.ArrayAdapter;
class ActionBarHandler {
private static final String TAG = ActionBarHandler.class.toString();
private static final String KORE_PACKET = "org.xbmc.kore";
private String websiteUrl = "";
private AppCompatActivity activity;
private VideoInfo.VideoStream[] videoStreams = null;
private VideoInfo.AudioStream audioStream = null;
private int selectedStream = -1;
private String videoTitle = "";
private int selectedVideoStream = -1;
private SharedPreferences defaultPreferences = null;
private int startPosition;
@SuppressWarnings("deprecation")
private class FormatItemSelectListener implements ActionBar.OnNavigationListener {
@Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
selectFormatItem((int)itemId);
return true;
}
private Menu menu;
// Only callbacks are listed here, there are more actions which don't need a callback.
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
private OnActionListener onShareListener = null;
private OnActionListener onOpenInBrowserListener = null;
private OnActionListener onDownloadListener = null;
private OnActionListener onPlayWithKodiListener = null;
private OnActionListener onPlayAudioListener = null;
// Triggered when a stream related action is triggered.
public interface OnActionListener {
void onActionSelected(int selectedStreamId);
}
public ActionBarHandler(AppCompatActivity activity) {
@@ -68,109 +70,114 @@ class ActionBarHandler {
this.activity = activity;
try {
activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
} catch(NullPointerException e) {
} catch (NullPointerException e) {
e.printStackTrace();
}
}
@SuppressWarnings("deprecation")
public void setStreams(VideoInfo.VideoStream[] videoStreams, VideoInfo.AudioStream[] audioStreams) {
this.videoStreams = videoStreams;
selectedStream = 0;
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
String[] itemArray = new String[videoStreams.length];
String defaultResolution = defaultPreferences
.getString(activity.getString(R.string.defaultResolutionPreference),
activity.getString(R.string.defaultResolutionListItem));
int defaultResolutionPos = 0;
public void setupStreamList(final List<VideoInfo.VideoStream> videoStreams) {
if (activity != null) {
selectedVideoStream = 0;
for(int i = 0; i < videoStreams.length; i++) {
itemArray[i] = MediaFormat.getNameById(videoStreams[i].format) + " " + videoStreams[i].resolution;
if(defaultResolution.equals(videoStreams[i].resolution)) {
defaultResolutionPos = i;
// this array will be shown in the dropdown menu for selecting the stream/resolution.
String[] itemArray = new String[videoStreams.size()];
for (int i = 0; i < videoStreams.size(); i++) {
VideoInfo.VideoStream item = videoStreams.get(i);
itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution;
}
}
int defaultResolution = getDefaultResolution(videoStreams);
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(activity.getBaseContext(),
android.R.layout.simple_spinner_dropdown_item, itemArray);
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(activity.getBaseContext(),
android.R.layout.simple_spinner_dropdown_item, itemArray);
if(activity != null) {
ActionBar ab = activity.getSupportActionBar();
assert ab != null : "Could not get actionbar";
ab.setListNavigationCallbacks(itemAdapter
, new FormatItemSelectListener());
ab.setSelectedNavigationItem(defaultResolutionPos);
}
// set audioStream
audioStream = null;
String preferedFormat = defaultPreferences
.getString(activity.getString(R.string.defaultAudioFormatPreference), "webm");
if(preferedFormat.equals("webm")) {
for(VideoInfo.AudioStream s : audioStreams) {
if(s.format == MediaFormat.WEBMA.id) {
audioStream = s;
//todo: make this throwsable
assert ab != null : "Could not get actionbar";
ab.setListNavigationCallbacks(itemAdapter
, new ActionBar.OnNavigationListener() {
@Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
selectedVideoStream = (int) itemId;
return true;
}
}
} else if(preferedFormat.equals("m4a")){
for(VideoInfo.AudioStream s : audioStreams) {
if(s.format == MediaFormat.M4A.id &&
(audioStream == null || audioStream.bandwidth > s.bandwidth)) {
audioStream = s;
}
}
}
else {
Log.e(TAG, "FAILED to set audioStream value!");
});
ab.setSelectedNavigationItem(defaultResolution);
}
}
private void selectFormatItem(int i) {
selectedStream = i;
private int getDefaultResolution(final List<VideoInfo.VideoStream> videoStreams) {
String defaultResolution = defaultPreferences
.getString(activity.getString(R.string.default_resolution_key),
activity.getString(R.string.default_resolution_value));
for (int i = 0; i < videoStreams.size(); i++) {
VideoInfo.VideoStream item = videoStreams.get(i);
if (defaultResolution.equals(item.resolution)) {
return i;
}
}
// this is actually an error,
// but maybe there is really no stream fitting to the default value.
return 0;
}
public void setupMenu(Menu menu, MenuInflater inflater) {
this.menu = menu;
// CAUTION set item properties programmatically otherwise it would not be accepted by
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
inflater.inflate(R.menu.videoitem_detail, menu);
MenuItem castItem = menu.findItem(R.id.action_play_with_kodi);
castItem.setVisible(defaultPreferences
.getBoolean(activity.getString(R.string.showPlayWithKodiPreference), false));
showPlayWithKodiAction(defaultPreferences
.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false));
}
public boolean onItemSelected(MenuItem item) {
int id = item.getItemId();
switch(id) {
case R.id.menu_item_share:
if(!videoTitle.isEmpty()) {
switch (id) {
case R.id.menu_item_share: {
/*
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, websiteUrl);
intent.setType("text/plain");
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.shareDialogTitle)));
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
*/
if(onShareListener != null) {
onShareListener.onActionSelected(selectedVideoStream);
}
return true;
case R.id.menu_item_openInBrowser: {
openInBrowser();
}
return true;
case R.id.menu_item_openInBrowser: {
if(onOpenInBrowserListener != null) {
onOpenInBrowserListener.onActionSelected(selectedVideoStream);
}
}
return true;
case R.id.menu_item_download:
downloadVideo();
if(onDownloadListener != null) {
onDownloadListener.onActionSelected(selectedVideoStream);
}
return true;
case R.id.action_settings: {
Intent intent = new Intent(activity, SettingsActivity.class);
activity.startActivity(intent);
return true;
}
break;
case R.id.action_play_with_kodi:
playWithKodi();
if(onPlayWithKodiListener != null) {
onPlayWithKodiListener.onActionSelected(selectedVideoStream);
}
return true;
case R.id.menu_item_play_audio:
playAudio();
if(onPlayAudioListener != null) {
onPlayAudioListener.onActionSelected(selectedVideoStream);
}
return true;
default:
Log.e(TAG, "Menu Item not known");
@@ -178,175 +185,39 @@ class ActionBarHandler {
return false;
}
public void setVideoInfo(String websiteUrl, String videoTitle) {
this.websiteUrl = websiteUrl;
this.videoTitle = videoTitle;
public int getSelectedVideoStream() {
return selectedVideoStream;
}
public void playVideo() {
// ----------- THE MAGIC MOMENT ---------------
if(!videoTitle.isEmpty()) {
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.useExternalVideoPlayer), false)) {
// External Player
Intent intent = new Intent();
try {
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(videoStreams[selectedStream].url),
MediaFormat.getMimeById(videoStreams[selectedStream].format));
intent.putExtra(Intent.EXTRA_TITLE, videoTitle);
intent.putExtra("title", videoTitle);
activity.startActivity(intent); // HERE !!!
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.noPlayerFound)
.setPositiveButton(R.string.installStreamPlayer, 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.fdroidVLCurl)));
activity.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
} else {
// Internal Player
Intent intent = new Intent(activity, PlayVideoActivity.class);
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, videoTitle);
intent.putExtra(PlayVideoActivity.STREAM_URL, videoStreams[selectedStream].url);
intent.putExtra(PlayVideoActivity.VIDEO_URL, websiteUrl);
intent.putExtra(PlayVideoActivity.START_POSITION, startPosition);
activity.startActivity(intent); //also HERE !!!
}
}
// --------------------------------------------
public void setOnShareListener(OnActionListener listener) {
onShareListener = listener;
}
public void setStartPosition(int startPositionSeconds)
{
this.startPosition = startPositionSeconds;
public void setOnOpenInBrowserListener(OnActionListener listener) {
onOpenInBrowserListener = listener;
}
private void downloadVideo() {
if(!videoTitle.isEmpty()) {
String videoSuffix = "." + MediaFormat.getSuffixById(videoStreams[selectedStream].format);
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
Bundle args = new Bundle();
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
args.putString(DownloadDialog.TITLE, videoTitle);
args.putString(DownloadDialog.VIDEO_URL, videoStreams[selectedStream].url);
args.putString(DownloadDialog.AUDIO_URL, audioStream.url);
DownloadDialog downloadDialog = new DownloadDialog();
downloadDialog.setArguments(args);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
}
public void setOnDownloadListener(OnActionListener listener) {
onDownloadListener = listener;
}
private void openInBrowser() {
if(!videoTitle.isEmpty()) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(websiteUrl));
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.chooseBrowser)));
}
public void setOnPlayWithKodiListener(OnActionListener listener) {
onPlayWithKodiListener = listener;
}
private void playWithKodi() {
if(!videoTitle.isEmpty()) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(KORE_PACKET);
intent.setData(Uri.parse(websiteUrl.replace("https", "http")));
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.koreNotFound)
.setPositiveButton(R.string.installeKore, 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.fdroidKoreUrl)));
activity.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.create().show();
}
}
public void setOnPlayAudioListener(OnActionListener listener) {
onPlayAudioListener = listener;
}
public void playAudio() {
public void showAudioAction(boolean visible) {
menu.findItem(R.id.menu_item_play_audio).setVisible(visible);
}
boolean externalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.useExternalAudioPlayer), false);
Intent intent;
public void showDownloadAction(boolean visible) {
menu.findItem(R.id.menu_item_download).setVisible(visible);
}
if (!externalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18)//internal music player: explicit intent
{
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(Intent.EXTRA_TITLE, videoTitle);
intent.putExtra("title", videoTitle);
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, videoTitle);
intent.putExtra("title", videoTitle);
activity.startActivity(intent); // HERE !!!
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.noPlayerFound)
.setPositiveButton(R.string.installStreamPlayer, 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.fdroidVLCurl)));
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();
}
}
public void showPlayWithKodiAction(boolean visible) {
menu.findItem(R.id.action_play_with_kodi).setVisible(visible);
}
}

View File

@@ -0,0 +1,42 @@
package org.schabi.newpipe;
/**
* Created by Christian Schabesberger on 24.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* ActivityCommunicator.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import android.graphics.Bitmap;
/**
* Singleton:
* Used to send data between certain Activity/Services within the same process.
* This can be considered as hack inside the Android universe. **/
public class ActivityCommunicator {
private static ActivityCommunicator activityCommunicator = null;
public static ActivityCommunicator getCommunicator() {
if(activityCommunicator == null) {
activityCommunicator = new ActivityCommunicator();
}
return activityCommunicator;
}
// Thumbnail send from VideoItemDetailFragment to BackgroundPlayer
public volatile Bitmap backgroundPlayerThumbnail;
}

View File

@@ -0,0 +1,78 @@
package org.schabi.newpipe;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import info.guardianproject.netcipher.NetCipher;
import info.guardianproject.netcipher.proxy.OrbotHelper;
/**
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends Application {
private static boolean useTor;
@Override
public void onCreate() {
super.onCreate();
// Initialize image loader
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
ImageLoader.getInstance().init(config);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if(prefs.getBoolean(getString(R.string.use_tor_key), false)) {
OrbotHelper.requestStartTor(this);
configureTor(true);
} else {
configureTor(false);
}
// DO NOT REMOVE THIS FUNCTION!!!
// Otherwise downloadPathPreference has invalid value.
SettingsActivity.initSettings(this);
}
/**
* Set the proxy settings based on whether Tor should be enabled or not.
*/
static void configureTor(boolean enabled) {
useTor = enabled;
if (useTor) {
NetCipher.useTor();
} else {
NetCipher.setProxy(null);
}
}
static void checkStartTor(Context context) {
if (useTor) {
OrbotHelper.requestStartTor(context);
}
}
static boolean isUsingTor() {
return useTor;
}
}

View File

@@ -8,6 +8,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.wifi.WifiManager;
@@ -15,6 +17,7 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
import java.io.IOException;
@@ -43,8 +46,22 @@ import java.io.IOException;
public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ {
private static final String TAG = BackgroundPlayer.class.toString();
private static final String ACTION_STOP = TAG+".STOP";
private static final String ACTION_PLAYPAUSE = TAG+".PLAYPAUSE";
private static final String ACTION_STOP = TAG + ".STOP";
private static final String ACTION_PLAYPAUSE = TAG + ".PLAYPAUSE";
// Extra intent arguments
public static final String TITLE = "title";
public static final String WEB_URL = "web_url";
public static final String SERVICE_ID = "service_id";
public static final String CHANNEL_NAME = "channel_name";
private volatile String webUrl = "";
private volatile int serviceId = -1;
private volatile String channelName = "";
// Determines if the service is already running.
// Prevents launching the service twice.
public static volatile boolean isRunning = false;
public BackgroundPlayer() {
super();
@@ -58,16 +75,22 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "Playing in background", Toast.LENGTH_SHORT).show();//todo:translation string
Toast.makeText(this, R.string.background_player_playing_toast,
Toast.LENGTH_SHORT).show();
String source = intent.getDataString();
//Log.i(TAG, "backgroundPLayer source:"+source);
String videoTitle = intent.getStringExtra("title");
String videoTitle = intent.getStringExtra(TITLE);
webUrl = intent.getStringExtra(WEB_URL);
serviceId = intent.getIntExtra(SERVICE_ID, -1);
channelName = intent.getStringExtra(CHANNEL_NAME);
//do nearly everything in a separate thread
PlayerThread player = new PlayerThread(source, videoTitle, this);
player.start();
isRunning = true;
// If we get killed after returning here, don't restart
return START_NOT_STICKY;
}
@@ -81,6 +104,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
@Override
public void onDestroy() {
//Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
isRunning = false;
}
private class PlayerThread extends Thread {
@@ -90,8 +114,10 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
private int noteID = TAG.hashCode();
private BackgroundPlayer owner;
private NotificationManager noteMgr;
private NotificationCompat.Builder noteBuilder;
private WifiManager.WifiLock wifiLock;
private Bitmap videoThumbnail = null;
private NotificationCompat.Builder noteBuilder;
private Notification note;
public PlayerThread(String src, String title, BackgroundPlayer owner) {
this.source = src;
@@ -100,17 +126,16 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
@Override
public void run() {
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock
try {
mediaPlayer.setDataSource(source);
mediaPlayer.prepare(); //We are already in a separate worker thread,
//We are already in a separate worker thread,
//so calling the blocking prepare() method should be ok
mediaPlayer.prepare();
//alternatively:
//mediaPlayer.setOnPreparedListener(this);
//mediaPlayer.prepareAsync(); //prepare async to not block main thread
} catch (IOException ioe) {
ioe.printStackTrace();
Log.e(TAG, "video source:" + source);
@@ -119,10 +144,18 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
return;
}
try {
videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail;
} catch (Exception e) {
Log.e(TAG, "Could not get video thumbnail from ActivityCommunicator");
e.printStackTrace();
}
WifiManager wifiMgr = ((WifiManager)getSystemService(Context.WIFI_SERVICE));
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
mediaPlayer.setOnCompletionListener(new EndListener(wifiLock));//listen for end of video
//listen for end of video
mediaPlayer.setOnCompletionListener(new EndListener(wifiLock));
//get audio focus
/*
@@ -142,43 +175,9 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
filter.addAction(ACTION_STOP);
registerReceiver(broadcastReceiver, filter);
PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID, new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT);
note = buildNotification();
NotificationCompat.Action playButton = new NotificationCompat.Action.Builder
(R.drawable.ic_play_arrow_white_48dp, "Play", playPI).build();
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
(R.drawable.ic_play_arrow_white_48dp, "Pause", playPI).build();
PendingIntent stopPI = PendingIntent.getBroadcast(owner, noteID,
new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT);
//todo: make it so that tapping the notification brings you back to the Video's DetailActivity
//using setContentIntent
noteBuilder = new NotificationCompat.Builder(owner);
noteBuilder
.setPriority(Notification.PRIORITY_LOW)
.setCategory(Notification.CATEGORY_TRANSPORT)
.setContentTitle(title)
.setContentText("NewPipe is playing in the background")//todo: translation string
//.setAutoCancel(!mediaPlayer.isPlaying())
.setOngoing(true)
.setDeleteIntent(stopPI)
//.setProgress(vidLength, 0, false) //doesn't fit with Notification.MediaStyle
.setSmallIcon(R.mipmap.ic_launcher)
.setTicker(title + " - NewPipe")
.addAction(playButton);
/* .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLargeIcon(cover)*/
noteBuilder.setStyle(new NotificationCompat.MediaStyle()
//.setMediaSession(mMediaSession.getSessionToken())
.setShowActionsInCompactView(new int[] {0})
.setShowCancelButton(true)
.setCancelButtonIntent(stopPI)
);
startForeground(noteID, noteBuilder.build());
startForeground(noteID, note);
//currently decommissioned progressbar looping update code - works, but doesn't fit inside
//Notification.MediaStyle Notification layout.
@@ -195,34 +194,46 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
Log.d(TAG, "sleep failure");
}
}*/
}
/**Handles button presses from the notification. */
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.i(TAG, "received broadcast action:"+action);
//Log.i(TAG, "received broadcast action:"+action);
if(action.equals(ACTION_PLAYPAUSE)) {
if(mediaPlayer.isPlaying()) {
mediaPlayer.pause();
note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp);
if(android.os.Build.VERSION.SDK_INT >=16){
note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp);
}
noteMgr.notify(noteID, note);
}
else {
//reacquire CPU lock after releasing it on pause
//reacquire CPU lock after auto-releasing it on pause
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
mediaPlayer.start();
note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp);
if(android.os.Build.VERSION.SDK_INT >=16){
note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp);
}
noteMgr.notify(noteID, note);
}
}
else if(action.equals(ACTION_STOP)) {
mediaPlayer.stop();//this auto-releases CPU lock
//this auto-releases CPU lock
mediaPlayer.stop();
afterPlayCleanup();
}
}
};
private void afterPlayCleanup() {
//noteBuilder.setProgress(0, 0, false);
//remove progress bar
//noteBuilder.setProgress(0, 0, false);
//remove notification
noteMgr.cancel(noteID);
unregisterReceiver(broadcastReceiver);
@@ -231,7 +242,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
//release wifilock
wifiLock.release();
//remove foreground status of service; make us killable
//remove foreground status of service; make BackgroundPlayer killable
stopForeground(true);
stopSelf();
@@ -248,5 +259,77 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
afterPlayCleanup();
}
}
private Notification buildNotification() {
Notification note;
Resources res = getApplicationContext().getResources();
noteBuilder = new NotificationCompat.Builder(owner);
PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID,
new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent stopPI = PendingIntent.getBroadcast(owner, noteID,
new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT);
/*
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
(R.drawable.ic_pause_white_24dp, "Pause", playPI).build();
*/
//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.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID,
openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
noteBuilder
.setOngoing(true)
.setDeleteIntent(stopPI)
//doesn't fit with Notification.MediaStyle
//.setProgress(vidLength, 0, false)
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
.setTicker(
String.format(res.getString(
R.string.background_player_time_text), title))
.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
noteID, openDetailViewIntent,
PendingIntent.FLAG_UPDATE_CURRENT));
RemoteViews view =
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
view.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
view.setTextViewText(R.id.notificationSongName, title);
view.setTextViewText(R.id.notificationArtist, channelName);
view.setOnClickPendingIntent(R.id.notificationStop, stopPI);
view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
view.setOnClickPendingIntent(R.id.notificationBackgroundButton, openDetailView);
//possibly found the expandedView problem,
//but can't test it as I don't have a 5.0 device. -medavox
RemoteViews expandedView =
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
expandedView.setTextViewText(R.id.notificationSongName, title);
expandedView.setTextViewText(R.id.notificationArtist, channelName);
expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI);
expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
expandedView.setOnClickPendingIntent(R.id.notificationBackgroundButton, openDetailView);
noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT);
//Make notification appear on lockscreen
noteBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);
note = noteBuilder.build();
note.contentView = view;
if (android.os.Build.VERSION.SDK_INT > 16) {
note.bigContentView = expandedView;
}
return note;
}
}
}

View File

@@ -1,19 +1,24 @@
package org.schabi.newpipe;
import android.Manifest;
import android.app.Dialog;
import android.app.DownloadManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.DialogFragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Christian Schabesberger on 21.09.15.
@@ -50,42 +55,136 @@ public class DownloadDialog extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
arguments = getArguments();
super.onCreateDialog(savedInstanceState);
if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.downloadDialogTitle)
.setItems(R.array.downloadOptions, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = getActivity();
SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String suffix = "";
String title = arguments.getString(TITLE);
String url = "";
switch(which) {
case 0: // Video
suffix = arguments.getString(FILE_SUFFIX_VIDEO);
url = arguments.getString(VIDEO_URL);
break;
case 1:
suffix = arguments.getString(FILE_SUFFIX_AUDIO);
url = arguments.getString(AUDIO_URL);
break;
default:
Log.d(TAG, "lolz");
}
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(
Uri.parse(url));
request.setDestinationUri(Uri.fromFile(new File(
defaultPreferences.getString("download_path_preference", "/storage/emulated/0/NewPipe")
+ "/" + title + suffix)));
try {
dm.enqueue(request);
} catch (Exception e) {
e.printStackTrace();
}
builder.setTitle(R.string.download_dialog_title);
// If no audio stream available
if(arguments.getString(AUDIO_URL) == null) {
builder.setItems(R.array.download_options_no_audio, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = getActivity();
String title = arguments.getString(TITLE);
switch (which) {
case 0: // Video
download(arguments.getString(VIDEO_URL),
title,
arguments.getString(FILE_SUFFIX_VIDEO), context);
break;
default:
Log.d(TAG, "lolz");
}
});
}
});
// If no video stream available
} else if(arguments.getString(VIDEO_URL) == null) {
builder.setItems(R.array.download_options_no_video, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = getActivity();
String title = arguments.getString(TITLE);
switch (which) {
case 0: // Audio
download(arguments.getString(AUDIO_URL),
title,
arguments.getString(FILE_SUFFIX_AUDIO), context);
break;
default:
Log.d(TAG, "lolz");
}
}
});
//if both streams ar available
} else {
builder.setItems(R.array.download_options, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = getActivity();
String title = arguments.getString(TITLE);
switch (which) {
case 0: // Video
download(arguments.getString(VIDEO_URL),
title,
arguments.getString(FILE_SUFFIX_VIDEO), context);
break;
case 1:
download(arguments.getString(AUDIO_URL),
title,
arguments.getString(FILE_SUFFIX_AUDIO), context);
break;
default:
Log.d(TAG, "lolz");
}
}
});
}
return builder.create();
}
/**
* #143 #44 #42 #22: make shure that the filename does not contain illegal chars.
* This should fix some of the "cannot download" problems.
* */
private String createFileName(String fName) {
// from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html
List<String> forbiddenCharsPatterns = new ArrayList<String> ();
forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP
forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+"); // Windows
forbiddenCharsPatterns.add("[^\\w\\d\\.]+"); // last chance... only latin letters and digits
String nameToTest = fName;
for (String pattern : forbiddenCharsPatterns) {
nameToTest = nameToTest.replaceAll(pattern, "_");
}
return nameToTest;
}
private void download(String url, String title, String fileSuffix, Context context) {
File downloadDir = NewPipeSettings.getDownloadFolder();
if(!downloadDir.exists()) {
//attempt to create directory
boolean mkdir = downloadDir.mkdirs();
if(!mkdir && !downloadDir.isDirectory()) {
String message = context.getString(R.string.err_dir_create,downloadDir.toString());
Log.e(TAG, message);
Toast.makeText(context,message , Toast.LENGTH_LONG).show();
return;
}
String message = context.getString(R.string.info_dir_created,downloadDir.toString());
Log.e(TAG, message);
Toast.makeText(context,message , Toast.LENGTH_LONG).show();
}
File saveFilePath = new File(downloadDir,createFileName(title) + fileSuffix);
long id = 0;
if (App.isUsingTor()) {
// if using Tor, do not use DownloadManager because the proxy cannot be set
FileDownloader.downloadFile(getContext(), url, saveFilePath, title);
} else {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(
Uri.parse(url));
request.setDestinationUri(Uri.fromFile(saveFilePath));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setTitle(title);
request.setDescription("'" + url +
"' => '" + saveFilePath + "'");
request.allowScanningByMediaScanner();
try {
id = dm.enqueue(request);
} catch (Exception e) {
e.printStackTrace();
}
}
Log.i(TAG,"Started downloading '" + url +
"' => '" + saveFilePath + "' #" + id);
}
}

View File

@@ -3,14 +3,17 @@ package org.schabi.newpipe;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.UnknownHostException;
import javax.net.ssl.HttpsURLConnection;
import info.guardianproject.netcipher.NetCipher;
/**
* Created by Christian Schabesberger on 14.08.15.
* Created by Christian Schabesberger on 28.01.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* 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
@@ -27,70 +30,61 @@ import java.net.UnknownHostException;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class Downloader {
private static final String USER_AGENT = "Mozilla/5.0";
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
/**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*/
public static String download(String siteUrl, String language) {
String ret = "";
try {
URL url = new URL(siteUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestProperty("Accept-Language", language);
ret = dl(con);
}
catch(Exception e) {
e.printStackTrace();
}
return ret;
public String download(String siteUrl, String language) throws IOException {
URL url = new URL(siteUrl);
//HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
con.setRequestProperty("Accept-Language", language);
return dl(con);
}
/**Common functionality between download(String url) and download(String url, String language)*/
private static String dl(HttpURLConnection con) throws IOException {
private static String dl(HttpsURLConnection con) throws IOException {
StringBuilder response = new StringBuilder();
BufferedReader in = null;
try {
con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", USER_AGENT);
BufferedReader in = new BufferedReader(
in = new BufferedReader(
new InputStreamReader(con.getInputStream()));
String inputLine;
while((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
}
catch(UnknownHostException uhe) {//thrown when there's no internet connection
uhe.printStackTrace();
} catch(UnknownHostException uhe) {//thrown when there's no internet connection
throw new IOException("unknown host or no network", uhe);
//Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show();
} catch(Exception e) {
throw new IOException(e);
} finally {
if(in != null) {
in.close();
}
}
return response.toString();
}
/**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*/
public static String download(String siteUrl) {
String ret = "";
try {
URL url = new URL(siteUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
ret = dl(con);
}
catch(Exception e) {
e.printStackTrace();
}
return ret;
/**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*/
public String download(String siteUrl) throws IOException {
URL url = new URL(siteUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
return dl(con);
}
}

View File

@@ -0,0 +1,54 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
/**
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.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 ExitActivity extends Activity {
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= 21) {
finishAndRemoveTask();
} else {
finish();
}
System.exit(0);
}
public static void exitAndRemoveFromRecentApps(Activity activity) {
Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
}

View File

@@ -0,0 +1,169 @@
package org.schabi.newpipe;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.UnknownHostException;
import javax.net.ssl.HttpsURLConnection;
import info.guardianproject.netcipher.NetCipher;
/**
* Created by Christian Schabesberger on 14.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* FileDownloader.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 FileDownloader extends AsyncTask<Void, Integer, Void> {
public static final String TAG = "FileDownloader";
private NotificationManager nm;
private NotificationCompat.Builder builder;
private int notifyId = 0x1234;
private int fileSize = 0xffffffff;
private final Context context;
private final String fileURL;
private final File saveFilePath;
private final String title;
private final String debugContext;
public FileDownloader(Context context, String fileURL, File saveFilePath, String title) {
this.context = context;
this.fileURL = fileURL;
this.saveFilePath = saveFilePath;
this.title = title;
this.debugContext = "'" + fileURL +
"' => '" + saveFilePath + "'";
}
/**
* Downloads a file from a URL in the background using an {@link AsyncTask}.
*
* @param fileURL HTTP URL of the file to be downloaded
* @param saveFilePath path of the directory to save the file
* @param title
* @throws IOException
*/
public static void downloadFile(final Context context, final String fileURL, final File saveFilePath, String title) {
new FileDownloader(context, fileURL, saveFilePath, title).execute();
}
/** AsyncTask impl: executed in gui thread */
@Override
protected void onPreExecute() {
super.onPreExecute();
nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Drawable icon = context.getResources().getDrawable(R.mipmap.ic_launcher);
builder = new NotificationCompat.Builder(context)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon(((BitmapDrawable) icon).getBitmap())
.setContentTitle(saveFilePath.getName())
.setContentText(saveFilePath.getAbsolutePath())
.setProgress(fileSize, 0, false);
nm.notify(notifyId, builder.build());
}
/** AsyncTask impl: executed in background thread does the download */
@Override
protected Void doInBackground(Void... voids) {
HttpsURLConnection con = null;
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
con = NetCipher.getHttpsURLConnection(fileURL);
int responseCode = con.getResponseCode();
// always check HTTP response code first
if (responseCode == HttpURLConnection.HTTP_OK) {
fileSize = con.getContentLength();
inputStream = new BufferedInputStream(con.getInputStream());
outputStream = new FileOutputStream(saveFilePath);
int bufferSize = 8192;
int downloaded = 0;
int bytesRead = -1;
byte[] buffer = new byte[bufferSize];
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
downloaded += bytesRead;
if (downloaded % 50000 < bufferSize) {
publishProgress(downloaded);
}
}
publishProgress(bufferSize);
} else {
Log.i(TAG, "No file to download. Server replied HTTP code: " + responseCode);
}
} catch (IOException e) {
Log.e(TAG, "No file to download. Server replied HTTP code: ", e);
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (con != null) {
con.disconnect();
}
}
return null;
}
@Override
protected void onProgressUpdate(Integer... progress) {
builder.setProgress(fileSize, progress[0], false);
nm.notify(notifyId, builder.build());
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
nm.cancel(notifyId);
}
}

View File

@@ -0,0 +1,93 @@
package org.schabi.newpipe;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* Created by chschtsch on 12/29/15.
*
* Copyright (C) Gregory Arkhipov 2015
* Localization.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 Localization {
public static Locale getPreferredLocale(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
String languageCode = sp.getString(String.valueOf(R.string.search_language_key),
context.getString(R.string.default_language_value));
if(languageCode.length() == 2) {
return new Locale(languageCode);
}
else if(languageCode.contains("_")) {
String country = languageCode
.substring(languageCode.indexOf("_"), languageCode.length());
return new Locale(languageCode.substring(0, 2), country);
}
return Locale.getDefault();
}
public static String localizeViewCount(long viewCount, Context context) {
Locale locale = getPreferredLocale(context);
Resources res = context.getResources();
String viewsString = res.getString(R.string.view_count_text);
NumberFormat nf = NumberFormat.getInstance(locale);
String formattedViewCount = nf.format(viewCount);
return String.format(viewsString, formattedViewCount);
}
public static String localizeNumber(long number, Context context) {
Locale locale = getPreferredLocale(context);
NumberFormat nf = NumberFormat.getInstance(locale);
return nf.format(number);
}
private static String formatDate(String date, Context context) {
Locale locale = getPreferredLocale(context);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
Date datum = null;
try {
datum = formatter.parse(date);
} catch (ParseException e) {
e.printStackTrace();
}
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
return df.format(datum);
}
public static String localizeDate(String date, Context context) {
Resources res = context.getResources();
String dateString = res.getString(R.string.upload_date_text);
String formattedDate = formatDate(date, context);
return String.format(dateString, formattedDate);
}
}

View File

@@ -0,0 +1,72 @@
/**
* Created by k3b on 07.01.2016.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* NewPipeSettings.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/>.
*/
package org.schabi.newpipe;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
/**
* Helper for global settings
*/
public class NewPipeSettings {
public static void initSettings(Context context) {
PreferenceManager.setDefaultValues(context, R.xml.settings, false);
getVideoDownloadFolder(context);
getAudioDownloadFolder(context);
}
public static File getDownloadFolder() {
return getFolder(Environment.DIRECTORY_DOWNLOADS);
}
public static File getVideoDownloadFolder(Context context) {
return getFolder(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
}
public static File getAudioDownloadFolder(Context context) {
return getFolder(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
private static File getFolder(Context context, int keyID, String defaultDirectoryName) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
final File folder = getFolder(defaultDirectoryName);
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key
, new File(folder,"NewPipe").getAbsolutePath());
spEditor.apply();
return folder;
}
@NonNull
private static File getFolder(String defaultDirectoryName) {
return new File(Environment.getExternalStorageDirectory(),defaultDirectoryName);
}
}

View File

@@ -0,0 +1,50 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.media.AudioManager;
/**
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* PanicResponderActivity.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 PanicResponderActivity extends Activity {
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
// TODO explicitly clear the search results once they are restored when the app restarts
// or if the app reloads the current video after being killed, that should be cleared also
ExitActivity.exitAndRemoveFromRecentApps(this);
}
if (Build.VERSION.SDK_INT >= 21) {
finishAndRemoveTask();
} else {
finish();
}
}
}

View File

@@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.media.MediaPlayer;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -78,7 +79,7 @@ public class PlayVideoActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_play_video);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
isLandscape = checkIfLandscape();
hasSoftKeys = checkIfHasSoftKeys();
@@ -129,7 +130,7 @@ public class PlayVideoActivity extends AppCompatActivity {
public void onPrepared(MediaPlayer mp) {
progressBar.setVisibility(View.GONE);
videoView.seekTo(position);
if (position == 0) {
if (position <= 0) {
videoView.start();
showUi();
} else {
@@ -187,6 +188,18 @@ public class PlayVideoActivity extends AppCompatActivity {
videoView.pause();
}
@Override
public void onResume() {
super.onResume();
App.checkStartTor(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
prefs = getPreferences(Context.MODE_PRIVATE);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
@@ -199,7 +212,7 @@ public class PlayVideoActivity extends AppCompatActivity {
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, videoUrl);
intent.setType("text/plain");
startActivity(Intent.createChooser(intent, getString(R.string.shareDialogTitle)));
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
break;
case R.id.menu_item_screen_rotation:
toggleOrientation();

View File

@@ -1,10 +1,16 @@
package org.schabi.newpipe;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Environment;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
@@ -17,6 +23,8 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import info.guardianproject.netcipher.proxy.OrbotHelper;
/**
* Created by Christian Schabesberger on 31.08.15.
*
@@ -37,8 +45,9 @@ import android.view.ViewGroup;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class SettingsActivity extends PreferenceActivity {
public class SettingsActivity extends PreferenceActivity {
private static final int REQUEST_INSTALL_ORBOT = 0x1234;
private AppCompatDelegate mDelegate = null;
@Override
@@ -55,12 +64,108 @@ public class SettingsActivity extends PreferenceActivity {
}
public static class SettingsFragment extends PreferenceFragment {
public static class SettingsFragment extends PreferenceFragment{
SharedPreferences.OnSharedPreferenceChangeListener prefListener;
// get keys
String DEFAULT_RESOLUTION_PREFERENCE;
String DEFAULT_AUDIO_FORMAT_PREFERENCE;
String SEARCH_LANGUAGE_PREFERENCE;
String DOWNLOAD_PATH_PREFERENCE;
String DOWNLOAD_PATH_AUDIO_PREFERENCE;
String USE_TOR_KEY;
private ListPreference defaultResolutionPreference;
private ListPreference defaultAudioFormatPreference;
private ListPreference searchLanguagePreference;
private EditTextPreference downloadPathPreference;
private EditTextPreference downloadPathAudioPreference;
private CheckBoxPreference useTorCheckBox;
private SharedPreferences defaultPreferences;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings_screen);
addPreferencesFromResource(R.xml.settings);
final Activity activity = getActivity();
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
// get keys
DEFAULT_RESOLUTION_PREFERENCE =getString(R.string.default_resolution_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);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
USE_TOR_KEY = getString(R.string.use_tor_key);
// get pref objects
defaultResolutionPreference =
(ListPreference) findPreference(DEFAULT_RESOLUTION_PREFERENCE);
defaultAudioFormatPreference =
(ListPreference) findPreference(DEFAULT_AUDIO_FORMAT_PREFERENCE);
searchLanguagePreference =
(ListPreference) findPreference(SEARCH_LANGUAGE_PREFERENCE);
downloadPathPreference =
(EditTextPreference) findPreference(DOWNLOAD_PATH_PREFERENCE);
downloadPathAudioPreference =
(EditTextPreference) findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
useTorCheckBox = (CheckBoxPreference) findPreference(USE_TOR_KEY);
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
Activity a = getActivity();
updateSummary();
if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) {
if (OrbotHelper.isOrbotInstalled(a)) {
App.configureTor(true);
OrbotHelper.requestStartTor(a);
} else {
Intent intent = OrbotHelper.getOrbotInstallIntent(a);
a.startActivityForResult(intent, REQUEST_INSTALL_ORBOT);
}
} else {
App.configureTor(false);
}
}
};
defaultPreferences.registerOnSharedPreferenceChangeListener(prefListener);
updateSummary();
}
// This is used to show the status of some preference in the description
private void updateSummary() {
defaultResolutionPreference.setSummary(
defaultPreferences.getString(DEFAULT_RESOLUTION_PREFERENCE,
getString(R.string.default_resolution_value)));
defaultAudioFormatPreference.setSummary(
defaultPreferences.getString(DEFAULT_AUDIO_FORMAT_PREFERENCE,
getString(R.string.default_audio_format_value)));
searchLanguagePreference.setSummary(
defaultPreferences.getString(SEARCH_LANGUAGE_PREFERENCE,
getString(R.string.default_language_value)));
downloadPathPreference.setSummary(
defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE,
getString(R.string.download_path_summary)));
downloadPathAudioPreference.setSummary(
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE,
getString(R.string.download_path_audio_summary)));
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// try to start tor regardless of resultCode since clicking back after
// installing the app does not necessarily return RESULT_OK
App.configureTor(requestCode == REQUEST_INSTALL_ORBOT
&& OrbotHelper.requestStartTor(this));
}
@Override
@@ -150,15 +255,6 @@ public class SettingsActivity extends PreferenceActivity {
}
public static void initSettings(Context context) {
PreferenceManager.setDefaultValues(context, R.xml.settings_screen, false);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
if(sp.getString(context.getString(R.string.downloadPathPreference), "").isEmpty()){
SharedPreferences.Editor spEditor = sp.edit();
String newPipeDownloadStorage =
Environment.getExternalStorageDirectory().getAbsolutePath() + "/NewPipe";
spEditor.putString(context.getString(R.string.downloadPathPreference)
, newPipeDownloadStorage);
spEditor.apply();
}
NewPipeSettings.initSettings(context);
}
}

View File

@@ -1,101 +0,0 @@
package org.schabi.newpipe;
import org.schabi.newpipe.services.AbstractVideoInfo;
import java.util.List;
/**
* Created by Christian Schabesberger on 26.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* VideoInfo.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 VideoInfo extends AbstractVideoInfo {
public String uploader_thumbnail_url = "";
public String description = "";
public VideoStream[] videoStreams = null;
public AudioStream[] audioStreams = null;
public int videoAvailableStatus = VIDEO_AVAILABLE;
public int duration = -1;
/*YouTube-specific fields
todo: move these to a subclass*/
public int age_limit = 0;
public int like_count = -1;
public int dislike_count = -1;
public String average_rating = "";
public VideoPreviewInfo nextVideo = null;
public List<VideoPreviewInfo> relatedVideos = null;
public int startPosition = -1;//in seconds. some metadata is not passed using a VideoInfo object!
public static final int VIDEO_AVAILABLE = 0x00;
public static final int VIDEO_UNAVAILABLE = 0x01;
public static final int VIDEO_UNAVAILABLE_GEMA = 0x02;//German DRM organisation
public VideoInfo() {}
/**Creates a new VideoInfo object from an existing AbstractVideoInfo.
* All the shared properties are copied to the new VideoInfo.*/
@SuppressWarnings("WeakerAccess")
public VideoInfo(AbstractVideoInfo avi) {
this.id = avi.id;
this.title = avi.title;
this.uploader = avi.uploader;
this.thumbnail_url = avi.thumbnail_url;
this.thumbnail = avi.thumbnail;
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 VideoPreviewInfo) {//shitty String to convert code
String dur = ((VideoPreviewInfo)avi).duration;
int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":")));
int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length()));
this.duration = (minutes*60)+seconds;
}
}
public static class VideoStream {
public String url = ""; //url of the stream
public int format = -1;
public String resolution = "";
public VideoStream(String url, int format, String res) {
this.url = url; this.format = format; resolution = res;
}
}
@SuppressWarnings("unused")
public static class AudioStream {
public String url = "";
public int format = -1;
public int bandwidth = -1;
public int samplingRate = -1;
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
this.url = url; this.format = format;
this.bandwidth = bandwidth; this.samplingRate = samplingRate;
}
}
}

View File

@@ -1,11 +1,16 @@
package org.schabi.newpipe;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
/**
* Created by Christian Schabesberger on 24.10.15.
*
@@ -28,12 +33,14 @@ import android.widget.TextView;
class VideoInfoItemViewCreator {
private final LayoutInflater inflater;
private ImageLoader imageLoader = ImageLoader.getInstance();
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
public VideoInfoItemViewCreator(LayoutInflater inflater) {
this.inflater = inflater;
}
public View getViewByVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info) {
public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info, Context context) {
ViewHolder holder;
if(convertView == null) {
convertView = inflater.inflate(R.layout.video_item, parent, false);
@@ -43,6 +50,7 @@ class VideoInfoItemViewCreator {
holder.itemUploaderView = (TextView) convertView.findViewById(R.id.itemUploaderView);
holder.itemDurationView = (TextView) convertView.findViewById(R.id.itemDurationView);
holder.itemUploadDateView = (TextView) convertView.findViewById(R.id.itemUploadDateView);
holder.itemViewCountView = (TextView) convertView.findViewById(R.id.itemViewCountView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
@@ -56,19 +64,31 @@ class VideoInfoItemViewCreator {
holder.itemVideoTitleView.setText(info.title);
holder.itemUploaderView.setText(info.uploader);
holder.itemDurationView.setText(info.duration);
holder.itemViewCountView.setText(shortViewCount(info.view_count));
if(!info.upload_date.isEmpty()) {
holder.itemUploadDateView.setText(info.upload_date);
} else {
//tweak if necessary: This is a hack to prevent having white space in the layout :P
holder.itemUploadDateView.setText(String.format("%d", info.view_count));
holder.itemUploadDateView.setText(info.upload_date+"");
}
imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions);
return convertView;
}
private class ViewHolder {
public ImageView itemThumbnailView;
public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView;
public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView, itemViewCountView;
}
private String shortViewCount(Long view_count){
if(view_count >= 1000000000){
return Long.toString(view_count/1000000000)+"B views";
}else if(view_count>=1000000){
return Long.toString(view_count/1000000)+"M views";
}else if(view_count>=1000){
return Long.toString(view_count/1000)+"K views";
}else {
return Long.toString(view_count)+" views";
}
}
}

View File

@@ -1,6 +1,7 @@
package org.schabi.newpipe;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
@@ -10,8 +11,8 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import org.schabi.newpipe.services.ServiceList;
import org.schabi.newpipe.services.StreamingService;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
/**
@@ -44,7 +45,7 @@ public class VideoItemDetailActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_videoitem_detail);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
// Show the Up button in the action bar.
try {
//noinspection ConstantConditions
@@ -70,9 +71,9 @@ public class VideoItemDetailActivity extends AppCompatActivity {
if (getIntent().getData() != null) {
videoUrl = getIntent().getData().toString();
StreamingService[] serviceList = ServiceList.getServices();
//VideoExtractor videoExtractor = null;
//StreamExtractor videoExtractor = null;
for (int i = 0; i < serviceList.length; i++) {
if (serviceList[i].acceptUrl(videoUrl)) {
if (serviceList[i].getUrlIdHandler().acceptUrl(videoUrl)) {
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i);
currentStreamingService = i;
//videoExtractor = ServiceList.getService(i).getExtractorInstance();
@@ -80,7 +81,7 @@ public class VideoItemDetailActivity extends AppCompatActivity {
}
}
if(currentStreamingService == -1) {
Toast.makeText(this, R.string.urlNotSupportedText, Toast.LENGTH_LONG)
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
.show();
}
//arguments.putString(VideoItemDetailFragment.VIDEO_URL,
@@ -89,7 +90,7 @@ public class VideoItemDetailActivity extends AppCompatActivity {
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY,
PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(getString(R.string.autoPlayThroughIntent), false));
.getBoolean(getString(R.string.autoplay_through_intent_key), false));
} else {
videoUrl = getIntent().getStringExtra(VideoItemDetailFragment.VIDEO_URL);
currentStreamingService = getIntent().getIntExtra(VideoItemDetailFragment.STREAMING_SERVICE, -1);
@@ -113,6 +114,12 @@ public class VideoItemDetailActivity extends AppCompatActivity {
.commit();
}
@Override
public void onResume() {
super.onResume();
App.checkStartTor(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView;
@@ -15,7 +16,8 @@ import android.view.inputmethod.InputMethodManager;
import java.util.ArrayList;
import org.schabi.newpipe.services.ServiceList;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import org.schabi.newpipe.extractor.ServiceList;
/**
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
@@ -111,7 +113,7 @@ public class VideoItemListActivity extends AppCompatActivity
//------ todo: remove this line when multiservice support is implemented ------
currentStreamingServiceId = ServiceList.getIdOfService("Youtube");
//-----------------------------------------------------------------------------
//to solve issue 38
listFragment = (VideoItemListFragment) getSupportFragmentManager()
.findFragmentById(R.id.videoitem_list);
listFragment.setStreamingService(ServiceList.getService(currentStreamingServiceId));
@@ -171,7 +173,13 @@ public class VideoItemListActivity extends AppCompatActivity
}
}
SettingsActivity.initSettings(this);
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
}
@Override
public void onResume() {
super.onResume();
App.checkStartTor(this);
}
/**
@@ -190,7 +198,7 @@ public class VideoItemListActivity extends AppCompatActivity
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id);
//arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id);
arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpage_url);
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId);
videoFragment = new VideoItemDetailFragment();
@@ -209,7 +217,7 @@ public class VideoItemListActivity extends AppCompatActivity
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, VideoItemDetailActivity.class);
detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id);
//detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id);
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpage_url);
detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId);
startActivity(detailIntent);

View File

@@ -2,8 +2,6 @@ package org.schabi.newpipe;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
@@ -15,12 +13,13 @@ import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.Toast;
import java.net.URL;
import java.io.IOException;
import java.util.List;
import java.util.Vector;
import org.schabi.newpipe.services.SearchEngine;
import org.schabi.newpipe.services.StreamingService;
import org.schabi.newpipe.extractor.ExctractionException;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import org.schabi.newpipe.extractor.SearchEngine;
import org.schabi.newpipe.extractor.StreamingService;
/**
@@ -58,14 +57,15 @@ public class VideoItemListFragment extends ListFragment {
private Thread searchThread = null;
private SearchRunnable searchRunnable = null;
private Thread loadThumbsThread = null;
private LoadThumbsRunnable loadThumbsRunnable = null;
// used to track down if results posted by threads ar still valid
private int currentRequestId = -1;
private ListView list;
private View footer;
// used to suppress request for loading a new page while another page is already loading.
private boolean loadingNextPage = true;
private class ResultRunnable implements Runnable {
private final SearchEngine.Result result;
private final int requestId;
@@ -76,6 +76,9 @@ public class VideoItemListFragment extends ListFragment {
@Override
public void run() {
updateListOnResult(result, requestId);
if (android.os.Build.VERSION.SDK_INT >= 19) {
getListView().removeFooterView(footer);
}
}
}
@@ -84,7 +87,7 @@ public class VideoItemListFragment extends ListFragment {
private final String query;
private final int page;
final Handler h = new Handler();
private volatile boolean run = true;
private volatile boolean runs = true;
private final int requestId;
public SearchRunnable(SearchEngine engine, String query, int page, int requestId) {
this.engine = engine;
@@ -93,40 +96,36 @@ public class VideoItemListFragment extends ListFragment {
this.requestId = requestId;
}
void terminate() {
run = false;
runs = false;
}
@Override
public void run() {
try {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
String searchLanguageKey = getContext().getString(R.string.searchLanguage);
String searchLanguage = sp.getString(searchLanguageKey, "en");
SearchEngine.Result result = engine.search(query, page, searchLanguage);
String searchLanguageKey = getContext().getString(R.string.search_language_key);
String searchLanguage = sp.getString(searchLanguageKey,
getString(R.string.default_language_value));
SearchEngine.Result result = engine.search(query, page, searchLanguage,
new Downloader());
Log.i(TAG, "language code passed:\""+searchLanguage+"\"");
if(run) {
if(runs) {
h.post(new ResultRunnable(result, requestId));
}
} catch(Exception e) {
} catch(IOException e) {
postNewErrorToast(h, R.string.network_error);
e.printStackTrace();
} catch(ExctractionException ce) {
postNewErrorToast(h, R.string.parsing_error);
ce.printStackTrace();
} catch(Exception e) {
postNewErrorToast(h, R.string.general_error);
e.printStackTrace();
h.post(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), "Network Error", Toast.LENGTH_SHORT).show();
}
});
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (android.os.Build.VERSION.SDK_INT >= 19) {
getListView().removeFooterView(footer);
}
}
});
}
}
/*
<<<
private class LoadThumbsRunnable implements Runnable {
private final Vector<String> thumbnailUrlList = new Vector<>();
private final Vector<Boolean> downloadedList;
@@ -153,6 +152,7 @@ public class VideoItemListFragment extends ListFragment {
if(!downloadedList.get(i)) {
Bitmap thumbnail;
try {
//todo: make bitmaps not bypass tor
thumbnail = BitmapFactory.decodeStream(
new URL(thumbnailUrlList.get(i)).openConnection().getInputStream());
h.post(new SetThumbnailRunnable(i, thumbnail, requestId));
@@ -182,6 +182,9 @@ public class VideoItemListFragment extends ListFragment {
}
}
=======
>>>>>>> 6d1b4652fc98e5c2d5e19b0f98ba38a731137a70
*/
public void present(List<VideoPreviewInfo> videoList) {
mode = PRESENT_VIDEOS_MODE;
setListShown(true);
@@ -203,8 +206,9 @@ public class VideoItemListFragment extends ListFragment {
}
private void nextPage() {
loadingNextPage = true;
lastPage++;
Log.d(TAG, getString(R.string.searchPage) + Integer.toString(lastPage));
Log.d(TAG, getString(R.string.search_page) + Integer.toString(lastPage));
startSearch(query, lastPage);
}
@@ -228,7 +232,7 @@ public class VideoItemListFragment extends ListFragment {
Toast.makeText(getActivity(), result.errorMessage, Toast.LENGTH_LONG).show();
} else {
if (!result.suggestion.isEmpty()) {
Toast.makeText(getActivity(), getString(R.string.didYouMean) + result.suggestion + " ?",
Toast.makeText(getActivity(), getString(R.string.did_you_mean) + result.suggestion + " ?",
Toast.LENGTH_LONG).show();
}
updateList(result.resultList);
@@ -240,26 +244,16 @@ public class VideoItemListFragment extends ListFragment {
try {
videoListAdapter.addVideoList(list);
terminateThreads();
loadThumbsRunnable = new LoadThumbsRunnable(videoListAdapter.getVideoList(),
videoListAdapter.getDownloadedThumbnailList(), currentRequestId);
loadThumbsThread = new Thread(loadThumbsRunnable);
loadThumbsThread.start();
} catch(java.lang.IllegalStateException e) {
Log.w(TAG, "Trying to set value while activity doesn't exist anymore.");
} catch(Exception e) {
e.printStackTrace();
} finally {
loadingNextPage = false;
}
}
private void terminateThreads() {
if(loadThumbsRunnable != null && loadThumbsRunnable.isRunning()) {
loadThumbsRunnable.terminate();
try {
loadThumbsThread.join();
} catch(Exception e) {
e.printStackTrace();
}
}
if(searchThread != null) {
searchRunnable.terminate();
// No need to join, since we don't really terminate the thread. We just demand
@@ -297,7 +291,8 @@ public class VideoItemListFragment extends ListFragment {
super.onViewCreated(view, savedInstanceState);
list = getListView();
videoListAdapter = new VideoListAdapter(getActivity(), this);
footer = ((LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.paginate_footer, null, false);
footer = ((LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.paginate_footer, null, false);
setListAdapter(videoListAdapter);
@@ -318,13 +313,15 @@ public class VideoItemListFragment extends ListFragment {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (mode != PRESENT_VIDEOS_MODE
&& list.getChildAt(0) != null
&& list.getLastVisiblePosition() == list.getAdapter().getCount() - 1
&& list.getChildAt(list.getChildCount() - 1).getBottom() <= list.getHeight()) {
long time = System.currentTimeMillis();
if ((time - lastScrollDate) > 200) {
if ((time - lastScrollDate) > 200
&& !loadingNextPage) {
lastScrollDate = time;
getListView().addFooterView(footer);
nextPage();
@@ -376,4 +373,14 @@ public class VideoItemListFragment extends ListFragment {
mActivatedPosition = position;
}
private void postNewErrorToast(Handler h, final int stringResource) {
h.post(new Runnable() {
@Override
public void run() {
setListShown(true);
Toast.makeText(getActivity(), getString(R.string.network_error),
Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
@@ -9,6 +8,8 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import java.util.List;
import java.util.Vector;
@@ -36,7 +37,6 @@ class VideoListAdapter extends BaseAdapter {
private final Context context;
private final VideoInfoItemViewCreator viewCreator;
private Vector<VideoPreviewInfo> videoList = new Vector<>();
private Vector<Boolean> downloadedThumbnailList = new Vector<>();
private final ListView listView;
public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) {
@@ -49,15 +49,11 @@ class VideoListAdapter extends BaseAdapter {
public void addVideoList(List<VideoPreviewInfo> videos) {
videoList.addAll(videos);
for(int i = 0; i < videos.size(); i++) {
downloadedThumbnailList.add(false);
}
notifyDataSetChanged();
}
public void clearVideoList() {
videoList = new Vector<>();
downloadedThumbnailList = new Vector<>();
notifyDataSetChanged();
}
@@ -65,20 +61,6 @@ class VideoListAdapter extends BaseAdapter {
return videoList;
}
public void updateDownloadedThumbnailList(int index) {
downloadedThumbnailList.set(index, true);
}
public Vector<Boolean> getDownloadedThumbnailList() {
return downloadedThumbnailList;
}
public void setThumbnail(int index, Bitmap thumbnail) {
videoList.get(index).thumbnail = thumbnail;
downloadedThumbnailList.set(index, true);
notifyDataSetChanged();
}
@Override
public int getCount() {
return videoList.size();
@@ -96,10 +78,10 @@ class VideoListAdapter extends BaseAdapter {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = viewCreator.getViewByVideoInfoItem(convertView, parent, videoList.get(position));
convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position), context);
if(listView.isItemChecked(position)) {
convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.primaryColorYoutube));
convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color));
} else {
convertView.setBackgroundColor(0);
}

View File

@@ -0,0 +1,33 @@
package org.schabi.newpipe.extractor;
import android.graphics.Bitmap;
/**
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* AbstractVideoInfo.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 VideoInfo and VideoPreviewInfo.*/
public abstract class AbstractVideoInfo {
public String id = "";
public String title = "";
public String uploader = "";
public String thumbnail_url = "";
public Bitmap thumbnail = null;
public String webpage_url = "";
public String upload_date = "";
public long view_count = -1;
}

View File

@@ -0,0 +1,105 @@
package org.schabi.newpipe.extractor;
import android.util.Xml;
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 {
static class DashMpdParsingException extends ParsingException {
DashMpdParsingException(String message, Exception e) {
super(message, e);
}
}
public static List<VideoInfo.AudioStream> getAudioStreams(String dashManifestUrl,
Downloader downloader)
throws DashMpdParsingException {
String dashDoc;
try {
dashDoc = downloader.download(dashManifestUrl);
} catch(IOException ioe) {
throw new DashMpdParsingException("Could not get dash mpd: " + dashManifestUrl, ioe);
}
Vector<VideoInfo.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 VideoInfo.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

@@ -0,0 +1,41 @@
package org.schabi.newpipe.extractor;
import java.io.IOException;
/**
* 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;
/**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;
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 30.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ExctractionException.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 ExctractionException extends Exception {
public ExctractionException() {}
public ExctractionException(String message) {
super(message);
}
public ExctractionException(Throwable cause) {
super(cause);
}
public ExctractionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe;
package org.schabi.newpipe.extractor;
/**
* Created by Adam Howard on 08/11/15.
@@ -6,7 +6,7 @@ package org.schabi.newpipe;
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
* and Adam Howard <achdisposable1@gmail.com> 2015
*
* VideoListAdapter.java is part of NewPipe.
* 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
@@ -25,10 +25,12 @@ package org.schabi.newpipe;
/**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");

View File

@@ -0,0 +1,60 @@
package org.schabi.newpipe.extractor;
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 {
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[] split_arg = arg.split("=");
map.put(split_arg[0], URLDecoder.decode(split_arg[1], "UTF-8"));
}
return map;
}
}

View File

@@ -0,0 +1,35 @@
package org.schabi.newpipe.extractor;
/**
* 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 ExctractionException {
public ParsingException() {}
public ParsingException(String message) {
super(message);
}
public ParsingException(Throwable cause) {
super(cause);
}
public ParsingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.services;
import org.schabi.newpipe.VideoPreviewInfo;
package org.schabi.newpipe.extractor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
@@ -27,16 +27,16 @@ import java.util.Vector;
@SuppressWarnings("ALL")
public interface SearchEngine {
class Result {
public String errorMessage = "";
public String suggestion = "";
public final Vector<VideoPreviewInfo> resultList = new Vector<>();
public final List<VideoPreviewInfo> resultList = new Vector<>();
}
ArrayList<String> suggestionList(String query);
ArrayList<String> suggestionList(String query, Downloader dl)
throws ExctractionException, IOException;
//Result search(String query, int page);
Result search(String query, int page, String contentCountry);
Result search(String query, int page, String contentCountry, Downloader dl)
throws ExctractionException, IOException;
}

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe.services;
package org.schabi.newpipe.extractor;
import android.util.Log;
import org.schabi.newpipe.services.youtube.YoutubeService;
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
/**
* Created by Christian Schabesberger on 23.08.15.

View File

@@ -0,0 +1,78 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 10.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <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 java.util.List;
/**Scrapes information from a video streaming service (eg, YouTube).*/
@SuppressWarnings("ALL")
public interface StreamExtractor {
public class ExctractorInitException extends ExctractionException {
public ExctractorInitException() {}
public ExctractorInitException(String message) {
super(message);
}
public ExctractorInitException(Throwable cause) {
super(cause);
}
public ExctractorInitException(String message, Throwable cause) {
super(message, cause);
}
}
public class ContentNotAvailableException extends ParsingException {
public ContentNotAvailableException() {}
public ContentNotAvailableException(String message) {
super(message);
}
public ContentNotAvailableException(Throwable cause) {
super(cause);
}
public ContentNotAvailableException(String message, Throwable cause) {
super(message, cause);
}
}
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 int getLength() throws ParsingException;
public abstract long getViews() throws ParsingException;
public abstract String getUploadDate() throws ParsingException;
public abstract String getThumbnailUrl() throws ParsingException;
public abstract String getUploaderThumbnailUrl() throws ParsingException;
public abstract List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException;
public abstract List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException;
public abstract List<VideoInfo.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 VideoPreviewInfo getNextVideo() throws ParsingException;
public abstract List<VideoPreviewInfo> getRelatedVideos() throws ParsingException;
public abstract VideoUrlIdHandler getUrlIdConverter();
public abstract String getPageUrl();
}

View File

@@ -1,4 +1,6 @@
package org.schabi.newpipe.services;
package org.schabi.newpipe.extractor;
import java.io.IOException;
/**
* Created by Christian Schabesberger on 23.08.15.
@@ -25,11 +27,11 @@ public interface StreamingService {
public String name = "";
}
ServiceInfo getServiceInfo();
VideoExtractor getExtractorInstance(String url);
StreamExtractor getExtractorInstance(String url, Downloader downloader)
throws IOException, ExctractionException;
SearchEngine getSearchEngineInstance();
/**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);
VideoUrlIdHandler getUrlIdHandler();
}

View File

@@ -0,0 +1,302 @@
package org.schabi.newpipe.extractor;
import java.io.IOException;
import java.util.List;
import java.util.Vector;
/**
* Created by Christian Schabesberger on 26.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* VideoInfo.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 VideoInfo extends AbstractVideoInfo {
/**Fills out the video info fields which are common to all services.
* Probably needs to be overridden by subclasses*/
public static VideoInfo getVideoInfo(StreamExtractor extractor, Downloader downloader)
throws ExctractionException, IOException {
VideoInfo videoInfo = new VideoInfo();
videoInfo = extractImportantData(videoInfo, extractor, downloader);
videoInfo = extractStreams(videoInfo, extractor, downloader);
videoInfo = extractOptionalData(videoInfo, extractor, downloader);
return videoInfo;
}
private static VideoInfo extractImportantData(
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader)
throws ExctractionException, IOException {
/* ---- importand data, withoug the video can't be displayed goes here: ---- */
// if one of these is not available an exception is ment to be thrown directly into the frontend.
VideoUrlIdHandler uiconv = extractor.getUrlIdConverter();
videoInfo.webpage_url = extractor.getPageUrl();
videoInfo.id = uiconv.getVideoId(extractor.getPageUrl());
videoInfo.title = extractor.getTitle();
if((videoInfo.webpage_url == null || videoInfo.webpage_url.isEmpty())
|| (videoInfo.id == null || videoInfo.id.isEmpty())
|| (videoInfo.title == null /* videoInfo.title can be empty of course */));
return videoInfo;
}
private static VideoInfo extractStreams(
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader)
throws ExctractionException, 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 {
videoInfo.dashMpdUrl = extractor.getDashMpdUrl();
} catch(Exception e) {
videoInfo.addException(new ExctractionException("Couldn't get Dash manifest", e));
}
/* Load and extract audio */
try {
videoInfo.audio_streams = extractor.getAudioStreams();
} catch(Exception e) {
videoInfo.addException(new ExctractionException("Couldn't get audio streams", e));
}
// also try to get streams from the dashMpd
if(videoInfo.dashMpdUrl != null && !videoInfo.dashMpdUrl.isEmpty()) {
if(videoInfo.audio_streams == null) {
videoInfo.audio_streams = new Vector<AudioStream>();
}
//todo: make this quick and dirty solution a real fallback
// same as the quick and dirty aboth
try {
videoInfo.audio_streams.addAll(
DashMpdParser.getAudioStreams(videoInfo.dashMpdUrl, downloader));
} catch(Exception e) {
videoInfo.addException(
new ExctractionException("Couldn't get audio streams from dash mpd", e));
}
}
/* Extract video stream url*/
try {
videoInfo.video_streams = extractor.getVideoStreams();
} catch (Exception e) {
videoInfo.addException(
new ExctractionException("Couldn't get video streams", e));
}
/* Extract video only stream url*/
try {
videoInfo.video_only_streams = extractor.getVideoOnlyStreams();
} catch(Exception e) {
videoInfo.addException(
new ExctractionException("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((videoInfo.video_streams == null || videoInfo.video_streams.isEmpty())
&& (videoInfo.audio_streams == null || videoInfo.audio_streams.isEmpty())
&& (videoInfo.dashMpdUrl == null || videoInfo.dashMpdUrl.isEmpty())) {
throw new ExctractionException("Could not get any stream. See error variable to get further details.");
}
return videoInfo;
}
private static VideoInfo extractOptionalData(
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader) {
/* ---- optional data goes here: ---- */
// If one of these failes, the frontend neets to handle that they are not available.
// Exceptions are therfore not thrown into the frontend, but stored into the error List,
// so the frontend can afterwads check where errors happend.
try {
videoInfo.thumbnail_url = extractor.getThumbnailUrl();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.duration = extractor.getLength();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.uploader = extractor.getUploader();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.description = extractor.getDescription();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.view_count = extractor.getViews();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.upload_date = extractor.getUploadDate();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.start_position = extractor.getTimeStamp();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.average_rating = extractor.getAverageRating();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.like_count = extractor.getLikeCount();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.dislike_count = extractor.getDislikeCount();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.next_video = extractor.getNextVideo();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.related_videos = extractor.getRelatedVideos();
} catch(Exception e) {
videoInfo.addException(e);
}
return videoInfo;
}
public String uploader_thumbnail_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 = 0;
public int like_count = -1;
public int dislike_count = -1;
public String average_rating = "";
public VideoPreviewInfo next_video = null;
public List<VideoPreviewInfo> related_videos = null;
//in seconds. some metadata is not passed using a VideoInfo object!
public int start_position = 0;
//todo: public int service_id = -1;
public List<Exception> errors = new Vector<>();
public VideoInfo() {}
/**Creates a new VideoInfo object from an existing AbstractVideoInfo.
* All the shared properties are copied to the new VideoInfo.*/
@SuppressWarnings("WeakerAccess")
public VideoInfo(AbstractVideoInfo avi) {
this.id = avi.id;
this.title = avi.title;
this.uploader = avi.uploader;
this.thumbnail_url = avi.thumbnail_url;
this.thumbnail = avi.thumbnail;
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 VideoPreviewInfo) {
//shitty String to convert code
String dur = ((VideoPreviewInfo)avi).duration;
int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":")));
int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length()));
this.duration = (minutes*60)+seconds;
}
}
public static 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 equalStats(cmp)
&& url == cmp.url;
}
}
@SuppressWarnings("unused")
public static 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 wether two streams are the same, but have diferent urls
public boolean equalStats(AudioStream cmp) {
return format == cmp.format
&& bandwidth == cmp.bandwidth
&& sampling_rate == cmp.sampling_rate;
}
// revelas wether two streams are equal
public boolean equals(AudioStream cmp) {
return equalStats(cmp)
&& url == cmp.url;
}
}
public void addException(Exception e) {
errors.add(e);
}
}

View File

@@ -1,11 +1,9 @@
package org.schabi.newpipe;
package org.schabi.newpipe.extractor;
import android.graphics.Bitmap;
import android.os.Parcel;
import android.os.Parcelable;
import org.schabi.newpipe.services.AbstractVideoInfo;
/**
* Created by Christian Schabesberger on 26.08.15.
*

View File

@@ -0,0 +1,32 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* VideoUrlIdHandler.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 VideoUrlIdHandler {
String getVideoUrl(String videoId);
String getVideoId(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

@@ -0,0 +1,209 @@
package org.schabi.newpipe.extractor.services.youtube;
import android.net.Uri;
import android.util.Log;
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.ParsingException;
import org.schabi.newpipe.extractor.SearchEngine;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
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.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* 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 implements SearchEngine {
private static final String TAG = YoutubeSearchEngine.class.toString();
@Override
public Result search(String query, int page, String languageCode, Downloader downloader)
throws IOException, ParsingException {
Result result = new Result();
Uri.Builder builder = new Uri.Builder();
builder.scheme("https")
.authority("www.youtube.com")
.appendPath("results")
.appendQueryParameter("search_query", query)
.appendQueryParameter("page", Integer.toString(page))
.appendQueryParameter("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);
}
try {
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)) {
result.suggestion = el.select("a").first().text();
// search message item
} else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) {
result.errorMessage = el.text();
// video item type
} else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
VideoPreviewInfo resultItem = new VideoPreviewInfo();
Element dl = el.select("h3").first().select("a").first();
resultItem.webpage_url = dl.attr("abs:href");
try {
Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)");
Matcher m = p.matcher(resultItem.webpage_url);
resultItem.id = m.group(1);
} catch (Exception e) {
//e.printStackTrace();
}
resultItem.title = dl.text();
resultItem.duration = item.select("span[class=\"video-time\"]").first().text();
resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first()
.select("a").first()
.text();
resultItem.upload_date = item.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").first()
.text();
//todo: test against view_count
String viewCountInfo = item.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").get(1)
.text();
viewCountInfo = viewCountInfo.substring(0, viewCountInfo.indexOf(' '));
viewCountInfo = viewCountInfo.replaceAll("[,.]", "");
resultItem.view_count = Long.parseLong(viewCountInfo);
Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first()
.select("img").first();
resultItem.thumbnail_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 (resultItem.thumbnail_url.contains(".gif")) {
resultItem.thumbnail_url = te.attr("abs:data-thumb");
}
result.resultList.add(resultItem);
} else {
//noinspection ConstantConditions
Log.e(TAG, "unexpected element found:\"" + el + "\"");
}
}
} catch(Exception e) {
throw new ParsingException(e);
}
return result;
}
@Override
public ArrayList<String> suggestionList(String query, Downloader dl)
throws IOException, ParsingException {
ArrayList<String> suggestions = new ArrayList<>();
Uri.Builder builder = new Uri.Builder();
builder.scheme("https")
.authority("suggestqueries.google.com")
.appendPath("complete")
.appendPath("search")
.appendQueryParameter("client", "")
.appendQueryParameter("output", "toolbar")
.appendQueryParameter("ds", "yt")
.appendQueryParameter("q", query);
String url = builder.build().toString();
String response = dl.download(url);
try {
//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("utf-8"))));
doc.getDocumentElement().normalize();
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
if (doc != null) {
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"));
}
}
} else {
Log.e(TAG, "GREAT FUCKING ERROR");
}
return suggestions;
} catch(Exception e) {
throw new ParsingException(e);
}
}
}

View File

@@ -1,8 +1,13 @@
package org.schabi.newpipe.services.youtube;
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.services.StreamingService;
import org.schabi.newpipe.services.VideoExtractor;
import org.schabi.newpipe.services.SearchEngine;
import org.schabi.newpipe.extractor.ExctractionException;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.StreamExtractor;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
import org.schabi.newpipe.extractor.SearchEngine;
import java.io.IOException;
/**
@@ -33,9 +38,11 @@ public class YoutubeService implements StreamingService {
return serviceInfo;
}
@Override
public VideoExtractor getExtractorInstance(String url) {
if(acceptUrl(url)) {
return new YoutubeVideoExtractor(url);
public StreamExtractor getExtractorInstance(String url, Downloader downloader)
throws ExctractionException, IOException {
VideoUrlIdHandler urlIdHandler = new YoutubeVideoUrlIdHandler();
if(urlIdHandler.acceptUrl(url)) {
return new YoutubeStreamExtractor(url, downloader) ;
}
else {
throw new IllegalArgumentException("supplied String is not a valid Youtube URL");
@@ -45,9 +52,9 @@ public class YoutubeService implements StreamingService {
public SearchEngine getSearchEngineInstance() {
return new YoutubeSearchEngine();
}
@Override
public boolean acceptUrl(String videoUrl) {
return videoUrl.contains("youtube") ||
videoUrl.contains("youtu.be");
public VideoUrlIdHandler getUrlIdHandler() {
return new YoutubeVideoUrlIdHandler();
}
}

View File

@@ -0,0 +1,696 @@
package org.schabi.newpipe.extractor.services.youtube;
import android.util.Log;
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.ExctractionException;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.ParsingException;
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
import org.schabi.newpipe.extractor.StreamExtractor;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.VideoInfo;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Vector;
/**
* 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 implements StreamExtractor {
// 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 nececeary for the url aboth
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 = null;
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");
}
public class DecryptException extends ParsingException {
DecryptException(Throwable cause) {
super(cause);
}
DecryptException(String message, Throwable cause) {
super(message, cause);
}
}
// special content not available exceptions
public class GemaException extends ContentNotAvailableException {
GemaException(String message) {
super(message);
}
}
// ----------------
private static final String TAG = YoutubeStreamExtractor.class.toString();
private final Document doc;
private JSONObject playerArgs;
//private Map<String, String> videoInfoPage;
// static values
private static final String DECRYPTION_FUNC_NAME="decrypt";
// cached values
private static volatile String decryptionCode = "";
VideoUrlIdHandler urlidhandler = new YoutubeVideoUrlIdHandler();
String pageUrl = "";
private Downloader downloader;
public YoutubeStreamExtractor(String pageUrl, Downloader dl) throws ExctractionException, IOException {
//most common videoInfo fields are now set in our superclass, for all services
downloader = dl;
this.pageUrl = pageUrl;
String pageContent = downloader.download(urlidhandler.cleanUrl(pageUrl));
doc = Jsoup.parse(pageContent, pageUrl);
String ytPlayerConfigRaw;
JSONObject ytPlayerConfig;
//attempt to load the youtube js player JSON arguments
try {
ytPlayerConfigRaw =
Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent);
ytPlayerConfig = new JSONObject(ytPlayerConfigRaw);
playerArgs = ytPlayerConfig.getJSONObject("args");
} catch (Parser.RegexException e) {
String errorReason = findErrorReason(doc);
switch(errorReason) {
case "GEMA":
throw new GemaException(errorReason);
case "":
throw new ParsingException("player config empty", e);
default:
throw new ContentNotAvailableException("Content not available", e);
}
} catch (JSONException e) {
throw new ParsingException("Could not parse yt player config");
}
/* not yet nececeary
// get videoInfo page
try {
//Parser.unescapeEntities(url_data_str, true).split("&")
String getVideoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
videoInfoPage = Parser.compatParseMap(downloader.download(getVideoInfoUrl));
} catch(Exception e) {
throw new ParsingException("Could not load video info page.", e);
}
*/
//----------------------------------
// load and parse description code, if it isn't already initialised
//----------------------------------
if (decryptionCode.isEmpty()) {
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.
JSONObject ytAssets = ytPlayerConfig.getJSONObject("assets");
String playerUrl = ytAssets.getString("js");
if (playerUrl.startsWith("//")) {
playerUrl = "https:" + playerUrl;
}
decryptionCode = loadDecryptionCode(playerUrl);
} catch (JSONException e) {
throw new ParsingException(
"Could not load decryption code for the Youtube service.", e);
}
}
}
@Override
public String getTitle() throws ParsingException {
try {//json player args method
return playerArgs.getString("title");
} catch(JSONException je) {//html <meta> method
je.printStackTrace();
Log.w(TAG, "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 {//json player args method
return playerArgs.getString("author");
} catch(JSONException je) {
je.printStackTrace();
Log.w(TAG,
"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 {
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 getViews() 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 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) {
Log.w(TAG, "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);
}
}
@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<VideoInfo.AudioStream> getAudioStreams() throws ParsingException {
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
try{
String encoded_url_map = playerArgs.getString("adaptive_fmts");
for(String url_data_str : encoded_url_map.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 VideoInfo.AudioStream(streamUrl,
itagItem.mediaFormatId,
itagItem.bandWidth,
itagItem.samplingRate));
}
}
}
} catch (Exception e) {
throw new ParsingException("Could not get audiostreams", e);
}
return audioStreams;
}
@Override
public List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException {
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
try{
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
for(String url_data_str : encoded_url_map.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 VideoInfo.VideoStream(
streamUrl,
itagItem.mediaFormatId,
itagItem.resolutionString));
}
}
} catch (Exception e) {
Log.w(TAG, "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<VideoInfo.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{1,3})", 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));
int ret = seconds + (60 * minutes) + (3600 * hours);//don't trust BODMAS!
//Log.d(TAG, "derived timestamp value:"+ret);
return 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 {
// Not yet implemented.
// Also you need to be logged in to see age restricted videos on youtube,
// therefore NP is not able to receive such videos.
return 0;
}
@Override
public String getAverageRating() throws ParsingException {
try {
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 {
likesString = doc.select("button.like-button-renderer-like-button").first()
.select("span.yt-uix-button-content").first().text();
return Integer.parseInt(likesString.replaceAll("[^\\d]", ""));
} 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 {
dislikesString = doc.select("button.like-button-renderer-dislike-button").first()
.select("span.yt-uix-button-content").first().text();
return Integer.parseInt(dislikesString.replaceAll("[^\\d]", ""));
} 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 VideoPreviewInfo 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 Vector<VideoPreviewInfo> getRelatedVideos() throws ParsingException {
try {
Vector<VideoPreviewInfo> relatedVideos = new Vector<>();
for (Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
// first check if we have a playlist. If so leave them out
if (li.select("a[class*=\"content-link\"]").first() != null) {
relatedVideos.add(extractVideoPreviewInfo(li));
}
}
return relatedVideos;
} catch(Exception e) {
throw new ParsingException("Could not get related videos", e);
}
}
@Override
public VideoUrlIdHandler getUrlIdConverter() {
return new YoutubeVideoUrlIdHandler();
}
@Override
public String getPageUrl() {
return pageUrl;
}
/**Provides information about links to other videos on the video page, such as related videos.
* This is encapsulated in a VideoPreviewInfo object,
* which is a subset of the fields in a full VideoInfo.*/
private VideoPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException {
VideoPreviewInfo info = new VideoPreviewInfo();
try {
info.webpage_url = li.select("a.content-link").first()
.attr("abs:href");
info.id = Parser.matchGroup1("v=([0-9a-zA-Z-]*)", info.webpage_url);
//todo: check NullPointerException causing
info.title = 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
//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 {
info.view_count = Long.parseLong(li.select("span.view-count")
.first().text().replaceAll("[^\\d]", ""));
} catch (NullPointerException e) {//related videos sometimes have no view count
info.view_count = 0;
}
info.uploader = li.select("span.g-hovercard").first().text();
info.duration = li.select("span.video-time").first().text();
Element img = li.select("img").first();
info.thumbnail_url = 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 (info.thumbnail_url.contains(".gif")) {
info.thumbnail_url = img.attr("data-thumb");
}
if (info.thumbnail_url.startsWith("//")) {
info.thumbnail_url = "https:" + info.thumbnail_url;
}
} catch (Exception e) {
throw new ParsingException(e);
}
return info;
}
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 {
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(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

@@ -0,0 +1,80 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.ParsingException;
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
/**
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* YoutubeVideoUrlIdHandler.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 YoutubeVideoUrlIdHandler implements VideoUrlIdHandler {
@SuppressWarnings("WeakerAccess")
@Override
public String getVideoUrl(String videoId) {
return "https://www.youtube.com/watch?v=" + videoId;
}
@SuppressWarnings("WeakerAccess")
@Override
public String getVideoId(String url) throws ParsingException {
String id = "";
if(url.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=([\\-a-zA-Z0-9_]{11})", query);
} catch(UnsupportedEncodingException uee) {
throw new ParsingException("Could not parse attribution_link", uee);
}
} else {
id = Parser.matchGroup1("youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})", url);
}
}
else if(url.contains("youtu.be")) {
id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url);
}
else {
throw new ParsingException("Error no suitable url: " + url);
}
if(!id.isEmpty()){
//Log.i(TAG, "string \""+url+"\" matches!");
return id;
} else {
throw new ParsingException("Error could not parse url: " + url);
}
}
public String cleanUrl(String complexUrl) throws ParsingException {
return getVideoUrl(getVideoId(complexUrl));
}
@Override
public boolean acceptUrl(String videoUrl) {
return videoUrl.contains("youtube") ||
videoUrl.contains("youtu.be");
}
}

View File

@@ -1,16 +0,0 @@
package org.schabi.newpipe.services;
import android.graphics.Bitmap;
/**Common properties between VideoInfo and VideoPreviewInfo.*/
public abstract class AbstractVideoInfo {
public String id = "";
public String title = "";
public String uploader = "";
//public int duration = -1;
public String thumbnail_url = "";
public Bitmap thumbnail = null;
public String webpage_url = "";
public String upload_date = "";
public long view_count = -1;
}

View File

@@ -1,118 +0,0 @@
package org.schabi.newpipe.services;
/**
* Created by Christian Schabesberger on 10.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* VideoExtractor.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.VideoInfo;
/**Scrapes information from a video streaming service (eg, YouTube).*/
@SuppressWarnings("ALL")
public abstract class VideoExtractor {
protected final String pageUrl;
protected VideoInfo videoInfo;
@SuppressWarnings("WeakerAccess")
public VideoExtractor(String url) {
this.pageUrl = url;
}
/**Fills out the video info fields which are common to all services.
* Probably needs to be overridden by subclasses*/
public VideoInfo getVideoInfo()
{
if(videoInfo == null) {
videoInfo = new VideoInfo();
}
if(videoInfo.webpage_url.isEmpty()) {
videoInfo.webpage_url = pageUrl;
}
if(videoInfo.title.isEmpty()) {
videoInfo.title = getTitle();
}
if(videoInfo.duration < 1) {
videoInfo.duration = getLength();
}
if(videoInfo.uploader.isEmpty()) {
videoInfo.uploader = getUploader();
}
if(videoInfo.description.isEmpty()) {
videoInfo.description = getDescription();
}
if(videoInfo.view_count == -1) {
videoInfo.view_count = getViews();
}
if(videoInfo.upload_date.isEmpty()) {
videoInfo.upload_date = getUploadDate();
}
if(videoInfo.thumbnail_url.isEmpty()) {
videoInfo.thumbnail_url = getThumbnailUrl();
}
if(videoInfo.id.isEmpty()) {
videoInfo.id = getVideoId(pageUrl);
}
/** Load and extract audio*/
if(videoInfo.audioStreams == null) {
videoInfo.audioStreams = getAudioStreams();
}
/** Extract video stream url*/
if(videoInfo.videoStreams == null) {
videoInfo.videoStreams = getVideoStreams();
}
if(videoInfo.uploader_thumbnail_url.isEmpty()) {
videoInfo.uploader_thumbnail_url = getUploaderThumbnailUrl();
}
if(videoInfo.startPosition < 0) {
videoInfo.startPosition = getTimeStamp();
}
//Bitmap thumbnail = null;
//Bitmap uploader_thumbnail = null;
//int videoAvailableStatus = VIDEO_AVAILABLE;
return videoInfo;
}
protected abstract String getVideoUrl(String videoId);
protected abstract String getVideoId(String siteUrl);
protected abstract int getTimeStamp();
protected abstract String getTitle();
protected abstract String getDescription();
protected abstract String getUploader();
protected abstract int getLength();
protected abstract int getViews();
protected abstract String getUploadDate();
protected abstract String getThumbnailUrl();
protected abstract String getUploaderThumbnailUrl();
protected abstract VideoInfo.AudioStream[] getAudioStreams();
protected abstract VideoInfo.VideoStream[] getVideoStreams();
}

View File

@@ -1,190 +0,0 @@
package org.schabi.newpipe.services.youtube;
import android.net.Uri;
import android.util.Log;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.services.SearchEngine;
import org.schabi.newpipe.VideoPreviewInfo;
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.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* 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 implements SearchEngine {
private static final String TAG = YoutubeSearchEngine.class.toString();
@Override
public Result search(String query, int page, String languageCode) {
//String contentCountry = PreferenceManager.getDefaultSharedPreferences(this).getString(getString(R.string., "");
Uri.Builder builder = new Uri.Builder();
builder.scheme("https")
.authority("www.youtube.com")
.appendPath("results")
.appendQueryParameter("search_query", query)
.appendQueryParameter("page", Integer.toString(page))
.appendQueryParameter("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);
Result result = new Result();
Element list = doc.select("ol[class=\"item-section\"]").first();
int i = 0;
for(Element item : list.children()) {
i++;
/* 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)) {
result.suggestion = el.select("a").first().text();
// search message item
} else if(!((el = item.select("div[class*=\"search-message\"]").first()) == null)) {
result.errorMessage = el.text();
// video item type
} else if(!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
VideoPreviewInfo resultItem = new VideoPreviewInfo();
Element dl = el.select("h3").first().select("a").first();
resultItem.webpage_url = dl.attr("abs:href");
try {
Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)");
Matcher m = p.matcher(resultItem.webpage_url);
resultItem.id=m.group(1);
} catch (Exception e) {
//e.printStackTrace();
}
resultItem.title = dl.text();
resultItem.duration = item.select("span[class=\"video-time\"]").first().text();
resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first()
.select("a").first()
.text();
resultItem.upload_date = item.select("div[class=\"yt-lockup-meta\"]").first()
.select("li").first()
.text();
Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first()
.select("img").first();
resultItem.thumbnail_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(resultItem.thumbnail_url.contains(".gif")) {
resultItem.thumbnail_url = te.attr("abs:data-thumb");
}
result.resultList.add(resultItem);
} else {
//noinspection ConstantConditions
Log.e(TAG, "unexpected element found:\""+el+"\"");
}
}
return result;
}
@Override
public ArrayList<String> suggestionList(String query) {
ArrayList<String> suggestions = new ArrayList<>();
Uri.Builder builder = new Uri.Builder();
builder.scheme("https")
.authority("suggestqueries.google.com")
.appendPath("complete")
.appendPath("search")
.appendQueryParameter("client", "")
.appendQueryParameter("output", "toolbar")
.appendQueryParameter("ds", "yt")
.appendQueryParameter("q", query);
String url = builder.build().toString();
String response = Downloader.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("utf-8"))));
doc.getDocumentElement().normalize();
}catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
if(doc!=null){
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"));
}
}
}else {
Log.e(TAG, "GREAT FUCKING ERROR");
}
return suggestions;
}
}

View File

@@ -1,615 +0,0 @@
package org.schabi.newpipe.services.youtube;
import android.util.Log;
import android.util.Xml;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptableObject;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.services.VideoExtractor;
import org.schabi.newpipe.MediaFormat;
import org.schabi.newpipe.VideoInfo;
import org.schabi.newpipe.VideoPreviewInfo;
import org.xmlpull.v1.XmlPullParser;
import java.io.StringReader;
import java.net.URLDecoder;
import java.util.HashMap;
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>
* YoutubeVideoExtractor.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 YoutubeVideoExtractor extends VideoExtractor {
private static final String TAG = YoutubeVideoExtractor.class.toString();
private final Document doc;
private JSONObject jsonObj;
private JSONObject playerArgs;
// static values
private static final String DECRYPTION_FUNC_NAME="decrypt";
// cached values
private static volatile String decryptionCode = "";
public YoutubeVideoExtractor(String pageUrl) {
super(pageUrl);//most common videoInfo fields are now set in our superclass, for all services
String pageContents = Downloader.download(cleanUrl(pageUrl));
doc = Jsoup.parse(pageContents, pageUrl);
//attempt to load the youtube js player JSON arguments
try {
String jsonString = matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContents);
jsonObj = new JSONObject(jsonString);
playerArgs = jsonObj.getJSONObject("args");
} catch (Exception e) {//if this fails, the video is most likely not available.
// Determining why is done later.
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE;
Log.e(TAG, "Could not load JSON data for Youtube video \""+pageUrl+"\". This most likely means the video is unavailable");
}
//----------------------------------
// load and parse description code, if it isn't already initialised
//----------------------------------
if (decryptionCode.isEmpty()) {
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.
JSONObject ytAssets = jsonObj.getJSONObject("assets");
String playerUrl = ytAssets.getString("js");
if (playerUrl.startsWith("//")) {
playerUrl = "https:" + playerUrl;
}
decryptionCode = loadDecryptionCode(playerUrl);
} catch (Exception e){
Log.e(TAG, "Could not load decryption code for the Youtube service.");
e.printStackTrace();
}
}
}
@Override
public String getTitle() {
try {//json player args method
return playerArgs.getString("title");
} catch(JSONException je) {//html <meta> method
je.printStackTrace();
Log.w(TAG, "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) {
Log.e(TAG, "failed permanently to load title.");
e.printStackTrace();
return "";
}
}
@Override
public String getDescription() {
try {
return doc.select("p[id=\"eow-description\"]").first().html();
} catch (Exception e) {//todo: add fallback method
Log.e(TAG, "failed to load description.");
e.printStackTrace();
return "";
}
}
@Override
public String getUploader() {
try {//json player args method
return playerArgs.getString("author");
} catch(JSONException je) {
je.printStackTrace();
Log.w(TAG, "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) {
e.printStackTrace();
Log.e(TAG, "failed permanently to load uploader name.");
return "";
}
}
@Override
public int getLength() {
try {
return playerArgs.getInt("length_seconds");
} catch (JSONException je) {//todo: find fallback method
Log.e(TAG, "failed to load video duration from JSON args");
je.printStackTrace();
return -1;
}
}
@Override
public int getViews() {
try {
String viewCountString = doc.select("meta[itemprop=interactionCount]").attr("content");
return Integer.parseInt(viewCountString);
} catch (Exception e) {//todo: find fallback method
Log.e(TAG, "failed to number of views");
e.printStackTrace();
return -1;
}
}
@Override
public String getUploadDate() {
try {
return doc.select("meta[itemprop=datePublished]").attr("content");
} catch (Exception e) {//todo: add fallback method
Log.e(TAG, "failed to get upload date.");
e.printStackTrace();
return "";
}
}
@Override
public String getThumbnailUrl() {
//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) {
Log.w(TAG, "Could not find high res Thumbnail. Using low res instead");
//fall through to fallback
} try {
return playerArgs.getString("thumbnail_url");
} catch (JSONException je) {
je.printStackTrace();
Log.w(TAG, "failed to extract thumbnail URL from JSON args; trying to extract it from HTML");
return "";
}
}
@Override
public String getUploaderThumbnailUrl() {
try {
return doc.select("a[class*=\"yt-user-photo\"]").first()
.select("img").first()
.attr("abs:data-thumb");
} catch (Exception e) {//todo: add fallback method
Log.e(TAG, "failed to get uploader thumbnail URL.");
e.printStackTrace();
return "";
}
}
@Override
public VideoInfo.AudioStream[] getAudioStreams() {
try {
String dashManifest = playerArgs.getString("dashmpd");
return parseDashManifest(dashManifest, decryptionCode);
} catch (NullPointerException e) {
Log.e(TAG, "Could not find \"dashmpd\" upon the player args (maybe no dash manifest available).");
} catch (Exception e) {
e.printStackTrace();
}
return new VideoInfo.AudioStream[0];
}
@Override
public VideoInfo.VideoStream[] getVideoStreams() {
try{
//------------------------------------
// extract video stream url
//------------------------------------
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
for(String url_data_str : encoded_url_map.split(",")) {
Map<String, String> tags = new HashMap<>();
for(String raw_tag : Parser.unescapeEntities(url_data_str, true).split("&")) {
String[] split_tag = raw_tag.split("=");
tags.put(split_tag[0], split_tag[1]);
}
int itag = Integer.parseInt(tags.get("itag"));
String streamUrl = URLDecoder.decode(tags.get("url"), "UTF-8");
// 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);
}
if(resolveFormat(itag) != -1) {
videoStreams.add(new VideoInfo.VideoStream(
streamUrl,
resolveFormat(itag),
resolveResolutionString(itag)));
}
}
return videoStreams.toArray(new VideoInfo.VideoStream[videoStreams.size()]);
} catch (Exception e) {
Log.e(TAG, "Failed to get video stream");
e.printStackTrace();
return new VideoInfo.VideoStream[0];
}
}
/**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 */
@SuppressWarnings("WeakerAccess")
public static int resolveFormat(int itag) {
switch(itag) {
// video
case 17: return MediaFormat.v3GPP.id;
case 18: return MediaFormat.MPEG_4.id;
case 22: return MediaFormat.MPEG_4.id;
case 36: return MediaFormat.v3GPP.id;
case 37: return MediaFormat.MPEG_4.id;
case 38: return MediaFormat.MPEG_4.id;
case 43: return MediaFormat.WEBM.id;
case 44: return MediaFormat.WEBM.id;
case 45: return MediaFormat.WEBM.id;
case 46: return MediaFormat.WEBM.id;
default:
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
return -1;
}
}
@SuppressWarnings("WeakerAccess")
public static String resolveResolutionString(int itag) {
switch(itag) {
case 17: return "144p";
case 18: return "360p";
case 22: return "720p";
case 36: return "240p";
case 37: return "1080p";
case 38: return "1080p";
case 43: return "360p";
case 44: return "480p";
case 45: return "720p";
case 46: return "1080p";
default:
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
return null;
}
}
@SuppressWarnings("WeakerAccess")
@Override
public String getVideoId(String url) {
String id;
String pat;
if(url.contains("youtube")) {
pat = "youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})";
}
else if(url.contains("youtu.be")) {
pat = "youtu\\.be/([a-zA-Z0-9_-]{11})";
}
else {
Log.e(TAG, "Error could not parse url: " + url);
return "";
}
id = matchGroup1(pat, url);
if(!id.isEmpty()){
//Log.i(TAG, "string \""+url+"\" matches!");
return id;
}
//Log.i(TAG, "string \""+url+"\" does not match.");
return "";
}
@SuppressWarnings("WeakerAccess")
@Override
public String getVideoUrl(String videoId) {
return "https://www.youtube.com/watch?v=" + videoId;
}
/**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(){
String timeStamp = matchGroup1("((#|&)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)", pageUrl);
//TODO: test this
if(!timeStamp.isEmpty()) {
String secondsString = matchGroup1("(\\d{1,3})s", timeStamp);
String minutesString = matchGroup1("(\\d{1,3})m", timeStamp);
String hoursString = matchGroup1("(\\d{1,3})h", timeStamp);
if(secondsString.isEmpty()//if nothing was got,
&& minutesString.isEmpty()//treat as unlabelled seconds
&& hoursString.isEmpty())
secondsString = matchGroup1("t=(\\d{1,3})", 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));
int ret = seconds + (60*minutes) + (3600*hours);//don't trust BODMAS!
//Log.d(TAG, "derived timestamp value:"+ret);
return ret;
//the ordering varies internationally
}//else, return default 0
return 0;
}
@Override
public VideoInfo getVideoInfo() {
videoInfo = super.getVideoInfo();
//todo: replace this with a call to getVideoId, if possible
videoInfo.id = matchGroup1("v=([0-9a-zA-Z_-]{11})", pageUrl);
if(videoInfo.audioStreams == null
|| videoInfo.audioStreams.length == 0) {
Log.e(TAG, "uninitialised audio streams!");
}
if(videoInfo.videoStreams == null
|| videoInfo.videoStreams.length == 0) {
Log.e(TAG, "uninitialised video streams!");
}
videoInfo.age_limit = 0;
//average rating
try {
videoInfo.average_rating = playerArgs.getString("avg_rating");
} catch (JSONException e) {
e.printStackTrace();
}
//---------------------------------------
// extracting information from html page
//---------------------------------------
// Determine what went wrong when the Video is not available
if(videoInfo.videoAvailableStatus == VideoInfo.VIDEO_UNAVAILABLE) {
if(doc.select("h1[id=\"unavailable-message\"]").first().text().contains("GEMA")) {
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE_GEMA;
}
}
String likesString = "";
String dislikesString = "";
try {
// likes
likesString = doc.select("button.like-button-renderer-like-button").first()
.select("span.yt-uix-button-content").first().text();
videoInfo.like_count = Integer.parseInt(likesString.replaceAll("[^\\d]", ""));
// dislikes
dislikesString = doc.select("button.like-button-renderer-dislike-button").first()
.select("span.yt-uix-button-content").first().text();
videoInfo.dislike_count = Integer.parseInt(dislikesString.replaceAll("[^\\d]", ""));
} catch(NumberFormatException nfe) {
Log.e(TAG, "failed to parse likesString \""+likesString+"\" and dislikesString \""+
dislikesString+"\" as integers");
} catch(Exception e) {
// if it fails we know that the video does not offer dislikes.
e.printStackTrace();
videoInfo.like_count = 0;
videoInfo.dislike_count = 0;
}
// next video
videoInfo.nextVideo = extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first()
.select("li").first());
// related videos
Vector<VideoPreviewInfo> relatedVideos = new Vector<>();
for(Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
// first check if we have a playlist. If so leave them out
if(li.select("a[class*=\"content-link\"]").first() != null) {
relatedVideos.add(extractVideoPreviewInfo(li));
}
}
//todo: replace conversion
videoInfo.relatedVideos = relatedVideos;
//videoInfo.relatedVideos = relatedVideos.toArray(new VideoPreviewInfo[relatedVideos.size()]);
return videoInfo;
}
private VideoInfo.AudioStream[] parseDashManifest(String dashManifest, String decryptoinCode) {
if(!dashManifest.contains("/signature/")) {
String encryptedSig = matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifest);
String decryptedSig;
decryptedSig = decryptSignature(encryptedSig, decryptoinCode);
dashManifest = dashManifest.replace("/s/" + encryptedSig, "/signature/" + decryptedSig);
}
String dashDoc = Downloader.download(dashManifest);
Vector<VideoInfo.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:
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 VideoInfo.AudioStream(parser.getText(),
format, currentBandwidth, currentSamplingRate));
}
//missing break here?
case XmlPullParser.END_TAG:
if(tagName.equals("AdaptationSet")) {
currentMimeType = "";
} else if(tagName.equals("BaseURL")) {
currentTagIsBaseUrl = false;
}//no break needed here
}
}
} catch(Exception e) {
e.printStackTrace();
}
return audioStreams.toArray(new VideoInfo.AudioStream[audioStreams.size()]);
}
/**Provides information about links to other videos on the video page, such as related videos.
* This is encapsulated in a VideoPreviewInfo object,
* which is a subset of the fields in a full VideoInfo.*/
private VideoPreviewInfo extractVideoPreviewInfo(Element li) {
VideoPreviewInfo info = new VideoPreviewInfo();
info.webpage_url = li.select("a.content-link").first()
.attr("abs:href");
try {
info.id = matchGroup1("v=([0-9a-zA-Z-]*)", info.webpage_url);
} catch (Exception e) {
e.printStackTrace();
}
//todo: check NullPointerException causing
info.title = 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
String views = li.select("span.view-count").first().text();
//Log.i(TAG, "title:"+info.title);
//Log.i(TAG, "view count:"+views);
try {
info.view_count = Long.parseLong(li.select("span.view-count")
.first().text().replaceAll("[^\\d]", ""));
} catch (NullPointerException e) {//related videos sometimes have no view count
info.view_count = 0;
}
info.uploader = li.select("span.g-hovercard").first().text();
info.duration = li.select("span.video-time").first().text();
Element img = li.select("img").first();
info.thumbnail_url = 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(info.thumbnail_url.contains(".gif")) {
info.thumbnail_url = img.attr("data-thumb");
}
if(info.thumbnail_url.startsWith("//")) {
info.thumbnail_url = "https:" + info.thumbnail_url;
}
return info;
}
private String loadDecryptionCode(String playerUrl) {
String playerCode = Downloader.download(playerUrl);
String decryptionFuncName = "";
String decryptionFunc = "";
String helperObjectName;
String helperObject = "";
String callerFunc = "function " + DECRYPTION_FUNC_NAME + "(a){return %%(a);}";
String decryptionCode;
try {
decryptionFuncName = matchGroup1("\\.sig\\|\\|([a-zA-Z0-9$]+)\\(", playerCode);
String functionPattern = "(" + decryptionFuncName.replace("$", "\\$") +"=function\\([a-zA-Z0-9_]*\\)\\{.+?\\})";
decryptionFunc = "var " + matchGroup1(functionPattern, playerCode) + ";";
helperObjectName = matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(", decryptionFunc);
String helperPattern = "(var " + helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)";
helperObject = matchGroup1(helperPattern, playerCode);
} catch (Exception e) {
e.printStackTrace();
}
callerFunc = callerFunc.replace("%%", decryptionFuncName);
decryptionCode = helperObject + decryptionFunc + callerFunc;
return decryptionCode;
}
private String decryptSignature(String encryptedSig, String decryptionCode) {
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) {
e.printStackTrace();
}
Context.exit();
return (result == null ? "" : result.toString());
}
private String cleanUrl(String complexUrl) {
return getVideoUrl(getVideoId(complexUrl));
}
private String matchGroup1(String pattern, String input) {
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+"\"");
new Exception("failed to find pattern \""+pattern+"\"").printStackTrace();
return "";
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".VideoItemDetailFragment"
@@ -10,24 +10,13 @@
android:layout_height="match_parent"
android:id="@+id/videoitem_detail">
<ImageView android:id="@+id/detailThumbnailView"
android:contentDescription="@string/detailThumbnailViewDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="@android:color/black"
android:src="@drawable/dummy_thumbnail_dark"
tools:ignore="RtlHardcoded" />
<ScrollView
<com.nirhart.parallaxscroll.views.ParallaxScrollView
android:id="@+id/detailMainContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
android:visibility="visible"
app:parallax_factor="1.9"
tools:ignore="UselessParent">
<RelativeLayout
android:layout_width="match_parent"
@@ -36,24 +25,34 @@
<RelativeLayout
android:id="@+id/detailVideoThumbnailWindowLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/video_item_detail_thumbnail_image_height"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<ImageView android:id="@+id/detailThumbnailView"
android:contentDescription="@string/detail_thumbnail_view_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="@android:color/black"
android:src="@drawable/dummy_thumbnail_dark"/>
<ProgressBar android:id="@+id/detailProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/playVideoButton"
android:visibility="invisible"
<ImageView android:id="@+id/playArrowView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_centerInParent="true"
app:backgroundTint="@color/primaryColorYoutube"
android:src="@drawable/ic_play_arrow_black"
android:layout_margin="@dimen/video_item_detail_play_fab_margin"/>
android:src="@drawable/ic_play_circle_transparent"
android:visibility="invisible"/>
<Button
android:id="@+id/detailVideoThumbnailWindowBackgroundButton"
@@ -64,121 +63,173 @@
</RelativeLayout>
<RelativeLayout android:id="@+id/detailTextContentLayout"
android:visibility="invisible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_detail_info_text_padding"
android:layout_height="match_parent"
android:layout_below="@id/detailVideoThumbnailWindowLayout"
android:background="@color/background_gray">
android:background="@color/light_background_color"
android:visibility="gone">
<TextView android:id="@+id/detailVideoTitleView"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_title_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
android:id="@+id/detailTopView">
<ImageView android:id="@+id/detailUploaderThumbnailView"
android:contentDescription="@string/detailUploaderThumbnailViewDescription"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:layout_below="@id/detailVideoTitleView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@drawable/buddy"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailVideoTitleView"
android:layout_width="0dp"
android:layout_weight=".7"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_title_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_marginTop="12dp"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:text="Title"/>
<TextView android:id="@+id/detailUploaderView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploaderThumbnailView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
<ImageView
android:layout_width="15dp"
android:layout_height="30dp"
android:id="@+id/toggleDescriptionView"
android:src="@drawable/arrow_down"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="8dp"/>
</LinearLayout>
<TextView android:id="@+id/detailViewCountView"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailVideoTitleView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="@dimen/video_item_detail_views_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:text="10,069,948 views"
android:layout_below="@id/detailTopView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="5dp" />
<TextView android:id="@+id/detailThumbsDownCountView"
android:layout_width="wrap_content"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailViewCountView"
android:id="@+id/detailExtraView"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:visibility="gone">
<TextView android:id="@+id/detailUploadDateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Upload date"
android:layout_marginTop="3dp" />
<TextView android:id="@+id/detailDescriptionView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_description_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_below="@id/detailUploadDateView"
android:text="Description............."
android:layout_marginTop="3dp" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_below="@+id/detailExtraView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:ignore="RtlHardcoded" />
android:layout_marginTop="5dp">
<ImageView android:id="@+id/detailThumbsUpImgView"
android:contentDescription="@string/detail_likes_img_view_description"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:src="@drawable/thumbs_up" />
<ImageView android:id="@+id/detailThumbsDownImgView"
android:contentDescription="@string/detailThumbsDownImgViewDescription"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsDownCountView"
android:layout_toStartOf="@id/detailThumbsDownCountView"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:src="@drawable/thumbs_down"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailThumbsUpCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="200" />
<TextView android:id="@+id/detailThumbsUpCountView"
android:layout_width="wrap_content"
<ImageView android:id="@+id/detailThumbsDownImgView"
android:contentDescription="@string/detail_dislikes_img_view_description"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:src="@drawable/thumbs_down"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"/>
<TextView android:id="@+id/detailThumbsDownCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="100" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsDownImgView"
android:layout_toStartOf="@id/detailThumbsDownImgView"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:ignore="RtlHardcoded" />
android:layout_below="@+id/linearLayout"
android:id="@+id/detailUploaderWrapView"
android:layout_marginTop="12dp">
<ImageView android:id="@+id/detailThumbsUpImgView"
android:contentDescription="@string/detailThumbsUpImgViewDescription"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsUpCountView"
android:layout_toStartOf="@id/detailThumbsUpCountView"
android:src="@drawable/thumbs_up"
tools:ignore="RtlHardcoded" />
<View
android:background="#000"
android:layout_width="match_parent"
android:layout_height="1px" />
<TextView android:id="@+id/detailUploadDateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploaderView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
<de.hdodenhof.circleimageview.CircleImageView android:id="@+id/detailUploaderThumbnailView"
android:contentDescription="@string/detail_uploader_thumbnail_view_description"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:src="@drawable/buddy"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"/>
<TextView android:id="@+id/detailDescriptionView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploadDateView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="@dimen/video_item_detail_description_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailUploaderView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Uploader"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/detailUploaderThumbnailView"
android:layout_toEndOf="@+id/detailUploaderThumbnailView"
android:layout_marginLeft="15dp"
android:layout_marginStart="28dp" />
<View
android:background="#000"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_below="@id/detailUploaderThumbnailView"/>
</RelativeLayout>
<RelativeLayout android:id="@+id/detailNextVideoRootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_detail_info_text_padding"
android:layout_below="@id/detailDescriptionView" >
android:layout_height="match_parent"
android:layout_gravity="center_horizontal|bottom"
android:layout_below="@+id/detailUploaderWrapView"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp">
<TextView android:id="@+id/detailNextVideoTitle"
android:layout_width="wrap_content"
@@ -186,12 +237,12 @@
android:layout_centerHorizontal="true"
android:textSize="@dimen/video_item_detail_next_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/nextVideoTitle"
android:text="@string/next_video_title"
android:textAllCaps="true" />
<RelativeLayout android:id="@+id/detailNextVidButtonAndContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_below="@id/detailNextVideoTitle">
<FrameLayout
android:id="@+id/detailNextVideoFrame"
@@ -205,15 +256,24 @@
android:layout_alignBottom="@id/detailNextVideoFrame"
android:background="?attr/selectableItemBackground"/>
</RelativeLayout>
<Button android:id="@+id/detailShowSimilarButton"
android:layout_width="match_parent"
<TextView android:id="@+id/detailSimilarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="@dimen/video_item_detail_next_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/similar_videos_btn_text"
android:layout_below="@id/detailNextVidButtonAndContentLayout"
android:textSize="@dimen/video_item_detail_similar_text_size"
android:text="@string/showSimilarVideosButtonText"/>
android:textAllCaps="true" />
<LinearLayout
android:id="@+id/similarVideosView"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/detailSimilarTitle">
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</ScrollView>
</RelativeLayout>
</com.nirhart.parallaxscroll.views.ParallaxScrollView>
</FrameLayout>

View File

@@ -1,221 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".VideoItemDetailFragment"
android:textIsSelectable="true"
style="?android:attr/textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/videoitem_detail">
<ImageView android:id="@+id/detailThumbnailView"
android:contentDescription="@string/detailThumbnailViewDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="@android:color/black"
android:src="@drawable/dummy_thumbnail_dark"
tools:ignore="RtlHardcoded" />
<ScrollView
android:id="@+id/detailMainContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/detailVideoThumbnailWindowLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/video_item_detail_thumbnail_image_height"
android:background="?attr/selectableItemBackground">
<ProgressBar android:id="@+id/detailProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/primaryColorYoutube"
android:indeterminateTintMode="src_in"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/playVideoButton"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:backgroundTint="@color/primaryColorYoutube"
android:src="@drawable/ic_play_arrow_black"
android:layout_margin="@dimen/video_item_detail_play_fab_margin"/>
<Button
android:id="@+id/detailVideoThumbnailWindowBackgroundButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"/>
</RelativeLayout>
<RelativeLayout android:id="@+id/detailTextContentLayout"
android:visibility="invisible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_detail_info_text_padding"
android:layout_below="@id/detailVideoThumbnailWindowLayout"
android:background="@color/background_gray">
<TextView android:id="@+id/detailVideoTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_title_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
<ImageView android:id="@+id/detailUploaderThumbnailView"
android:contentDescription="@string/detailUploaderThumbnailViewDescription"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:layout_below="@id/detailVideoTitleView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@drawable/buddy"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailUploaderView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploaderThumbnailView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailViewCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailVideoTitleView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="@dimen/video_item_detail_views_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailThumbsDownCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailViewCountView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:ignore="RtlHardcoded" />
<ImageView android:id="@+id/detailThumbsDownImgView"
android:contentDescription="@string/detailThumbsDownImgViewDescription"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsDownCountView"
android:layout_toStartOf="@id/detailThumbsDownCountView"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:src="@drawable/thumbs_down"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailThumbsUpCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsDownImgView"
android:layout_toStartOf="@id/detailThumbsDownImgView"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:ignore="RtlHardcoded" />
<ImageView android:id="@+id/detailThumbsUpImgView"
android:contentDescription="@string/detailThumbsUpImgViewDescription"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsUpCountView"
android:layout_toStartOf="@id/detailThumbsUpCountView"
android:src="@drawable/thumbs_up"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailUploadDateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploaderView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="RtlHardcoded" />
<TextView android:id="@+id/detailDescriptionView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploadDateView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="@dimen/video_item_detail_description_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:ignore="RtlHardcoded" />
<RelativeLayout android:id="@+id/detailNextVideoRootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_detail_info_text_padding"
android:layout_below="@id/detailDescriptionView" >
<TextView android:id="@+id/detailNextVideoTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="@dimen/video_item_detail_next_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/nextVideoTitle"
android:textAllCaps="true" />
<RelativeLayout android:id="@+id/detailNextVidButtonAndContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailNextVideoTitle">
<FrameLayout
android:id="@+id/detailNextVideoFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/detailNextVideoButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignTop="@id/detailNextVideoFrame"
android:layout_alignBottom="@id/detailNextVideoFrame"
android:background="?attr/selectableItemBackground"/>
</RelativeLayout>
<Button android:id="@+id/detailShowSimilarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailNextVidButtonAndContentLayout"
android:textSize="@dimen/video_item_detail_similar_text_size"
android:text="@string/showSimilarVideosButtonText"/>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</ScrollView>
</RelativeLayout>

View File

@@ -10,11 +10,12 @@
android:layout_height="match_parent"
android:id="@+id/videoitem_detail">
<ScrollView
<com.nirhart.parallaxscroll.views.ParallaxScrollView
android:id="@+id/detailMainContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
app:parallax_factor="1.9"
tools:ignore="UselessParent">
<RelativeLayout
@@ -28,7 +29,7 @@
android:background="?attr/selectableItemBackground">
<ImageView android:id="@+id/detailThumbnailView"
android:contentDescription="@string/detailThumbnailViewDescription"
android:contentDescription="@string/detail_thumbnail_view_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
@@ -51,7 +52,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:backgroundTint="@color/primaryColorYoutube"
app:backgroundTint="@color/light_youtube_primary_color"
android:src="@drawable/ic_play_arrow_black"
android:layout_margin="@dimen/video_item_detail_play_fab_margin"/>
@@ -64,110 +65,173 @@
</RelativeLayout>
<RelativeLayout android:id="@+id/detailTextContentLayout"
android:visibility="invisible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_detail_info_text_padding"
android:layout_height="match_parent"
android:layout_below="@id/detailVideoThumbnailWindowLayout"
android:background="@color/background_gray">
android:background="@color/light_background_color"
android:visibility="gone">
<TextView android:id="@+id/detailVideoTitleView"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_title_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"/>
android:id="@+id/detailTopView">
<ImageView android:id="@+id/detailUploaderThumbnailView"
android:contentDescription="@string/detailUploaderThumbnailViewDescription"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:layout_below="@id/detailVideoTitleView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@drawable/buddy" />
<TextView android:id="@+id/detailVideoTitleView"
android:layout_width="0dp"
android:layout_weight=".7"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_title_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_marginTop="12dp"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:text="Title"/>
<TextView android:id="@+id/detailUploaderView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploaderThumbnailView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textAppearance="?android:attr/textAppearanceLarge" />
<ImageView
android:layout_width="15dp"
android:layout_height="30dp"
android:id="@+id/toggleDescriptionView"
android:src="@drawable/arrow_down"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="8dp"/>
</LinearLayout>
<TextView android:id="@+id/detailViewCountView"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailVideoTitleView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="@dimen/video_item_detail_views_text_size"
android:textAppearance="?android:attr/textAppearanceLarge" />
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:text="10,069,948 views"
android:layout_below="@id/detailTopView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="5dp" />
<TextView android:id="@+id/detailThumbsDownCountView"
android:layout_width="wrap_content"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailViewCountView"
android:id="@+id/detailExtraView"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:visibility="gone">
<TextView android:id="@+id/detailUploadDateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Upload date"
android:layout_marginTop="3dp" />
<TextView android:id="@+id/detailDescriptionView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_description_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_below="@id/detailUploadDateView"
android:text="Description............."
android:layout_marginTop="3dp" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_below="@+id/detailExtraView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium" />
android:layout_marginTop="5dp">
<ImageView android:id="@+id/detailThumbsUpImgView"
android:contentDescription="@string/detail_likes_img_view_description"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:src="@drawable/thumbs_up" />
<ImageView android:id="@+id/detailThumbsDownImgView"
android:contentDescription="@string/detailThumbsDownImgViewDescription"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsDownCountView"
android:layout_toStartOf="@id/detailThumbsDownCountView"
android:src="@drawable/thumbs_down" />
<TextView android:id="@+id/detailThumbsUpCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="200" />
<TextView android:id="@+id/detailThumbsUpCountView"
android:layout_width="wrap_content"
<ImageView android:id="@+id/detailThumbsDownImgView"
android:contentDescription="@string/detail_dislikes_img_view_description"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:src="@drawable/thumbs_down"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"/>
<TextView android:id="@+id/detailThumbsDownCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="100" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsDownImgView"
android:layout_toStartOf="@id/detailThumbsDownImgView"
android:textSize="@dimen/video_item_detail_likes_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"/>
android:layout_below="@+id/linearLayout"
android:id="@+id/detailUploaderWrapView"
android:layout_marginTop="12dp">
<ImageView android:id="@+id/detailThumbsUpImgView"
android:contentDescription="@string/detailThumbsUpImgViewDescription"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/detailViewCountView"
android:layout_toLeftOf="@id/detailThumbsUpCountView"
android:layout_toStartOf="@id/detailThumbsUpCountView"
android:src="@drawable/thumbs_up" />
<View
android:background="#000"
android:layout_width="match_parent"
android:layout_height="1px" />
<TextView android:id="@+id/detailUploadDateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploaderView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="@dimen/video_item_detail_upload_date_text_size"
android:textAppearance="?android:attr/textAppearanceLarge" />
<de.hdodenhof.circleimageview.CircleImageView android:id="@+id/detailUploaderThumbnailView"
android:contentDescription="@string/detail_uploader_thumbnail_view_description"
android:layout_width="@dimen/video_item_detail_uploader_image_size"
android:layout_height="@dimen/video_item_detail_uploader_image_size"
android:src="@drawable/buddy"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"/>
<TextView android:id="@+id/detailDescriptionView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/detailUploadDateView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textSize="@dimen/video_item_detail_description_text_size"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView android:id="@+id/detailUploaderView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="@dimen/video_item_detail_uploader_text_size"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Uploader"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/detailUploaderThumbnailView"
android:layout_toEndOf="@+id/detailUploaderThumbnailView"
android:layout_marginLeft="15dp"
android:layout_marginStart="28dp" />
<View
android:background="#000"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_below="@id/detailUploaderThumbnailView"/>
</RelativeLayout>
<RelativeLayout android:id="@+id/detailNextVideoRootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_detail_info_text_padding"
android:layout_below="@id/detailDescriptionView" >
android:layout_height="match_parent"
android:layout_gravity="center_horizontal|bottom"
android:layout_below="@+id/detailUploaderWrapView"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp">
<TextView android:id="@+id/detailNextVideoTitle"
android:layout_width="wrap_content"
@@ -175,12 +239,12 @@
android:layout_centerHorizontal="true"
android:textSize="@dimen/video_item_detail_next_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/nextVideoTitle"
android:text="@string/next_video_title"
android:textAllCaps="true" />
<RelativeLayout android:id="@+id/detailNextVidButtonAndContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_below="@id/detailNextVideoTitle">
<FrameLayout
android:id="@+id/detailNextVideoFrame"
@@ -194,15 +258,24 @@
android:layout_alignBottom="@id/detailNextVideoFrame"
android:background="?attr/selectableItemBackground"/>
</RelativeLayout>
<Button android:id="@+id/detailShowSimilarButton"
android:layout_width="match_parent"
<TextView android:id="@+id/detailSimilarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="@dimen/video_item_detail_next_text_size"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/similar_videos_btn_text"
android:layout_below="@id/detailNextVidButtonAndContentLayout"
android:textSize="@dimen/video_item_detail_similar_text_size"
android:text="@string/showSimilarVideosButtonText"/>
android:textAllCaps="true" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/similarVideosView"
android:layout_below="@id/detailSimilarTitle">
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</ScrollView>
</com.nirhart.parallaxscroll.views.ParallaxScrollView>
</FrameLayout>

View File

@@ -5,16 +5,11 @@
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Loading"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ProgressBar
android:id="@+id/paginate_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp" />
android:gravity="center"
android:paddingBottom="10dp"/>
</LinearLayout>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="@color/background_notification_color"
tools:targetApi="jelly_bean">
<Button
android:id="@+id/notificationBackgroundButton"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@android:color/transparent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/notificationCover"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/dummy_thumbnail"
android:scaleType="centerCrop"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:orientation="vertical" >
<TextView
android:id="@+id/notificationSongName"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:text="title" />
<TextView
android:id="@+id/notificationArtist"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:text="artist" />
</LinearLayout>
<ImageButton
android:id="@+id/notificationPlayPause"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="5dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_pause_white_24dp" />
<ImageButton
android:id="@+id/notificationStop"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="5dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_close_white_24dp" />
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:background="@color/background_notification_color"
tools:targetApi="jelly_bean" >
<Button
android:id="@+id/notificationBackgroundButton"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@android:color/transparent"/>
<ImageView
android:id="@+id/notificationCover"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginRight="8dp"
android:src="@drawable/dummy_thumbnail"
android:scaleType="centerCrop"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="@+id/notificationButtons"
android:layout_toRightOf="@+id/notificationCover"
android:gravity="center_vertical"
android:orientation="vertical" >
<TextView
android:id="@+id/notificationSongName"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="40dp"
android:ellipsize="marquee"
android:singleLine="true"
android:text="title" />
<TextView
android:id="@+id/notificationArtist"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:text="artist" />
</LinearLayout>
<ImageButton
android:id="@+id/notificationStop"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentRight="true"
android:layout_margin="5dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_close_white_24dp" />
<RelativeLayout
android:id="@+id/notificationButtons"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignBottom="@id/notificationCover"
android:layout_alignParentRight="true"
android:layout_toRightOf="@+id/notificationCover"
android:orientation="horizontal" >
<ImageButton
android:id="@+id/notificationPlayPause"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#00ffffff"
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_pause_white_24dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
</RelativeLayout>

View File

@@ -1,29 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_gravity="center"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="@dimen/video_item_search_card_radius"
android:layout_marginTop="@dimen/video_item_search_card_vertical_margin"
android:layout_marginBottom="@dimen/video_item_search_card_vertical_margin"
android:layout_marginLeft="@dimen/video_item_search_card_horizontal_margin"
android:layout_marginRight="@dimen/video_item_search_card_horizontal_margin" >
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/video_item_search_card_padding">
android:layout_margin="12dp">
<RelativeLayout android:id="@+id/itemThumbnailViewContainer"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
@@ -32,7 +18,7 @@
tools:ignore="RtlHardcoded">
<ImageView android:id="@+id/itemThumbnailView"
android:contentDescription="@string/itemThumbnailViewDescription"
android:contentDescription="@string/list_thumbnail_view_description"
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
android:scaleType="centerCrop"
@@ -56,8 +42,8 @@
android:paddingLeft="@dimen/video_item_search_duration_horizontal_padding"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_duration_text_size"
android:background="@color/durationBackground"
android:textColor="@color/durationText"/>
android:background="@color/duration_dackground_color"
android:textColor="@color/duration_text_color"/>
</RelativeLayout>
@@ -81,16 +67,29 @@
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_uploader_text_size"/>
<TextView android:id="@+id/itemUploadDateView"
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"/>
android:layout_height="wrap_content">
<TextView android:id="@+id/itemUploadDateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"/>
<TextView android:id="@+id/itemViewCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>
</LinearLayout>

View File

@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_item_screen_rotation"
android:title="@string/screenRotation"
android:title="@string/screen_rotation"
app:showAsAction="always"
android:icon="@drawable/ic_screen_rotation_white"/>
</menu>

View File

@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_item_play_audio"
android:title="@string/playAudio"
android:title="@string/play_audio"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_headset_black" />
@@ -18,7 +18,7 @@
android:icon="@drawable/ic_share_black"/>
<item android:id="@+id/action_play_with_kodi"
android:title="@string/playWithKodiTitle"
android:title="@string/play_with_kodi_title"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_cast_black"/>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="abc_action_bar_home_description">Navigate home</string>
<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>
<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>
<string name="abc_action_bar_up_description">Navigate up</string>
<string name="abc_action_menu_overflow_description">More options</string>
<string name="abc_action_mode_done">Done</string>
<string name="abc_activity_chooser_view_see_all">See all</string>
<string name="abc_activitychooserview_choose_application">Choose an app</string>
<string name="abc_capital_off">OFF</string>
<string name="abc_capital_on">ON</string>
<string name="abc_search_hint">Search…</string>
<string name="abc_searchview_description_clear">Clear query</string>
<string name="abc_searchview_description_query">Search query</string>
<string name="abc_searchview_description_search">Search</string>
<string name="abc_searchview_description_submit">Submit query</string>
<string name="abc_searchview_description_voice">Voice search</string>
<string name="abc_shareactionprovider_share_with">Share with</string>
<string name="abc_shareactionprovider_share_with_application">Share with %s</string>
<string name="abc_toolbar_collapse_description">Collapse</string>
<string name="status_bar_notification_info_overflow">999+</string>
<string name="autoplay_through_intent_summary">"بدء تشغيل الفيديو تلقائيًا عندما يتم فتحه من تطبيق أخر."</string>
<string name="autoplay_through_intent_title">التشغيل التلقائي</string>
<string name="background_player_name">مشغل NewPipe في الخلفية</string>
<string name="background_player_playing_toast">جاري التشغيل في الخلفية</string>
<string name="cancel">إلغاء</string>
<string name="choose_browser">إختر متصفح:</string>
<string name="dark_theme_title">مظلم</string>
<string name="default_audio_format_title">صيغة الصوت الإفتراضية</string>
<string name="default_resolution_title">الدقة الإفتراضية</string>
<string name="detail_dislikes_img_view_description">عدم الإعجاب</string>
<string name="detail_likes_img_view_description">الإعجابات</string>
<string name="detail_thumbnail_view_description">صور معاينة الفيديو</string>
<string name="detail_uploader_thumbnail_view_description">"Uploader's userpic thumbnail"</string>
<string name="did_you_mean">هل تقصد:</string>
<string name="download">تنزيل</string>
<string name="download_dialog_title">تنزيل</string>
<string name="download_path_audio_dialog_title">أدخل مسار التنزيل للملفات الصوتية.</string>
<string name="download_path_audio_summary">مسار حفظ التنزيلات الصوتية في.</string>
<string name="download_path_audio_title">مسار الصوتيات المحفوظة</string>
<string name="download_path_dialog_title">أدخل مسار التنزيل لملفات الفيديو</string>
<string name="download_path_summary">مسار حفظ تنزيلات الفيديو في.</string>
<string name="download_path_title">مسار الفيديوهات المحفوظة</string>
<string name="err_dir_create">"لا يمكن إنشاء مجلد للتنزيلات في '%1$s'"</string>
<string name="info_dir_created">"تم إنشاء مجلد تنزيلات في '%1$s'"</string>
<string name="install">تثبيت</string>
<string name="kore_not_found">تطبيق Kore غير موجود. هل تريد تثبيته؟</string>
<string name="light_theme_title">مضيء</string>
<string name="list_thumbnail_view_description">صور معاينة الفيديو</string>
<string name="loading">جاري التحميل</string>
<string name="m4a_description">m4a — جودة أفضل</string>
<string name="network_error">خطأ في الشبكة</string>
<string name="next_video_title">الفيديو التالي</string>
<string name="no_player_found">لا يوجد مشغل فيديو. هل تريد تثبيت VLC ؟</string>
<string name="open_in_browser">فتح في المتصفح</string>
<string name="play_audio">صوت</string>
<string name="play_btn_text">تشغيل</string>
<string name="play_with_kodi_title">تشغيل بواسطة Kodi</string>
<string name="screen_rotation">تدوير</string>
<string name="search">بحث</string>
<string name="search_language_title">لغة المحتوى المفضل</string>
<string name="search_page">صفحة البحث:</string>
<string name="settings">الإعدادات</string>
<string name="settings_activity_title">الإعدادات</string>
<string name="settings_category_appearance_title">المظهر</string>
<string name="settings_category_other_title">تعريب JetSub مدونة درويديات</string>
<string name="settings_category_video_audio_title">الفيديو والصوتيات</string>
<string name="share">مشاركة</string>
<string name="share_dialog_title">مشاركة بواسطة:</string>
<string name="show_next_and_similar_title">عرض التالي والفيديوهات المشابهة</string>
<string name="show_play_with_kodi_summary">عرض خيار لتشغيل الفيديو بواسطة Kodi Media Center.</string>
<string name="show_play_with_kodi_title">عرض خيار التشغيل بواسطة Kodi.</string>
<string name="similar_videos_btn_text">الفيديوهات المشابهة</string>
<string name="theme_title">الثيم</string>
<string name="upload_date_text">تم الرفع في %1$s</string>
<string name="url_not_supported_toast">الرابط غير مدعوم</string>
<string name="use_external_audio_player_title">استخدام مشغل صوتيات خارجي</string>
<string name="use_external_video_player_title">استخدام مشغل فيديو خارجي</string>
<string name="use_tor_summary">إجراء التنزيلات من خلال استخدام بروكسي Tor لزيادة الخصوصية ( تشغيل الفيديو المباشر غير مدعوم حتى الأن )</string>
<string name="use_tor_title">استخدام Tor</string>
<string name="view_count_text">%1$s المشاهدات</string>
<string name="webm_description">WebM</string>
<string name="blocked_by_gema">Blocked by GEMA.</string>
<string name="content_not_available">المحتوى غير متاح.</string>
<string name="could_not_load_thumbnails">لم يتمكن من تحميل كل صور المعاينة</string>
<string name="general_error">خطأ</string>
<string name="parsing_error">لا يمكن تحليل الموقع.</string>
<string name="youtube_signature_decryption_error">لا يمكن فك تشفير توقيع رابط الفيديو.</string>
<string name="app_name">NewPipe</string>
<string name="appbar_scrolling_view_behavior">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string>
<string name="autoplay_through_intent_key">autoplay_through_intent</string>
<string name="background_player_time_text">%1$s - NewPipe</string>
<string name="c3s_url">https://www.c3s.cc/</string>
<string name="character_counter_pattern">%1$d / %2$d</string>
<string name="default_audio_format_key">default_audio_format</string>
<string name="default_audio_format_value">m4a</string>
<string name="default_language_value">en</string>
<string name="default_resolution_key">default_resolution_preference</string>
<string name="default_resolution_value">360p</string>
<string name="default_theme_value">0</string>
<string name="download_path_audio_key">download_path_audio</string>
<string name="download_path_key">download_path</string>
<string name="fdroid_kore_url">https://f-droid.org/repository/browse/?fdfilter=Kore&amp;fdid=org.xbmc.kore</string>
<string name="fdroid_vlc_url">https://f-droid.org/repository/browse/?fdfilter=vlc&amp;fdid=org.videolan.vlc</string>
<string name="search_language_key">search_language</string>
<string name="settings_category_appearance">settings_category_appearance</string>
<string name="settings_category_other">settings_category_other</string>
<string name="settings_category_video_audio">settings_category_video_audio</string>
<string name="show_next_video_key">show_next_video</string>
<string name="show_play_with_kodi_key">show_play_with_kodi</string>
<string name="theme_key">الثيمات</string>
<string name="title_videoitem_detail">NewPipe</string>
<string name="use_external_audio_player_key">use_external_audio_player</string>
<string name="use_external_video_player_key">use_external_video_player</string>
<string name="use_tor_key">use_tor</string>
</resources>

View File

@@ -0,0 +1,75 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="upload_date_text">Publikováno %1$s</string>
<string name="no_player_found">Žádný přehrávač nenalezen. Nainstalovat VLC?</string>
<string name="install">Instalovat</string>
<string name="cancel">Zrušit</string>
<string name="open_in_browser">Otevřít v prohlížeči</string>
<string name="share">Sdílet</string>
<string name="loading">Načítám</string>
<string name="download">Stáhnout</string>
<string name="search">Vyhledat</string>
<string name="settings">Nastavení</string>
<string name="did_you_mean">"Měli jste na mysli: "</string>
<string name="search_page">"Vyhledat stránku: "</string>
<string name="share_dialog_title">Sdílet s:</string>
<string name="choose_browser">Vybrat prohlížeč:</string>
<string name="screen_rotation">otočení</string>
<string name="settings_activity_title">Nastavení</string>
<string name="use_external_video_player_title">Použít externí video přehrávač</string>
<string name="use_external_audio_player_title">Použít externí audio přehrávač</string>
<string name="download_path_audio_summary">Cesta, kde se uloží audio po stažení.</string>
<string name="download_path_audio_dialog_title">Zadejte umístění pro uložení audio souborů.</string>
<string name="download_path_audio_title">Umístění pro stažené audio</string>
<string name="autoplay_through_intent_title">Automatické přehrávání skrze Intent</string>
<string name="autoplay_through_intent_summary">Automaticky přehrávat video, jestliže je volané z jiné aplikace.</string>
<string name="default_resolution_title">Výchozí rozlišení</string>
<string name="play_with_kodi_title">Přehrát s Kodi</string>
<string name="kore_not_found">Aplikace Kore nenalezena. Nainstalovat Kore?</string>
<string name="view_count_text">%1$s zhlédnutí</string>
<string name="background_player_name">NewPipe Přehrávač na pozadí</string>
<string name="download_path_title">Umístění pro stažené video</string>
<string name="download_path_summary">Cesta, kde se uloží video po stažení.</string>
<string name="download_path_dialog_title">Zadejte umístění pro uložená videa</string>
<string name="show_play_with_kodi_title">Zobrazit možnost \"Přehrát s Kodi\"</string>
<string name="show_play_with_kodi_summary">Zobrazit možnost přehrát video s multimediálním centrem Kodi.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Výchozí audio formát</string>
<string name="webm_description">WebM — svobodný formát</string>
<string name="m4a_description">m4a — lepší kvalita</string>
<string name="theme_title">Téma</string>
<string name="dark_theme_title">Tmavý</string>
<string name="light_theme_title">Světlý</string>
<string name="download_dialog_title">Stažení</string>
<string name="next_video_title">Následující video</string>
<string name="show_next_and_similar_title">Zobrazit následující a související videa</string>
<string name="url_not_supported_toast">URL není podporováno</string>
<string name="similar_videos_btn_text">Související videa</string>
<string name="search_language_title">Preferovaný jazyk obsahu</string>
<string name="settings_category_video_audio_title">Video &amp; audio</string>
<string name="settings_category_appearance_title">Vzhled</string>
<string name="settings_category_other_title">Ostatní</string>
<string name="background_player_playing_toast">Přehrávám na pozadí</string>
<string name="play_btn_text">Přehrát</string>
<string name="general_error">Chyba</string>
<string name="network_error">Chyba sítě</string>
<string name="could_not_load_thumbnails">Nebylo možné stáhnout všechny náhledy</string>
<string name="youtube_signature_decryption_error">Nebylo možné dekódovat URL videa.</string>
<string name="parsing_error">Nebylo možné analyzovat webovou stránku.</string>
<string name="content_not_available">Obsah není k dispozici.</string>
<string name="blocked_by_gema">Obsah blokuje GEMA.</string>
<string name="list_thumbnail_view_description">Náhled videa</string>
<string name="detail_thumbnail_view_description">Náhled videa</string>
<string name="detail_uploader_thumbnail_view_description">Náhled uploadera</string>
<string name="detail_likes_img_view_description">To se mi líbí</string>
<string name="detail_dislikes_img_view_description">To se mi nelíbí</string>
<string name="use_tor_title">Použít Tor</string>
<string name="use_tor_summary">Vynutit stahování skrz Tor pro zvýšené soukromí (streaming není zatím podporován)</string>
<string name="err_dir_create">Nebylo možné vytvořit složku pro stažené soubory \'%1$s\'</string>
<string name="info_dir_created">Vytvořena složka pro stažené soubory \'%1$s\'</string>
</resources>

View File

@@ -1,53 +1,81 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="viewCountText">%1$s Aufrufe</string>
<string name="uploadDateText">Hochgeladen am %1$s</string>
<string name="noPlayerFound">Keinen Streamplayer gefunden. Vielleicht möchtest du einen installieren.</string>
<string name="installStreamPlayer">Jetzt installieren</string>
<string name="view_count_text">%1$s Aufrufe</string>
<string name="upload_date_text">Hochgeladen am %1$s</string>
<string name="no_player_found">Keinen Streamplayer gefunden. Möchtest du VLC installieren?</string>
<string name="install">Jetzt installieren</string>
<string name="cancel">Abbrechen</string>
<string name="open_in_browser">In Browser öffnen</string>
<string name="share">Teilen</string>
<string name="download">Download</string>
<string name="search">Suchen</string>
<string name="settings">Einstellungen</string>
<string name="didYouMean">Meintest du: </string>
<string name="searchPage">Suchseite: </string>
<string name="shareDialogTitle">Teilen mit:</string>
<string name="chooseBrowser">Browser:</string>
<string name="screenRotation">Rotation</string>
<string name="title_activity_settings">Einstellungen</string>
<string name="did_you_mean">Meintest du: </string>
<string name="search_page">Suchseite: </string>
<string name="share_dialog_title">Teilen mit:</string>
<string name="choose_browser">Browser:</string>
<string name="screen_rotation">Rotation</string>
<string name="settings_activity_title">Einstellungen</string>
<string name="useExternalPlayerTitle">Externen Player benutzen</string>
<string name="downloadLocation">Downloadverzeichnis</string>
<string name="downloadLocationSummary">Verzeichnis in dem heruntergeladene Videos gespeichert werden.</string>
<string name="downloadLocationDialogTitle">Download Verzeichnis eingeben</string>
<string name="autoPlayThroughIntentTitle">Automatisches Abspielen durch Intent</string>
<string name="autoPlayThroughIntentSummary">Startet ein Video automatisch wenn es von einer anderen App aufgerufen wurde.</string>
<string name="defaultResolutionPreferenceTitle">Standard Auflösung</string>
<string name="playWithKodiTitle">Mit Kodi abspielen</string>
<string name="koreNotFound">Kore app wurde nicht gefunden. Kore wird benötigt, um Videos mit Kodi wieder zu geben.</string>
<string name="download_path_title">Downloadverzeichnis für Videos</string>
<string name="download_path_summary">Verzeichnis in dem heruntergeladene Videos gespeichert werden.</string>
<string name="download_path_dialog_title">Downloadverzeichnis für Videos eingeben</string>
<string name="autoplay_through_intent_title">Automatisches Abspielen durch Intent</string>
<string name="autoplay_through_intent_summary">Startet ein Video automatisch wenn es von einer anderen App aufgerufen wurde.</string>
<string name="default_resolution_title">Standardauflösung</string>
<string name="play_with_kodi_title">Mit Kodi abspielen</string>
<string name="kore_not_found">Kore App wurde nicht gefunden. Möchten sie Kore jetzt installieren?</string>
<string name="installeKore">Kore installieren</string>
<string name="showPlayWithKodiTitle">Zeige \"Mit Kodi abspielen\" Option</string>
<string name="showPlayWithKodiSummary">Zeigt eine Option an, über die man Videos mit dem Kodi Mediacenter abspielen kann.</string>
<string name="playAudio">Audio</string>
<string name="defaultAudioFormatTitle">Bevorzugtes Audio Format</string>
<string name="webMAudioDescription">WebM - freies Format</string>
<string name="m4aAudioDescription">m4a - bessere Qualität</string>
<string name="downloadDialogTitle">Herunterladen</string>
<string name="show_play_with_kodi_title">Zeige \"Mit Kodi abspielen\" Option</string>
<string name="show_play_with_kodi_summary">Zeigt eine Option an, über die man Videos mit dem Kodi Mediacenter abspielen kann.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Bevorzugtes Audioformat</string>
<string name="webm_description">WebM freies Format</string>
<string name="m4a_description">m4a bessere Qualität</string>
<string name="download_dialog_title">Herunterladen</string>
<string-array name="downloadOptions">
<item>Video</item>
<item>Audio</item>
</string-array>
<string name="nextVideoTitle">Nächstes Video</string>
<string name="showNextAndSimilarTitle">Zeige nächstes und ähnliche Videos</string>
<string name="urlNotSupportedText">URL wird nicht unterstützt.</string>
<string name="showSimilarVideosButtonText">Ähnliche Videos</string>
<string name="settingsCategoryVideoAudioTitle">VIDEO &amp; AUDIO</string>
<string name="settingsCategoryVideoInfoTittle">INFO</string>
<string name="settingsCategoryEtcTitle">ETC</string>
<string name="searchLanguageTitle">Bevorzugte Sprache</string>
<string name="itemThumbnailViewDescription">Video-Vorschau-Bild</string>
<string name="detailThumbnailViewDescription">Video-Vorschau-Bild</string>
<string name="detailUploaderThumbnailViewDescription">Nutzerbild</string>
<string name="detailThumbsDownImgViewDescription">gefällt nicht</string>
<string name="detailThumbsUpImgViewDescription">gefällt</string>
</resources>
<string name="next_video_title">Nächstes Video</string>
<string name="show_next_and_similar_title">Zeige nächstes und ähnliche Videos</string>
<string name="url_not_supported_toast">URL wird nicht unterstützt</string>
<string name="similar_videos_btn_text">Ähnliche Videos</string>
<string name="settings_category_video_audio_title">Video &amp; Audio</string>
<string name="search_language_title">Bevorzugte Sprache des Inhalts</string>
<string name="list_thumbnail_view_description">Video-Vorschaubild</string>
<string name="detail_thumbnail_view_description">Video-Vorschaubild</string>
<string name="detail_uploader_thumbnail_view_description">Nutzerbild</string>
<string name="detail_dislikes_img_view_description">Gefällt nicht</string>
<string name="detail_likes_img_view_description">Gefällt</string>
<string name="loading">Lade</string>
<string name="use_external_video_player_title">Benutze externen Videoabspieler</string>
<string name="use_external_audio_player_title">Benutze externen Audioabspieler</string>
<string name="background_player_playing_toast">Spiele im Hintergrund ab</string>
<string name="play_btn_text">Abspielen</string>
<string name="use_tor_title">Benutze TOR</string>
<string name="use_tor_summary">Erzwinge das Herunterladen durch TOR für verbesserte Privatsphäre (Videostream noch nicht unterstützt)</string>
<string name="background_player_name">NewPipe Hintergrundwiedergabe</string>
<string name="network_error">Netzwerkfehler</string>
<string name="download_path_audio_title">Downloadverzeichnis für Musik</string>
<string name="download_path_audio_summary">Verzeichnis zum Speichern heruntergeladener Audiodateien</string>
<string name="download_path_audio_dialog_title">Pfad für heruntergeladene Audiodateien eingeben.</string>
<string name="theme_title">Aussehen</string>
<string name="dark_theme_title">Dunkel</string>
<string name="light_theme_title">Hell</string>
<string name="settings_category_appearance_title">Aussehen</string>
<string name="settings_category_other_title">Andere</string>
<string name="err_dir_create">Kann Downloadverzeichnis nicht anlegen \'%1$s\'</string>
<string name="info_dir_created">Downloadverzeichnis \'%1$s\' erstellt</string>
<string name="general_error">Fehler</string>
<string name="could_not_load_thumbnails">Konnte nicht alle Vorschaubilder laden</string>
<string name="youtube_signature_decryption_error">Konnte Video-URL-Signatur nicht entschlüsseln.</string>
<string name="parsing_error">Konnte Webseite nicht parsen.</string>
<string name="content_not_available">Inhalt nicht verfügbar.</string>
<string name="blocked_by_gema">Durch die GEMA gesperrt.</string>
</resources>

View File

@@ -0,0 +1,69 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="background_player_name">NewPipe Background Player</string>
<string name="view_count_text">%1$s προβολές</string>
<string name="upload_date_text">Ανέβηκε στις %1$s</string>
<string name="no_player_found">Δεν βρέθηκε πρόγραμμα αναπαραγωγής. Εγκατάσταση του VLC;</string>
<string name="install">Εγκατάσταση</string>
<string name="cancel">Ακύρωση</string>
<string name="open_in_browser">Άνοιγμα στον browser</string>
<string name="share">Κοινοποίηση</string>
<string name="loading">Φορτώνει</string>
<string name="download">Λήψη</string>
<string name="search">Αναζήτηση</string>
<string name="settings">Ρυθμίσεις</string>
<string name="did_you_mean">"Μήπως εννοείτε: "</string>
<string name="search_page">"Αναζήτηση σελίδας: "</string>
<string name="share_dialog_title">Κοινοποίηση με:</string>
<string name="choose_browser">Επιλέξτε browser:</string>
<string name="screen_rotation">περιστροφή</string>
<string name="settings_activity_title">Ρυθμίσεις</string>
<string name="use_external_video_player_title">Χρήση εξωτερικού video player</string>
<string name="use_external_audio_player_title">Χρήση εξωτερικού audio player</string>
<string name="download_path_title">Διαδρομή λήψης video</string>
<string name="download_path_summary">Διαδρομή για αποθήκευση των video.</string>
<string name="download_path_dialog_title">Εισάγετε διαδρομή για λήψη των video</string>
<string name="download_path_audio_title">Διαδρομή λήψης αρχείων ήχου</string>
<string name="download_path_audio_summary">Διαδρομή για αποθήκευση αρχείων ήχου</string>
<string name="download_path_audio_dialog_title">Εισάγετε διαδρομή για λήψη αρχείων ήχου.</string>
<string name="autoplay_through_intent_title">Αυτόματη αναπαραγωγή μέσω Intent</string>
<string name="autoplay_through_intent_summary">Αυτόματη αναπαραγωγή video όταν καλείται από άλλη εφαρμογή.</string>
<string name="default_resolution_title">Προεπιλεγμένη ανάλυση</string>
<string name="play_with_kodi_title">Αναπαραγωγή με το Kodi</string>
<string name="kore_not_found">Η εφαρμογή Kore δεν βρέθηκε. Εγκατάσταση;</string>
<string name="show_play_with_kodi_title">Εμφάνιση της επιλογής \"Αναπαραγωγή με το Kodi\"</string>
<string name="show_play_with_kodi_summary">Προβολή μιας επιλογής για αναπαραγωγή με το Kodi media center.</string>
<string name="play_audio">Ήχος</string>
<string name="default_audio_format_title">Προεπιλεγμένη μορφή ήχου</string>
<string name="webm_description">WebM — δωρεάν format</string>
<string name="m4a_description">m4a — καλύτερη ποιότητα</string>
<string name="theme_title">Θέμα</string>
<string name="dark_theme_title">Σκοτεινό</string>
<string name="light_theme_title">Φωτεινό</string>
<string name="download_dialog_title">Λήψη</string>
<string name="next_video_title">Επόμενο video</string>
<string name="show_next_and_similar_title">Προβολή επόμενου και σχετικών video</string>
<string name="url_not_supported_toast">Δεν υποστηρίζεται η διεύθυνση URL</string>
<string name="similar_videos_btn_text">Σχετικά video</string>
<string name="search_language_title">Προτιμώμενη γλώσσα περιεχομένου</string>
<string name="settings_category_video_audio_title">Video &amp; Ήχος</string>
<string name="settings_category_appearance_title">Εμφάνιση</string>
<string name="settings_category_other_title">Άλλα</string>
<string name="background_player_playing_toast">Αναπαραγωγή στο υπόβαθρο</string>
<string name="play_btn_text">Αναπαραγωγή</string>
<string name="network_error">Σφάλμα δικτύου</string>
<string name="list_thumbnail_view_description">Εικόνα προεπισκόπησης video</string>
<string name="detail_thumbnail_view_description">Εικόνα προεπισκόπησης video</string>
<string name="detail_uploader_thumbnail_view_description">Εικόνα προφίλ του uploader</string>
<string name="detail_likes_img_view_description">Like</string>
<string name="detail_dislikes_img_view_description">Dislike</string>
<string name="use_tor_title">Χρήση του Tor</string>
<string name="use_tor_summary">Αναγκάζει την κίνηση λήψης μέσω Tor για αυξημένη ανωνυμία (η αναπαραγωγή δεν υποστηρίζεται ακόμη)</string>
<string name="err_dir_create">Δεν μπόρεσε να δημιουργηθεί ο φάκελος \'%1$s\'</string>
<string name="info_dir_created">Δημιουργήθηκε ο φάκελος \'%1$s\'</string>
</resources>

View File

@@ -0,0 +1,75 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="background_player_name">NewPipe-fonludilo</string>
<string name="view_count_text">%1$s vidoj</string>
<string name="upload_date_text">Alŝultita je %1$s</string>
<string name="install">Instali</string>
<string name="cancel">Nuligi</string>
<string name="open_in_browser">Malfermi per retumilo</string>
<string name="share">Konigi</string>
<string name="loading">Ŝargado</string>
<string name="download">Elŝuti</string>
<string name="search">Serĉi</string>
<string name="settings">Agordoj</string>
<string name="did_you_mean">"Ĉu vi intencis: "</string>
<string name="search_page">"Serĉpaĝo: "</string>
<string name="share_dialog_title">Konigi kun:</string>
<string name="choose_browser">Elekti retumilon:</string>
<string name="screen_rotation">turno</string>
<string name="settings_activity_title">Agordoj</string>
<string name="use_external_video_player_title">Uzi eksteran videoludilon</string>
<string name="use_external_audio_player_title">Uzi eksteran sonludilon</string>
<string name="default_resolution_title">Defaŭlta distingivo</string>
<string name="play_with_kodi_title">Ludi per Kodi</string>
<string name="show_play_with_kodi_title">Montri \"Ludi per Kodi\"-opcion</string>
<string name="play_audio">Sono</string>
<string name="default_audio_format_title">Defaŭlta sondosierformo</string>
<string name="webm_description">WebM — libera dosierformo</string>
<string name="m4a_description">m4a — pli bona kvalito</string>
<string name="theme_title">Etoso</string>
<string name="dark_theme_title">Malluma</string>
<string name="light_theme_title">Luma</string>
<string name="download_dialog_title">Elŝuti</string>
<string name="next_video_title">Sekva video</string>
<string name="url_not_supported_toast">Ligilo ne subtenita</string>
<string name="similar_videos_btn_text">Similaj videoj</string>
<string name="search_language_title">Preferata enhavlingvo</string>
<string name="settings_category_video_audio_title">Video kaj sono</string>
<string name="settings_category_appearance_title">Apero</string>
<string name="settings_category_other_title">Alia</string>
<string name="background_player_playing_toast">Ludado fone</string>
<string name="play_btn_text">Ludi</string>
<string name="general_error">Eraro</string>
<string name="network_error">Reteraro</string>
<string name="content_not_available">Enhavo ne estas disponebla.</string>
<string name="blocked_by_gema">Blokita de GEMA.</string>
<string name="detail_likes_img_view_description">Ŝatoj</string>
<string name="detail_dislikes_img_view_description">Malŝatoj</string>
<string name="use_tor_title">Uzi la programon Tor</string>
<string name="autoplay_through_intent_title">Ludi aŭtomate per Intent</string>
<string name="no_player_found">Neniu elsendlflua ludilo trovita. Ĉu instali la aplikaĵon VLC?</string>
<string name="kore_not_found">La aplikaĵo Kore ne estas trovita. Ĉu instali la aplikaĵon Kore?</string>
<string name="show_next_and_similar_title">Montri la sekvan videon kaj similajn videojn</string>
<string name="could_not_load_thumbnails">Ĉiuj miniaturoj ne ŝargeblas</string>
<string name="youtube_signature_decryption_error">La subskribo de la ligilo de la video ne malĉifreblas.</string>
<string name="parsing_error">La retejo ne analizeblas.</string>
<string name="list_thumbnail_view_description">Miniaturo de la antaŭrigardo de la video</string>
<string name="detail_thumbnail_view_description">Miniaturo de la antaŭrigardo de la video</string>
<string name="detail_uploader_thumbnail_view_description">Miniaturo de la bildo de la alŝutinto</string>
<string name="err_dir_create">La elŝutujo \'%1$s\' ne kreeblas</string>
<string name="info_dir_created">Elŝutujo \'%1$s\' kreita</string>
<string name="download_path_title">Elŝutujo por videoj</string>
<string name="download_path_audio_title">Elŝutujo por muziko</string>
<string name="use_tor_summary">Devigi elŝuttrafikon tra Tor por pli bona privateco (elsendfluaj videoj estas ankoraŭ ne subtenitaj)</string>
<string name="show_play_with_kodi_summary">Montri opcion por ludi videon per la aplikaĵo Kodi.</string>
<string name="download_path_summary">Dosierujo por konservi elŝutitajn videojn.</string>
<string name="download_path_audio_summary">Dosierujo por konservi elŝutitan muzikon.</string>
<string name="autoplay_through_intent_summary">Ludi videon aŭtomate kiam ĝi estas vokita de alia aplikaĵo.</string>
<string name="download_path_dialog_title">Elektu lokon por konservi elŝutitajn videojn</string>
<string name="download_path_audio_dialog_title">Elektu lokon por konservi elŝutitan muzikon.</string>
</resources>

View File

@@ -1,39 +1,77 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="viewCountText">%1$s visitas</string>
<string name="uploadDateText">Subido el %1$s</string>
<string name="noPlayerFound">No se ha encontrado ningún reproductor de vídeo. Quizás quieras instalar alguno.</string>
<string name="installStreamPlayer">Instalarlo</string>
<string name="view_count_text">%1$s visitas</string>
<string name="upload_date_text">Subido el %1$s</string>
<string name="no_player_found">No se ha encontrado ningún reproductor de vídeo. Quizás quieras instalar alguno. Deseas instalar VLC?</string>
<string name="install">Instalarlo</string>
<string name="cancel">Cancelar</string>
<string name="open_in_browser">Abrir en el navegador</string>
<string name="share">Compartir</string>
<string name="download">Descargar</string>
<string name="search">Buscar</string>
<string name="settings">Ajustes</string>
<string name="didYouMean">"¿Querías decir?: "</string>
<string name="searchPage">Buscar página: </string>
<string name="shareDialogTitle">Compartir con:</string>
<string name="chooseBrowser">Selecciona navegador:</string>
<string name="screenRotation">rotación</string>
<string name="title_activity_settings">Ajustes</string>
<string name="did_you_mean">"¿Querías decir?: "</string>
<string name="search_page">Buscar página: </string>
<string name="share_dialog_title">Compartir con:</string>
<string name="choose_browser">Selecciona navegador:</string>
<string name="screen_rotation">rotación</string>
<string name="settings_activity_title">Ajustes</string>
<string name="useExternalPlayerTitle">Usar reproductor externo</string>
<string name="downloadLocation">Descargar en&#8230;</string>
<string name="downloadLocationSummary">Ruta donde guardar los vídeos descargados.</string>
<string name="downloadLocationDialogTitle">Localización del directorio de descargas</string>
<string name="autoPlayThroughIntentTitle">Reproducción automática</string>
<string name="autoPlayThroughIntentSummary">Reproducir los vídeos automaticamente cuando se llama desde otra aplicación.</string>
<string name="defaultResolutionPreferenceTitle">Resolución por defecto</string>
<string name="playWithKodiTitle">Reproducir con Kodi</string>
<string name="koreNotFound">Aplicación Kore no encontrada. Kore es necesario para reproducir vídeos con Kodi media center.</string>
<string name="download_path_title">Ruta de descarga de video</string>
<string name="download_path_summary">Ruta donde guardar los vídeos descargados.</string>
<string name="download_path_dialog_title">Ingrese el directorio de descargas para videos</string>
<string name="autoplay_through_intent_title">Intentar reproducción automática</string>
<string name="autoplay_through_intent_summary">Reproducir vídeos automáticamente cuando se llamen desde otra aplicación.</string>
<string name="default_resolution_title">Resolución por defecto</string>
<string name="play_with_kodi_title">Reproducir con Kodi</string>
<string name="kore_not_found">Aplicación Kore no encontrada. Kore es necesario para reproducir vídeos con Kodi media center. Instalar Kore?</string>
<string name="installeKore">Instalar Kore</string>
<string name="showPlayWithKodiTitle">Mostrar la opción \"Reproducir con Kodi\"</string>
<string name="showPlayWithKodiSummary">Muestra una opción para reproducir el vídeo con Kodi media center.</string>
<string name="playAudio">Audio</string>
<string name="defaultAudioFormatTitle">Formato de audio por defecto</string>
<string name="webMAudioDescription">WebM - formato libre</string>
<string name="m4aAudioDescription">m4a - mejor calidad</string>
<string name="downloadDialogTitle">Descargar</string>
<string name="nextVideoTitle">Siguiente vídeo</string>
<string name="urlNotSupportedText">URL no soportada.</string>
<string name="showSimilarVideosButtonText">Vídeos similares</string>
</resources>
<string name="show_play_with_kodi_title">Mostrar la opción \"Reproducir con Kodi\"</string>
<string name="show_play_with_kodi_summary">Muestra una opción para reproducir el vídeo con Kodi media center.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Formato de audio por defecto</string>
<string name="webm_description">WebM formato libre</string>
<string name="m4a_description">m4a mejor calidad</string>
<string name="download_dialog_title">Descargar</string>
<string name="next_video_title">Siguiente vídeo</string>
<string name="url_not_supported_toast">URL no soportada</string>
<string name="similar_videos_btn_text">Vídeos similares</string>
<string name="background_player_name">Reproductor de fondo NewPipe</string>
<string name="loading">Cargando</string>
<string name="use_external_video_player_title">Usar un reproductor de vídeo externo</string>
<string name="use_external_audio_player_title">Usar un reproductor de audio externo</string>
<string name="theme_title">Tema</string>
<string name="dark_theme_title">Oscuro</string>
<string name="light_theme_title">Claro</string>
<string name="settings_category_appearance_title">Apariencia</string>
<string name="settings_category_other_title">Otro</string>
<string name="background_player_playing_toast">Reproduciendo en Segundo plano</string>
<string name="content_not_available">Contenido no disponible.</string>
<string name="use_tor_title">Usar Tor</string>
<string name="use_tor_summary">Forzar la descarga a través de Tor para una mayor privacidad (transmisión de videos aún no es compatible)</string>
<string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string>
<string name="info_dir_created">Capeta de descarga creada \'%1$s\'</string>
<string name="download_path_audio_summary">Ruta para almacenar el audio descargado.</string>
<string name="download_path_audio_dialog_title">Ingrese la ruta de descarga para los archivos de audio.</string>
<string name="blocked_by_gema">Bloqueado por GEMA.</string>
<string name="download_path_audio_title">Ruta de descarga de audio</string>
<string name="settings_category_video_audio_title">Video &amp; Audio</string>
<string name="play_btn_text">Reproducir</string>
<string name="general_error">Error</string>
<string name="network_error">Error de Conexión</string>
<string name="could_not_load_thumbnails">No se pudo cargar las miniaturas</string>
<string name="youtube_signature_decryption_error">No se pudo descifrar la url del video.</string>
<string name="parsing_error">No se pudo analizar el sitio web.</string>
<string name="show_next_and_similar_title">Mostrar videos similares</string>
<string name="search_language_title">Idioma del contenido</string>
<string name="list_thumbnail_view_description">Vista previa del video</string>
<string name="detail_thumbnail_view_description">Vista previa del video</string>
<string name="detail_likes_img_view_description">Me gusta</string>
<string name="detail_dislikes_img_view_description">No me gusta</string>
<string name="detail_uploader_thumbnail_view_description">Foto miniatura del usuario</string>
</resources>

View File

@@ -0,0 +1,54 @@
<?xml version='1.0' encoding='utf-8'?>
<resources><string name="view_count_text">%1$s ikustaldi</string>
<string name="upload_date_text">Argitaratze-data: %1$s</string>
<string name="install">Instalatu</string>
<string name="cancel">Utzi</string>
<string name="open_in_browser">Nabigatzailean ireki</string>
<string name="share">Partekatu</string>
<string name="loading">Kargatzen</string>
<string name="download">Deskargatu</string>
<string name="search">Bilatu</string>
<string name="settings">Ezarpenak</string>
<string name="share_dialog_title">Partekatu honekin:</string>
<string name="choose_browser">Nabigatzailea aukeratu:</string>
<string name="screen_rotation">biratzea</string>
<string name="settings_activity_title">Ezarpenak</string>
<string name="download_path_title">Deskargatzeko kokapena</string>
<string name="download_path_summary">Deskargatutako bideoak gordetzeko lekua.</string>
<string name="download_path_dialog_title">Sar ezazu deskargatzeko lekua</string>
<string name="default_resolution_title">Lehenetsitako bereizmena</string>
<string name="play_with_kodi_title">Kodirekin erreproduzitu</string>
<string name="kore_not_found">Kore aplikazioa ez da aurkitu. Kore beharrezkoa da Kodi multimedia zentroarekin bideoak erreproduzitzeko.</string>
<string name="installeKore">Kore instalatu</string>
<string name="show_play_with_kodi_title">\"Kodirekin erreproduzitu\" aukera erakutsi</string>
<string name="show_play_with_kodi_summary">Kodi multimedia zentroarekin bideoa erreproduzitzeko aukera erakusten du.</string>
<string name="play_audio">Audioa</string>
<string name="default_audio_format_title">Audio formatu lehenetsia</string>
<string name="webm_description">WebM &#8212; formatu askea</string>
<string name="m4a_description">m4a &#8212; kalitate hobea</string>
<string name="download_dialog_title">Deskargatu</string>
<string name="next_video_title">Hurrengo bideoa</string>
<string name="show_next_and_similar_title">Hurrengo bideoa eta antzekoak erakutsi</string>
<string name="url_not_supported_toast">URLa ez da onartzen.</string>
<string name="similar_videos_btn_text">Antzeko bideoak</string>
<string name="search_language_title">Edukiaren hizkuntz lehenetsia</string>
<string name="settings_category_video_audio_title">Bideoa eta Audioa</string>
<string name="play_btn_text">Erreproduzitu</string>
<string name="list_thumbnail_view_description">Bideoaren aurreikuspen argazkitxoa</string>
<string name="detail_thumbnail_view_description">Bideoaren aurreikuspen argazkitxoa</string>
<string name="detail_uploader_thumbnail_view_description">Igotzailearen argazkitxoa</string>
<string name="detail_dislikes_img_view_description">Ez dute gustoko</string>
<string name="detail_likes_img_view_description">Gustoko dute</string>
<string name="use_tor_title">Tor erabili</string>
<string name="use_tor_summary">Trafikoa Tor bidez deskargatzea behartzen du pribatutasuna hobetzeko (jario bideoak ez daude oraindik onartuta)</string>
<string name="background_player_name">NewPipe atzeko planoko erreproduzitzailea</string>
<string name="no_player_found">Jario erreproduzitzailerik ez da aurkitu. Agian bat instalatu nahi dezakezu.</string>
<string name="did_you_mean">"Hau esan nahi al zenuen: "</string>
<string name="search_page">"Orrialdea bilatu: "</string>
<string name="use_external_video_player_title">Kanpoko bideo erreproduzitzailea erabili</string>
<string name="use_external_audio_player_title">Kanpoko audio erreproduzitzailea erabili</string>
<string name="autoplay_through_intent_title">Intent bidez automatikoki erreproduzitu</string>
<string name="autoplay_through_intent_summary">Bideoa automatikoki hasten du beste aplikazio batetik deitu denean.</string>
<string name="background_player_playing_toast">Atzeko planoan erreproduzitzen</string>
</resources>

View File

@@ -1,42 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="viewCountText">%1$s نماها</string>
<string name="uploadDateText">بارگذاری‌شده در: %1$s</string>
<string name="noPlayerFound">هیچ پخش‌کننده‌ی جریانی یافت نشد. ممکن است بخواهید یکی نصب کنید.</string>
<string name="installStreamPlayer">نصب کنید</string>
<string name="view_count_text">%1$s نماها</string>
<string name="upload_date_text">بارگذاری‌شده در: %1$s</string>
<string name="no_player_found">هیچ پخش‌کننده‌ی جریانی یافت نشد. ممکن است بخواهید یکی نصب کنید.</string>
<string name="install">نصب کنید</string>
<string name="cancel">انصراف</string>
<string name="open_in_browser">بازکردن در مرورگر</string>
<string name="share">هم‌رسانی</string>
<string name="download">بارگیری</string>
<string name="search">جستجو</string>
<string name="settings">تنظیمات</string>
<string name="didYouMean">منظورتان این است: </string>
<string name="searchPage">صفحه‌ی جستجو: </string>
<string name="shareDialogTitle">هم‌رسانی با:</string>
<string name="chooseBrowser">مرورگر را برگزینید:</string>
<string name="screenRotation">چرخش</string>
<string name="title_activity_settings">تنظیمات</string>
<string name="did_you_mean">منظورتان این است: </string>
<string name="search_page">صفحه‌ی جستجو: </string>
<string name="share_dialog_title">هم‌رسانی با:</string>
<string name="choose_browser">مرورگر را برگزینید:</string>
<string name="screen_rotation">چرخش</string>
<string name="settings_activity_title">تنظیمات</string>
<string name="useExternalPlayerTitle">استفاده از پخش‌کننده‌ی خارجی</string>
<string name="downloadLocation">محل بارگیری</string>
<string name="downloadLocationSummary">مسیری که ویدئوهای دریافت شده در آن ذخیره می‌شوند.</string>
<string name="downloadLocationDialogTitle">مسیر دریافت را وارد کنید</string>
<string name="autoPlayThroughIntentTitle">پخش خودکار از Intent</string>
<string name="autoPlayThroughIntentSummary">ویدئو هنگامی که از برنامه‌ی دیگری فراخوانده شد خودکار پخش می‌شود.</string>
<string name="defaultResolutionPreferenceTitle">وضوح پیش‌فرض</string>
<string name="playWithKodiTitle">پخش با Kodi</string>
<string name="koreNotFound">برنامه‌ی Kore نصب نیست. برای پخش کردن ویدئوها با مرکز رسانه‌ی Kodi، به Kore نیاز دارید.</string>
<string name="download_path_title">محل بارگیری</string>
<string name="download_path_summary">مسیری که ویدئوهای دریافت شده در آن ذخیره می‌شوند.</string>
<string name="download_path_dialog_title">مسیر دریافت را وارد کنید</string>
<string name="autoplay_through_intent_title">پخش خودکار از Intent</string>
<string name="autoplay_through_intent_summary">ویدئو هنگامی که از برنامه‌ی دیگری فراخوانده شد خودکار پخش می‌شود.</string>
<string name="default_resolution_title">وضوح پیش‌فرض</string>
<string name="play_with_kodi_title">پخش با Kodi</string>
<string name="kore_not_found">برنامه‌ی Kore نصب نیست. برای پخش کردن ویدئوها با مرکز رسانه‌ی Kodi، به Kore نیاز دارید.</string>
<string name="installeKore">نصب Kore</string>
<string name="showPlayWithKodiTitle">نمایش گزینه‌ی «پخش با Kodi»</string>
<string name="showPlayWithKodiSummary">گزینه‌ای برای پخش کردن ویدئو با مرکز رسانه‌ی Kodi نشان می‌دهد.</string>
<string name="playAudio">صدا</string>
<string name="defaultAudioFormatTitle">قالب پیش‌فرض صدا</string>
<string name="webMAudioDescription">WebM - قالبی آزاد</string>
<string name="m4aAudioDescription">m4a - کیفیت بهتر</string>
<string name="downloadDialogTitle">دریافت</string>
<string name="show_play_with_kodi_title">نمایش گزینه‌ی «پخش با Kodi»</string>
<string name="show_play_with_kodi_summary">گزینه‌ای برای پخش کردن ویدئو با مرکز رسانه‌ی Kodi نشان می‌دهد.</string>
<string name="play_audio">صدا</string>
<string name="default_audio_format_title">قالب پیش‌فرض صدا</string>
<string name="webm_description">WebM &#8212; قالبی آزاد</string>
<string name="m4a_description">m4a &#8212; کیفیت بهتر</string>
<string name="download_dialog_title">دریافت</string>
<string-array name="downloadOptions">
<item>ویدئو</item>
<item>صدا</item>
</string-array>
<string name="nextVideoTitle">ویدئوی بعدی</string>
<string name="urlNotSupportedText">پیوند پشتیبانی نمی‌شود.</string>
<string name="next_video_title">ویدئوی بعدی</string>
<string name="url_not_supported_toast">پیوند پشتیبانی نمی‌شود.</string>
</resources>

View File

@@ -1,50 +1,76 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="autoPlayThroughIntentSummary">Démarrer automatiquement la vidéo si elle a été appelée depuis une autre application.</string>
<string name="autoplay_through_intent_summary">Lire automatiquement une vidéo lorsquelle a été appelée depuis une autre application.</string>
<string name="cancel">Annuler</string>
<string name="chooseBrowser">Choisir un navigateur :</string>
<string name="defaultResolutionPreferenceTitle">Résolution par défaut</string>
<string name="didYouMean">"S\'agirait-il de : "</string>
<string name="choose_browser">Choisir un navigateur :</string>
<string name="default_resolution_title">Définition par défaut</string>
<string name="did_you_mean">"Sagirait-il de : "</string>
<string name="download">Télécharger</string>
<string name="downloadLocation">Emplacement des téléchargements</string>
<string name="downloadLocationDialogTitle">Entrez l\'emplacement du téléchargement</string>
<string name="downloadLocationSummary">Emplacement des vidéos téléchargées.</string>
<string name="installStreamPlayer">Installer</string>
<string name="download_path_title">Répertoire de téléchargement des vidéos</string>
<string name="download_path_dialog_title">Entrer le chemin du répertoire de téléchargement des vidéos</string>
<string name="download_path_summary">Chemin du répertoire stockant les vidéos téléchargées.</string>
<string name="install">Installer</string>
<string name="installeKore">Installer Kore</string>
<string name="koreNotFound">L\'application Kore est introuvable. Kore est nécessaire afin de lire des vidéos dans Kodi media center.</string>
<string name="noPlayerFound">Aucun lecteur de streaming détecté. Vous devriez en installer un.</string>
<string name="kore_not_found">Lapplication Kore est introuvable. Installer Kore?</string>
<string name="no_player_found">Aucun lecteur de flux réseau trouvé. Installer VLC?</string>
<string name="open_in_browser">Ouvrir dans le navigateur</string>
<string name="autoPlayThroughIntentTitle">Lecture automatique via Intent</string>
<string name="playWithKodiTitle">Lire avec Kodi</string>
<string name="screenRotation">rotation</string>
<string name="autoplay_through_intent_title">Lecture automatique via Intent</string>
<string name="play_with_kodi_title">Lire avec Kodi</string>
<string name="screen_rotation">rotation</string>
<string name="search">Rechercher</string>
<string name="searchPage">"Rechercher dans la page : "</string>
<string name="search_page">"Rechercher dans la page : "</string>
<string name="settings">Paramètres</string>
<string name="share">Partager</string>
<string name="shareDialogTitle">Partager avec :</string>
<string name="showPlayWithKodiSummary">Afficher une option pour lire la vidéo avec Kodi media center.</string>
<string name="showPlayWithKodiTitle">Afficher l\'option \"Lire avec Kodi\"</string>
<string name="title_activity_settings">Paramètres</string>
<string name="uploadDateText">Mise en ligne le %1$s</string>
<string name="share_dialog_title">Partager avec :</string>
<string name="show_play_with_kodi_summary">Afficher une option pour lire la vidéo via la médiathèque Kodi.</string>
<string name="show_play_with_kodi_title">Afficher loption « Lire avec Kodi »</string>
<string name="settings_activity_title">Paramètres</string>
<string name="upload_date_text">Mise en ligne le %1$s</string>
<string name="useExternalPlayerTitle">Utiliser un lecteur externe</string>
<string name="viewCountText">%1$s vues</string>
<string name="playAudio">Audio</string>
<string name="defaultAudioFormatTitle">Format audio par défaut</string>
<string name="webMAudioDescription">WebM- format libre</string>
<string name="m4aAudioDescription">m4a - meilleur qualité</string>
<string name="downloadDialogTitle">Télécharger</string>
<string name="nextVideoTitle">Vidéo suivante</string>
<string name="showNextAndSimilarTitle">Afficher les vidéos suivantes et similaires</string>
<string name="urlNotSupportedText">URL non supportée.</string>
<string name="showSimilarVideosButtonText">Vidéos similaires</string>
<string name="settingsCategoryVideoAudioTitle">VIDÉO &amp; AUDIO</string>
<string name="settingsCategoryVideoInfoTittle">INFORMATION</string>
<string name="settingsCategoryEtcTitle">DIVERS</string>
<string name="view_count_text">%1$s vues</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Format audio par défaut</string>
<string name="webm_description">WebM — format libre</string>
<string name="m4a_description">m4a — meilleure qualité</string>
<string name="download_dialog_title">Télécharger</string>
<string name="next_video_title">Vidéo suivante</string>
<string name="show_next_and_similar_title">Afficher les vidéos suivantes et similaires</string>
<string name="url_not_supported_toast">URL non pris en charge</string>
<string name="similar_videos_btn_text">Vidéos similaires</string>
<string name="settings_category_video_audio_title">Vidéo et audio</string>
<string name="settings_category_other_title">Autre</string>
<string name="list_thumbnail_view_description">Miniature daperçu vidéo</string>
<string name="detail_thumbnail_view_description">Miniature daperçu vidéo</string>
<string name="detail_dislikes_img_view_description">Je naime pas</string>
<string name="detail_likes_img_view_description">Jaime</string>
<string name="search_language_title">Langue préférée du contenu</string>
<string name="detail_uploader_thumbnail_view_description">Miniature de lavatar de lutilisateur</string>
<string name="use_external_video_player_title">Utiliser un lecteur vidéo externe</string>
<string name="use_external_audio_player_title">Utiliser un lecteur audio externe</string>
<string name="background_player_playing_toast">Lecture en arrière-plan</string>
<string name="background_player_name">Lecteur en arrière-plan NewPipe</string>
<string name="loading">Chargement</string>
<string name="play_btn_text">Lecture</string>
<string name="use_tor_title">Utiliser Tor</string>
<string name="use_tor_summary">Forcer le trafic de téléchargement via Tor pour plus de confidentialité (vidéos diffusés en flux réseau non pris en charge pour linstant)</string>
<string name="theme_title">Thème</string>
<string name="dark_theme_title">Sombre</string>
<string name="light_theme_title">Clair</string>
<string name="settings_category_appearance_title">Apparence</string>
<string name="network_error">Erreur réseau</string>
<string name="download_path_audio_title">Répertoire de téléchargement des audios</string>
<string name="download_path_audio_summary">Chemin de stockage des fichiers audio téléchargés</string>
<string name="download_path_audio_dialog_title">Entrez le chemin de stockage fichiers audio téléchargés</string>
<string name="err_dir_create">Impossible de créer le répertoire de téléchargement « %1$s»</string>
<string name="info_dir_created">Répertoire de téléchargement « %1$s» créé</string>
<string name="general_error">Erreur</string>
<string name="parsing_error">Impossible de parser ce site web.</string>
<string name="content_not_available">Contenu non disponible.</string>
<string name="blocked_by_gema">Bloqué par GEMA.</string>
<string name="itemThumbnailViewDescription">Miniature d\'aperçu vidéo</string>
<string name="detailThumbnailViewDescription">Miniature d\'aperçu vidéo</string>
<string name="detailThumbsDownImgViewDescription">Je n\'aime pas</string>
<string name="detailThumbsUpImgViewDescription">J\'aime</string>
<string name="searchLanguageTitle">Langue du contenu</string>
<string name="detailUploaderThumbnailViewDescription">Avatar de l\'utilisateur</string>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<resources><string name="view_count_text">%1$s צפיות</string>
<string name="upload_date_text">הועלה בתאריך %1$s</string>
<string name="share">שתף</string>
<string name="search">חפש</string>
<string name="next_video_title">הבא</string>
<string name="download">הורדה</string>
<string name="settings">הגדרות</string>
<string name="settings_activity_title">הגדרות</string>
</resources>

View File

@@ -1,43 +1,75 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="viewCountText">%1$s megtekintés</string>
<string name="uploadDateText">Feltöltve: %1$s</string>
<string name="noPlayerFound">Nem található lejátszó. Telepítsen egyet!</string>
<string name="installStreamPlayer">Telepítsen egyet</string>
<string name="view_count_text">%1$s megtekintés</string>
<string name="upload_date_text">Feltöltve: %1$s</string>
<string name="no_player_found">Nem található megfelelő lejátszó. Feltelepíti a VLC lejátszót?</string>
<string name="install">Telepítés</string>
<string name="cancel">Mégse</string>
<string name="open_in_browser">Megnyitás böngészőben</string>
<string name="share">Megosztás</string>
<string name="download">Letöltés</string>
<string name="search">Keresés</string>
<string name="settings">Beállítások</string>
<string name="didYouMean">Erre gondolt: </string>
<string name="searchPage">Keresési lap: </string>
<string name="shareDialogTitle">Megosztás ezzel:</string>
<string name="chooseBrowser">Válasszon böngészőt:</string>
<string name="screenRotation">forgatás</string>
<string name="title_activity_settings">Beállítások</string>
<string name="did_you_mean">Erre gondolt: </string>
<string name="search_page">"Keresőlap: "</string>
<string name="share_dialog_title">Megosztás ezzel:</string>
<string name="choose_browser">Válasszon böngészőt:</string>
<string name="screen_rotation">forgatás</string>
<string name="settings_activity_title">Beállítások</string>
<string name="useExternalPlayerTitle">Külső lejátszó használata</string>
<string name="downloadLocation">Letöltések helye</string>
<string name="downloadLocationSummary">Útvonal a letöltött videók tárolásához</string>
<string name="downloadLocationDialogTitle">Adja meg a letöltési útvonalat</string>
<string name="autoPlayThroughIntentTitle">Automatikus lejátszás Intent-en keresztül</string>
<string name="autoPlayThroughIntentSummary">Automatikusan elindítja a videót, ha az külső alkalmazásl volt hívva</string>
<string name="defaultResolutionPreferenceTitle">Alapértelmezett felbontás</string>
<string name="playWithKodiTitle">Lejátszás Kodi-val</string>
<string name="koreNotFound">A Kore alkalmazás nem található. A Kore szükséges a videók Kodi médiaközponttal való lejátszásához.</string>
<string name="download_path_title">Letöltött videófájlok helye</string>
<string name="download_path_summary">Útvonal a letöltött videók tárolásához.</string>
<string name="download_path_dialog_title">Adja meg a videófájlok letöltési helyét</string>
<string name="autoplay_through_intent_title">Automatikus lejátszás Intent-en keresztül</string>
<string name="autoplay_through_intent_summary">Videó automatikus lejátszása külső alkalmazással való megnyitás esetén.</string>
<string name="default_resolution_title">Alapértelmezett felbontás</string>
<string name="play_with_kodi_title">Lejátszás Kodi-val</string>
<string name="kore_not_found">A Kore alkalmazás nem található. Feltelepíti a Kore lejátszót?</string>
<string name="installeKore">Kore telepítése</string>
<string name="showPlayWithKodiTitle">\"Lejátszás Kodi-val\" opció mutatása</string>
<string name="showPlayWithKodiSummary">Mutat egy opciót a videók Kodi médiaközponttal való lejátszására</string>
<string name="playAudio">Hang</string>
<string name="defaultAudioFormatTitle">Alapértelmezett hang formátum</string>
<string name="webMAudioDescription">WebM - szabad formátum</string>
<string name="m4aAudioDescription">m4a - jobb minőség</string>
<string name="downloadDialogTitle">Letöltés</string>
<string name="show_play_with_kodi_title">\"Lejátszás Kodi-val\" opció mutatása</string>
<string name="show_play_with_kodi_summary">Mutat egy opciót a videók Kodi médiaközponttal való lejátszására</string>
<string name="play_audio">Hang</string>
<string name="default_audio_format_title">Alapértelmezett hang formátum</string>
<string name="webm_description">WebM szabad formátum</string>
<string name="m4a_description">m4a jobb minőség</string>
<string name="download_dialog_title">Letöltés</string>
<string-array name="downloadOptions">
<item>Videó</item>
<item>Hang</item>
</string-array>
<string name="nextVideoTitle">Következő videó</string>
<string name="urlNotSupportedText">A webcím nem támogatott.</string>
<string name="showSimilarVideosButtonText">Hasonló videók</string>
</resources>
<string name="next_video_title">Következő videó</string>
<string name="url_not_supported_toast">A webcím nem támogatott</string>
<string name="similar_videos_btn_text">Hasonló videók</string>
<string name="use_external_video_player_title">Külső videólejátszó használata</string>
<string name="use_external_audio_player_title">Külső hanglejátszó használata</string>
<string name="download_path_audio_dialog_title">Adja meg a hangfájlok letöltési helyét.</string>
<string name="background_player_name">NewPipe Háttérlejátszó</string>
<string name="loading">Töltés</string>
<string name="use_tor_title">Tor használata</string>
<string name="use_tor_summary">Adatforgalom Tor-on keresztüli kényszerítése a biztonság fokozása érdekében (a videó stream-elés még nem támogatott)</string>
<string name="play_btn_text">Lejátszás</string>
<string name="network_error">Hálózati hiba</string>
<string name="background_player_playing_toast">Lejátszás háttérben</string>
<string name="settings_category_video_audio_title">Videó és audio</string>
<string name="settings_category_appearance_title">Megjelenés</string>
<string name="search_language_title">Tartalom preferált nyelve</string>
<string name="settings_category_other_title">További beállítások</string>
<string name="show_next_and_similar_title">Következő hasonló videók mutatása</string>
<string name="dark_theme_title">Sötét</string>
<string name="light_theme_title">Világos</string>
<string name="theme_title">Téma</string>
<string name="download_path_audio_title">Letöltött hangfájlok helye</string>
<string name="download_path_audio_summary">Útvonal a letöltött hangfájlok tárolásához.</string>
<string name="detail_likes_img_view_description">Tetszik</string>
<string name="detail_dislikes_img_view_description">Nem tetszik</string>
<string name="list_thumbnail_view_description">Videó előnézet</string>
<string name="detail_thumbnail_view_description">Videó előnézet</string>
<string name="detail_uploader_thumbnail_view_description">Fetöltő profilképe</string>
<string name="err_dir_create">Nem lehet létrehozni a letöltés mappát \'%1$s\'</string>
<string name="info_dir_created">Letöltés mappa létrehozása \'%1$s\'</string>
</resources>

1
app/src/main/res/values-id Symbolic link
View File

@@ -0,0 +1 @@
values-in

View File

@@ -0,0 +1,3 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
</resources>

View File

@@ -1,49 +1,49 @@
<?xml version='1.0' encoding='utf-8'?>
<resources><string name="viewCountText">%1$s visite</string>
<string name="uploadDateText">Caricato in %1$s</string>
<string name="noPlayerFound">Nessun riproduttore trovato. Dovresti installarne uno.</string>
<string name="installStreamPlayer">Installa</string>
<resources><string name="view_count_text">%1$s visite</string>
<string name="upload_date_text">Caricato in %1$s</string>
<string name="no_player_found">Nessun riproduttore trovato. Dovresti installarne uno.</string>
<string name="install">Installa</string>
<string name="cancel">Cancella</string>
<string name="open_in_browser">Apri nel browser</string>
<string name="share">Condividi</string>
<string name="download">Scarica</string>
<string name="search">Cerca</string>
<string name="settings">Impostazioni</string>
<string name="didYouMean">"Intendevi: "</string>
<string name="searchPage">"Cerca pagina: "</string>
<string name="shareDialogTitle">Condividi con:</string>
<string name="chooseBrowser">Scegli browser:</string>
<string name="screenRotation">rotazione</string>
<string name="title_activity_settings">Impostazioni</string>
<string name="did_you_mean">"Intendevi: "</string>
<string name="search_page">"Cerca pagina: "</string>
<string name="share_dialog_title">Condividi con:</string>
<string name="choose_browser">Scegli browser:</string>
<string name="screen_rotation">rotazione</string>
<string name="settings_activity_title">Impostazioni</string>
<string name="useExternalPlayerTitle">Usa un riproduttore video esterno</string>
<string name="downloadLocation">Cartella di download</string>
<string name="downloadLocationSummary">Percorso dove memorizzare i video scaricati.</string>
<string name="downloadLocationDialogTitle">Inserisci il percorso di download</string>
<string name="autoPlayThroughIntentTitle">Auto riproduzione attraverso internet</string>
<string name="autoPlayThroughIntentSummary">Avvia automaticamente un video quando è stato chiamato da un\'altra applicazione.</string>
<string name="defaultResolutionPreferenceTitle">Risoluzione predefinita</string>
<string name="playWithKodiTitle">Riproduci con Kodi</string>
<string name="koreNotFound">Kore app non trovata. Kore è richiesto per riprodurre video con Kodi media center.</string>
<string name="download_path_title">Cartella di download</string>
<string name="download_path_summary">Percorso dove memorizzare i video scaricati.</string>
<string name="download_path_dialog_title">Inserisci il percorso di download</string>
<string name="autoplay_through_intent_title">Auto riproduzione attraverso internet</string>
<string name="autoplay_through_intent_summary">Avvia automaticamente un video quando è stato chiamato da un\'altra applicazione.</string>
<string name="default_resolution_title">Risoluzione predefinita</string>
<string name="play_with_kodi_title">Riproduci con Kodi</string>
<string name="kore_not_found">Kore app non trovata. Kore è richiesto per riprodurre video con Kodi media center.</string>
<string name="installeKore">Installa Kore</string>
<string name="showPlayWithKodiTitle">Mostra l\'opzione \"Riproduci con Kodi\"</string>
<string name="showPlayWithKodiSummary">Mostra un opzione per riprodurre un video attraverso Kodi media center.</string>
<string name="playAudio">Audio</string>
<string name="defaultAudioFormatTitle">Formato audio predefinito</string>
<string name="webMAudioDescription">WedM - formato libero</string>
<string name="m4aAudioDescription">m4a - qualità migliore</string>
<string name="downloadDialogTitle">Scarica</string>
<string name="nextVideoTitle">Prossimo video</string>
<string name="showNextAndSimilarTitle">Mostra i video successivi e simili</string>
<string name="urlNotSupportedText">URL non supportato.</string>
<string name="showSimilarVideosButtonText">Video simili</string>
<string name="searchLanguageTitle">Lingua preferita dei contenuti</string>
<string name="settingsCategoryVideoAudioTitle">VIDEO &amp; AUDIO</string>
<string name="show_play_with_kodi_title">Mostra l\'opzione \"Riproduci con Kodi\"</string>
<string name="show_play_with_kodi_summary">Mostra un opzione per riprodurre un video attraverso Kodi media center.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Formato audio predefinito</string>
<string name="webm_description">WedM &#8212; formato libero</string>
<string name="m4a_description">m4a &#8212; qualità migliore</string>
<string name="download_dialog_title">Scarica</string>
<string name="next_video_title">Prossimo video</string>
<string name="show_next_and_similar_title">Mostra i video successivi e simili</string>
<string name="url_not_supported_toast">URL non supportato.</string>
<string name="similar_videos_btn_text">Video simili</string>
<string name="search_language_title">Lingua preferita dei contenuti</string>
<string name="settings_category_video_audio_title">VIDEO &amp; AUDIO</string>
<string name="settingsCategoryVideoInfoTittle">INFO</string>
<string name="settingsCategoryEtcTitle">ETC</string>
<string name="itemThumbnailViewDescription">Anteprima video</string>
<string name="detailThumbnailViewDescription">Anteprima video</string>
<string name="detailUploaderThumbnailViewDescription">Miniatura caricata</string>
<string name="detailThumbsDownImgViewDescription">Non mi piace</string>
<string name="detailThumbsUpImgViewDescription">Mi piace</string>
<string name="list_thumbnail_view_description">Anteprima video</string>
<string name="detail_thumbnail_view_description">Anteprima video</string>
<string name="detail_uploader_thumbnail_view_description">Miniatura caricata</string>
<string name="detail_dislikes_img_view_description">Non mi piace</string>
<string name="detail_likes_img_view_description">Mi piace</string>
</resources>

1
app/src/main/res/values-iw Symbolic link
View File

@@ -0,0 +1 @@
values-he

View File

@@ -1,49 +1,81 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="uploadDateText">"アップロード: "%1$s</string>
<string name="noPlayerFound">StreamPlayer が見つかりませんでした。インストールが必要になるかもしれません。</string>
<string name="installStreamPlayer">インストール</string>
<string name="upload_date_text">"アップロード: "%1$s</string>
<string name="no_player_found">StreamPlayer が見つかりませんでした。インストールが必要になるかもしれません。</string>
<string name="install">インストール</string>
<string name="cancel">取り消し</string>
<string name="open_in_browser">ブラウザーで開く</string>
<string name="share">共有</string>
<string name="download">ダウンロード</string>
<string name="search">検索</string>
<string name="settings">設定</string>
<string name="didYouMean">"この意味ですか: "</string>
<string name="searchPage">"検索ページ: "</string>
<string name="shareDialogTitle">…共有:</string>
<string name="chooseBrowser">ブラウザーを選択:</string>
<string name="screenRotation">回転</string>
<string name="title_activity_settings">設定</string>
<string name="did_you_mean">"この意味ですか: "</string>
<string name="search_page">"検索ページ: "</string>
<string name="share_dialog_title">…共有:</string>
<string name="choose_browser">ブラウザーを選択:</string>
<string name="screen_rotation">回転</string>
<string name="settings_activity_title">設定</string>
<string name="useExternalPlayerTitle">外部プレーヤーを使用する</string>
<string name="downloadLocation">ダウンロードする場所</string>
<string name="downloadLocationSummary">ダウンロードした動画を保存する場所のパス。</string>
<string name="downloadLocationDialogTitle">ダウンロードパスを入力してください</string>
<string name="autoPlayThroughIntentTitle">インテントで自動再生</string>
<string name="autoPlayThroughIntentSummary">他のアプリケーションから呼び出されたとき、自動的に動画再生を開始します。</string>
<string name="defaultResolutionPreferenceTitle">基本の解像度</string>
<string name="playWithKodiTitle">Kodi で再生</string>
<string name="koreNotFound">Kore アプリが見つかりません。 Kodi メディアセンターで動画を再生するには、 Kore が必要です。</string>
<string name="download_path_title">動画をダウンロードする場所</string>
<string name="download_path_summary">ダウンロードした動画を保存する場所のパス。</string>
<string name="download_path_dialog_title">動画をダウンロードするパスを入力してください</string>
<string name="autoplay_through_intent_title">インテントで自動再生</string>
<string name="autoplay_through_intent_summary">他のアプリケーションから呼び出されたとき、自動的に動画再生を開始します。</string>
<string name="default_resolution_title">基本の解像度</string>
<string name="play_with_kodi_title">Kodi で再生</string>
<string name="kore_not_found">Kore アプリが見つかりません。 Kodi メディアセンターで動画を再生するには、 Kore が必要です。</string>
<string name="installeKore">Kore をインストール</string>
<string name="showPlayWithKodiTitle">\"Kodi で再生\" 設定を表示</string>
<string name="showPlayWithKodiSummary">Kodi メディアセンター経由で動画を再生するための設定を表示します.</string>
<string name="playAudio">オーディオ</string>
<string name="defaultAudioFormatTitle">基本のオーディオフォーマット</string>
<string name="webMAudioDescription">.WebM - フリーフォーマット</string>
<string name="m4aAudioDescription">.m4a - より良い品質</string>
<string name="downloadDialogTitle">ダウンロード</string>
<string name="nextVideoTitle">次の動画</string>
<string name="showNextAndSimilarTitle">次の同様の動画を表示します。</string>
<string name="urlNotSupportedText">URL は使用できません。</string>
<string name="showSimilarVideosButtonText">同様の動画</string>
<string name="searchLanguageTitle">優先される言語</string>
<string name="settingsCategoryVideoAudioTitle">動画とオーディオ</string>
<string name="settingsCategoryVideoInfoTittle">情報</string>
<string name="settingsCategoryEtcTitle">その他</string>
<string name="viewCountText">%1$s ビュー</string>
<string name="itemThumbnailViewDescription">ビデオ プレビュー サムネイル</string>
<string name="detailThumbnailViewDescription">ビデオ プレビュー サムネイル</string>
<string name="detailUploaderThumbnailViewDescription">アップローダー サムネイル</string>
<string name="detailThumbsDownImgViewDescription">残念だね</string>
<string name="detailThumbsUpImgViewDescription">いいね</string>
</resources>
<string name="show_play_with_kodi_title">\"Kodi で再生\" 設定を表示</string>
<string name="show_play_with_kodi_summary">Kodi メディアセンター経由で動画を再生するための設定を表示します.</string>
<string name="play_audio">オーディオ</string>
<string name="default_audio_format_title">基本のオーディオフォーマット</string>
<string name="webm_description">WebM フリーフォーマット</string>
<string name="m4a_description">m4a より良い品質</string>
<string name="download_dialog_title">ダウンロード</string>
<string name="next_video_title">次の動画</string>
<string name="show_next_and_similar_title">次の同様の動画を表示します。</string>
<string name="url_not_supported_toast">URL は使用できません。</string>
<string name="similar_videos_btn_text">同様の動画</string>
<string name="search_language_title">優先される言語</string>
<string name="settings_category_video_audio_title">動画とオーディオ</string>
<string name="view_count_text">%1$s ビュー</string>
<string name="list_thumbnail_view_description">ビデオ プレビュー サムネイル</string>
<string name="detail_thumbnail_view_description">ビデオ プレビュー サムネイル</string>
<string name="detail_uploader_thumbnail_view_description">アップローダー サムネイル</string>
<string name="detail_dislikes_img_view_description">残念だね</string>
<string name="detail_likes_img_view_description">いいね</string>
<string name="use_external_video_player_title">外部ビデオ プレイヤーを使用する</string>
<string name="use_external_audio_player_title">外部オーディオ プレイヤーを使用する</string>
<string name="background_player_playing_toast">バックグラウンドで再生しています</string>
<string name="background_player_name">NewPipe バックグラウンド プレーヤー</string>
<string name="loading">読み込み中</string>
<string name="play_btn_text">再生</string>
<string name="use_tor_title">Tor を使用する</string>
<string name="use_tor_summary">強制的に Tor を経由したプライバシーを高めたトラフィックでダウンロードします (ビデオのストリーミングはまだサポートされていません)</string>
<string name="theme_title">テーマ</string>
<string name="dark_theme_title">ダーク</string>
<string name="light_theme_title">ライト</string>
<string name="settings_category_appearance_title">外観</string>
<string name="settings_category_other_title">その他</string>
<string name="network_error">ネットワーク エラー</string>
<string name="download_path_audio_title">オーディオをダウンロードするパス</string>
<string name="download_path_audio_summary">オーディオをダウンロードするパス。</string>
<string name="download_path_audio_dialog_title">オーデォファイルをダウンロードするパスを入力してください。</string>
<string name="err_dir_create">ダウンロード ディレクトリ \'%1$s\' を作成できません</string>
<string name="info_dir_created">ダウンロード ディレクトリ \'%1$s\' を作成しました</string>
<string name="general_error">エラー</string>
<string name="could_not_load_thumbnails">すべてのサムネイルを読み込むことができません</string>
<string name="youtube_signature_decryption_error">ビデオの url 署名を復号化できませんでした。</string>
<string name="parsing_error">Web サイトを解析できませんでした。</string>
<string name="content_not_available">コンテンツがありません。</string>
<string name="blocked_by_gema">GEMA によってブロックされました。</string>
<string name="could_not_setup_download_menu">ダウンロード メニューを設定できませんでした。</string>
</resources>

View File

@@ -1,49 +1,73 @@
<?xml version='1.0' encoding='utf-8'?>
<resources><string name="viewCountText">시청 횟수 %1$s</string>
<string name="uploadDateText">%1$s에 업로드됨</string>
<string name="noPlayerFound">스트리밍 플레이어가 발견되지 않았습니다. 플레이어를 설치하시기 바랍니다.</string>
<string name="installStreamPlayer">설치</string>
<resources><string name="view_count_text">시청 횟수 %1$s</string>
<string name="upload_date_text">%1$s에 업로드됨</string>
<string name="no_player_found">스트리밍 플레이어가 발견되지 않았습니다. VLC를 설치할까요?</string>
<string name="install">설치</string>
<string name="cancel">취소</string>
<string name="open_in_browser">브라우저에서 열기</string>
<string name="share">공유</string>
<string name="download">다운로드</string>
<string name="search">검색</string>
<string name="settings">설정</string>
<string name="didYouMean">"혹시 이것을 검색하셨습니까? "</string>
<string name="searchPage">"검색 페이지: "</string>
<string name="shareDialogTitle">다음으로 공유:</string>
<string name="chooseBrowser">브라우저 선택:</string>
<string name="screenRotation">회전</string>
<string name="title_activity_settings">설정</string>
<string name="did_you_mean">"혹시 이것을 검색하셨습니까? "</string>
<string name="search_page">"검색 페이지: "</string>
<string name="share_dialog_title">다음으로 공유:</string>
<string name="choose_browser">브라우저 선택:</string>
<string name="screen_rotation">회전</string>
<string name="settings_activity_title">설정</string>
<string name="useExternalPlayerTitle">외부 플레이어 사용</string>
<string name="downloadLocation">다운로드 위치</string>
<string name="downloadLocationSummary">다운로드된 비디오가 저장될 경로를 선택하세요.</string>
<string name="downloadLocationDialogTitle">다운로드 경로 입력</string>
<string name="autoPlayThroughIntentTitle">인텐트로 경유할 경우 자동 재생</string>
<string name="autoPlayThroughIntentSummary">다른 앱으로부터 호출되었을 경우에 비디오를 자동으로 재생합니다.</string>
<string name="defaultResolutionPreferenceTitle">기본 해상도</string>
<string name="playWithKodiTitle">Kodi로 재생</string>
<string name="koreNotFound">Kore 앱이 발견되지 않았습니다. Kodi media center로 비디오를 재생하려면 Kore가 필요합니다.</string>
<string name="download_path_title">비디오 다운로드 위치</string>
<string name="download_path_summary">다운로드된 비디오가 저장될 경로를 선택하세요.</string>
<string name="download_path_dialog_title">비디오 다운로드 경로 입력</string>
<string name="autoplay_through_intent_title">인텐트로 경유할 경우 자동 재생</string>
<string name="autoplay_through_intent_summary">다른 앱으로부터 호출되었을 경우에 비디오를 자동으로 재생합니다.</string>
<string name="default_resolution_title">기본 해상도</string>
<string name="play_with_kodi_title">Kodi로 재생</string>
<string name="kore_not_found">Kore 앱이 발견되지 않았습니다. Kore를 설치할까요?</string>
<string name="installeKore">Kore 설치</string>
<string name="showPlayWithKodiTitle">\"Kodi로 재생\" 옵션 표시</string>
<string name="showPlayWithKodiSummary">비디오를 Kodi media center를 사용해 재생하는 옵션을 표시합니다.</string>
<string name="playAudio">오디오</string>
<string name="defaultAudioFormatTitle">기본 오디오 포맷</string>
<string name="webMAudioDescription">WebM - 무료 자유 포맷입니다</string>
<string name="m4aAudioDescription">m4a - 보다 나은 품질</string>
<string name="downloadDialogTitle">다운로드</string>
<string name="nextVideoTitle">다음 비디오</string>
<string name="showNextAndSimilarTitle">다음 및 유사한 비디오 표시</string>
<string name="urlNotSupportedText">지원하지 않는 URL 입니다.</string>
<string name="showSimilarVideosButtonText">유사한 비디오</string>
<string name="searchLanguageTitle">선호하는 컨텐츠 언어</string>
<string name="settingsCategoryVideoAudioTitle">비디오 &amp; 오디오</string>
<string name="show_play_with_kodi_title">\"Kodi로 재생\" 옵션 표시</string>
<string name="show_play_with_kodi_summary">비디오를 Kodi media center를 사용해 재생하는 옵션을 표시합니다.</string>
<string name="play_audio">오디오</string>
<string name="default_audio_format_title">기본 오디오 포맷</string>
<string name="webm_description">WebM 무료 자유 포맷입니다</string>
<string name="m4a_description">m4a 보다 나은 품질</string>
<string name="download_dialog_title">다운로드</string>
<string name="next_video_title">다음 비디오</string>
<string name="show_next_and_similar_title">다음 및 유사한 비디오 표시</string>
<string name="url_not_supported_toast">지원하지 않는 URL 입니다</string>
<string name="similar_videos_btn_text">유사한 비디오</string>
<string name="search_language_title">선호하는 컨텐츠 언어</string>
<string name="settings_category_video_audio_title">비디오 &amp; 오디오</string>
<string name="settingsCategoryVideoInfoTittle">정보</string>
<string name="settingsCategoryEtcTitle">기타</string>
<string name="itemThumbnailViewDescription">비디오 미리보기 썸네일</string>
<string name="detailThumbnailViewDescription">비디오 미리보기 썸네일</string>
<string name="detailUploaderThumbnailViewDescription">업로더 썸네일</string>
<string name="detailThumbsDownImgViewDescription">싫어요</string>
<string name="detailThumbsUpImgViewDescription">좋아요</string>
<string name="list_thumbnail_view_description">비디오 미리보기 썸네일</string>
<string name="detail_thumbnail_view_description">비디오 미리보기 썸네일</string>
<string name="detail_uploader_thumbnail_view_description">업로더 썸네일</string>
<string name="detail_dislikes_img_view_description">싫어요</string>
<string name="detail_likes_img_view_description">좋아요</string>
<string name="background_player_name">NewPipe 백그라운드 플레이어</string>
<string name="loading">불러오는 중</string>
<string name="use_external_video_player_title">외부 비디오 플레이어 사용</string>
<string name="use_external_audio_player_title">외부 오디오 플레이어 사용</string>
<string name="download_path_audio_title">오디오 다운로드 경로</string>
<string name="download_path_audio_summary">다운로드된 오디오를 저장할 경로입니다.</string>
<string name="download_path_audio_dialog_title">오디오 파일 다운로드 경로를 입력하세요.</string>
<string name="theme_title">테마</string>
<string name="dark_theme_title">어두운 테마</string>
<string name="light_theme_title">밝은 테마</string>
<string name="settings_category_appearance_title">외관</string>
<string name="settings_category_other_title">기타</string>
<string name="background_player_playing_toast">백그라운드에서 재생 중</string>
<string name="play_btn_text">재생</string>
<string name="network_error">네트워크 오류</string>
<string name="use_tor_title">Tor 사용</string>
<string name="use_tor_summary">향상된 프라이버시를 위해 다운로드 트래픽을 강제로 Tor를 통해 전송 (스트리밍 비디오는 아직 지원되지 않습니다)</string>
<string name="err_dir_create">다운로드 디렉토리를 만들 수 없습니다 \'%1$s\'</string>
<string name="info_dir_created">다운로드 디렉토리를 만들었습니다 \'%1$s\'</string>
</resources>

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