Compare commits
512 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a12ca9f7 | ||
|
|
5a594173fa | ||
|
|
1e989945b9 | ||
|
|
679bef849c | ||
|
|
1c17be9760 | ||
|
|
6def0e3115 | ||
|
|
fd3436d5c0 | ||
|
|
a94f9fd3e5 | ||
|
|
77850464d4 | ||
|
|
3e9edba189 | ||
|
|
3d168542fe | ||
|
|
0de00e26d8 | ||
|
|
f0705c612e | ||
|
|
648b9b5d02 | ||
|
|
b15a0b92f9 | ||
|
|
80482c0578 | ||
|
|
16701923a1 | ||
|
|
67cee06523 | ||
|
|
685dab39af | ||
|
|
0ff9555cc4 | ||
|
|
4f808dd17d | ||
|
|
69d71dac36 | ||
|
|
4880319991 | ||
|
|
13b9850b39 | ||
|
|
366f645eda | ||
|
|
dcf7190c90 | ||
|
|
ae6647276c | ||
|
|
460ff37f7a | ||
|
|
e7ef913416 | ||
|
|
c149050a3a | ||
|
|
a4e5ca54db | ||
|
|
cb81fd3353 | ||
|
|
352a883b5d | ||
|
|
5247b22ef4 | ||
|
|
1bf00e80b0 | ||
|
|
6331d9b7a4 | ||
|
|
0deb0ad851 | ||
|
|
3b42041c4e | ||
|
|
d8987aa94a | ||
|
|
19ed16efc6 | ||
|
|
0e32d70aef | ||
|
|
5b71f279fd | ||
|
|
6d170ffb16 | ||
|
|
72d90374a0 | ||
|
|
fc8160acda | ||
|
|
751ffb9de9 | ||
|
|
60d636940d | ||
|
|
7a5cca519a | ||
|
|
1e93d06a25 | ||
|
|
4341f8aaec | ||
|
|
46022d60f3 | ||
|
|
80ddc76926 | ||
|
|
bd999b4b0a | ||
|
|
4cdc387338 | ||
|
|
df258a0003 | ||
|
|
7cc1c0fbdd | ||
|
|
42644c956b | ||
|
|
00eaedcbfa | ||
|
|
1ff5a97c85 | ||
|
|
241414f81b | ||
|
|
1bf046a8ba | ||
|
|
61471fdd3c | ||
|
|
a0524fb136 | ||
|
|
fa39389a64 | ||
|
|
697a24e699 | ||
|
|
cf3bb87a54 | ||
|
|
14fb7d8a7a | ||
|
|
d097363b24 | ||
|
|
bad576c23d | ||
|
|
8d9e4e7442 | ||
|
|
431d03c63f | ||
|
|
24321731d9 | ||
|
|
fddcade1fb | ||
|
|
a0aa7dcdc1 | ||
|
|
d3e9f354b3 | ||
|
|
e958334406 | ||
|
|
a42029970d | ||
|
|
2108e09e13 | ||
|
|
8c010093e8 | ||
|
|
adc98be441 | ||
|
|
fb942912db | ||
|
|
7f12b58722 | ||
|
|
05d58f8f98 | ||
|
|
46c2db310a | ||
|
|
607408b9b9 | ||
|
|
b81e8cb81a | ||
|
|
44ef04d90a | ||
|
|
3e9d84b109 | ||
|
|
aa2d8d4833 | ||
|
|
55f63b948b | ||
|
|
cece543d6b | ||
|
|
9204a89319 | ||
|
|
f8ed96bb25 | ||
|
|
54d318bf04 | ||
|
|
f152d66cd8 | ||
|
|
576786c751 | ||
|
|
a6b7cd3202 | ||
|
|
ea9caaaaf0 | ||
|
|
22d8f434be | ||
|
|
8360c10141 | ||
|
|
750dc6f2cc | ||
|
|
47b556544d | ||
|
|
f6cee739a6 | ||
|
|
d1afe0028c | ||
|
|
a4cace1ef7 | ||
|
|
707ac8be27 | ||
|
|
2f2254966a | ||
|
|
eb57675f55 | ||
|
|
dde93c5ccf | ||
|
|
886b5959f7 | ||
|
|
dc44546ac7 | ||
|
|
42c6698732 | ||
|
|
bd014a107f | ||
|
|
3a7298bb99 | ||
|
|
aa06e3490d | ||
|
|
ece889b36b | ||
|
|
9848f19ce6 | ||
|
|
e59b087057 | ||
|
|
5b451d1ac7 | ||
|
|
f050c05b3c | ||
|
|
06b8edefbf | ||
|
|
f3a2a28398 | ||
|
|
9b0a1fc2ec | ||
|
|
c87ab234eb | ||
|
|
68468756a8 | ||
|
|
9016df0195 | ||
|
|
7acece9705 | ||
|
|
d1896c23c0 | ||
|
|
dae19f03e0 | ||
|
|
9dafccf0f7 | ||
|
|
5f270c41ae | ||
|
|
f9f80e9003 | ||
|
|
c5063d4269 | ||
|
|
ccbe18ec1c | ||
|
|
321a8a8b25 | ||
|
|
77a9560376 | ||
|
|
058a039a82 | ||
|
|
7c744703e4 | ||
|
|
a934cbb085 | ||
|
|
cf4158c0d0 | ||
|
|
dc56eab9b6 | ||
|
|
37d1f59132 | ||
|
|
ab0ce55411 | ||
|
|
c1d66596d1 | ||
|
|
cbfccdf0d3 | ||
|
|
9362037177 | ||
|
|
e25c93bae2 | ||
|
|
367c434010 | ||
|
|
02d8463e15 | ||
|
|
9d5a1d5c43 | ||
|
|
c8d94f541f | ||
|
|
27d06eaa6b | ||
|
|
7f32857e00 | ||
|
|
2f060f0f52 | ||
|
|
f89d405226 | ||
|
|
fd4459e570 | ||
|
|
eb0df2b101 | ||
|
|
6c178cfb7e | ||
|
|
8ced68430d | ||
|
|
0aade598ff | ||
|
|
95949fd1ab | ||
|
|
1a56382112 | ||
|
|
d610e4b19b | ||
|
|
fc44d9e36e | ||
|
|
c07686576a | ||
|
|
c2400aea4d | ||
|
|
fb4bf0dde4 | ||
|
|
e4f638d1ce | ||
|
|
5c492c01a1 | ||
|
|
f451e11f82 | ||
|
|
95b73f35f7 | ||
|
|
58147e9e12 | ||
|
|
a8830e2ede | ||
|
|
9804bb95cc | ||
|
|
0da1aef763 | ||
|
|
0a334804a3 | ||
|
|
94d2f03e9b | ||
|
|
9127f7f0c2 | ||
|
|
0bb0226bc2 | ||
|
|
b3a1a5dcc2 | ||
|
|
984dd1cc25 | ||
|
|
5663e543a4 | ||
|
|
d3879a0398 | ||
|
|
6bd2468d44 | ||
|
|
e63d43151b | ||
|
|
0265da4ae6 | ||
|
|
ef255d12ae | ||
|
|
eeb612f9a2 | ||
|
|
dfcb4edb81 | ||
|
|
d3500e9036 | ||
|
|
adcb8c6469 | ||
|
|
592eee7d3d | ||
|
|
7dadb2b26c | ||
|
|
7cbb135f28 | ||
|
|
966ac0673c | ||
|
|
d715eae0d1 | ||
|
|
ccdd13d136 | ||
|
|
efe5de4c75 | ||
|
|
2a93e9bd2e | ||
|
|
28dd53ae50 | ||
|
|
3c1e64d8dc | ||
|
|
4fe3cb2bca | ||
|
|
b31490c4e3 | ||
|
|
5533f6ba86 | ||
|
|
8aa5f87a1c | ||
|
|
6deb674377 | ||
|
|
12d1d998a3 | ||
|
|
d90162d06f | ||
|
|
97c924341c | ||
|
|
e91fc225e1 | ||
|
|
43149fd832 | ||
|
|
78df579703 | ||
|
|
f61b915894 | ||
|
|
cd3f405bff | ||
|
|
7cfdca7a81 | ||
|
|
216063dba8 | ||
|
|
b647bacd72 | ||
|
|
4f77937e3e | ||
|
|
48e299b2ac | ||
|
|
40f00af196 | ||
|
|
bd6cc22e63 | ||
|
|
8760792426 | ||
|
|
870b0bf7aa | ||
|
|
afd0bd4318 | ||
|
|
f829ac1d34 | ||
|
|
86a0177855 | ||
|
|
718d4fd0bd | ||
|
|
365137c32b | ||
|
|
e83ca0dfda | ||
|
|
6a9f6ef651 | ||
|
|
99122ccc03 | ||
|
|
97923697e1 | ||
|
|
68888b15e0 | ||
|
|
f48b26067b | ||
|
|
4bf2d5837d | ||
|
|
4eb2d09c75 | ||
|
|
a146c1c4b6 | ||
|
|
2f1ea9aa5d | ||
|
|
a90da62deb | ||
|
|
c1c3fbdf26 | ||
|
|
e07a824d82 | ||
|
|
2aff660a5b | ||
|
|
abfcbe6f0e | ||
|
|
28bf72ed75 | ||
|
|
21b054d4ca | ||
|
|
34f115b322 | ||
|
|
8b67354076 | ||
|
|
b62e0a8b40 | ||
|
|
f46d5376fe | ||
|
|
dc3640578f | ||
|
|
0a43494de5 | ||
|
|
2544e45d2d | ||
|
|
4a53e9e018 | ||
|
|
9d7dc99416 | ||
|
|
c89dc4ba5b | ||
|
|
72289ced39 | ||
|
|
3554ccde05 | ||
|
|
73e2c42931 | ||
|
|
b11778ec55 | ||
|
|
18bc937958 | ||
|
|
9f618f6678 | ||
|
|
0c3c7493de | ||
|
|
8f3f02e9f7 | ||
|
|
69903ba889 | ||
|
|
25c5f95ad9 | ||
|
|
2546d1107e | ||
|
|
fdbeaf8692 | ||
|
|
63c0316af2 | ||
|
|
f2e761c07c | ||
|
|
6a741de7d1 | ||
|
|
3e94d18fe1 | ||
|
|
95d3651e29 | ||
|
|
cd2d88781a | ||
|
|
2178e86d09 | ||
|
|
f9ad0f12d0 | ||
|
|
63b16d925d | ||
|
|
53b9ffcbc8 | ||
|
|
df01f41980 | ||
|
|
b0c40d3b09 | ||
|
|
5880dcbcfd | ||
|
|
489bbc45f5 | ||
|
|
5e67502729 | ||
|
|
842079c928 | ||
|
|
b0bab07a15 | ||
|
|
4dbb12c65d | ||
|
|
db500e9791 | ||
|
|
fd8f600fec | ||
|
|
2e2d7d02fb | ||
|
|
d068cd7f75 | ||
|
|
5a05ffcbdd | ||
|
|
d8c7f50b39 | ||
|
|
988e6e1c82 | ||
|
|
f6af19444c | ||
|
|
0581e50e0c | ||
|
|
a95da9a42d | ||
|
|
be10b9750f | ||
|
|
29a3cbc688 | ||
|
|
4f57d3a201 | ||
|
|
c9be1398b0 | ||
|
|
30eef4db12 | ||
|
|
b932dbf514 | ||
|
|
320ac82dea | ||
|
|
af1b21db23 | ||
|
|
0e892ff60e | ||
|
|
074963aee0 | ||
|
|
37d9be9095 | ||
|
|
26e36454ef | ||
|
|
78b95f67eb | ||
|
|
6c63841d0c | ||
|
|
6ec2d91d91 | ||
|
|
3299b90c20 | ||
|
|
7b6d6da9a6 | ||
|
|
7c7c61fc35 | ||
|
|
c5408fb6b8 | ||
|
|
1c49102f67 | ||
|
|
f9dd88c1cb | ||
|
|
9ed4a65fd2 | ||
|
|
10bebf8a89 | ||
|
|
36260dac18 | ||
|
|
2e4e993967 | ||
|
|
ef1bfe98f7 | ||
|
|
2e123aa617 | ||
|
|
cf2ef0f2a8 | ||
|
|
520f40d862 | ||
|
|
856bdac84b | ||
|
|
deb7e38fcf | ||
|
|
e5f47a4563 | ||
|
|
d1b465d8be | ||
|
|
5c73b2f324 | ||
|
|
7d3e992b3f | ||
|
|
296640930e | ||
|
|
1c42735e3a | ||
|
|
0800bc1790 | ||
|
|
bd76a12b90 | ||
|
|
d3daea6383 | ||
|
|
df1acd5413 | ||
|
|
0d65cc09f6 | ||
|
|
892b082b9e | ||
|
|
9069ef1325 | ||
|
|
47cc493ab5 | ||
|
|
f43d7837f8 | ||
|
|
91921ae672 | ||
|
|
eba6afc12b | ||
|
|
ff826a9eeb | ||
|
|
6c01a30af5 | ||
|
|
53f8d09d31 | ||
|
|
ae191aaafe | ||
|
|
d1694d563b | ||
|
|
75fadf79da | ||
|
|
23ce5a6b1e | ||
|
|
bb65e2b84d | ||
|
|
aa42b1d95a | ||
|
|
32d5a18198 | ||
|
|
63faefe9c3 | ||
|
|
a90c49d030 | ||
|
|
b1ef3fa4df | ||
|
|
b8cf67cba9 | ||
|
|
4780e10ade | ||
|
|
401e606fbc | ||
|
|
6b4ef8f397 | ||
|
|
7c66d07779 | ||
|
|
92aed0cc3a | ||
|
|
457b08d3cc | ||
|
|
1e5f6fd2b8 | ||
|
|
aebfeb98aa | ||
|
|
f6974e8315 | ||
|
|
2ce6313ac1 | ||
|
|
e98a113a59 | ||
|
|
ba7bed9c2c | ||
|
|
c9ea451c53 | ||
|
|
4261ff32c7 | ||
|
|
cb4b20af45 | ||
|
|
b8a27adb93 | ||
|
|
15b58128f4 | ||
|
|
237282db28 | ||
|
|
71bb59dbb8 | ||
|
|
e41c46c075 | ||
|
|
6ca9e52f2f | ||
|
|
2afee89de3 | ||
|
|
189bee3e44 | ||
|
|
6b9a4d5e0a | ||
|
|
451e2b2182 | ||
|
|
f6ff41cfb4 | ||
|
|
d6d144c927 | ||
|
|
7f86872139 | ||
|
|
dc0fc05a9e | ||
|
|
6b2c3217ab | ||
|
|
0f93a45b9d | ||
|
|
943027ffdd | ||
|
|
5d28b2400f | ||
|
|
67324bfc80 | ||
|
|
3c340e7144 | ||
|
|
c834405a92 | ||
|
|
4ee9e26847 | ||
|
|
94293ca9d9 | ||
|
|
b9cd9f8d35 | ||
|
|
812dd9282d | ||
|
|
fa1d386fcc | ||
|
|
0392bf6a02 | ||
|
|
97f771ff50 | ||
|
|
6adcc72a8a | ||
|
|
2c11bd1889 | ||
|
|
23e0196fcc | ||
|
|
91f98c125e | ||
|
|
7f01e9a4d9 | ||
|
|
320a4e2351 | ||
|
|
0829ce51fc | ||
|
|
97ec50c202 | ||
|
|
975a3e8103 | ||
|
|
658ef2ef26 | ||
|
|
bd1e531d7b | ||
|
|
e440d1d1bd | ||
|
|
2110020165 | ||
|
|
dac74dc30d | ||
|
|
fa4b971254 | ||
|
|
2d31ae0baa | ||
|
|
5eebfa132f | ||
|
|
522febef93 | ||
|
|
a421645ea5 | ||
|
|
0aac4b1347 | ||
|
|
36697825cf | ||
|
|
b0182ed604 | ||
|
|
c4ea73ca7a | ||
|
|
2aa28d6453 | ||
|
|
2d51085ccf | ||
|
|
4eae02e47e | ||
|
|
f3bf9f9e5d | ||
|
|
75d0b84bdb | ||
|
|
3643ddcf5c | ||
|
|
35500e8ef7 | ||
|
|
6badcf5391 | ||
|
|
d4898043f6 | ||
|
|
2322ba833a | ||
|
|
5aabb2917f | ||
|
|
eacbf6ce50 | ||
|
|
031300a132 | ||
|
|
f905611e69 | ||
|
|
e475f9f876 | ||
|
|
b65263349e | ||
|
|
5cb8026f6d | ||
|
|
cc7ce5cf93 | ||
|
|
af506639a9 | ||
|
|
d377d67174 | ||
|
|
cf926353d1 | ||
|
|
d686c744d0 | ||
|
|
501c60b180 | ||
|
|
da36687e25 | ||
|
|
f13f9a066a | ||
|
|
8eaa4f7654 | ||
|
|
145a7f8e0d | ||
|
|
67ba126602 | ||
|
|
c5084901b5 | ||
|
|
3bfc82f7c0 | ||
|
|
3411b53450 | ||
|
|
873564f2aa | ||
|
|
353ed90d12 | ||
|
|
799faecc5b | ||
|
|
731321b1f9 | ||
|
|
7ff43caf96 | ||
|
|
d0877c3132 | ||
|
|
c589b03dcb | ||
|
|
862b5aaef6 | ||
|
|
596443bf5e | ||
|
|
27b450f1e3 | ||
|
|
61a09e97ca | ||
|
|
224e7a8969 | ||
|
|
9e7d9ee973 | ||
|
|
586bad345c | ||
|
|
abdd7dc7d3 | ||
|
|
aee32f7a3e | ||
|
|
696760e65a | ||
|
|
200db15d4b | ||
|
|
33e332f105 | ||
|
|
bb2955e442 | ||
|
|
2fc2fa56c3 | ||
|
|
c87458590c | ||
|
|
8aff134c56 | ||
|
|
4a938b81df | ||
|
|
4def715b25 | ||
|
|
821acf12d8 | ||
|
|
fc707b6c7e | ||
|
|
85ac000479 | ||
|
|
68a0eefa20 | ||
|
|
fc32377ce7 | ||
|
|
59e512a64d | ||
|
|
c51a5a51f1 | ||
|
|
9546a276dc | ||
|
|
a18353df5f | ||
|
|
3c72113f4c | ||
|
|
7e193751c4 | ||
|
|
56c96eb712 | ||
|
|
4106a984ca | ||
|
|
10f1ab0598 | ||
|
|
bca9603440 | ||
|
|
c32c267889 | ||
|
|
627e987bda | ||
|
|
7c18e147f3 | ||
|
|
6a8fb5910d | ||
|
|
b865326d51 | ||
|
|
a2d5b0893d | ||
|
|
db0508b9ab | ||
|
|
1850dee93a | ||
|
|
7b1eb8a6dc | ||
|
|
95a9f2f5e3 | ||
|
|
ae7ed2d226 | ||
|
|
20cf82bab1 | ||
|
|
5dcb1e26b5 | ||
|
|
8076589180 | ||
|
|
edbd4003be | ||
|
|
122b089bf0 | ||
|
|
a28d917990 | ||
|
|
5b605a1100 |
7
.gitignore
vendored
@@ -1,7 +1,10 @@
|
||||
.gitignore
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/app/app.iml
|
||||
/.idea
|
||||
/*.iml
|
||||
gradle.properties
|
||||
|
||||
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
NewPipe
|
||||
22
.idea/compiler.xml
generated
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/copyright/profiles_settings.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
3
.idea/dictionaries/the_scrabi.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="the-scrabi" />
|
||||
</component>
|
||||
19
.idea/gradle.xml
generated
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="LOCAL" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.4" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
46
.idea/misc.xml
generated
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/modules.xml
generated
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/NewPipe.iml" filepath="$PROJECT_DIR$/NewPipe.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
30
.travis.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-23.0.2
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-23
|
||||
|
||||
# Additional components
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
|
||||
# Emulators
|
||||
- sys-img-armeabi-v7a-android-21
|
||||
- sys-img-armeabi-v7a-android-19
|
||||
- sys-img-armeabi-v7a-android-15
|
||||
|
||||
env:
|
||||
global:
|
||||
- ADB_INSTALL_TIMEOUT=8 # minutes (2 by default)
|
||||
matrix:
|
||||
- ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a
|
||||
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
- emulator -avd test -no-skin -no-audio -no-window &
|
||||
- android-wait-for-emulator
|
||||
- adb shell input keyevent 82 &
|
||||
33
CONTRIBUTING.md
Normal 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!
|
||||
19
NewPipe.iml
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id="NewPipe" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="java-gradle" name="Java-Gradle">
|
||||
<configuration>
|
||||
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
||||
<option name="BUILDABLE" value="false" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
54
README.md
@@ -1,30 +1,44 @@
|
||||
# NewPipe
|
||||
NewPipe: A free lightweight Youtube frontend for Android.
|
||||
|
||||
[](http://dasochan.nl/newpipe/)
|
||||
|
||||
Project status:
|
||||
[](https://hosted.weblate.org/engage/NewPipe/)
|
||||
[](https://travis-ci.org/theScrabi/NewPipe)
|
||||
|
||||
[](http://dasochan.nl/newpipe/)
|
||||
NewPipe: A free lightweight Youtube fronted for Android.
|
||||
## Get NewPipe
|
||||
|
||||
[](https://f-droid.org/repository/browse/?fdfilter=newpipe&fdid=org.schabi.newpipe)
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img src="screenshots/screenshot_1.png" width=150>](screenshots/screenshot_1.png)
|
||||
[<img src="screenshots/screenshot_2.png" width=150>](screenshots/screenshot_2.png)
|
||||
[<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)
|
||||
|
||||
## Description
|
||||
|
||||
NewPipe does not use any Google framework libraries, or the Youtube api. It only parses the website in order to gain the information it needs. Therefore this app can be used on devices without g-services installed. Also NewPipe does not store data on the Youtube website (no login), and it's free software.
|
||||
NewPipe does not use any Google framework libraries, or the YouTube API. It only parses the website in order to gain the information it needs. Therefore this app can be used on devices without Google Services installed. Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.
|
||||
|
||||
## Features
|
||||
### Features
|
||||
|
||||
* Search videos
|
||||
* Display general information about a video
|
||||
* Watch Youtube videos
|
||||
* Listen to Youtube videos (audio only streaming)
|
||||
* Watch YouTube videos
|
||||
* Listen to YouTube videos (experimental)
|
||||
* Select the streaming player to watch the video with
|
||||
* Download videos (working, but it could be better)
|
||||
* Download audio only (working but, but it could be better)
|
||||
* Download videos (experimental)
|
||||
* Download audio only (experimental)
|
||||
* Open a video in Kodi
|
||||
* Show Next/Related videos
|
||||
* Search YouTube in a specific language
|
||||
* Orbot/Tor support (no streaming yet, experimental)
|
||||
|
||||
## Coming Features
|
||||
### Coming Features
|
||||
|
||||
* Shows Next/Related videos
|
||||
* Improved Downloading
|
||||
* Bookmarks
|
||||
* View history
|
||||
@@ -36,10 +50,20 @@ NewPipe does not use any Google framework libraries, or the Youtube api. It only
|
||||
* Search/Watch Playlists
|
||||
* ... and many more
|
||||
|
||||
### Multi service support
|
||||
Generally NewPipe is designed to not only support YouTube, but many more streaming services. How ever, right now NewPipe is not stable enough to support more than only youtube. But if all works as plant, NewPipe will get such support by the version 2.0.
|
||||
|
||||
# Help is always welcome !!!
|
||||
Whether its about ideas, translation, design changes, code cleaning, or real heavy code changes. Help is always welcome.
|
||||
### Multiservice support
|
||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
||||
|
||||
## Contribution
|
||||
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!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe is Free Software: You can use, study share and improve it at your
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
2
app/.gitignore
vendored
@@ -1 +1,3 @@
|
||||
.gitignore
|
||||
/build
|
||||
app.iml
|
||||
|
||||
103
app/app.iml
@@ -1,103 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="NewPipe" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="android-gradle" name="Android-Gradle">
|
||||
<configuration>
|
||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="debug" />
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
|
||||
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
|
||||
<afterSyncTasks>
|
||||
<task>generateDebugAndroidTestSources</task>
|
||||
<task>generateDebugSources</task>
|
||||
</afterSyncTasks>
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.1.0/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/design/23.1.0/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/23.1.0/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.1.0/jars" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="recyclerview-v7-23.1.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="jsoup-1.8.3" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-v4-23.1.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="rhino-1.7.7" level="project" />
|
||||
<orderEntry type="library" exported="" name="design-23.1.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="appcompat-v7-23.1.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-annotations-23.1.0" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.1"
|
||||
buildToolsVersion '23.0.2'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
versionCode 5
|
||||
versionName "0.5.0"
|
||||
versionCode 14
|
||||
versionName "0.7.5"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -17,13 +17,29 @@ 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 {
|
||||
compile fileTree(include: ['*.jar'], dir: 'libs')
|
||||
compile 'com.android.support:appcompat-v7:23.1.0'
|
||||
compile 'com.android.support:support-v4:23.1.0'
|
||||
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:recyclerview-v7:23.1.1'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'com.android.support:design:23.1.0'
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe" >
|
||||
|
||||
<uses-permission android:name= "android.permission.INTERNET" />
|
||||
<uses-permission android:name= "android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
android:theme="@style/AppTheme"
|
||||
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" />
|
||||
|
||||
@@ -22,57 +26,82 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".VideoItemDetailActivity"
|
||||
android:label="@string/title_videoitem_detail" >
|
||||
android:label="@string/title_videoitem_detail"
|
||||
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"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/FullscreenTheme"
|
||||
>
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
android:parentActivityName=".VideoItemDetailActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
</activity>
|
||||
<service
|
||||
android:name=".BackgroundPlayer"
|
||||
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>
|
||||
|
||||
@@ -1,14 +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.v4.view.MenuItemCompat;
|
||||
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;
|
||||
@@ -16,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.
|
||||
*
|
||||
@@ -36,141 +36,148 @@ import android.widget.ArrayAdapter;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ActionBarHandler {
|
||||
|
||||
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;
|
||||
|
||||
SharedPreferences defaultPreferences = null;
|
||||
private SharedPreferences defaultPreferences = null;
|
||||
|
||||
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) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "ConstantConditions"})
|
||||
public void setupNavMenu(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
|
||||
try {
|
||||
activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void setStreams(VideoInfo.VideoStream[] videoStreams, VideoInfo.AudioStream[] audioStreams) {
|
||||
this.videoStreams = videoStreams;
|
||||
selectedStream = 0;
|
||||
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] = VideoInfo.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<String>(activity.getBaseContext(),
|
||||
android.R.layout.simple_spinner_dropdown_item, itemArray);
|
||||
if(activity != null) {
|
||||
ActionBar ab = activity.getSupportActionBar();
|
||||
//todo: make this throwsable
|
||||
assert ab != null : "Could not get actionbar";
|
||||
ab.setListNavigationCallbacks(itemAdapter
|
||||
,new FormatItemSelectListener());
|
||||
ab.setSelectedNavigationItem(defaultResolutionPos);
|
||||
}
|
||||
, new ActionBar.OnNavigationListener() {
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
|
||||
selectedVideoStream = (int) itemId;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// set audioStream
|
||||
audioStream = null;
|
||||
String preferedFormat = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(activity.getString(R.string.defaultAudioFormatPreference), "webm");
|
||||
if(preferedFormat.equals("webm")) {
|
||||
for(VideoInfo.AudioStream s : audioStreams) {
|
||||
if(s.format == VideoInfo.I_WEBMA) {
|
||||
audioStream = s;
|
||||
}
|
||||
}
|
||||
} else if(preferedFormat.equals("m4a")){
|
||||
for(VideoInfo.AudioStream s : audioStreams) {
|
||||
Log.d(TAG, VideoInfo.getMimeById(s.format) + " : " + Integer.toString(s.bandwidth));
|
||||
if(s.format == VideoInfo.I_M4A &&
|
||||
(audioStream == null || audioStream.bandwidth > s.bandwidth)) {
|
||||
audioStream = s;
|
||||
Log.d(TAG, "last choosen");
|
||||
}
|
||||
}
|
||||
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 boolean setupMenu(Menu menu, MenuInflater inflater) {
|
||||
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 playItem = menu.findItem(R.id.menu_item_play);
|
||||
MenuItem shareItem = menu.findItem(R.id.menu_item_share);
|
||||
MenuItem castItem = menu.findItem(R.id.action_play_with_kodi);
|
||||
|
||||
MenuItemCompat.setShowAsAction(playItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS
|
||||
| MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
MenuItemCompat.setShowAsAction(shareItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM
|
||||
| MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
|
||||
castItem.setVisible(defaultPreferences
|
||||
.getBoolean(activity.getString(R.string.showPlayWidthKodiPreference), false));
|
||||
|
||||
return true;
|
||||
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_play:
|
||||
playVideo();
|
||||
return true;
|
||||
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,150 +185,39 @@ public 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.useExternalPlayer), false)) {
|
||||
|
||||
// External Player
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
|
||||
intent.setDataAndType(Uri.parse(videoStreams[selectedStream].url),
|
||||
VideoInfo.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);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
}
|
||||
// --------------------------------------------
|
||||
public void setOnShareListener(OnActionListener listener) {
|
||||
onShareListener = listener;
|
||||
}
|
||||
|
||||
public void downloadVideo() {
|
||||
Log.d(TAG, "bla");
|
||||
if(!videoTitle.isEmpty()) {
|
||||
String videoSuffix = "." + VideoInfo.getSuffixById(videoStreams[selectedStream].format);
|
||||
String audioSuffix = "." + VideoInfo.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 setOnOpenInBrowserListener(OnActionListener listener) {
|
||||
onOpenInBrowserListener = listener;
|
||||
}
|
||||
|
||||
public 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 setOnDownloadListener(OnActionListener listener) {
|
||||
onDownloadListener = listener;
|
||||
}
|
||||
|
||||
public 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 setOnPlayWithKodiListener(OnActionListener listener) {
|
||||
onPlayWithKodiListener = listener;
|
||||
}
|
||||
|
||||
public void playAudio() {
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
VideoInfo.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.d(TAG, "Either no Streaming player for audio was installed, or something importand crashed:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
public void setOnPlayAudioListener(OnActionListener listener) {
|
||||
onPlayAudioListener = listener;
|
||||
}
|
||||
|
||||
public void showAudioAction(boolean visible) {
|
||||
menu.findItem(R.id.menu_item_play_audio).setVisible(visible);
|
||||
}
|
||||
|
||||
public void showDownloadAction(boolean visible) {
|
||||
menu.findItem(R.id.menu_item_download).setVisible(visible);
|
||||
}
|
||||
|
||||
public void showPlayWithKodiAction(boolean visible) {
|
||||
menu.findItem(R.id.action_play_with_kodi).setVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
78
app/src/main/java/org/schabi/newpipe/App.java
Normal 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;
|
||||
}
|
||||
}
|
||||
335
app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java
Normal file
@@ -0,0 +1,335 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.wifi.WifiManager;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
* Copyright (c) Adam Howard <achdisposable1@gmail.com> 2015
|
||||
*
|
||||
* BackgroundPlayer.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/>.
|
||||
*/
|
||||
|
||||
/**Plays the audio stream of videos in the background.*/
|
||||
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";
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
/*PendingIntent pi = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);*/
|
||||
super.onCreate();
|
||||
}
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// We don't provide binding (yet?), so return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
private class PlayerThread extends Thread {
|
||||
MediaPlayer mediaPlayer;
|
||||
private String source;
|
||||
private String title;
|
||||
private int noteID = TAG.hashCode();
|
||||
private BackgroundPlayer owner;
|
||||
private NotificationManager noteMgr;
|
||||
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;
|
||||
this.title = title;
|
||||
this.owner = owner;
|
||||
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);
|
||||
//We are already in a separate worker thread,
|
||||
//so calling the blocking prepare() method should be ok
|
||||
mediaPlayer.prepare();
|
||||
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
Log.e(TAG, "video source:" + source);
|
||||
Log.e(TAG, "video title:" + title);
|
||||
//can't do anything useful without a file to play; exit early
|
||||
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);
|
||||
|
||||
//listen for end of video
|
||||
mediaPlayer.setOnCompletionListener(new EndListener(wifiLock));
|
||||
|
||||
//get audio focus
|
||||
/*
|
||||
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN);
|
||||
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
// could not get audio focus.
|
||||
}*/
|
||||
wifiLock.acquire();
|
||||
mediaPlayer.start();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.setPriority(Integer.MAX_VALUE);
|
||||
filter.addAction(ACTION_PLAYPAUSE);
|
||||
filter.addAction(ACTION_STOP);
|
||||
registerReceiver(broadcastReceiver, filter);
|
||||
|
||||
note = buildNotification();
|
||||
|
||||
startForeground(noteID, note);
|
||||
|
||||
//currently decommissioned progressbar looping update code - works, but doesn't fit inside
|
||||
//Notification.MediaStyle Notification layout.
|
||||
noteMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
/*
|
||||
//update every 2s or 4 times in the video, whichever is shorter
|
||||
int sleepTime = Math.min(2000, (int)((double)vidLength/4));
|
||||
while(mediaPlayer.isPlaying()) {
|
||||
noteBuilder.setProgress(vidLength, mediaPlayer.getCurrentPosition(), false);
|
||||
noteMgr.notify(noteID, noteBuilder.build());
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException e) {
|
||||
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);
|
||||
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 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)) {
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void afterPlayCleanup() {
|
||||
//remove progress bar
|
||||
//noteBuilder.setProgress(0, 0, false);
|
||||
|
||||
//remove notification
|
||||
noteMgr.cancel(noteID);
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
//release mediaPlayer's system resources
|
||||
mediaPlayer.release();
|
||||
|
||||
//release wifilock
|
||||
wifiLock.release();
|
||||
//remove foreground status of service; make BackgroundPlayer killable
|
||||
stopForeground(true);
|
||||
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private class EndListener implements MediaPlayer.OnCompletionListener {
|
||||
private WifiManager.WifiLock wl;
|
||||
public EndListener(WifiManager.WifiLock wifiLock) {
|
||||
this.wl = wifiLock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +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.
|
||||
@@ -42,48 +48,143 @@ public class DownloadDialog extends DialogFragment {
|
||||
public static final String FILE_SUFFIX_VIDEO = "file_suffix_video";
|
||||
public static final String AUDIO_URL = "audio_url";
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
Bundle arguments;
|
||||
private Bundle arguments;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
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
|
||||
@@ -25,29 +30,61 @@ import java.net.URL;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class Downloader {
|
||||
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";
|
||||
|
||||
private static final String USER_AGENT = "Mozilla/5.0";
|
||||
public static String download(String siteUrl) {
|
||||
/**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 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(HttpsURLConnection con) throws IOException {
|
||||
StringBuilder response = new StringBuilder();
|
||||
BufferedReader in = null;
|
||||
|
||||
StringBuffer response = new StringBuffer();
|
||||
try {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpURLConnection con = (HttpURLConnection) url.openConnection();
|
||||
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 (Exception e) {
|
||||
e.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 String download(String siteUrl) throws IOException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
||||
return dl(con);
|
||||
}
|
||||
}
|
||||
|
||||
54
app/src/main/java/org/schabi/newpipe/ExitActivity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
169
app/src/main/java/org/schabi/newpipe/FileDownloader.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
93
app/src/main/java/org/schabi/newpipe/Localization.java
Normal 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);
|
||||
}
|
||||
}
|
||||
72
app/src/main/java/org/schabi/newpipe/NewPipeSettings.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -15,6 +16,7 @@ import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -52,9 +54,9 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
public static final String STREAM_URL = "stream_url";
|
||||
public static final String VIDEO_TITLE = "video_title";
|
||||
private static final String POSITION = "position";
|
||||
public static final String START_POSITION = "start_position";
|
||||
|
||||
private static final long HIDING_DELAY = 3000;
|
||||
private static final long TAB_HIDING_DELAY = 100;
|
||||
|
||||
private String videoUrl = "";
|
||||
|
||||
@@ -77,17 +79,43 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_play_video);
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
isLandscape = checkIfLandscape();
|
||||
hasSoftKeys = checkIfHasSoftKeys();
|
||||
|
||||
actionBar = getSupportActionBar();
|
||||
assert actionBar != null;
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
Intent intent = getIntent();
|
||||
if(mediaController == null) {
|
||||
mediaController = new MediaController(this);
|
||||
//prevents back button hiding media controller controls (after showing them)
|
||||
//instead of exiting video
|
||||
//see http://stackoverflow.com/questions/6051825
|
||||
//also solves https://github.com/theScrabi/NewPipe/issues/99
|
||||
mediaController = new MediaController(this) {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int keyCode = event.getKeyCode();
|
||||
final boolean uniqueDown = event.getRepeatCount() == 0
|
||||
&& event.getAction() == KeyEvent.ACTION_DOWN;
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (uniqueDown)
|
||||
{
|
||||
if (isShowing()) {
|
||||
finish();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
|
||||
|
||||
videoView = (VideoView) findViewById(R.id.video_view);
|
||||
progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar);
|
||||
try {
|
||||
@@ -102,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 {
|
||||
@@ -145,11 +173,6 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreatePanelMenu(int featured, Menu menu) {
|
||||
super.onCreatePanelMenu(featured, menu);
|
||||
@@ -159,17 +182,24 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
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();
|
||||
@@ -182,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();
|
||||
@@ -274,11 +304,9 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private boolean checkIfHasSoftKeys(){
|
||||
if(Build.VERSION.SDK_INT >= 17) {
|
||||
return getNavigationBarHeight() != 0 || getNavigationBarWidth() != 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return Build.VERSION.SDK_INT >= 17 ||
|
||||
getNavigationBarHeight() != 0 ||
|
||||
getNavigationBarWidth() != 0;
|
||||
}
|
||||
|
||||
private int getNavigationBarHeight() {
|
||||
@@ -315,7 +343,7 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkIfLandscape() {
|
||||
private boolean checkIfLandscape() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return displayMetrics.heightPixels < displayMetrics.widthPixels;
|
||||
@@ -331,6 +359,6 @@ public class PlayVideoActivity extends AppCompatActivity {
|
||||
}
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(PREF_IS_LANDSCAPE, isLandscape);
|
||||
editor.commit();
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
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;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuInflater;
|
||||
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.
|
||||
*
|
||||
@@ -38,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
|
||||
@@ -56,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
|
||||
@@ -70,14 +174,11 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public ActionBar getSupportActionBar() {
|
||||
private ActionBar getSupportActionBar() {
|
||||
return getDelegate().getSupportActionBar();
|
||||
}
|
||||
|
||||
public void setSupportActionBar(@Nullable Toolbar toolbar) {
|
||||
getDelegate().setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
@@ -154,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.commit();
|
||||
}
|
||||
NewPipeSettings.initSettings(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
public class VideoInfo {
|
||||
|
||||
private static final String TAG = VideoInfo.class.toString();
|
||||
|
||||
// format identifier
|
||||
public static final int I_MPEG_4 = 0x0;
|
||||
public static final int I_3GPP = 0x1;
|
||||
public static final int I_WEBM = 0x2;
|
||||
public static final int I_M4A = 0x3;
|
||||
public static final int I_WEBMA = 0x4;
|
||||
|
||||
// format name
|
||||
public static final String F_MPEG_4 = "MPEG-4";
|
||||
public static final String F_3GPP = "3GPP";
|
||||
public static final String F_WEBM = "WebM";
|
||||
public static final String F_M4A = "m4a";
|
||||
public static final String F_WEBMA = "WebM";
|
||||
|
||||
// file suffix
|
||||
public static final String C_MPEG_4 = "mp4";
|
||||
public static final String C_3GPP = "3gp";
|
||||
public static final String C_WEBM = "webm";
|
||||
public static final String C_M4A = "m4a";
|
||||
public static final String C_WEBMA = "webm";
|
||||
|
||||
// mimeType
|
||||
public static final String M_MPEG_4 = "video/mp4";
|
||||
public static final String M_3GPP = "video/3gpp";
|
||||
public static final String M_WEBM = "video/webm";
|
||||
public static final String M_M4A = "audio/mp4";
|
||||
public static final String M_WEBMA = "audio/webm";
|
||||
|
||||
public static final int VIDEO_AVAILABLE = 0x00;
|
||||
public static final int VIDEO_UNAVAILABLE = 0x01;
|
||||
public static final int VIDEO_UNAVAILABLE_GEMA = 0x02;
|
||||
|
||||
public static String getNameById(int id) {
|
||||
switch(id) {
|
||||
case I_MPEG_4: return F_MPEG_4;
|
||||
case I_3GPP: return F_3GPP;
|
||||
case I_WEBM: return F_WEBM;
|
||||
case I_M4A: return F_M4A;
|
||||
case I_WEBMA: return F_WEBMA;
|
||||
default: Log.e(TAG, "format not known: " +
|
||||
Integer.toString(id) + "call the programmer he messed it up.");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String getSuffixById(int id) {
|
||||
switch(id) {
|
||||
case I_MPEG_4: return C_MPEG_4;
|
||||
case I_3GPP: return C_3GPP;
|
||||
case I_WEBM: return C_WEBM;
|
||||
case I_M4A: return C_M4A;
|
||||
case I_WEBMA: return C_WEBMA;
|
||||
default: Log.e(TAG, "format not known: " +
|
||||
Integer.toString(id) + "call the programmer he messed it up.");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String getMimeById(int id) {
|
||||
switch(id) {
|
||||
case I_MPEG_4: return M_MPEG_4;
|
||||
case I_3GPP: return M_3GPP;
|
||||
case I_WEBM: return M_WEBM;
|
||||
case I_M4A: return M_M4A;
|
||||
case I_WEBMA: return M_WEBMA;
|
||||
default: Log.e(TAG, "format not known: " +
|
||||
Integer.toString(id) + "call the programmer he messed it up.");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static class VideoStream {
|
||||
public VideoStream(String url, int format, String res) {
|
||||
this.url = url; this.format = format; resolution = res;
|
||||
}
|
||||
public String url = ""; //url of the stream
|
||||
public int format = -1;
|
||||
public String resolution = "";
|
||||
}
|
||||
|
||||
public static class AudioStream {
|
||||
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
|
||||
this.url = url; this.format = format;
|
||||
this.bandwidth = bandwidth; this.samplingRate = samplingRate;
|
||||
}
|
||||
public String url = "";
|
||||
public int format = -1;
|
||||
public int bandwidth = -1;
|
||||
public int samplingRate = -1;
|
||||
|
||||
}
|
||||
|
||||
public String id = "";
|
||||
public String uploader = "";
|
||||
public String upload_date = "";
|
||||
public String uploader_thumbnail_url = "";
|
||||
public Bitmap uploader_thumbnail = null;
|
||||
public String title = "";
|
||||
public String thumbnail_url = "";
|
||||
public Bitmap thumbnail = null;
|
||||
public String description = "";
|
||||
public int duration = -1;
|
||||
public int age_limit = 0;
|
||||
public String webpage_url = "";
|
||||
public String view_count = "";
|
||||
public String like_count = "";
|
||||
public String dislike_count = "";
|
||||
public String average_rating = "";
|
||||
public VideoStream[] videoStreams = null;
|
||||
public AudioStream[] audioStreams = null;
|
||||
public VideoInfoItem nextVideo = null;
|
||||
public Vector<VideoInfoItem> relatedVideos = null;
|
||||
public int videoAvailableStatus = VIDEO_AVAILABLE;
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
@@ -26,16 +31,16 @@ import android.widget.TextView;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class VideoInfoItemViewCreator {
|
||||
private static final String TAG = VideoInfoItemViewCreator.class.toString();
|
||||
|
||||
LayoutInflater inflater;
|
||||
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, VideoInfoItem 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);
|
||||
@@ -45,32 +50,45 @@ public 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();
|
||||
}
|
||||
|
||||
if(info.thumbnail == null) {
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummi_thumbnail);
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail);
|
||||
} else {
|
||||
holder.itemThumbnailView.setImageBitmap(info.thumbnail);
|
||||
}
|
||||
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 {
|
||||
//tewak if nececeary: This is a hack preventing to have a white space in the layout :P
|
||||
holder.itemUploadDateView.setText(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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
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;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -33,18 +37,23 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = VideoItemDetailActivity.class.toString();
|
||||
|
||||
VideoItemDetailFragment fragment;
|
||||
private VideoItemDetailFragment fragment;
|
||||
|
||||
private String videoUrl;
|
||||
private int currentStreamingService = -1;
|
||||
private Menu menu = null;
|
||||
|
||||
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.
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
} catch(Exception e) {
|
||||
Log.d(TAG, "Could not get SupportActionBar");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// savedInstanceState is non-null when there is fragment state
|
||||
// saved from previous configurations of this activity
|
||||
@@ -62,29 +71,26 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
if (getIntent().getData() != null) {
|
||||
videoUrl = getIntent().getData().toString();
|
||||
StreamingService[] serviceList = ServiceList.getServices();
|
||||
Extractor extractor = 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);
|
||||
try {
|
||||
currentStreamingService = i;
|
||||
extractor = (Extractor) ServiceList.getService(i)
|
||||
.getExtractorClass().newInstance();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
currentStreamingService = i;
|
||||
//videoExtractor = ServiceList.getService(i).getExtractorInstance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(extractor == null) {
|
||||
Toast.makeText(this, R.string.urlNotSupportedText, Toast.LENGTH_LONG)
|
||||
if(currentStreamingService == -1) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
||||
extractor.getVideoUrl(extractor.getVideoId(videoUrl)));
|
||||
//arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
||||
// videoExtractor.getVideoUrl(videoExtractor.getVideoId(videoUrl)));//cleans URL
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||
|
||||
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);
|
||||
@@ -108,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);
|
||||
@@ -123,9 +135,9 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
// activity, the Up button is shown. Use NavUtils to allow users
|
||||
// to navigate up one level in the application structure. For
|
||||
// more details, see the Navigation pattern on Android Design:
|
||||
//
|
||||
|
||||
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||
//
|
||||
|
||||
Intent intent = new Intent(this, VideoItemListActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
|
||||
@@ -1,36 +1,60 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.net.URL;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.VideoInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -53,23 +77,36 @@ import java.util.Vector;
|
||||
public class VideoItemDetailFragment extends Fragment {
|
||||
|
||||
private static final String TAG = VideoItemDetailFragment.class.toString();
|
||||
private static final String KORE_PACKET = "org.xbmc.kore";
|
||||
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String ARG_ITEM_ID = "item_id";
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
public static final String STREAMING_SERVICE = "streaming_service";
|
||||
public static final String AUTO_PLAY = "auto_play";
|
||||
|
||||
private AppCompatActivity activity;
|
||||
private ActionBarHandler actionBarHandler;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private int streamingServiceId = -1;
|
||||
|
||||
private boolean autoPlayEnabled = false;
|
||||
private Thread extractorThread = null;
|
||||
private VideoInfo currentVideoInfo = null;
|
||||
private boolean showNextVideoItem = false;
|
||||
private Bitmap videoThumbnail;
|
||||
|
||||
private View thumbnailWindowLayout;
|
||||
//this only remains due to downwards compatibility
|
||||
private FloatingActionButton playVideoButton;
|
||||
private final Point initialThumbnailPos = new Point(0, 0);
|
||||
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
|
||||
public interface OnInvokeCreateOptionsMenuListener {
|
||||
void createOptionsMenu();
|
||||
@@ -77,193 +114,564 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
|
||||
private OnInvokeCreateOptionsMenuListener onInvokeCreateOptionsMenuListener = null;
|
||||
|
||||
private class ExtractorRunnable implements Runnable {
|
||||
private Handler h = new Handler();
|
||||
private Class extractorClass;
|
||||
private String videoUrl;
|
||||
public ExtractorRunnable(String videoUrl, Class extractorClass, VideoItemDetailFragment f) {
|
||||
this.extractorClass = extractorClass;
|
||||
private class VideoExtractorRunnable implements Runnable {
|
||||
private final Handler h = new Handler();
|
||||
private StreamExtractor streamExtractor;
|
||||
private final StreamingService service;
|
||||
private final String videoUrl;
|
||||
|
||||
public VideoExtractorRunnable(String videoUrl, StreamingService service) {
|
||||
this.service = service;
|
||||
this.videoUrl = videoUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Extractor extractor = (Extractor) extractorClass.newInstance();
|
||||
VideoInfo videoInfo = extractor.getVideoInfo(videoUrl);
|
||||
streamExtractor = service.getExtractorInstance(videoUrl, new Downloader());
|
||||
VideoInfo videoInfo = VideoInfo.getVideoInfo(streamExtractor, new Downloader());
|
||||
|
||||
h.post(new VideoResultReturnedRunnable(videoInfo));
|
||||
if (videoInfo.videoAvailableStatus == VideoInfo.VIDEO_AVAILABLE) {
|
||||
h.post(new SetThumbnailRunnable(
|
||||
BitmapFactory.decodeStream(
|
||||
new URL(videoInfo.thumbnail_url)
|
||||
.openConnection()
|
||||
.getInputStream()), SetThumbnailRunnable.VIDEO_THUMBNAIL));
|
||||
h.post(new SetThumbnailRunnable(
|
||||
BitmapFactory.decodeStream(
|
||||
new URL(videoInfo.uploader_thumbnail_url)
|
||||
.openConnection()
|
||||
.getInputStream()), SetThumbnailRunnable.CHANNEL_THUMBNAIL));
|
||||
if(showNextVideoItem) {
|
||||
h.post(new SetThumbnailRunnable(
|
||||
BitmapFactory.decodeStream(
|
||||
new URL(videoInfo.nextVideo.thumbnail_url)
|
||||
.openConnection()
|
||||
.getInputStream()), SetThumbnailRunnable.NEXT_VIDEO_THUMBNAIL));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (IOException e) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
e.printStackTrace();
|
||||
}
|
||||
// custom service related exceptions
|
||||
catch (YoutubeStreamExtractor.DecryptException de) {
|
||||
postNewErrorToast(h, R.string.youtube_signature_decryption_error);
|
||||
de.printStackTrace();
|
||||
} catch (YoutubeStreamExtractor.GemaException ge) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onErrorBlockedByGema();
|
||||
}
|
||||
});
|
||||
}
|
||||
// ----------------------------------------
|
||||
catch(StreamExtractor.ContentNotAvailableException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onNotSpecifiedContentError();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (ParsingException e) {
|
||||
postNewErrorToast(h, e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch(Exception e) {
|
||||
postNewErrorToast(h, R.string.general_error);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class VideoResultReturnedRunnable implements Runnable {
|
||||
private VideoInfo videoInfo;
|
||||
private final VideoInfo videoInfo;
|
||||
public VideoResultReturnedRunnable(VideoInfo videoInfo) {
|
||||
this.videoInfo = videoInfo;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
//todo: fix expired thread error:
|
||||
// If the thread calling this runnable is expired, the following function will crash.
|
||||
updateInfo(videoInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private class SetThumbnailRunnable implements Runnable {
|
||||
public static final int VIDEO_THUMBNAIL = 1;
|
||||
public static final int CHANNEL_THUMBNAIL = 2;
|
||||
public static final int NEXT_VIDEO_THUMBNAIL = 3;
|
||||
private Bitmap thumbnail;
|
||||
private int thumbnailId;
|
||||
public SetThumbnailRunnable(Bitmap thumbnail, int id) {
|
||||
this.thumbnail = thumbnail;
|
||||
this.thumbnailId = id;
|
||||
}
|
||||
private class ThumbnailLoadingListener implements ImageLoadingListener {
|
||||
@Override
|
||||
public void run() {
|
||||
updateThumbnail(thumbnail, thumbnailId);
|
||||
public void onLoadingStarted(String imageUri, View view) {}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show();
|
||||
failReason.getCause().printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {}
|
||||
}
|
||||
|
||||
public void updateThumbnail(Bitmap thumbnail, int id) {
|
||||
Activity a = getActivity();
|
||||
ImageView thumbnailView = null;
|
||||
try {
|
||||
switch (id) {
|
||||
case SetThumbnailRunnable.VIDEO_THUMBNAIL:
|
||||
thumbnailView = (ImageView) a.findViewById(R.id.detailThumbnailView);
|
||||
break;
|
||||
case SetThumbnailRunnable.CHANNEL_THUMBNAIL:
|
||||
thumbnailView = (ImageView) a.findViewById(R.id.detailUploaderThumbnailView);
|
||||
break;
|
||||
case SetThumbnailRunnable.NEXT_VIDEO_THUMBNAIL:
|
||||
FrameLayout nextVideoFrame = (FrameLayout) a.findViewById(R.id.detailNextVideoFrame);
|
||||
thumbnailView = (ImageView) nextVideoFrame.findViewById(R.id.itemThumbnailView);
|
||||
currentVideoInfo.nextVideo.thumbnail = thumbnail;
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Error: Thumbnail id not known");
|
||||
return;
|
||||
}
|
||||
|
||||
if (thumbnailView != null) {
|
||||
thumbnailView.setImageBitmap(thumbnail);
|
||||
}
|
||||
|
||||
} catch (java.lang.NullPointerException e) {
|
||||
// No god programm design i know. :/
|
||||
Log.w(TAG, "updateThumbnail(): Fragment closed before thread ended work");
|
||||
}
|
||||
}
|
||||
|
||||
public void updateInfo(VideoInfo info) {
|
||||
Activity a = getActivity();
|
||||
currentVideoInfo = info;
|
||||
private void updateInfo(final VideoInfo info) {
|
||||
try {
|
||||
Context c = getContext();
|
||||
VideoInfoItemViewCreator videoItemViewCreator =
|
||||
new VideoInfoItemViewCreator(LayoutInflater.from(getActivity()));
|
||||
|
||||
ScrollView contentMainView = (ScrollView) a.findViewById(R.id.detailMainContent);
|
||||
ProgressBar progressBar = (ProgressBar) a.findViewById(R.id.detailProgressBar);
|
||||
TextView videoTitleView = (TextView) a.findViewById(R.id.detailVideoTitleView);
|
||||
TextView uploaderView = (TextView) a.findViewById(R.id.detailUploaderView);
|
||||
TextView viewCountView = (TextView) a.findViewById(R.id.detailViewCountView);
|
||||
TextView thumbsUpView = (TextView) a.findViewById(R.id.detailThumbsUpCountView);
|
||||
TextView thumbsDownView = (TextView) a.findViewById(R.id.detailThumbsDownCountView);
|
||||
TextView uploadDateView = (TextView) a.findViewById(R.id.detailUploadDateView);
|
||||
TextView descriptionView = (TextView) a.findViewById(R.id.detailDescriptionView);
|
||||
ImageView thumbnailView = (ImageView) a.findViewById(R.id.detailThumbnailView);
|
||||
FrameLayout nextVideoFrame = (FrameLayout) a.findViewById(R.id.detailNextVideoFrame);
|
||||
RelativeLayout textContentLayout =
|
||||
(RelativeLayout) activity.findViewById(R.id.detailTextContentLayout);
|
||||
final TextView videoTitleView =
|
||||
(TextView) activity.findViewById(R.id.detailVideoTitleView);
|
||||
TextView uploaderView = (TextView) activity.findViewById(R.id.detailUploaderView);
|
||||
TextView viewCountView = (TextView) activity.findViewById(R.id.detailViewCountView);
|
||||
TextView thumbsUpView = (TextView) activity.findViewById(R.id.detailThumbsUpCountView);
|
||||
TextView thumbsDownView =
|
||||
(TextView) activity.findViewById(R.id.detailThumbsDownCountView);
|
||||
TextView uploadDateView = (TextView) activity.findViewById(R.id.detailUploadDateView);
|
||||
TextView descriptionView = (TextView) activity.findViewById(R.id.detailDescriptionView);
|
||||
FrameLayout nextVideoFrame =
|
||||
(FrameLayout) activity.findViewById(R.id.detailNextVideoFrame);
|
||||
RelativeLayout nextVideoRootFrame =
|
||||
(RelativeLayout) a.findViewById(R.id.detailNextVideoRootLayout);
|
||||
View nextVideoView = videoItemViewCreator
|
||||
.getViewByVideoInfoItem(null, nextVideoFrame, info.nextVideo);
|
||||
nextVideoFrame.addView(nextVideoView);
|
||||
Button nextVideoButton = (Button) a.findViewById(R.id.detailNextVideoButton);
|
||||
|
||||
contentMainView.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if(!showNextVideoItem) {
|
||||
nextVideoRootFrame.setVisibility(View.GONE);
|
||||
(RelativeLayout) activity.findViewById(R.id.detailNextVideoRootLayout);
|
||||
Button nextVideoButton = (Button) activity.findViewById(R.id.detailNextVideoButton);
|
||||
TextView similarTitle = (TextView) activity.findViewById(R.id.detailSimilarTitle);
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton);
|
||||
View topView = activity.findViewById(R.id.detailTopView);
|
||||
View nextVideoView = null;
|
||||
if(info.next_video != null) {
|
||||
nextVideoView = videoItemViewCreator
|
||||
.getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video, getContext());
|
||||
} else {
|
||||
activity.findViewById(R.id.detailNextVidButtonAndContentLayout).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detailNextVideoTitle).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detailNextVideoButton).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
switch (info.videoAvailableStatus) {
|
||||
case VideoInfo.VIDEO_AVAILABLE: {
|
||||
videoTitleView.setText(info.title);
|
||||
uploaderView.setText(info.uploader);
|
||||
viewCountView.setText(info.view_count + " " + a.getString(R.string.viewSufix));
|
||||
thumbsUpView.setText(info.like_count);
|
||||
thumbsDownView.setText(info.dislike_count);
|
||||
uploadDateView.setText(a.getString(R.string.uploadDatePrefix) + " " + info.upload_date);
|
||||
descriptionView.setText(Html.fromHtml(info.description));
|
||||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if(nextVideoView != null) {
|
||||
nextVideoFrame.addView(nextVideoView);
|
||||
}
|
||||
|
||||
actionBarHandler.setVideoInfo(info.webpage_url, info.title);
|
||||
initThumbnailViews(info, nextVideoFrame);
|
||||
|
||||
// parse streams
|
||||
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
|
||||
for (VideoInfo.VideoStream i : info.videoStreams) {
|
||||
if (useStream(i, streamsToUse)) {
|
||||
streamsToUse.add(i);
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
ImageView playArrowView = (ImageView) activity.findViewById(R.id.playArrowView);
|
||||
playArrowView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (!showNextVideoItem) {
|
||||
nextVideoRootFrame.setVisibility(View.GONE);
|
||||
similarTitle.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
videoTitleView.setText(info.title);
|
||||
|
||||
topView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == android.view.MotionEvent.ACTION_UP) {
|
||||
ImageView arrow = (ImageView) activity.findViewById(R.id.toggleDescriptionView);
|
||||
View extra = activity.findViewById(R.id.detailExtraView);
|
||||
if (extra.getVisibility() == View.VISIBLE) {
|
||||
extra.setVisibility(View.GONE);
|
||||
arrow.setImageResource(R.drawable.arrow_down);
|
||||
} else {
|
||||
extra.setVisibility(View.VISIBLE);
|
||||
arrow.setImageResource(R.drawable.arrow_up);
|
||||
}
|
||||
}
|
||||
VideoInfo.VideoStream[] streamList = new VideoInfo.VideoStream[streamsToUse.size()];
|
||||
for (int i = 0; i < streamList.length; i++) {
|
||||
streamList[i] = streamsToUse.get(i);
|
||||
}
|
||||
actionBarHandler.setStreams(streamList, info.audioStreams);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
nextVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent detailIntent = new Intent(getActivity(), VideoItemDetailActivity.class);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, currentVideoInfo.nextVideo.id);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, currentVideoInfo.nextVideo.webpage_url);
|
||||
//todo: make id dynamic the following line is crap
|
||||
detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, 0);
|
||||
startActivity(detailIntent);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case VideoInfo.VIDEO_UNAVAILABLE_GEMA:
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.gruese_die_gema_unangebracht));
|
||||
break;
|
||||
case VideoInfo.VIDEO_UNAVAILABLE:
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.not_available_monkey));
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Video Available Status not known.");
|
||||
// Since newpipe is designed to work even if certain information is not available,
|
||||
// the UI has to react on missing information.
|
||||
videoTitleView.setText(info.title);
|
||||
if(!info.uploader.isEmpty()) {
|
||||
uploaderView.setText(info.uploader);
|
||||
} else {
|
||||
activity.findViewById(R.id.detailUploaderWrapView).setVisibility(View.GONE);
|
||||
}
|
||||
if(info.view_count >= 0) {
|
||||
viewCountView.setText(Localization.localizeViewCount(info.view_count, c));
|
||||
} else {
|
||||
viewCountView.setVisibility(View.GONE);
|
||||
}
|
||||
if(info.dislike_count >= 0) {
|
||||
thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, c));
|
||||
} else {
|
||||
thumbsDownView.setVisibility(View.INVISIBLE);
|
||||
activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE);
|
||||
}
|
||||
if(info.like_count >= 0) {
|
||||
thumbsUpView.setText(Localization.localizeNumber(info.like_count, c));
|
||||
} else {
|
||||
thumbsUpView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detailThumbsUpImgView).setVisibility(View.GONE);
|
||||
thumbsDownView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE);
|
||||
}
|
||||
if(!info.upload_date.isEmpty()) {
|
||||
uploadDateView.setText(Localization.localizeDate(info.upload_date, c));
|
||||
} else {
|
||||
uploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
if(!info.description.isEmpty()) {
|
||||
descriptionView.setText(Html.fromHtml(info.description));
|
||||
} else {
|
||||
descriptionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
// parse streams
|
||||
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
|
||||
for (VideoInfo.VideoStream i : info.video_streams) {
|
||||
if (useStream(i, streamsToUse)) {
|
||||
streamsToUse.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
nextVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent detailIntent =
|
||||
new Intent(getActivity(), VideoItemDetailActivity.class);
|
||||
/*detailIntent.putExtra(
|
||||
VideoItemDetailFragment.ARG_ITEM_ID, currentVideoInfo.nextVideo.id); */
|
||||
detailIntent.putExtra(
|
||||
VideoItemDetailFragment.VIDEO_URL, info.next_video.webpage_url);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
|
||||
startActivity(detailIntent);
|
||||
}
|
||||
});
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
if(info.related_videos != null && !info.related_videos.isEmpty()) {
|
||||
initSimilarVideos(info, videoItemViewCreator);
|
||||
} else {
|
||||
activity.findViewById(R.id.detailSimilarTitle).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.similarVideosView).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setupActionBarHandler(info);
|
||||
|
||||
if(autoPlayEnabled) {
|
||||
actionBarHandler.playVideo();
|
||||
playVideo(info);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (java.lang.NullPointerException e) {
|
||||
Log.w(TAG, "updateInfo(): Fragment closed before thread ended work... or else");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void initThumbnailViews(VideoInfo info, View nextVideoFrame) {
|
||||
ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
ImageView uploaderThumb
|
||||
= (ImageView) activity.findViewById(R.id.detailUploaderThumbnailView);
|
||||
ImageView nextVideoThumb =
|
||||
(ImageView) nextVideoFrame.findViewById(R.id.itemThumbnailView);
|
||||
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.thumbnail_url, videoThumbnailView,
|
||||
displayImageOptions, new ImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingStarted(String imageUri, View view) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show();
|
||||
failReason.getCause().printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
videoThumbnail = loadedImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
videoThumbnailView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
if(info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.uploader_thumbnail_url,
|
||||
uploaderThumb, displayImageOptions, new ThumbnailLoadingListener());
|
||||
}
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.next_video.thumbnail_url,
|
||||
nextVideoThumb, displayImageOptions, new ThumbnailLoadingListener());
|
||||
}
|
||||
}
|
||||
|
||||
private void setupActionBarHandler(final VideoInfo info) {
|
||||
actionBarHandler.setupStreamList(info.video_streams);
|
||||
|
||||
actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url);
|
||||
intent.setType("text/plain");
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(info.webpage_url));
|
||||
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setPackage(KORE_PACKET);
|
||||
intent.setData(Uri.parse(info.webpage_url.replace("https", "http")));
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.kore_not_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_kore_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
try {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
// Sometimes it may be that some information is not available due to changes fo the
|
||||
// website which was crawled. Then the ui has to understand this and act right.
|
||||
|
||||
if (info.audio_streams != null) {
|
||||
VideoInfo.AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
|
||||
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
|
||||
args.putString(DownloadDialog.AUDIO_URL, audioStream.url);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
|
||||
}
|
||||
|
||||
if (info.video_streams != null) {
|
||||
VideoInfo.VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
|
||||
String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
|
||||
args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url);
|
||||
}
|
||||
|
||||
args.putString(DownloadDialog.TITLE, info.title);
|
||||
DownloadDialog downloadDialog = new DownloadDialog();
|
||||
downloadDialog.setArguments(args);
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(info.audio_streams == null) {
|
||||
actionBarHandler.showAudioAction(false);
|
||||
} else {
|
||||
actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
Intent intent;
|
||||
VideoInfo.AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||
//internal music player: explicit intent
|
||||
if (!BackgroundPlayer.isRunning && videoThumbnail != null) {
|
||||
ActivityCommunicator.getCommunicator()
|
||||
.backgroundPlayerThumbnail = videoThumbnail;
|
||||
intent = new Intent(activity, BackgroundPlayer.class);
|
||||
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
Log.i(TAG, "audioStream is null:" + (audioStream == null));
|
||||
Log.i(TAG, "audioStream.url is null:" + (audioStream.url == null));
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
MediaFormat.getMimeById(audioStream.format));
|
||||
intent.putExtra(BackgroundPlayer.TITLE, info.title);
|
||||
intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url);
|
||||
intent.putExtra(BackgroundPlayer.SERVICE_ID, streamingServiceId);
|
||||
intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader);
|
||||
activity.startService(intent);
|
||||
}
|
||||
} else {
|
||||
intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
MediaFormat.getMimeById(audioStream.format));
|
||||
intent.putExtra(Intent.EXTRA_TITLE, info.title);
|
||||
intent.putExtra("title", info.title);
|
||||
// HERE !!!
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Log.i(TAG, "You unlocked a secret unicorn.");
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private int getPreferredAudioStreamId(final VideoInfo info) {
|
||||
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getString(activity.getString(R.string.default_audio_format_key), "webm");
|
||||
|
||||
int preferredFormat = MediaFormat.WEBMA.id;
|
||||
switch(preferredFormatString) {
|
||||
case "webm":
|
||||
preferredFormat = MediaFormat.WEBMA.id;
|
||||
break;
|
||||
case "m4a":
|
||||
preferredFormat = MediaFormat.M4A.id;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for(int i = 0; i < info.audio_streams.size(); i++) {
|
||||
if(info.audio_streams.get(i).format == preferredFormat) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
//todo: make this a proper error
|
||||
Log.e(TAG, "FAILED to set audioStream value!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void initSimilarVideos(final VideoInfo info, VideoInfoItemViewCreator videoItemViewCreator) {
|
||||
LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similarVideosView);
|
||||
ArrayList<VideoPreviewInfo> similar = new ArrayList<>(info.related_videos);
|
||||
for (final VideoPreviewInfo item : similar) {
|
||||
View similarView = videoItemViewCreator
|
||||
.getViewFromVideoInfoItem(null, similarLayout, item, getContext());
|
||||
|
||||
similarView.setClickable(true);
|
||||
similarView.setFocusable(true);
|
||||
int[] attrs = new int[]{R.attr.selectableItemBackground};
|
||||
TypedArray typedArray = activity.obtainStyledAttributes(attrs);
|
||||
int backgroundResource = typedArray.getResourceId(0, 0);
|
||||
similarView.setBackgroundResource(backgroundResource);
|
||||
typedArray.recycle();
|
||||
|
||||
similarView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
Intent detailIntent = new Intent(activity, VideoItemDetailActivity.class);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, item.webpage_url);
|
||||
detailIntent.putExtra(
|
||||
VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
|
||||
startActivity(detailIntent);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
similarLayout.addView(similarView);
|
||||
ImageView rthumb = (ImageView)similarView.findViewById(R.id.itemThumbnailView);
|
||||
imageLoader.displayImage(item.thumbnail_url, rthumb,
|
||||
displayImageOptions, new ThumbnailLoadingListener());
|
||||
}
|
||||
}
|
||||
|
||||
private void onErrorBlockedByGema() {
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton);
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.gruese_die_gema));
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.c3s_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void onNotSpecifiedContentError() {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.not_available_monkey));
|
||||
Toast.makeText(activity, R.string.content_not_available, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean useStream(VideoInfo.VideoStream stream, Vector<VideoInfo.VideoStream> streams) {
|
||||
for(VideoInfo.VideoStream i : streams) {
|
||||
if(i.resolution.equals(stream.resolution)) {
|
||||
@@ -277,21 +685,22 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
public VideoItemDetailFragment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
activity = (AppCompatActivity) getActivity();
|
||||
showNextVideoItem = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(activity.getString(R.string.showNextVideo), true);
|
||||
.getBoolean(activity.getString(R.string.show_next_video_key), true);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false);
|
||||
progressBar = (ProgressBar) rootView.findViewById(R.id.detailProgressBar);
|
||||
|
||||
actionBarHandler = new ActionBarHandler(activity);
|
||||
actionBarHandler.setupNavMenu(activity);
|
||||
if(onInvokeCreateOptionsMenuListener != null) {
|
||||
@@ -304,46 +713,107 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceBundle) {
|
||||
super.onActivityCreated(savedInstanceBundle);
|
||||
FloatingActionButton playVideoButton = (FloatingActionButton) getActivity().findViewById(R.id.playVideoButton);
|
||||
|
||||
if(playVideoButton != null) {
|
||||
Activity a = getActivity();
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton = (FloatingActionButton) a.findViewById(R.id.playVideoButton);
|
||||
}
|
||||
thumbnailWindowLayout = a.findViewById(R.id.detailVideoThumbnailWindowLayout);
|
||||
Button backgroundButton = (Button)
|
||||
a.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton);
|
||||
|
||||
// Sometimes when this fragment is not visible it still gets initiated
|
||||
// then we must not try to access objects of this fragment.
|
||||
// Otherwise the applications would crash.
|
||||
if(backgroundButton != null) {
|
||||
try {
|
||||
StreamingService streamingService = ServiceList.getService(
|
||||
getArguments().getInt(STREAMING_SERVICE));
|
||||
extractorThread = new Thread(new ExtractorRunnable(
|
||||
getArguments().getString(VIDEO_URL), streamingService.getExtractorClass(), this));
|
||||
streamingServiceId = getArguments().getInt(STREAMING_SERVICE);
|
||||
StreamingService streamingService = ServiceList.getService(streamingServiceId);
|
||||
Thread videoExtractorThread = new Thread(new VideoExtractorRunnable(
|
||||
getArguments().getString(VIDEO_URL), streamingService));
|
||||
|
||||
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
||||
extractorThread.start();
|
||||
videoExtractorThread.start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(getString(R.string.leftHandLayout), false) && checkIfLandscape()) {
|
||||
RelativeLayout.LayoutParams oldLayout = (RelativeLayout.LayoutParams) playVideoButton.getLayoutParams();
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
|
||||
RelativeLayout.LayoutParams.WRAP_CONTENT,
|
||||
RelativeLayout.LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
||||
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
layoutParams.setMargins(oldLayout.leftMargin, oldLayout.topMargin, oldLayout.rightMargin, oldLayout.bottomMargin);
|
||||
playVideoButton.setLayoutParams(layoutParams);
|
||||
}
|
||||
if(Build.VERSION.SDK_INT >= 18) {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
// This is used to synchronize the thumbnailWindowButton and the playVideoButton
|
||||
// inside the ScrollView with the actual size of the thumbnail.
|
||||
//todo: onLayoutChage sometimes not triggered
|
||||
// background buttons area seem to overlap the thumbnail view
|
||||
// So although you just clicked slightly beneath the thumbnail the action still
|
||||
// gets triggered.
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
RelativeLayout.LayoutParams newWindowLayoutParams =
|
||||
(RelativeLayout.LayoutParams) thumbnailWindowLayout.getLayoutParams();
|
||||
newWindowLayoutParams.height = bottom - top;
|
||||
thumbnailWindowLayout.setLayoutParams(newWindowLayoutParams);
|
||||
|
||||
playVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
actionBarHandler.playVideo();
|
||||
}
|
||||
});
|
||||
//noinspection SuspiciousNameCombination
|
||||
initialThumbnailPos.set(top, left);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkIfLandscape() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return displayMetrics.heightPixels < displayMetrics.widthPixels;
|
||||
public void playVideo(final VideoInfo info) {
|
||||
// ----------- THE MAGIC MOMENT ---------------
|
||||
VideoInfo.VideoStream selectedVideoStream =
|
||||
info.video_streams.get(actionBarHandler.getSelectedVideoStream());
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false)) {
|
||||
|
||||
// External Player
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
|
||||
intent.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||
MediaFormat.getMimeById(selectedVideoStream.format));
|
||||
intent.putExtra(Intent.EXTRA_TITLE, info.title);
|
||||
intent.putExtra("title", info.title);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
} else {
|
||||
// Internal Player
|
||||
Intent intent = new Intent(activity, PlayVideoActivity.class);
|
||||
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title);
|
||||
intent.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url);
|
||||
intent.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url);
|
||||
intent.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
|
||||
activity.startActivity(intent); //also HERE !!!
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -359,4 +829,24 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
public void setOnInvokeCreateOptionsMenuListener(OnInvokeCreateOptionsMenuListener listener) {
|
||||
this.onInvokeCreateOptionsMenuListener = listener;
|
||||
}
|
||||
|
||||
private void postNewErrorToast(Handler h, final int stringResource) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
stringResource, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void postNewErrorToast(Handler h, final String message) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,21 @@ 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;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -34,17 +41,27 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
implements VideoItemListFragment.Callbacks {
|
||||
|
||||
private static final String TAG = VideoItemListFragment.class.toString();
|
||||
|
||||
// arguments to give to this activity
|
||||
public static final String VIDEO_INFO_ITEMS = "video_info_items";
|
||||
|
||||
// savedInstanceBundle arguments
|
||||
private static final String QUERY = "query";
|
||||
private static final String STREAMING_SERVICE = "streaming_service";
|
||||
|
||||
// activity modes
|
||||
private static final int SEARCH_MODE = 0;
|
||||
private static final int PRESENT_VIDEOS_MODE = 1;
|
||||
|
||||
private int mode = SEARCH_MODE;
|
||||
private int currentStreamingServiceId = -1;
|
||||
private String searchQuery = "";
|
||||
|
||||
private VideoItemListFragment listFragment;
|
||||
private VideoItemDetailFragment videoFragment = null;
|
||||
Menu menu = null;
|
||||
private Menu menu = null;
|
||||
|
||||
public class SearchVideoQueryListener implements SearchView.OnQueryTextListener {
|
||||
private class SearchVideoQueryListener implements SearchView.OnQueryTextListener {
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
@@ -55,8 +72,14 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
// hide virtual keyboard
|
||||
InputMethodManager inputManager =
|
||||
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
} catch(NullPointerException e) {
|
||||
Log.e(TAG, "Could not get widget with focus");
|
||||
e.printStackTrace();
|
||||
}
|
||||
// clear focus
|
||||
// 1. to not open up the keyboard after switching back to this
|
||||
// 2. It's a workaround to a seeming bug by the Android OS it self, causing
|
||||
@@ -87,17 +110,36 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_videoitem_list);
|
||||
|
||||
listFragment = (VideoItemListFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.videoitem_list);
|
||||
|
||||
//-------- remove this line when multiservice support is implemented ----------
|
||||
//------ todo: remove this line when multiservice support is implemented ------
|
||||
currentStreamingServiceId = ServiceList.getIdOfService("Youtube");
|
||||
//-----------------------------------------------------------------------------
|
||||
VideoItemListFragment listFragment = (VideoItemListFragment) getSupportFragmentManager()
|
||||
//to solve issue 38
|
||||
listFragment = (VideoItemListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.videoitem_list);
|
||||
listFragment.setStreamingService(ServiceList.getService(currentStreamingServiceId));
|
||||
|
||||
if(savedInstanceState != null) {
|
||||
Bundle arguments = getIntent().getExtras();
|
||||
|
||||
if(arguments != null) {
|
||||
//Parcelable[] p = arguments.getParcelableArray(VIDEO_INFO_ITEMS);
|
||||
ArrayList<VideoPreviewInfo> p = arguments.getParcelableArrayList(VIDEO_INFO_ITEMS);
|
||||
if(p != null) {
|
||||
mode = PRESENT_VIDEOS_MODE;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "Could not get SupportActionBar");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
listFragment.present(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(savedInstanceState != null
|
||||
&& mode != PRESENT_VIDEOS_MODE) {
|
||||
searchQuery = savedInstanceState.getString(QUERY);
|
||||
currentStreamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
|
||||
if(!searchQuery.isEmpty()) {
|
||||
@@ -120,15 +162,24 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
.setActivateOnItemClick(true);
|
||||
|
||||
SearchView searchView = (SearchView)findViewById(R.id.searchViewTablet);
|
||||
// Somehow the seticonifiedbydefault property set by the layout xml is not working on
|
||||
// the support version on SearchView, so it needs to be set programmatically.
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchVideoQueryListener());
|
||||
if(mode != PRESENT_VIDEOS_MODE) {
|
||||
// Somehow the seticonifiedbydefault property set by the layout xml is not working on
|
||||
// the support version on SearchView, so it needs to be set programmatically.
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchVideoQueryListener());
|
||||
} else {
|
||||
searchView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsActivity.initSettings(this);
|
||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
App.checkStartTor(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,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();
|
||||
@@ -166,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);
|
||||
@@ -178,7 +229,8 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
super.onCreateOptionsMenu(menu);
|
||||
this.menu = menu;
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
if(findViewById(R.id.videoitem_detail_container) == null) {
|
||||
if(mode != PRESENT_VIDEOS_MODE &&
|
||||
findViewById(R.id.videoitem_detail_container) == null) {
|
||||
inflater.inflate(R.menu.videoitem_list, menu);
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
@@ -198,14 +250,23 @@ public class VideoItemListActivity extends AppCompatActivity
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if(id == R.id.action_settings) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
return videoFragment.onOptionsItemSelected(item) ||
|
||||
|
||||
switch(id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, VideoItemListActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return videoFragment.onOptionsItemSelected(item) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Vector;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.schabi.newpipe.extractor.ExctractionException;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
|
||||
|
||||
/**
|
||||
@@ -41,19 +47,28 @@ public class VideoItemListFragment extends ListFragment {
|
||||
private StreamingService streamingService = null;
|
||||
private VideoListAdapter videoListAdapter;
|
||||
|
||||
// activity modes
|
||||
private static final int SEARCH_MODE = 0;
|
||||
private static final int PRESENT_VIDEOS_MODE = 1;
|
||||
|
||||
private int mode = SEARCH_MODE;
|
||||
private String query = "";
|
||||
private int lastPage = 0;
|
||||
|
||||
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 SearchEngine.Result result;
|
||||
private int requestId;
|
||||
private final SearchEngine.Result result;
|
||||
private final int requestId;
|
||||
public ResultRunnable(SearchEngine.Result result, int requestId) {
|
||||
this.result = result;
|
||||
this.requestId = requestId;
|
||||
@@ -61,60 +76,65 @@ public class VideoItemListFragment extends ListFragment {
|
||||
@Override
|
||||
public void run() {
|
||||
updateListOnResult(result, requestId);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 19) {
|
||||
getListView().removeFooterView(footer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchRunnable implements Runnable {
|
||||
private Class engineClass = null;
|
||||
private String query;
|
||||
private int page;
|
||||
Handler h = new Handler();
|
||||
private volatile boolean run = true;
|
||||
private int requestId;
|
||||
public SearchRunnable(Class engineClass, String query, int page, int requestId) {
|
||||
this.engineClass = engineClass;
|
||||
private final SearchEngine engine;
|
||||
private final String query;
|
||||
private final int page;
|
||||
final Handler h = new Handler();
|
||||
private volatile boolean runs = true;
|
||||
private final int requestId;
|
||||
public SearchRunnable(SearchEngine engine, String query, int page, int requestId) {
|
||||
this.engine = engine;
|
||||
this.query = query;
|
||||
this.page = page;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
void terminate() {
|
||||
run = false;
|
||||
runs = false;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
SearchEngine engine = null;
|
||||
try {
|
||||
engine = (SearchEngine) engineClass.newInstance();
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SearchEngine.Result result = engine.search(query, page);
|
||||
if(run) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
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(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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
<<<
|
||||
private class LoadThumbsRunnable implements Runnable {
|
||||
private Vector<String> thumbnailUrlList = new Vector<>();
|
||||
private Vector<Boolean> downloadedList;
|
||||
Handler h = new Handler();
|
||||
private final Vector<String> thumbnailUrlList = new Vector<>();
|
||||
private final Vector<Boolean> downloadedList;
|
||||
final Handler h = new Handler();
|
||||
private volatile boolean run = true;
|
||||
private int requestId;
|
||||
public LoadThumbsRunnable(Vector<VideoInfoItem> videoList,
|
||||
private final int requestId;
|
||||
public LoadThumbsRunnable(Vector<VideoPreviewInfo> videoList,
|
||||
Vector<Boolean> downloadedList, int requestId) {
|
||||
for(VideoInfoItem item : videoList) {
|
||||
for(VideoPreviewInfo item : videoList) {
|
||||
thumbnailUrlList.add(item.thumbnail_url);
|
||||
}
|
||||
this.downloadedList = downloadedList;
|
||||
@@ -130,8 +150,9 @@ public class VideoItemListFragment extends ListFragment {
|
||||
public void run() {
|
||||
for(int i = 0; i < thumbnailUrlList.size() && run; i++) {
|
||||
if(!downloadedList.get(i)) {
|
||||
Bitmap thumbnail = null;
|
||||
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));
|
||||
@@ -144,9 +165,9 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
private class SetThumbnailRunnable implements Runnable {
|
||||
private int index;
|
||||
private Bitmap thumbnail;
|
||||
private int requestId;
|
||||
private final int index;
|
||||
private final Bitmap thumbnail;
|
||||
private final int requestId;
|
||||
public SetThumbnailRunnable(int index, Bitmap thumbnail, int requestId) {
|
||||
this.index = index;
|
||||
this.thumbnail = thumbnail;
|
||||
@@ -155,31 +176,47 @@ public class VideoItemListFragment extends ListFragment {
|
||||
@Override
|
||||
public void run() {
|
||||
if(requestId == currentRequestId) {
|
||||
videoListAdapter.updateDownloadedThumbnailList(index, true);
|
||||
videoListAdapter.updateDownloadedThumbnailList(index);
|
||||
videoListAdapter.setThumbnail(index, thumbnail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
=======
|
||||
>>>>>>> 6d1b4652fc98e5c2d5e19b0f98ba38a731137a70
|
||||
*/
|
||||
public void present(List<VideoPreviewInfo> videoList) {
|
||||
mode = PRESENT_VIDEOS_MODE;
|
||||
setListShown(true);
|
||||
getListView().smoothScrollToPosition(0);
|
||||
|
||||
updateList(videoList);
|
||||
}
|
||||
|
||||
public void search(String query) {
|
||||
mode = SEARCH_MODE;
|
||||
this.query = query;
|
||||
this.lastPage = 1;
|
||||
videoListAdapter.clearVideoList();
|
||||
setListShown(false);
|
||||
startSearch(query, lastPage);
|
||||
//todo: Somehow this command is not working on older devices,
|
||||
// although it was introduced with API level 8. Test this and find a solution.
|
||||
getListView().smoothScrollToPosition(0);
|
||||
}
|
||||
|
||||
public void nextPage() {
|
||||
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);
|
||||
}
|
||||
|
||||
private void startSearch(String query, int page) {
|
||||
currentRequestId++;
|
||||
terminateThreads();
|
||||
searchRunnable = new SearchRunnable(streamingService.getSearchEngineClass(), query, page, currentRequestId);
|
||||
searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(),
|
||||
query, page, currentRequestId);
|
||||
searchThread = new Thread(searchRunnable);
|
||||
searchThread.start();
|
||||
}
|
||||
@@ -188,14 +225,14 @@ public class VideoItemListFragment extends ListFragment {
|
||||
this.streamingService = streamingService;
|
||||
}
|
||||
|
||||
public void updateListOnResult(SearchEngine.Result result, int requestId) {
|
||||
private void updateListOnResult(SearchEngine.Result result, int requestId) {
|
||||
if(requestId == currentRequestId) {
|
||||
setListShown(true);
|
||||
if (result.resultList.isEmpty()) {
|
||||
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);
|
||||
@@ -203,30 +240,20 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateList(Vector<VideoInfoItem> list) {
|
||||
private void updateList(List<VideoPreviewInfo> list) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public void terminateThreads() {
|
||||
if(loadThumbsRunnable != null && loadThumbsRunnable.isRunning()) {
|
||||
loadThumbsRunnable.terminate();
|
||||
try {
|
||||
loadThumbsThread.join();
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
private void terminateThreads() {
|
||||
if(searchThread != null) {
|
||||
searchRunnable.terminate();
|
||||
// No need to join, since we don't really terminate the thread. We just demand
|
||||
@@ -234,10 +261,6 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
}
|
||||
|
||||
void displayList() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The serialization (saved instance state) Bundle key representing the
|
||||
* activated item position. Only used on tablets.
|
||||
@@ -261,17 +284,17 @@ public class VideoItemListFragment extends ListFragment {
|
||||
void onItemSelected(String id);
|
||||
}
|
||||
|
||||
Callbacks mCallbacks = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
private Callbacks mCallbacks = null;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
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);
|
||||
|
||||
|
||||
setListAdapter(videoListAdapter);
|
||||
|
||||
// Restore the previously serialized activated item position.
|
||||
@@ -281,9 +304,8 @@ public class VideoItemListFragment extends ListFragment {
|
||||
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
|
||||
}
|
||||
|
||||
|
||||
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||
private static final float OVERSCROLL_THRESHOLD_IN_PIXELS = 100;
|
||||
private float downY;
|
||||
long lastScrollDate = 0;
|
||||
|
||||
@Override
|
||||
@@ -291,14 +313,17 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
||||
ListView list = getListView();
|
||||
if (list.getChildAt(0) != null
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -308,20 +333,15 @@ public class VideoItemListFragment extends ListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Activities containing this fragment must implement its callbacks.
|
||||
if (!(activity instanceof Callbacks)) {
|
||||
if (!(context instanceof Callbacks)) {
|
||||
throw new IllegalStateException("Activity must implement fragment's callbacks.");
|
||||
}
|
||||
|
||||
mCallbacks = (Callbacks) activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
mCallbacks = (Callbacks) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -331,22 +351,11 @@ public class VideoItemListFragment extends ListFragment {
|
||||
mCallbacks.onItemSelected(Long.toString(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
/*
|
||||
if (mActivatedPosition != ListView.INVALID_POSITION) {
|
||||
// Serialize and persist the activated item position.
|
||||
outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on activate-on-click mode. When this mode is on, list items will be
|
||||
* given the 'activated' state when touched.
|
||||
*/
|
||||
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||
public void setActivateOnItemClick(@SuppressWarnings("SameParameterValue") boolean activateOnItemClick) {
|
||||
// When setting CHOICE_MODE_SINGLE, ListView will automatically
|
||||
// give items the 'activated' state when touched.
|
||||
getListView().setChoiceMode(activateOnItemClick
|
||||
@@ -364,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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -30,55 +33,34 @@ import java.util.Vector;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class VideoListAdapter extends BaseAdapter {
|
||||
private static final String TAG = VideoListAdapter.class.toString();
|
||||
|
||||
private Context context;
|
||||
private VideoInfoItemViewCreator viewCreator;
|
||||
private Vector<VideoInfoItem> videoList = new Vector<>();
|
||||
private Vector<Boolean> downloadedThumbnailList = new Vector<>();
|
||||
VideoItemListFragment videoListFragment;
|
||||
ListView listView;
|
||||
class VideoListAdapter extends BaseAdapter {
|
||||
private final Context context;
|
||||
private final VideoInfoItemViewCreator viewCreator;
|
||||
private Vector<VideoPreviewInfo> videoList = new Vector<>();
|
||||
private final ListView listView;
|
||||
|
||||
public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) {
|
||||
viewCreator = new VideoInfoItemViewCreator(LayoutInflater.from(context));
|
||||
this.videoListFragment = videoListFragment;
|
||||
this.listView = videoListFragment.getListView();
|
||||
this.listView.setDivider(null);
|
||||
this.listView.setDividerHeight(0);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void addVideoList(Vector<VideoInfoItem> videos) {
|
||||
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();
|
||||
}
|
||||
|
||||
public Vector<VideoInfoItem> getVideoList() {
|
||||
public Vector<VideoPreviewInfo> getVideoList() {
|
||||
return videoList;
|
||||
}
|
||||
|
||||
public void updateDownloadedThumbnailList(int index, boolean val) {
|
||||
downloadedThumbnailList.set(index, val);
|
||||
}
|
||||
|
||||
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 @@ public 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(context.getResources().getColor(R.color.primaryColorYoutube));
|
||||
convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color));
|
||||
} else {
|
||||
convertView.setBackgroundColor(0);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoInfoItem.java is part of NewPipe.
|
||||
* 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
|
||||
@@ -22,14 +20,14 @@ import android.graphics.Bitmap;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class VideoInfoItem {
|
||||
/**Common properties between VideoInfo and VideoPreviewInfo.*/
|
||||
public abstract class AbstractVideoInfo {
|
||||
public String id = "";
|
||||
public String title = "";
|
||||
public String uploader = "";
|
||||
public String duration = "";
|
||||
public String thumbnail_url = "";
|
||||
public Bitmap thumbnail = null;
|
||||
public String webpage_url = "";
|
||||
public String upload_date = "";
|
||||
public String view_count = "";
|
||||
}
|
||||
public long view_count = -1;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
*
|
||||
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||
* and Adam Howard <achdisposable1@gmail.com> 2015
|
||||
*
|
||||
* MediaFormat.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Static data about various media formats support by Newpipe, eg mime type, extension*/
|
||||
|
||||
public enum MediaFormat {
|
||||
//video and audio combined formats
|
||||
// id name suffix mime type
|
||||
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
||||
v3GPP (0x1, "3GPP", "3gp", "video/3gpp"),
|
||||
WEBM (0x2, "WebM", "webm", "video/webm"),
|
||||
// audio formats
|
||||
M4A (0x3, "m4a", "m4a", "audio/mp4"),
|
||||
WEBMA (0x4, "WebM", "webm", "audio/webm");
|
||||
|
||||
public final int id;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public final String name;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public final String suffix;
|
||||
public final String mimeType;
|
||||
|
||||
MediaFormat(int id, String name, String suffix, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.suffix = suffix;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
/**Return the friendly name of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the friendly name of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getNameById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**Return the file extension of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the file extension of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getSuffixById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.suffix;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**Return the MIME type of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the MIME type of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getMimeById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.mimeType;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
60
app/src/main/java/org/schabi/newpipe/extractor/Parser.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 10.08.15.
|
||||
* Created by Christian Schabesberger on 31.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* Extractor.java is part of NewPipe.
|
||||
* 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
|
||||
@@ -21,8 +21,15 @@ package org.schabi.newpipe;
|
||||
*/
|
||||
|
||||
|
||||
public interface Extractor {
|
||||
VideoInfo getVideoInfo(String siteUrl);
|
||||
String getVideoUrl(String videoId);
|
||||
String getVideoId(String videoUrl);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
@@ -22,14 +25,18 @@ import java.util.Vector;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public interface SearchEngine {
|
||||
|
||||
|
||||
class Result {
|
||||
public String errorMessage = "";
|
||||
public String suggestion = "";
|
||||
public Vector<VideoInfoItem> resultList = new Vector<>();
|
||||
public final List<VideoPreviewInfo> resultList = new Vector<>();
|
||||
}
|
||||
|
||||
Result search(String query, int page);
|
||||
ArrayList<String> suggestionList(String query, Downloader dl)
|
||||
throws ExctractionException, IOException;
|
||||
|
||||
//Result search(String query, int page);
|
||||
Result search(String query, int page, String contentCountry, Downloader dl)
|
||||
throws ExctractionException, IOException;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.youtube.YoutubeService;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
@@ -24,6 +24,10 @@ import org.schabi.newpipe.youtube.YoutubeService;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Provides access to the video streaming services supported by NewPipe.
|
||||
* Currently only Youtube until the API becomes more stable.*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class ServiceList {
|
||||
private static final String TAG = ServiceList.class.toString();
|
||||
private static final StreamingService[] services = {
|
||||
@@ -40,7 +44,7 @@ public class ServiceList {
|
||||
}
|
||||
public static int getIdOfService(String serviceName) {
|
||||
for(int i = 0; i < services.length; i++) {
|
||||
if(services[i].getServiceInfo().name == serviceName) {
|
||||
if(services[i].getServiceInfo().name.equals(serviceName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
package org.schabi.newpipe;
|
||||
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();
|
||||
Class getExtractorClass();
|
||||
Class getSearchEngineClass();
|
||||
StreamExtractor getExtractorInstance(String url, Downloader downloader)
|
||||
throws IOException, ExctractionException;
|
||||
SearchEngine getSearchEngineInstance();
|
||||
|
||||
VideoUrlIdHandler getUrlIdHandler();
|
||||
|
||||
|
||||
// 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 callean through ACTIONs.
|
||||
boolean acceptUrl(String videoUrl);
|
||||
}
|
||||
302
app/src/main/java/org/schabi/newpipe/extractor/VideoInfo.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoPreviewInfo.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 previews of unopened videos, eg search results, related videos*/
|
||||
public class VideoPreviewInfo extends AbstractVideoInfo implements Parcelable {
|
||||
public String duration = "";
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
protected VideoPreviewInfo(Parcel in) {
|
||||
id = in.readString();
|
||||
title = in.readString();
|
||||
uploader = in.readString();
|
||||
duration = in.readString();
|
||||
thumbnail_url = in.readString();
|
||||
thumbnail = (Bitmap) in.readValue(Bitmap.class.getClassLoader());
|
||||
webpage_url = in.readString();
|
||||
upload_date = in.readString();
|
||||
view_count = in.readLong();
|
||||
}
|
||||
|
||||
public VideoPreviewInfo() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(id);
|
||||
dest.writeString(title);
|
||||
dest.writeString(uploader);
|
||||
dest.writeString(duration);
|
||||
dest.writeString(thumbnail_url);
|
||||
dest.writeValue(thumbnail);
|
||||
dest.writeString(webpage_url);
|
||||
dest.writeString(upload_date);
|
||||
dest.writeLong(view_count);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final Parcelable.Creator<VideoPreviewInfo> CREATOR = new Parcelable.Creator<VideoPreviewInfo>() {
|
||||
@Override
|
||||
public VideoPreviewInfo createFromParcel(Parcel in) {
|
||||
return new VideoPreviewInfo(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoPreviewInfo[] newArray(int size) {
|
||||
return new VideoPreviewInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
package org.schabi.newpipe.youtube;
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
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;
|
||||
|
||||
import org.schabi.newpipe.StreamingService;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
@@ -30,16 +38,23 @@ public class YoutubeService implements StreamingService {
|
||||
return serviceInfo;
|
||||
}
|
||||
@Override
|
||||
public Class getExtractorClass() {
|
||||
return YoutubeExtractor.class;
|
||||
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");
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public Class getSearchEngineClass() {
|
||||
return YoutubeSearchEngine.class;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
package org.schabi.newpipe.youtube;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
|
||||
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.Extractor;
|
||||
import org.schabi.newpipe.VideoInfo;
|
||||
import org.schabi.newpipe.VideoInfoItem;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.net.URI;
|
||||
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>
|
||||
* YoutubeExtractor.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 YoutubeExtractor implements Extractor {
|
||||
|
||||
private static final String TAG = YoutubeExtractor.class.toString();
|
||||
|
||||
// These lists only contain itag formats that are supported by the common Android Video player.
|
||||
// How ever if you are heading for a list showing all itag formats lock at
|
||||
// https://github.com/rg3/youtube-dl/issues/1687
|
||||
|
||||
public static int resolveFormat(int itag) {
|
||||
switch(itag) {
|
||||
// video
|
||||
case 17: return VideoInfo.I_3GPP;
|
||||
case 18: return VideoInfo.I_MPEG_4;
|
||||
case 22: return VideoInfo.I_MPEG_4;
|
||||
case 36: return VideoInfo.I_3GPP;
|
||||
case 37: return VideoInfo.I_MPEG_4;
|
||||
case 38: return VideoInfo.I_MPEG_4;
|
||||
case 43: return VideoInfo.I_WEBM;
|
||||
case 44: return VideoInfo.I_WEBM;
|
||||
case 45: return VideoInfo.I_WEBM;
|
||||
case 46: return VideoInfo.I_WEBM;
|
||||
default:
|
||||
//Log.i(TAG, "Itag " + Integer.toString(itag) + " not known or not supported.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptionCode = "";
|
||||
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
||||
|
||||
@Override
|
||||
public String getVideoId(String videoUrl) {
|
||||
try {
|
||||
URI uri = new URI(videoUrl);
|
||||
if(uri.getHost().contains("youtube")) {
|
||||
String query = uri.getFragment();
|
||||
if(query == null) {
|
||||
query = uri.getQuery();
|
||||
} else {
|
||||
query = query.replace("/watch?", "");
|
||||
}
|
||||
String queryElements[] = query.split("&");
|
||||
Map<String, String> queryArguments = new HashMap<>();
|
||||
for (String e : queryElements) {
|
||||
String[] s = e.split("=");
|
||||
queryArguments.put(s[0], s[1]);
|
||||
}
|
||||
return queryArguments.get("v");
|
||||
} else if(uri.getHost().contains("youtu.be")) {
|
||||
// uri.getRawPath() does somehow not return the last character.
|
||||
// so we do a workaround instead.
|
||||
//return uri.getRawPath();
|
||||
String url[] = videoUrl.split("/");
|
||||
return url[url.length-1];
|
||||
} else {
|
||||
Log.e(TAG, "Error could not parse url: " + videoUrl);
|
||||
|
||||
}
|
||||
} catch(Exception e) {
|
||||
Log.e(TAG, "Error could not parse url: " + videoUrl);
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVideoUrl(String videoId) {
|
||||
return "https://www.youtube.com/watch?v=" + videoId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoInfo getVideoInfo(String siteUrl) {
|
||||
String site = Downloader.download(siteUrl);
|
||||
VideoInfo videoInfo = new VideoInfo();
|
||||
|
||||
Document doc = Jsoup.parse(site, siteUrl);
|
||||
|
||||
try {
|
||||
Pattern p = Pattern.compile("v=([0-9a-zA-Z]*)");
|
||||
Matcher m = p.matcher(siteUrl);
|
||||
m.find();
|
||||
videoInfo.id = m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
videoInfo.age_limit = 0;
|
||||
videoInfo.webpage_url = siteUrl;
|
||||
|
||||
//-------------------------------------
|
||||
// extracting form player args
|
||||
//-------------------------------------
|
||||
JSONObject playerArgs = null;
|
||||
JSONObject ytAssets = null;
|
||||
String dashManifest;
|
||||
{
|
||||
Pattern p = Pattern.compile("ytplayer.config\\s*=\\s*(\\{.*?\\});");
|
||||
Matcher m = p.matcher(site);
|
||||
m.find();
|
||||
|
||||
try {
|
||||
playerArgs = (new JSONObject(m.group(1)))
|
||||
.getJSONObject("args");
|
||||
ytAssets = (new JSONObject(m.group(1)))
|
||||
.getJSONObject("assets");
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// If we fail in this part the video is most likely not available.
|
||||
// Determining why is done later.
|
||||
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
videoInfo.uploader = playerArgs.getString("author");
|
||||
videoInfo.title = playerArgs.getString("title");
|
||||
//first attempt gating a small image version
|
||||
//in the html extracting part we try to get a thumbnail with a higher resolution
|
||||
videoInfo.thumbnail_url = playerArgs.getString("thumbnail_url");
|
||||
videoInfo.duration = playerArgs.getInt("length_seconds");
|
||||
videoInfo.average_rating = playerArgs.getString("avg_rating");
|
||||
String playerUrl = ytAssets.getString("js");
|
||||
if(playerUrl.startsWith("//")) {
|
||||
playerUrl = "https:" + playerUrl;
|
||||
}
|
||||
if(decryptionCode.isEmpty()) {
|
||||
decryptionCode = loadDecryptionCode(playerUrl);
|
||||
}
|
||||
|
||||
// extract audio
|
||||
try {
|
||||
dashManifest = playerArgs.getString("dashmpd");
|
||||
videoInfo.audioStreams = parseDashManifest(dashManifest, decryptionCode);
|
||||
} catch (Exception e) {
|
||||
//todo: check if the following statement is true
|
||||
Log.e(TAG, "Dash manifest seems not to bee available.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//------------------------------------
|
||||
// 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 = terrible_unescape_workaround_fuck(tags.get("url"));
|
||||
|
||||
// if video has a signature: decrypt it and add it to the url
|
||||
if(tags.get("s") != null) {
|
||||
if(decryptionCode.isEmpty()) {
|
||||
decryptionCode = loadDecryptionCode(playerUrl);
|
||||
}
|
||||
streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptionCode);
|
||||
}
|
||||
|
||||
if(resolveFormat(itag) != -1) {
|
||||
videoStreams.add(new VideoInfo.VideoStream(
|
||||
streamUrl,
|
||||
resolveFormat(itag),
|
||||
resolveResolutionString(itag)));
|
||||
}
|
||||
}
|
||||
videoInfo.videoStreams = new VideoInfo.VideoStream[videoStreams.size()];
|
||||
for(int i = 0; i < videoStreams.size(); i++) {
|
||||
videoInfo.videoStreams[i] = videoStreams.get(i);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//-------------------------------
|
||||
// extracting 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get high resolution thumbnail if it fails use low res from the player instead
|
||||
try {
|
||||
videoInfo.thumbnail_url = doc.select("link[itemprop=\"thumbnailUrl\"]").first()
|
||||
.attr("abs:href");
|
||||
} catch(Exception e) {
|
||||
Log.i(TAG, "Could not find high res Thumbnail. Use low res instead");
|
||||
}
|
||||
|
||||
// upload date
|
||||
videoInfo.upload_date = doc.select("strong[class=\"watch-time-text\"").first()
|
||||
.text();
|
||||
// Try to only use date not the text around it
|
||||
try {
|
||||
Pattern p = Pattern.compile("([0-9.]*$)");
|
||||
Matcher m = p.matcher(videoInfo.upload_date);
|
||||
m.find();
|
||||
videoInfo.upload_date = m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// description
|
||||
videoInfo.description = doc.select("p[id=\"eow-description\"]").first()
|
||||
.html();
|
||||
|
||||
try {
|
||||
// likes
|
||||
videoInfo.like_count = doc.select("span[class=\"like-button-renderer \"]").first()
|
||||
.getAllElements().select("button")
|
||||
.select("span").get(0).text();
|
||||
|
||||
|
||||
// dislikes
|
||||
videoInfo.dislike_count = doc.select("span[class=\"like-button-renderer \"]").first()
|
||||
.getAllElements().select("button")
|
||||
.select("span").get(2).text();
|
||||
} catch(Exception e) {
|
||||
// if it fails we know that the video does not offer dislikes.
|
||||
videoInfo.like_count = "0";
|
||||
videoInfo.dislike_count = "0";
|
||||
}
|
||||
|
||||
// uploader thumbnail
|
||||
videoInfo.uploader_thumbnail_url = doc.select("a[class*=\"yt-user-photo\"]").first()
|
||||
.select("img").first()
|
||||
.attr("abs:data-thumb");
|
||||
|
||||
// view count
|
||||
videoInfo.view_count = doc.select("div[class=\"watch-view-count\"]").first().text();
|
||||
|
||||
// next video
|
||||
videoInfo.nextVideo = extractVideoInfoItem(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
||||
.select("li").first());
|
||||
|
||||
int i = 0;
|
||||
// related videos
|
||||
videoInfo.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) {
|
||||
videoInfo.relatedVideos.add(extractVideoInfoItem(li));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
private VideoInfo.AudioStream[] parseDashManifest(String dashManifest, String decryptoinCode) {
|
||||
if(!dashManifest.contains("/signature/")) {
|
||||
String encryptedSig = "";
|
||||
String decryptedSig;
|
||||
try {
|
||||
Pattern p = Pattern.compile("/s/([a-fA-F0-9\\.]+)");
|
||||
Matcher m = p.matcher(dashManifest);
|
||||
m.find();
|
||||
encryptedSig = m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
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));
|
||||
int eventType = parser.getEventType();
|
||||
String tagName = "";
|
||||
String currentMimeType = "";
|
||||
int currentBandwidth = -1;
|
||||
int currentSamplingRate = -1;
|
||||
boolean currentTagIsBaseUrl = false;
|
||||
while(eventType != XmlPullParser.END_DOCUMENT) {
|
||||
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(VideoInfo.M_WEBMA)) {
|
||||
format = VideoInfo.I_WEBMA;
|
||||
} else if(currentMimeType.equals(VideoInfo.M_M4A)) {
|
||||
format = VideoInfo.I_M4A;
|
||||
}
|
||||
audioStreams.add(new VideoInfo.AudioStream(parser.getText(),
|
||||
format, currentBandwidth, currentSamplingRate));
|
||||
}
|
||||
case XmlPullParser.END_TAG:
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = "";
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
VideoInfo.AudioStream[] output = new VideoInfo.AudioStream[audioStreams.size()];
|
||||
for(int i = 0; i < output.length; i++) {
|
||||
output[i] = audioStreams.get(i);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private VideoInfoItem extractVideoInfoItem(Element li) {
|
||||
VideoInfoItem info = new VideoInfoItem();
|
||||
info.webpage_url = li.select("a[class*=\"content-link\"]").first()
|
||||
.attr("abs:href");
|
||||
try {
|
||||
Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)");
|
||||
Matcher m = p.matcher(info.webpage_url);
|
||||
m.find();
|
||||
info.id=m.group(1);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
info.title = li.select("span[class=\"title\"]").first().text();
|
||||
info.view_count = li.select("span[class*=\"view-count\"]").first().text();
|
||||
info.uploader = li.select("span[class=\"g-hovercard\"]").first().text();
|
||||
info.duration = li.select("span[class=\"video-time\"]").first().text();
|
||||
|
||||
Element img = li.select("img").first();
|
||||
info.thumbnail_url = img.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files witch somehow seam 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 terrible_unescape_workaround_fuck(String shit) {
|
||||
String[] splitAtEscape = shit.split("%");
|
||||
String retval = "";
|
||||
retval += splitAtEscape[0];
|
||||
for(int i = 1; i < splitAtEscape.length; i++) {
|
||||
String escNum = splitAtEscape[i].substring(0, 2);
|
||||
char c = (char) Integer.parseInt(escNum,16);
|
||||
retval += c;
|
||||
retval += splitAtEscape[i].substring(2);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
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 {
|
||||
Pattern p = Pattern.compile("\\.sig\\|\\|([a-zA-Z0-9$]+)\\(");
|
||||
Matcher m = p.matcher(playerCode);
|
||||
m.find();
|
||||
decryptionFuncName = m.group(1);
|
||||
|
||||
String functionPattern = "(function " + decryptionFuncName.replace("$", "\\$") + "\\([a-zA-Z0-9_]*\\)\\{.+?\\})";
|
||||
p = Pattern.compile(functionPattern);
|
||||
m = p.matcher(playerCode);
|
||||
m.find();
|
||||
decryptionFunc = m.group(1);
|
||||
|
||||
p = Pattern.compile(";([A-Za-z0-9_\\$]{2})\\...\\(");
|
||||
m = p.matcher(decryptionFunc);
|
||||
m.find();
|
||||
helperObjectName = m.group(1);
|
||||
|
||||
String helperPattern = "(var " + helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)function";
|
||||
p = Pattern.compile(helperPattern);
|
||||
m = p.matcher(playerCode);
|
||||
m.find();
|
||||
helperObject = m.group(1);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
callerFunc = callerFunc.replace("%%", decryptionFuncName);
|
||||
decryptionCode = helperObject + decryptionFunc + callerFunc;
|
||||
|
||||
return decryptionCode;
|
||||
}
|
||||
|
||||
private String decryptSignature(String encryptedSig, String decryptoinCode) {
|
||||
Context context = Context.enter();
|
||||
context.setOptimizationLevel(-1);
|
||||
Object result = null;
|
||||
try {
|
||||
ScriptableObject scope = context.initStandardObjects();
|
||||
context.evaluateString(scope, decryptoinCode, "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.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.schabi.newpipe.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.SearchEngine;
|
||||
import org.schabi.newpipe.VideoInfoItem;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 url = builder.build().toString();
|
||||
|
||||
String 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 witch kind of item we are working with.
|
||||
Youtube depicts fife different kinds if items at 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 net 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)) {
|
||||
VideoInfoItem resultItem = new VideoInfoItem();
|
||||
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);
|
||||
m.find();
|
||||
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 witch somehow seam 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(resultItem.thumbnail_url.contains(".gif")) {
|
||||
resultItem.thumbnail_url = te.attr("abs:data-thumb");
|
||||
}
|
||||
result.resultList.add(resultItem);
|
||||
} else {
|
||||
Log.e(TAG, "GREAT FUCKING ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 221 B |
BIN
app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
app/src/main/res/drawable-hdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 358 B |
BIN
app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 83 B |
BIN
app/src/main/res/drawable-mdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 255 B |
BIN
app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-nodpi/arrow_down.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/drawable-nodpi/arrow_up.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png
Normal file
|
After Width: | Height: | Size: 188 B |
BIN
app/src/main/res/drawable-nodpi/gruese_die_gema.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 869 B After Width: | Height: | Size: 869 B |
BIN
app/src/main/res/drawable-nodpi/ic_file_download_black.png
Normal file
|
After Width: | Height: | Size: 209 B |
|
Before Width: | Height: | Size: 786 B After Width: | Height: | Size: 786 B |
|
Before Width: | Height: | Size: 320 B After Width: | Height: | Size: 320 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 888 B |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
BIN
app/src/main/res/drawable-nodpi/thumbs_down.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/drawable-nodpi/thumbs_up.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 343 B |
|
After Width: | Height: | Size: 464 B |
BIN
app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 92 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 461 B |
|
After Width: | Height: | Size: 666 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png
Normal file
|
After Width: | Height: | Size: 94 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png
Normal file
|
After Width: | Height: | Size: 605 B |
|
After Width: | Height: | Size: 883 B |