mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 02:32:40 +00:00
Compare commits
344 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f015349e8 | ||
|
|
a37802c2b9 | ||
|
|
4fa3baf5e1 | ||
|
|
ef8c2c81d5 | ||
|
|
8c80d8c457 | ||
|
|
1596872c54 | ||
|
|
da8873fa78 | ||
|
|
ecd8439b3f | ||
|
|
5d178532ac | ||
|
|
08e40a013d | ||
|
|
c136f7363c | ||
|
|
a60f10d739 | ||
|
|
ae46afcb42 | ||
|
|
bffb9f6800 | ||
|
|
79aa9ad04b | ||
| ff5714f04a | |||
|
|
ce499a9766 | ||
|
|
d3bb8b7651 | ||
|
|
6d9c23c4cb | ||
|
|
b95a9332a9 | ||
|
|
609715eb5c | ||
|
|
a1266c919c | ||
|
|
a1925a0302 | ||
|
|
a7a4c03372 | ||
|
|
37201600e0 | ||
|
|
a94f40ed62 | ||
|
|
0b08cf8c76 | ||
|
|
33e29be7db | ||
|
|
bd1c7851c7 | ||
|
|
82faea5965 | ||
|
|
0bbbfd3217 | ||
|
|
fb578ecda8 | ||
|
|
e804647a65 | ||
|
|
c3c3a94593 | ||
|
|
9d55569f80 | ||
|
|
5dd8271c15 | ||
|
|
7a4a54c3ea | ||
|
|
7c9078a625 | ||
|
|
71ae342f52 | ||
|
|
b43c56085d | ||
|
|
83d2ab95e0 | ||
|
|
20a8d7372c | ||
|
|
c36ba88db7 | ||
|
|
4aa23023ee | ||
|
|
8735cf931a | ||
|
|
2473069326 | ||
|
|
03bab57a97 | ||
|
|
e3baf69533 | ||
|
|
461f747af1 | ||
|
|
028872a7d8 | ||
|
|
a1483b6c55 | ||
|
|
e406ba094c | ||
|
|
c100d15ba8 | ||
|
|
b4ea592638 | ||
|
|
16b757d9a3 | ||
|
|
2aa801a392 | ||
|
|
b838344526 | ||
|
|
233a3df222 | ||
|
|
da6661b1ea | ||
|
|
c6e120fc51 | ||
|
|
c416a1254d | ||
|
|
2aa4f6ddda | ||
|
|
129597023d | ||
|
|
02aed86b7e | ||
|
|
e44f4b5823 | ||
|
|
2f0c0f0fc2 | ||
|
|
e5ce3f3007 | ||
|
|
095a2be748 | ||
|
|
c75fe88757 | ||
|
|
a8ff4b0744 | ||
|
|
c02c511e31 | ||
|
|
b9550fb528 | ||
|
|
a37d8f083a | ||
|
|
abff1f537b | ||
|
|
483fde5e42 | ||
|
|
761b7ea57b | ||
|
|
af92631a0c | ||
|
|
ffe832d061 | ||
|
|
a08cbfcef1 | ||
|
|
d2d6ac1bf8 | ||
|
|
615ffca64b | ||
|
|
2fcf6197c5 | ||
|
|
22d31ae14f | ||
|
|
8c786e121b | ||
|
|
99d2a33c8c | ||
|
|
010a24db3f | ||
|
|
9408da453b | ||
|
|
dbda4202fd | ||
|
|
5776e91459 | ||
|
|
664d7b69b1 | ||
|
|
6b9140c60d | ||
|
|
383857d110 | ||
|
|
cffd049c8a | ||
|
|
ecabc65e94 | ||
|
|
780f1d5da3 | ||
|
|
e2d0fc34a3 | ||
|
|
3404231457 | ||
|
|
b17928570d | ||
|
|
8ed86261ef | ||
|
|
a313b91a0a | ||
|
|
7b6dae20be | ||
|
|
552c70bb0d | ||
|
|
8f734737f0 | ||
|
|
800e7bcb7a | ||
|
|
2002234d86 | ||
|
|
5923663e08 | ||
|
|
4cdf20ab8c | ||
|
|
af65a1cfef | ||
|
|
927057ab83 | ||
|
|
ffbc001ad5 | ||
|
|
f5625a1151 | ||
|
|
ce2ceb8a1b | ||
|
|
c14771534f | ||
|
|
553cec16d5 | ||
|
|
d17496f720 | ||
|
|
89e70626eb | ||
|
|
2ccae841d6 | ||
|
|
07f6d0f149 | ||
|
|
319d769233 | ||
|
|
6ec393699e | ||
|
|
f8d9e0fa60 | ||
|
|
50ed962a82 | ||
|
|
8654705e9b | ||
|
|
b79ed8185f | ||
|
|
0d6c67f64f | ||
|
|
e97a6569a6 | ||
|
|
d0d41c6b16 | ||
|
|
f7a531e71b | ||
|
|
c28fddc4dd | ||
|
|
b6ea10fc73 | ||
|
|
320eb44061 | ||
|
|
7a6b5dd5b7 | ||
|
|
460653ed16 | ||
|
|
b6fda788c5 | ||
|
|
da4b9306fa | ||
|
|
d76c02cbf4 | ||
|
|
4d5466b5cd | ||
|
|
e9c20ac8b0 | ||
|
|
961820a250 | ||
|
|
4cc2976061 | ||
|
|
477f182b43 | ||
|
|
a5252bb765 | ||
|
|
3f0078f38a | ||
|
|
91434dd2ac | ||
|
|
f29bd0a6e7 | ||
|
|
7186f58374 | ||
|
|
5c8bcf15ba | ||
|
|
130757ee99 | ||
|
|
5020a4f9dc | ||
|
|
ef15902ec4 | ||
|
|
ed8d13f837 | ||
|
|
c2fcae7c43 | ||
|
|
27f2c65e6d | ||
|
|
0b3428ede9 | ||
|
|
3e0102ad2a | ||
|
|
c42002ccc5 | ||
|
|
e8101d5d91 | ||
|
|
ecdf4ad502 | ||
|
|
af760f9cdc | ||
|
|
b3491da49f | ||
|
|
c322feeaff | ||
|
|
b26f46a063 | ||
|
|
9f0944dc5a | ||
|
|
e869098434 | ||
|
|
4baa23af5f | ||
|
|
78fc5bbbd5 | ||
|
|
a69784d168 | ||
|
|
d48a7f1a18 | ||
|
|
082c6128ad | ||
|
|
7c9771873b | ||
|
|
7257cdacc8 | ||
|
|
140b480f82 | ||
|
|
77db2f8a48 | ||
|
|
22bc81b0eb | ||
|
|
b52b3d4be0 | ||
|
|
f845098b42 | ||
|
|
0f7397e3b8 | ||
|
|
0b9d99fdc9 | ||
|
|
9a1da5cc75 | ||
|
|
9e76f94cf6 | ||
|
|
7d6b92e064 | ||
|
|
849a45a3ca | ||
|
|
7ddea5a71b | ||
|
|
5d81358c15 | ||
|
|
492aad9d70 | ||
|
|
647cfcd401 | ||
|
|
c60d98e52d | ||
|
|
d3cfac6b15 | ||
|
|
7fb4e5a143 | ||
|
|
8e451b2a83 | ||
|
|
8083f06fe7 | ||
|
|
44521a2e56 | ||
|
|
dfeed3d0eb | ||
|
|
081a45b70d | ||
|
|
616a721bba | ||
|
|
c9aa553b32 | ||
|
|
e3d59c3cff | ||
|
|
df51635674 | ||
|
|
1e81f61760 | ||
|
|
a4043eab83 | ||
|
|
60dc19d2bc | ||
|
|
a2e4585fe8 | ||
|
|
b5df281447 | ||
|
|
650917f9f9 | ||
|
|
3a9c95a9ae | ||
|
|
5458acfcad | ||
|
|
68e80e6054 | ||
|
|
e4aa69b8d3 | ||
|
|
dfd40e43da | ||
|
|
7efd111d9c | ||
|
|
6809172203 | ||
|
|
40a4343f06 | ||
|
|
cd9333b39e | ||
|
|
82d426c781 | ||
|
|
c7e773de25 | ||
|
|
2ded33110f | ||
|
|
b26c0aa9da | ||
|
|
2fc8fb6f17 | ||
|
|
ea76f1d6e2 | ||
|
|
7cda1d116b | ||
|
|
8bf7a1a9db | ||
|
|
0aa0ad65c0 | ||
|
|
53ff58daa3 | ||
|
|
b3225bebe6 | ||
|
|
4beafad71f | ||
|
|
c96f933626 | ||
|
|
bea6359d5f | ||
|
|
92db9cb59b | ||
|
|
410c4ca736 | ||
|
|
80c9dbf180 | ||
|
|
c9edac2820 | ||
|
|
143df9a529 | ||
|
|
c87da9903f | ||
|
|
0f69e6c64d | ||
|
|
1b9a6e53ce | ||
|
|
c7abf377eb | ||
|
|
175d8ce572 | ||
|
|
1708a401cf | ||
|
|
141278e668 | ||
|
|
754bd82699 | ||
|
|
999efb6660 | ||
|
|
0b391a9ef3 | ||
|
|
2a99ac4430 | ||
|
|
a5589d0865 | ||
|
|
83541a0d5d | ||
|
|
ac0dff7aa1 | ||
|
|
f22b5157f5 | ||
|
|
659d0d6115 | ||
|
|
fd8c99fd8d | ||
|
|
9494f3a299 | ||
|
|
8021848b03 | ||
|
|
5a127c26e6 | ||
|
|
a7d734c20c | ||
|
|
7c7129f9a1 | ||
|
|
05cbc7891d | ||
|
|
14623456ff | ||
|
|
5064ec3ac4 | ||
|
|
892888796d | ||
|
|
4f2826d2c2 | ||
|
|
7f824d725b | ||
|
|
da4096c4ef | ||
|
|
1aed11c156 | ||
|
|
e99e944ac3 | ||
|
|
6e523d37ba | ||
|
|
2aebf6b522 | ||
|
|
3f740980a3 | ||
|
|
66b73d1592 | ||
|
|
be4b03b84b | ||
|
|
3594037efe | ||
|
|
38c5cb50fb | ||
|
|
3767a96e0f | ||
|
|
937a387f4e | ||
|
|
cd65f1dffc | ||
|
|
d4e6856cbe | ||
|
|
f61d779108 | ||
|
|
b8b22d4d91 | ||
|
|
25d0e39736 | ||
|
|
79a9497e65 | ||
|
|
c55dcccb1e | ||
|
|
820e606719 | ||
|
|
96291a8522 | ||
|
|
e7b52bd3b0 | ||
|
|
dd3251c08d | ||
|
|
f4aabdd9b8 | ||
|
|
cb1fe5f017 | ||
|
|
83837bde11 | ||
|
|
05189dadbf | ||
|
|
dbb1f371b3 | ||
|
|
5cc1bbc4bb | ||
|
|
53796043c3 | ||
|
|
a5ac528c02 | ||
|
|
54eb353d0d | ||
|
|
3391067cab | ||
|
|
b4f595eb75 | ||
|
|
58bc0c17d0 | ||
|
|
4385404ad6 | ||
|
|
61aadcffd2 | ||
|
|
f575826394 | ||
|
|
389959dd18 | ||
|
|
af4734eee3 | ||
|
|
f76b37ec39 | ||
|
|
379149fe2f | ||
|
|
3bd477631c | ||
|
|
72c0987bad | ||
|
|
5bba8e02a6 | ||
|
|
b270de3335 | ||
|
|
e22bcf0ac5 | ||
|
|
c1d55d828f | ||
|
|
da77970328 | ||
|
|
32f3caaee0 | ||
|
|
8bbacb1d78 | ||
|
|
5904510410 | ||
|
|
e16624251b | ||
|
|
389f22a0e5 | ||
|
|
7b91aa16b6 | ||
|
|
89ec688632 | ||
|
|
fdd0d586c9 | ||
|
|
6f5604791f | ||
|
|
eb9fba4147 | ||
|
|
afb62f729f | ||
|
|
1ccc23dc9c | ||
|
|
7ea5cb9c5c | ||
|
|
c301d6e5d5 | ||
|
|
44ad69b94d | ||
|
|
d14515ab88 | ||
|
|
01875b389d | ||
|
|
5eef116aaa | ||
|
|
442220debc | ||
|
|
4914caad51 | ||
|
|
08cab863f1 | ||
|
|
02458d4fc1 | ||
|
|
6ed4130b66 | ||
|
|
5f7ee15d1e | ||
|
|
43afd5a2b8 | ||
|
|
f9ac199c1f | ||
|
|
920c169d55 | ||
|
|
76ba2824a2 | ||
|
|
ca0d594547 | ||
|
|
44b6d900f0 | ||
|
|
3b2c0186aa | ||
|
|
efa605700d | ||
|
|
cac360d37b | ||
|
|
75e28893fb | ||
|
|
360a44b5a0 |
39
.github/CONTRIBUTING.md
vendored
Normal file
39
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
READ THIS GUIDELINES CAREFULLY BEFORE CONTRIBUTING.
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report if a crash occures.
|
||||
|
||||
## Issue reporting/feature request
|
||||
|
||||
* Search the [existing issues](https://github.com/theScrabi/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before
|
||||
* Check if this issue/feature is already fixed/implemented in the repository
|
||||
* If you are an android/java developer you are always welcome to fix/implement an issue/a feature yourself
|
||||
|
||||
## Bugfixing
|
||||
* If you want to help NewPipe getting bug free, you can send me a mail to tnp@newpipe.schabi.org to let me know that you intent to help, and than register at our [sentry](https://support.schabi.org) setup.
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
|
||||
|
||||
## Code contribution
|
||||
|
||||
* Stick to NewPipe style guidelines (just look the other code and than do it the same way :) )
|
||||
* Do not bring nonfree software/binary blobs into the project (keep it google free)
|
||||
* Stick to [f-droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
||||
* Make changes on a separate branch, not on the master branch (Feature-branching)
|
||||
* When submitting changes, you agree that your code will be licensed under GPLv3
|
||||
* Please test (compile and run) your code before you submit changes!!!
|
||||
* Try to figure out you selves why CI fails, or why a merge request collides
|
||||
* Please maintain your code after you contributed it.
|
||||
* Respond yourselves if someone request changes or notifies issues
|
||||
|
||||
## Communication
|
||||
|
||||
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
|
||||
* If you want to get in contact with me or one of our other contributors you can send me an email at tnp(at)schabi.org
|
||||
* Feel free to post suggestions, changes, ideas etc!
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
- [ ] I checked if the issue/feature exists in the latest version.
|
||||
1
.github/PULL_REQUEST_TEAMPLATE.md
vendored
Normal file
1
.github/PULL_REQUEST_TEAMPLATE.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[ ] I carefully reed the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@
|
||||
/.idea
|
||||
/*.iml
|
||||
gradle.properties
|
||||
*~
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "app/src/main/java/org/schabi/newpipe/extractor"]
|
||||
path = app/src/main/java/org/schabi/newpipe/extractor
|
||||
url = https://github.com/TeamNewPipe/NewPipeExtractor.git
|
||||
25
.travis.yml
25
.travis.yml
@@ -5,31 +5,16 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-23.0.3
|
||||
- build-tools-25.0.0
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-24
|
||||
- android-25
|
||||
|
||||
# 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
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
env:
|
||||
global:
|
||||
- ADB_INSTALL_TIMEOUT=8 # minutes (2 by default)
|
||||
- GRADLE_OPTS=-Xmx512m # give gradle more memory since it seem to fail otherwise
|
||||
matrix:
|
||||
- ANDROID_TARGET=android-21 ANDROID_ABI=armeabi-v7a
|
||||
licenses:
|
||||
- '.+'
|
||||
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
|
||||
- emulator -avd test -no-skin -no-audio -no-window &
|
||||
- android-wait-for-emulator
|
||||
- adb shell input keyevent 82 &
|
||||
|
||||
script: ./gradlew --info build connectedCheck
|
||||
|
||||
BIN
CCC_NewPipe_presentation.pdf
Normal file
BIN
CCC_NewPipe_presentation.pdf
Normal file
Binary file not shown.
@@ -1,33 +0,0 @@
|
||||
#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](https://newpipe.slack.com/) is being used for project communication. Ask [me](https://github.com/theScrabi) to sign up.
|
||||
* Feel free to post suggestions, changes, ideas etc!
|
||||
15
README.md
15
README.md
@@ -9,7 +9,7 @@ NewPipe: A free lightweight Youtube frontend for Android.
|
||||
|
||||
Project status:
|
||||
[](https://hosted.weblate.org/engage/NewPipe/)
|
||||
[](https://travis-ci.org/theScrabi/NewPipe)
|
||||
[](https://travis-ci.org/TeamNewPipe/NewPipe)
|
||||
|
||||
## Donate
|
||||

|
||||
@@ -37,24 +37,27 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
* Listen to YouTube videos (experimental)
|
||||
* Select the streaming player to watch the video with
|
||||
* Download videos
|
||||
* Download audio only
|
||||
* Download audio only
|
||||
* Open a video in Kodi
|
||||
* Show Next/Related videos
|
||||
* Search YouTube in a specific language
|
||||
* Watch age restricted material
|
||||
* Display general information about channels
|
||||
* Search channels
|
||||
* Watch videos from a channel
|
||||
* Orbot/Tor support (not yet directly)
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Orbot/Tor support
|
||||
* Bookmarks
|
||||
* View history
|
||||
* Search history
|
||||
* Search channels
|
||||
* Subscribe to channels
|
||||
* Watch videos from a channel
|
||||
* Search/Watch Playlists
|
||||
* Queeing videos
|
||||
* Subtitles support
|
||||
* 1080p support
|
||||
* livestream support
|
||||
* ... and many more
|
||||
|
||||
### Multiservice support
|
||||
@@ -64,7 +67,7 @@ Although NewPipe only supports YouTube at the moment, it's designed to support m
|
||||
Whether you have ideas, translation, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
The more is done the better it gets!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](CONTRIBUTING.md).
|
||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion '23.0.3'
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion '25.0.0'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 24
|
||||
versionCode 19
|
||||
versionName "0.8.5"
|
||||
targetSdkVersion 25
|
||||
versionCode 28
|
||||
versionName "0.9.1"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -32,19 +32,21 @@ android {
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'com.android.support:appcompat-v7:24.2.1'
|
||||
compile 'com.android.support:support-v4:24.2.1'
|
||||
compile 'com.android.support:design:24.2.1'
|
||||
compile 'com.android.support:recyclerview-v7:24.2.1'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile 'org.json:json:20160810'
|
||||
|
||||
compile 'com.android.support:appcompat-v7:25.1.0'
|
||||
compile 'com.android.support:support-v4:25.1.0'
|
||||
compile 'com.android.support:design:25.1.0'
|
||||
compile 'com.android.support:recyclerview-v7:25.1.0'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r1.5.5'
|
||||
compile 'com.google.code.gson:gson:2.4'
|
||||
compile 'com.nononsenseapps:filepicker:3.0.0'
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'ch.acra:acra:4.9.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r2.3.1'
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.youtube;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.SearchResult;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger 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 SearchResult result;
|
||||
private List<String> suggestionReply;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
SearchEngine engine = ServiceList.getService("Youtube")
|
||||
.getSearchEngineInstance(new Downloader());
|
||||
|
||||
result = engine.search("this is something boring",
|
||||
0, "de", new Downloader()).getSearchResult();
|
||||
suggestionReply = engine.suggestionList("hello", "de", new Downloader());
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.youtube;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 11.03.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeStreamExtractorLiveStreamTest.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
public class YoutubeStreamExtractorLiveStreamTest extends AndroidTestCase {
|
||||
|
||||
private StreamExtractor extractor;
|
||||
|
||||
public void setUp() throws IOException, ExtractionException {
|
||||
//todo: make the extractor not throw over a livestream
|
||||
/*
|
||||
extractor = ServiceList.getService("Youtube")
|
||||
.getExtractorInstance("https://www.youtube.com/watch?v=J0s6NjqdjLE", new Downloader());
|
||||
*/
|
||||
}
|
||||
|
||||
public void testStreamType() throws ParsingException {
|
||||
assertTrue(true);
|
||||
// assertTrue(extractor.getStreamType() == AbstractVideoInfo.StreamType.LIVE_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -16,7 +17,6 @@
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="AllowBackup">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
@@ -29,11 +29,83 @@
|
||||
<activity
|
||||
android:name=".detail.VideoItemDetailActivity"
|
||||
android:label="@string/title_videoitem_detail"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".player.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"
|
||||
android:label="@string/background_player_name" />
|
||||
|
||||
<activity
|
||||
android:name=".player.ExoPlayerActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/PlayerTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings_activity_title" />
|
||||
<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:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".report.ErrorActivity" />
|
||||
|
||||
<!-- giga get related -->
|
||||
<activity
|
||||
android:name=".download.DownloadActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<service android:name="us.shandian.giga.service.DownloadManagerService" />
|
||||
|
||||
<activity
|
||||
android:name="com.nononsenseapps.filepicker.FilePickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/FilePickerTheme"/>
|
||||
<activity
|
||||
android:name=".ChannelActivity"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/reCaptchaActivity" />
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<activity android:name=".RouterActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
@@ -47,9 +119,14 @@
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -75,83 +152,75 @@
|
||||
<data android:scheme="vnd.youtube" />
|
||||
<data android:scheme="vnd.youtube.launch" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".player.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"
|
||||
android:label="@string/background_player_name" />
|
||||
|
||||
<activity
|
||||
android:name=".player.ExoPlayerActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<intent-filter>
|
||||
<action android:name="org.schabi.newpipe.exoplayer.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".PopupActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/popup_mode_share_menu_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="asset" />
|
||||
<data android:scheme="file" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"
|
||||
android:label="@string/background_player_name" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings_activity_title" />
|
||||
<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" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="vnd.youtube" />
|
||||
<data android:scheme="vnd.youtube.launch" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".report.ErrorActivity" />
|
||||
|
||||
<!-- giga get related -->
|
||||
<activity
|
||||
android:name=".download.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<service android:name="us.shandian.giga.service.DownloadManagerService" />
|
||||
|
||||
<activity
|
||||
android:name="com.nononsenseapps.filepicker.FilePickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ChannelActivity"
|
||||
android:label="@string/title_activity_channel"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<service android:name=".player.PopupVideoPlayer"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.acra.config.ACRAConfiguration;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.ConfigurationBuilder;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
@@ -47,6 +48,7 @@ public class App extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// init crashreport
|
||||
try {
|
||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||
@@ -60,6 +62,9 @@ public class App extends Application {
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
//init NewPipe
|
||||
NewPipe.init(Downloader.getInstance());
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
|
||||
ImageLoader.getInstance().init(config);
|
||||
|
||||
@@ -4,34 +4,40 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.design.widget.CollapsingToolbarLayout;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.extractor.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelActivity.java is part of NewPipe.
|
||||
@@ -51,15 +57,9 @@ import java.io.IOException;
|
||||
*/
|
||||
|
||||
public class ChannelActivity extends AppCompatActivity {
|
||||
|
||||
|
||||
private static final String TAG = ChannelActivity.class.toString();
|
||||
private View rootView = null;
|
||||
|
||||
// intent const
|
||||
public static final String CHANNEL_URL = "channel_url";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
|
||||
private int serviceId = -1;
|
||||
private String channelUrl = "";
|
||||
private int pageNumber = 0;
|
||||
@@ -69,30 +69,43 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private InfoListAdapter infoListAdapter = null;
|
||||
|
||||
private String subS = "";
|
||||
|
||||
ProgressBar progressBar = null;
|
||||
ImageView channelBanner = null;
|
||||
ImageView avatarView = null;
|
||||
TextView titleView = null;
|
||||
TextView subscirberView = null;
|
||||
Button subscriberButton = null;
|
||||
View subscriberLayout = null;
|
||||
|
||||
View header = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_channel);
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
rootView = findViewById(R.id.rootView);
|
||||
setSupportActionBar(toolbar);
|
||||
Intent i = getIntent();
|
||||
channelUrl = i.getStringExtra(CHANNEL_URL);
|
||||
serviceId = i.getIntExtra(SERVICE_ID, -1);
|
||||
rootView = findViewById(android.R.id.content);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(this, rootView);
|
||||
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.channel_streams_view);
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
header = getLayoutInflater().inflate(R.layout.channel_header, recyclerView, false);
|
||||
infoListAdapter.setHeader(header);
|
||||
infoListAdapter.setFooter(
|
||||
getLayoutInflater().inflate(R.layout.pignate_footer, recyclerView, false));
|
||||
recyclerView.setAdapter(infoListAdapter);
|
||||
infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(
|
||||
new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url) {
|
||||
Intent detailIntent = new Intent(ChannelActivity.this, VideoItemDetailActivity.class);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
|
||||
detailIntent.putExtra(
|
||||
VideoItemDetailFragment.STREAMING_SERVICE, serviceId);
|
||||
startActivity(detailIntent);
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openDetailActivity(ChannelActivity.this, url, serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,39 +132,80 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
subS = getString(R.string.subscriber);
|
||||
|
||||
progressBar = (ProgressBar) findViewById(R.id.progressBar);
|
||||
channelBanner = (ImageView) header.findViewById(R.id.channel_banner_image);
|
||||
avatarView = (ImageView) header.findViewById(R.id.channel_avatar_view);
|
||||
titleView = (TextView) header.findViewById(R.id.channel_title_view);
|
||||
subscirberView = (TextView) header.findViewById(R.id.channel_subscriber_view);
|
||||
subscriberButton = (Button) header.findViewById(R.id.channel_subscribe_button);
|
||||
subscriberLayout = header.findViewById(R.id.channel_subscriber_layout);
|
||||
|
||||
if(savedInstanceState == null) {
|
||||
handleIntent(getIntent());
|
||||
} else {
|
||||
channelUrl = savedInstanceState.getString(NavStack.URL);
|
||||
serviceId = savedInstanceState.getInt(NavStack.SERVICE_ID);
|
||||
NavStack.getInstance()
|
||||
.restoreSavedInstanceState(savedInstanceState);
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent i) {
|
||||
channelUrl = i.getStringExtra(NavStack.URL);
|
||||
serviceId = i.getIntExtra(NavStack.SERVICE_ID, -1);
|
||||
requestData(false);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(NavStack.URL, channelUrl);
|
||||
outState.putInt(NavStack.SERVICE_ID, serviceId);
|
||||
NavStack.getInstance()
|
||||
.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void updateUi(final ChannelInfo info) {
|
||||
CollapsingToolbarLayout ctl = (CollapsingToolbarLayout) findViewById(R.id.channel_toolbar_layout);
|
||||
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
|
||||
ImageView channelBanner = (ImageView) findViewById(R.id.channel_banner_image);
|
||||
FloatingActionButton feedButton = (FloatingActionButton) findViewById(R.id.channel_rss_fab);
|
||||
ImageView avatarView = (ImageView) findViewById(R.id.channel_avatar_view);
|
||||
ImageView haloView = (ImageView) findViewById(R.id.channel_avatar_halo);
|
||||
|
||||
findViewById(R.id.channel_header_layout).setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
|
||||
if(info.channel_name != null && !info.channel_name.isEmpty()) {
|
||||
ctl.setTitle(info.channel_name);
|
||||
getSupportActionBar().setTitle(info.channel_name);
|
||||
titleView.setText(info.channel_name);
|
||||
}
|
||||
|
||||
if(info.banner_url != null && !info.banner_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.banner_url, channelBanner,
|
||||
new ImageErrorLoadingListener(this, rootView ,info.service_id));
|
||||
new ImageErrorLoadingListener(this, rootView ,info.service_id));
|
||||
}
|
||||
|
||||
if(info.avatar_url != null && !info.avatar_url.isEmpty()) {
|
||||
avatarView.setVisibility(View.VISIBLE);
|
||||
haloView.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(info.avatar_url, avatarView,
|
||||
new ImageErrorLoadingListener(this, rootView ,info.service_id));
|
||||
}
|
||||
|
||||
if(info.subscriberCount != -1) {
|
||||
subscirberView.setText(buildSubscriberString(info.subscriberCount));
|
||||
}
|
||||
|
||||
if((info.feed_url != null && !info.feed_url.isEmpty()) ||
|
||||
(info.subscriberCount != -1)) {
|
||||
subscriberLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if(info.feed_url != null && !info.feed_url.isEmpty()) {
|
||||
feedButton.setOnClickListener(new View.OnClickListener() {
|
||||
subscriberButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.d(TAG, info.feed_url);
|
||||
@@ -160,12 +214,13 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
feedButton.setVisibility(View.GONE);
|
||||
subscriberButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void addVideos(final ChannelInfo info) {
|
||||
infoListAdapter.addStreamItemList(info.related_streams);
|
||||
infoListAdapter.addInfoItemList(info.related_streams);
|
||||
}
|
||||
|
||||
private void postNewErrorToast(Handler h, final int stringResource) {
|
||||
@@ -181,6 +236,22 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
private void requestData(final boolean onlyVideos) {
|
||||
// start processing
|
||||
isLoading = true;
|
||||
|
||||
if(!onlyVideos) {
|
||||
//delete already displayed content
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
infoListAdapter.clearSteamItemList();
|
||||
pageNumber = 0;
|
||||
subscriberLayout.setVisibility(View.GONE);
|
||||
titleView.setText("");
|
||||
getSupportActionBar().setTitle("");
|
||||
if (SDK_INT >= 21) {
|
||||
channelBanner.setImageDrawable(getDrawable(R.drawable.channel_banner));
|
||||
avatarView.setImageDrawable(getDrawable(R.drawable.buddy));
|
||||
}
|
||||
infoListAdapter.showFooter(false);
|
||||
}
|
||||
|
||||
Thread channelExtractorThread = new Thread(new Runnable() {
|
||||
Handler h = new Handler();
|
||||
|
||||
@@ -188,11 +259,11 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
public void run() {
|
||||
StreamingService service = null;
|
||||
try {
|
||||
service = ServiceList.getService(serviceId);
|
||||
service = NewPipe.getService(serviceId);
|
||||
ChannelExtractor extractor = service.getChannelExtractorInstance(
|
||||
channelUrl, pageNumber, new Downloader());
|
||||
channelUrl, pageNumber);
|
||||
|
||||
final ChannelInfo info = ChannelInfo.getInfo(extractor, new Downloader());
|
||||
final ChannelInfo info = ChannelInfo.getInfo(extractor);
|
||||
|
||||
|
||||
h.post(new Runnable() {
|
||||
@@ -201,8 +272,12 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
isLoading = false;
|
||||
if(!onlyVideos) {
|
||||
updateUi(info);
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
hasNextPage = info.hasNextPage;
|
||||
if(!hasNextPage) {
|
||||
infoListAdapter.showFooter(false);
|
||||
}
|
||||
addVideos(info);
|
||||
}
|
||||
});
|
||||
@@ -226,7 +301,7 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
ioe.printStackTrace();
|
||||
} catch(ParsingException pe) {
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, pe, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, pe, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
service.getServiceInfo().name, channelUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@@ -237,9 +312,13 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
});
|
||||
pe.printStackTrace();
|
||||
} catch(ExtractionException ex) {
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailFragment.class, null,
|
||||
String name = "none";
|
||||
if(service != null) {
|
||||
name = service.getServiceInfo().name;
|
||||
}
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
service.getServiceInfo().name, channelUrl, R.string.parsing_error));
|
||||
name, channelUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -248,7 +327,7 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
});
|
||||
ex.printStackTrace();
|
||||
} catch(Exception e) {
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.reportError(h, ChannelActivity.this, e, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL,
|
||||
service.getServiceInfo().name, channelUrl, R.string.general_error));
|
||||
h.post(new Runnable() {
|
||||
@@ -265,4 +344,64 @@ public class ChannelActivity extends AppCompatActivity {
|
||||
channelExtractorThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
try {
|
||||
NavStack.getInstance()
|
||||
.navBack(this);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu_channel, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch(item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(channelUrl));
|
||||
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
|
||||
}
|
||||
case R.id.menu_item_share:
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
case android.R.id.home:
|
||||
NavStack.getInstance().openMainActivity(this);
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSubscriberString(long count) {
|
||||
String out = "";
|
||||
if(count >= 1000000000){
|
||||
out += Long.toString((count/1000000000)%1000)+".";
|
||||
}
|
||||
if(count>=1000000){
|
||||
out += Long.toString((count/1000000)%1000) + ".";
|
||||
}
|
||||
if(count>=1000){
|
||||
out += Long.toString((count/1000)%1000)+".";
|
||||
}
|
||||
out += Long.toString(count%1000) + " " + subS;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -35,13 +37,37 @@ import javax.net.ssl.HttpsURLConnection;
|
||||
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 String mCookies = "";
|
||||
|
||||
private static Downloader instance = null;
|
||||
|
||||
private Downloader() {}
|
||||
|
||||
public static Downloader getInstance() {
|
||||
if(instance == null) {
|
||||
synchronized (Downloader.class) {
|
||||
if (instance == null) {
|
||||
instance = new Downloader();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static synchronized void setCookies(String cookies) {
|
||||
Downloader.mCookies = cookies;
|
||||
}
|
||||
|
||||
public static synchronized String getCookies() {
|
||||
return Downloader.mCookies;
|
||||
}
|
||||
|
||||
/**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 {
|
||||
public String download(String siteUrl, String language) throws IOException, ReCaptchaException {
|
||||
Map<String, String> requestProperties = new HashMap<>();
|
||||
requestProperties.put("Accept-Language", language);
|
||||
return download(siteUrl, requestProperties);
|
||||
@@ -54,7 +80,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
* @param customProperties set request header properties
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException*/
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException {
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
Iterator it = customProperties.entrySet().iterator();
|
||||
@@ -66,7 +92,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
}
|
||||
|
||||
/**Common functionality between download(String url) and download(String url, String language)*/
|
||||
private static String dl(HttpsURLConnection con) throws IOException {
|
||||
private static String dl(HttpsURLConnection con) throws IOException, ReCaptchaException {
|
||||
StringBuilder response = new StringBuilder();
|
||||
BufferedReader in = null;
|
||||
|
||||
@@ -74,6 +100,10 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
con.setRequestMethod("GET");
|
||||
con.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
if (getCookies().length() > 0) {
|
||||
con.setRequestProperty("Cookie", getCookies());
|
||||
}
|
||||
|
||||
in = new BufferedReader(
|
||||
new InputStreamReader(con.getInputStream()));
|
||||
String inputLine;
|
||||
@@ -85,6 +115,14 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
throw new IOException("unknown host or no network", uhe);
|
||||
//Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show();
|
||||
} catch(Exception e) {
|
||||
/*
|
||||
* HTTP 429 == Too Many Request
|
||||
* Receive from Youtube.com = ReCaptcha challenge request
|
||||
* See : https://github.com/rg3/youtube-dl/issues/5138
|
||||
*/
|
||||
if (con.getResponseCode() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||
}
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
if(in != null) {
|
||||
@@ -99,7 +137,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
* 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 {
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
@@ -50,7 +50,7 @@ public class ImageErrorLoadingListener implements ImageLoadingListener {
|
||||
ErrorActivity.reportError(activity,
|
||||
failReason.getCause(), null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE,
|
||||
ServiceList.getNameOfService(serviceId), imageUri,
|
||||
NewPipe.getNameOfService(serviceId), imageUri,
|
||||
R.string.could_not_load_image));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,23 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* MainActivity.java is part of NewPipe.
|
||||
* DownloadActivity.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
|
||||
@@ -32,27 +34,26 @@ import org.schabi.newpipe.settings.SettingsActivity;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private Fragment mainFragment = null;
|
||||
private static final String TAG = MainActivity.class.toString();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
mainFragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.search_fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
|
||||
inflater.inflate(R.menu.main_menu, menu);
|
||||
|
||||
mainFragment.onCreateOptionsMenu(menu, inflater);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
||||
switch(id) {
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
@@ -73,13 +74,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
return true;
|
||||
}
|
||||
case R.id.action_show_downloads: {
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.download.MainActivity.class);
|
||||
if (!PermissionHelper.checkStoragePermissions(this)) {
|
||||
return false;
|
||||
}
|
||||
Intent intent = new Intent(this, DownloadActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return mainFragment.onOptionsItemSelected(item) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
app/src/main/java/org/schabi/newpipe/PopupActivity.java
Normal file
136
app/src/main/java/org/schabi/newpipe/PopupActivity.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* This activity is thought to open video streams form an external app using the popup playser.
|
||||
*/
|
||||
|
||||
public class PopupActivity extends Activity {
|
||||
private static final String TAG = RouterActivity.class.toString();
|
||||
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
handleIntent(getIntent());
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private static String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.substring(start, input.length());
|
||||
}
|
||||
|
||||
private static String trim(final String input) {
|
||||
if (input == null || input.length() < 1) {
|
||||
return input;
|
||||
} else {
|
||||
String output = input;
|
||||
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(1);
|
||||
}
|
||||
while (output.length() > 0
|
||||
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(0, output.length() - 1);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all Strings which look remotely like URLs from a text.
|
||||
* Used if NewPipe was called through share menu.
|
||||
*
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
private String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
for (String s : array) {
|
||||
s = trim(s);
|
||||
if (s.length() != 0) {
|
||||
if (s.matches(".+://.+")) {
|
||||
result.add(removeHeadingGibberish(s));
|
||||
} else if (s.matches(".+\\..+")) {
|
||||
result.add("http://" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !PermissionHelper.checkSystemAlertWindowPermission(this)) {
|
||||
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
String videoUrl = "";
|
||||
StreamingService service = null;
|
||||
|
||||
// first gather data and find service
|
||||
if (intent.getData() != null) {
|
||||
// this means the video was called though another app
|
||||
videoUrl = intent.getData().toString();
|
||||
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
}
|
||||
|
||||
service = NewPipe.getServiceByUrl(videoUrl);
|
||||
if (service == null) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
} else {
|
||||
Intent callIntent = new Intent();
|
||||
switch (service.getLinkTypeByUrl(videoUrl)) {
|
||||
case STREAM:
|
||||
callIntent.setClass(this, PopupVideoPlayer.class);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "NOT YET DEFINED");
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
callIntent.putExtra(NavStack.URL, videoUrl);
|
||||
callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId());
|
||||
startService(callIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java
Normal file
151
app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java
Normal file
@@ -0,0 +1,151 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
/**
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ReCaptchaActivity.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 ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final int RECAPTCHA_REQUEST = 10;
|
||||
|
||||
public static final String TAG = ReCaptchaActivity.class.toString();
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
|
||||
// Set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.reCaptcha_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
|
||||
WebView myWebView = (WebView) findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// Enable Javascript
|
||||
WebSettings webSettings = myWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
|
||||
ReCaptchaWebViewClient webClient = new ReCaptchaWebViewClient(this);
|
||||
myWebView.setWebViewClient(webClient);
|
||||
|
||||
// Cleaning cache, history and cookies from webView
|
||||
myWebView.clearCache(true);
|
||||
myWebView.clearHistory();
|
||||
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean aBoolean) {}
|
||||
});
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
|
||||
myWebView.loadUrl(YT_URL);
|
||||
}
|
||||
|
||||
private class ReCaptchaWebViewClient extends WebViewClient {
|
||||
private Activity context;
|
||||
private String mCookies;
|
||||
|
||||
ReCaptchaWebViewClient(Activity ctx) {
|
||||
context = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
// TODO: Start Loader
|
||||
super.onPageStarted(view, url, favicon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
String cookies = CookieManager.getInstance().getCookie(url);
|
||||
|
||||
// TODO: Stop Loader
|
||||
|
||||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
||||
if (find_access_cookies(cookies)) {
|
||||
// Give cookies to Downloader class
|
||||
Downloader.setCookies(mCookies);
|
||||
|
||||
// Closing activity and return to parent
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean find_access_cookies(String cookies) {
|
||||
boolean ret = false;
|
||||
String c_s_gl = "";
|
||||
String c_goojf = "";
|
||||
|
||||
String[] parts = cookies.split("; ");
|
||||
for (String part : parts) {
|
||||
if (part.trim().startsWith("s_gl")) {
|
||||
c_s_gl = part.trim();
|
||||
}
|
||||
if (part.trim().startsWith("goojf")) {
|
||||
c_goojf = part.trim();
|
||||
}
|
||||
}
|
||||
if (c_s_gl.length() > 0 && c_goojf.length() > 0) {
|
||||
ret = true;
|
||||
//mCookies = c_s_gl + "; " + c_goojf;
|
||||
// Youtube seems to also need the other cookies:
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
156
app/src/main/java/org/schabi/newpipe/RouterActivity.java
Normal file
156
app/src/main/java/org/schabi/newpipe/RouterActivity.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* RouterActivity .java is part of NewPipe.
|
||||
*
|
||||
* OpenHitboxStreams is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* OpenHitboxStreams is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with OpenHitboxStreams. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This Acitivty is designed to route share/open intents to the specified service, and
|
||||
* to the part of the service which can handle the url.
|
||||
*/
|
||||
public class RouterActivity extends Activity {
|
||||
private static final String TAG = RouterActivity.class.toString();
|
||||
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
handleIntent(getIntent());
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private static String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.substring(start, input.length());
|
||||
}
|
||||
|
||||
private static String trim(final String input) {
|
||||
if (input == null || input.length() < 1) {
|
||||
return input;
|
||||
} else {
|
||||
String output = input;
|
||||
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(1);
|
||||
}
|
||||
while (output.length() > 0
|
||||
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(0, output.length() - 1);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all Strings which look remotely like URLs from a text.
|
||||
* Used if NewPipe was called through share menu.
|
||||
*
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
private String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
for (String s : array) {
|
||||
s = trim(s);
|
||||
if (s.length() != 0) {
|
||||
if (s.matches(".+://.+")) {
|
||||
result.add(removeHeadingGibberish(s));
|
||||
} else if (s.matches(".+\\..+")) {
|
||||
result.add("http://" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
String videoUrl = "";
|
||||
StreamingService service = null;
|
||||
|
||||
// first gather data and find service
|
||||
if (intent.getData() != null) {
|
||||
// this means the video was called though another app
|
||||
videoUrl = intent.getData().toString();
|
||||
} else if(intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
}
|
||||
|
||||
service = NewPipe.getServiceByUrl(videoUrl);
|
||||
if(service == null) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
} else {
|
||||
Intent callIntent = new Intent();
|
||||
switch(service.getLinkTypeByUrl(videoUrl)) {
|
||||
case CHANNEL:
|
||||
callIntent.setClass(this, ChannelActivity.class);
|
||||
break;
|
||||
case STREAM:
|
||||
callIntent.setClass(this, VideoItemDetailActivity.class);
|
||||
callIntent.putExtra(VideoItemDetailActivity.AUTO_PLAY,
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(
|
||||
getString(R.string.autoplay_through_intent_key), false));
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "NOT YET DEFINED");
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
callIntent.putExtra(NavStack.URL, videoUrl);
|
||||
callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId());
|
||||
startActivity(callIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/src/main/java/org/schabi/newpipe/ThemableActivity.java
Normal file
28
app/src/main/java/org/schabi/newpipe/ThemableActivity.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
public class ThemableActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString("theme", getResources().getString(R.string.light_theme_title)).
|
||||
equals(getResources().getString(R.string.dark_theme_title))) {
|
||||
setTheme(R.style.DarkTheme);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString("theme", getResources().getString(R.string.light_theme_title)).
|
||||
equals(getResources().getString(R.string.dark_theme_title))) {
|
||||
setTheme(R.style.DarkTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import android.widget.ArrayAdapter;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.VideoStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -53,6 +53,7 @@ class ActionBarHandler {
|
||||
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
|
||||
private OnActionListener onShareListener;
|
||||
private OnActionListener onOpenInBrowserListener;
|
||||
private OnActionListener onOpenInPopupListener;
|
||||
private OnActionListener onDownloadListener;
|
||||
private OnActionListener onPlayWithKodiListener;
|
||||
private OnActionListener onPlayAudioListener;
|
||||
@@ -111,19 +112,39 @@ class ActionBarHandler {
|
||||
|
||||
|
||||
private int getDefaultResolution(final List<VideoStream> videoStreams) {
|
||||
if (defaultPreferences == null)
|
||||
return 0;
|
||||
|
||||
String defaultResolution = defaultPreferences
|
||||
.getString(activity.getString(R.string.default_resolution_key),
|
||||
activity.getString(R.string.default_resolution_value));
|
||||
|
||||
String preferedFormat = defaultPreferences
|
||||
.getString(activity.getString(R.string.preferred_video_format_key),
|
||||
activity.getString(R.string.preferred_video_format_default));
|
||||
|
||||
// first try to find the one with the right resolution
|
||||
int selectedFormat = 0;
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (defaultResolution.equals(item.resolution)) {
|
||||
return i;
|
||||
selectedFormat = i;
|
||||
}
|
||||
}
|
||||
|
||||
// than try to find the one with the right resolution and format
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoStream item = videoStreams.get(i);
|
||||
if (defaultResolution.equals(item.resolution)
|
||||
&& preferedFormat.equals(MediaFormat.getNameById(item.format))) {
|
||||
selectedFormat = i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// this is actually an error,
|
||||
// but maybe there is really no stream fitting to the default value.
|
||||
return 0;
|
||||
return selectedFormat;
|
||||
}
|
||||
|
||||
public void setupMenu(Menu menu, MenuInflater inflater) {
|
||||
@@ -183,10 +204,16 @@ class ActionBarHandler {
|
||||
return true;
|
||||
case R.id.menu_item_downloads: {
|
||||
Intent intent =
|
||||
new Intent(activity, org.schabi.newpipe.download.MainActivity.class);
|
||||
new Intent(activity, org.schabi.newpipe.download.DownloadActivity.class);
|
||||
activity.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_popup: {
|
||||
if(onOpenInPopupListener != null) {
|
||||
onOpenInPopupListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
}
|
||||
@@ -205,6 +232,10 @@ class ActionBarHandler {
|
||||
onOpenInBrowserListener = listener;
|
||||
}
|
||||
|
||||
public void setOnOpenInPopupListener(OnActionListener listener) {
|
||||
onOpenInPopupListener = listener;
|
||||
}
|
||||
|
||||
public void setOnDownloadListener(OnActionListener listener) {
|
||||
onDownloadListener = listener;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Extract {@link StreamInfo} with {@link StreamExtractor} from the given url of the given service
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StreamExtractorWorker extends Thread {
|
||||
private static final String TAG = "StreamExtractorWorker";
|
||||
|
||||
private Activity activity;
|
||||
private final String videoUrl;
|
||||
private final int serviceId;
|
||||
private OnStreamInfoReceivedListener callback;
|
||||
|
||||
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
|
||||
public interface OnStreamInfoReceivedListener {
|
||||
void onReceive(StreamInfo info);
|
||||
void onError(int messageId);
|
||||
void onReCaptchaException();
|
||||
void onBlockedByGemaError();
|
||||
void onContentErrorWithMessage(int messageId);
|
||||
void onContentError();
|
||||
}
|
||||
|
||||
public StreamExtractorWorker(Activity activity, String videoUrl, int serviceId, OnStreamInfoReceivedListener callback) {
|
||||
this.serviceId = serviceId;
|
||||
this.videoUrl = videoUrl;
|
||||
this.activity = activity;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance <b>already</b> started of {@link StreamExtractorWorker}.<br>
|
||||
* The caller is responsible to check if {@link StreamExtractorWorker#isRunning()}, or {@link StreamExtractorWorker#cancel()} it
|
||||
*
|
||||
* @param serviceId id of the request service
|
||||
* @param url videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k)
|
||||
* @param activity activity for error reporting purposes
|
||||
* @param callback listener that will be called-back when events occur (check {@link OnStreamInfoReceivedListener})
|
||||
* @return new instance already started of {@link StreamExtractorWorker}
|
||||
*/
|
||||
public static StreamExtractorWorker startExtractorThread(int serviceId, String url, Activity activity, OnStreamInfoReceivedListener callback) {
|
||||
StreamExtractorWorker extractorThread = getExtractorThread(serviceId, url, activity, callback);
|
||||
extractorThread.start();
|
||||
return extractorThread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of {@link StreamExtractorWorker}.<br>
|
||||
* The caller is responsible to check if {@link StreamExtractorWorker#isRunning()}, or {@link StreamExtractorWorker#cancel()}
|
||||
* when it doesn't need it anymore
|
||||
* <p>
|
||||
* <b>Note:</b> this instance is <b>not</b> started yet
|
||||
*
|
||||
* @param serviceId id of the request service
|
||||
* @param url videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k)
|
||||
* @param activity activity for error reporting purposes
|
||||
* @param callback listener that will be called-back when events occur (check {@link OnStreamInfoReceivedListener})
|
||||
* @return instance of {@link StreamExtractorWorker}
|
||||
*/
|
||||
public static StreamExtractorWorker getExtractorThread(int serviceId, String url, Activity activity, OnStreamInfoReceivedListener callback) {
|
||||
return new StreamExtractorWorker(activity, url, serviceId, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
//Just ignore the errors for now
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public void run() {
|
||||
// TODO: Improve error checking
|
||||
// and this method in general
|
||||
|
||||
StreamInfo streamInfo = null;
|
||||
StreamingService service;
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
"", videoUrl, R.string.could_not_get_stream));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
isRunning.set(true);
|
||||
StreamExtractor streamExtractor = service.getExtractorInstance(videoUrl);
|
||||
streamInfo = StreamInfo.getVideoInfo(streamExtractor);
|
||||
|
||||
final StreamInfo info = streamInfo;
|
||||
if (callback != null) handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onReceive(info);
|
||||
}
|
||||
});
|
||||
isRunning.set(false);
|
||||
// look for errors during extraction
|
||||
// this if statement only covers extra information.
|
||||
// if these are not available or caused an error, they are just not available
|
||||
// but don't render the stream information unusalbe.
|
||||
if (streamInfo != null && !streamInfo.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for (Throwable e : streamInfo.errors) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
View rootView = activity != null ? activity.findViewById(R.id.video_item_detail) : null;
|
||||
ErrorActivity.reportError(handler, activity,
|
||||
streamInfo.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, 0 /* no message for the user */));
|
||||
}
|
||||
|
||||
// These errors render the stream information unusable.
|
||||
} catch (ReCaptchaException e) {
|
||||
if (callback != null) handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onReCaptchaException();
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
if (callback != null) handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
if (callback != null) e.printStackTrace();
|
||||
} catch (YoutubeStreamExtractor.DecryptException de) {
|
||||
// custom service related exceptions
|
||||
ErrorActivity.reportError(handler, activity, de, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.youtube_signature_decryption_error));
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
activity.finish();
|
||||
}
|
||||
});
|
||||
de.printStackTrace();
|
||||
} catch (YoutubeStreamExtractor.GemaException ge) {
|
||||
if (callback != null) handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onBlockedByGemaError();
|
||||
}
|
||||
});
|
||||
} catch (YoutubeStreamExtractor.LiveStreamException e) {
|
||||
if (callback != null) handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onContentErrorWithMessage(R.string.live_streams_not_supported);
|
||||
}
|
||||
});
|
||||
}
|
||||
// ----------------------------------------
|
||||
catch (StreamExtractor.ContentNotAvailableException e) {
|
||||
if (callback != null) handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onContentError();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (StreamInfo.StreamExctractException e) {
|
||||
if (!streamInfo.errors.isEmpty()) {
|
||||
// !!! if this case ever kicks in someone gets kicked out !!!
|
||||
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
} else {
|
||||
ErrorActivity.reportError(handler, activity, streamInfo.errors, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
}
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
activity.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (ParsingException e) {
|
||||
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.parsing_error));
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
activity.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.general_error));
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
activity.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the extraction is not completed yet
|
||||
*
|
||||
* @return the value of the AtomicBoolean {@link #isRunning}
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return isRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel this ExtractorThread, setting the callback to null, the AtomicBoolean {@link #isRunning} to false and interrupt this thread.
|
||||
* <p>
|
||||
* <b>Note:</b> Any I/O that is active in the moment that this method is called will be canceled and a Exception will be thrown, because of the {@link #interrupt()}.<br>
|
||||
* This is useful when you don't want the resulting {@link StreamInfo} anymore, but don't want to waste bandwidth, otherwise it'd run till it receives the StreamInfo.
|
||||
*/
|
||||
public void cancel() {
|
||||
this.callback = null;
|
||||
this.isRunning.set(false);
|
||||
this.interrupt();
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 02.08.16.
|
||||
*/
|
||||
|
||||
public class StreamInfoWorker {
|
||||
|
||||
private static final String TAG = StreamInfoWorker.class.toString();
|
||||
|
||||
public interface OnStreamInfoReceivedListener {
|
||||
void onReceive(StreamInfo info);
|
||||
void onError(int messageId);
|
||||
void onBlockedByGemaError();
|
||||
void onContentErrorWithMessage(int messageId);
|
||||
void onContentError();
|
||||
}
|
||||
|
||||
private class StreamExtractorRunnable implements Runnable {
|
||||
private final Handler h = new Handler();
|
||||
private StreamExtractor streamExtractor;
|
||||
private final int serviceId;
|
||||
private final String videoUrl;
|
||||
private Activity a;
|
||||
|
||||
public StreamExtractorRunnable(Activity a, String videoUrl, int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
this.videoUrl = videoUrl;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
StreamInfo streamInfo = null;
|
||||
StreamingService service = null;
|
||||
try {
|
||||
service = ServiceList.getService(serviceId);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
"", videoUrl, R.string.could_not_get_stream));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
streamExtractor = service.getExtractorInstance(videoUrl, new Downloader());
|
||||
streamInfo = StreamInfo.getVideoInfo(streamExtractor, new Downloader());
|
||||
|
||||
final StreamInfo info = streamInfo;
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onReceive(info);
|
||||
}
|
||||
});
|
||||
|
||||
// look for errors during extraction
|
||||
// this if statement only covers extra information.
|
||||
// if these are not available or caused an error, they are just not available
|
||||
// but don't render the stream information unusalbe.
|
||||
if(streamInfo != null &&
|
||||
!streamInfo.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for (Throwable e : streamInfo.errors) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
View rootView = a != null ? a.findViewById(R.id.video_item_detail) : null;
|
||||
ErrorActivity.reportError(h, a,
|
||||
streamInfo.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, 0 /* no message for the user */));
|
||||
}
|
||||
|
||||
// These errors render the stream information unusable.
|
||||
} catch (IOException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onError(R.string.network_error);
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
// custom service related exceptions
|
||||
catch (YoutubeStreamExtractor.DecryptException de) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onError(R.string.youtube_signature_decryption_error);
|
||||
}
|
||||
});
|
||||
de.printStackTrace();
|
||||
} catch (YoutubeStreamExtractor.GemaException ge) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onBlockedByGemaError();
|
||||
}
|
||||
});
|
||||
} catch(YoutubeStreamExtractor.LiveStreamException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener
|
||||
.onContentErrorWithMessage(R.string.live_streams_not_supported);
|
||||
}
|
||||
});
|
||||
}
|
||||
// ----------------------------------------
|
||||
catch(StreamExtractor.ContentNotAvailableException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener
|
||||
.onContentError();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(StreamInfo.StreamExctractException e) {
|
||||
if(!streamInfo.errors.isEmpty()) {
|
||||
// !!! if this case ever kicks in someone gets kicked out !!!
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
} else {
|
||||
ErrorActivity.reportError(h, a, streamInfo.errors, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
}
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (ParsingException e) {
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(Exception e) {
|
||||
ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.general_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
a.finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static StreamInfoWorker streamInfoWorker = null;
|
||||
private StreamExtractorRunnable runnable = null;
|
||||
private OnStreamInfoReceivedListener onStreamInfoReceivedListener = null;
|
||||
|
||||
private StreamInfoWorker() {
|
||||
|
||||
}
|
||||
|
||||
public static StreamInfoWorker getInstance() {
|
||||
return streamInfoWorker == null ? (streamInfoWorker = new StreamInfoWorker()) : streamInfoWorker;
|
||||
}
|
||||
|
||||
public void search(int serviceId, String url, Activity a) {
|
||||
runnable = new StreamExtractorRunnable(a, url, serviceId);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void setOnStreamInfoReceivedListener(
|
||||
OnStreamInfoReceivedListener onStreamInfoReceivedListener) {
|
||||
this.onStreamInfoReceivedListener = onStreamInfoReceivedListener;
|
||||
}
|
||||
}
|
||||
@@ -1,160 +1,867 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.Localization;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.player.AbstractPlayer;
|
||||
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.ExoPlayerActivity;
|
||||
import org.schabi.newpipe.player.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoItemDetailActivity.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/>.
|
||||
*/
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
public class VideoItemDetailActivity extends AppCompatActivity implements StreamExtractorWorker.OnStreamInfoReceivedListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
public class VideoItemDetailActivity extends AppCompatActivity {
|
||||
private static final String TAG = "VideoItemDetailActivity";
|
||||
private static final String KORE_PACKET = "org.xbmc.kore";
|
||||
|
||||
private static final String TAG = VideoItemDetailActivity.class.toString();
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String AUTO_PLAY = "auto_play";
|
||||
|
||||
private VideoItemDetailFragment fragment;
|
||||
private ActionBarHandler actionBarHandler;
|
||||
|
||||
private InfoItemBuilder infoItemBuilder = null;
|
||||
private StreamInfo currentStreamInfo = null;
|
||||
private StreamExtractorWorker curExtractorThread;
|
||||
private String videoUrl;
|
||||
private int currentStreamingService = -1;
|
||||
private int serviceId = -1;
|
||||
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
private AtomicBoolean isLoading = new AtomicBoolean(false);
|
||||
private boolean needUpdate = false;
|
||||
|
||||
private boolean autoPlayEnabled;
|
||||
private boolean showRelatedStreams;
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(true).build();
|
||||
private Bitmap streamThumbnail = null;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
private ParallaxScrollView parallaxScrollRootView;
|
||||
private RelativeLayout contentRootLayout;
|
||||
|
||||
private Button thumbnailBackgroundButton;
|
||||
private ImageView thumbnailImageView;
|
||||
private ImageView thumbnailPlayButton;
|
||||
|
||||
private View videoTitleRoot;
|
||||
private TextView videoTitleTextView;
|
||||
private ImageView videoTitleToggleArrow;
|
||||
private TextView videoCountView;
|
||||
|
||||
private RelativeLayout videoDescriptionRootLayout;
|
||||
private TextView videoUploadDateView;
|
||||
private TextView videoDescriptionView;
|
||||
|
||||
private Button uploaderButton;
|
||||
private TextView uploaderTextView;
|
||||
private ImageView uploaderThumb;
|
||||
|
||||
private TextView thumbsUpTextView;
|
||||
private ImageView thumbsUpImageView;
|
||||
private TextView thumbsDownTextView;
|
||||
private ImageView thumbsDownImageView;
|
||||
private TextView thumbsDisabledTextView;
|
||||
|
||||
private TextView nextStreamTitle;
|
||||
private RelativeLayout relatedStreamRootLayout;
|
||||
private LinearLayout relatedStreamsView;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(getString(R.string.show_next_video_key), true);
|
||||
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_videoitem_detail);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
// Show the Up button in the action bar.
|
||||
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
|
||||
// (e.g. when rotating the screen from portrait to landscape).
|
||||
// In this case, the fragment will automatically be re-added
|
||||
// to its container so we don't need to manually add it.
|
||||
// For more information, see the Fragments API guide at:
|
||||
//
|
||||
// http://developer.android.com/guide/components/fragments.html
|
||||
//
|
||||
if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
else Log.e(TAG, "Could not get SupportActionBar");
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
if (savedInstanceState == null) {
|
||||
// this means the video was called though another app
|
||||
if (getIntent().getData() != null) {
|
||||
videoUrl = getIntent().getData().toString();
|
||||
StreamingService[] serviceList = ServiceList.getServices();
|
||||
//StreamExtractor videoExtractor = null;
|
||||
for (int i = 0; i < serviceList.length; i++) {
|
||||
if (serviceList[i].getUrlIdHandlerInstance().acceptUrl(videoUrl)) {
|
||||
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i);
|
||||
currentStreamingService = i;
|
||||
//videoExtractor = ServiceList.getService(i).getExtractorInstance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(currentStreamingService == -1) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
//arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
||||
// videoExtractor.getUrl(videoExtractor.getId(videoUrl)));//cleans URL
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||
|
||||
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY,
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.autoplay_through_intent_key), false));
|
||||
} else {
|
||||
videoUrl = getIntent().getStringExtra(VideoItemDetailFragment.VIDEO_URL);
|
||||
currentStreamingService = getIntent().getIntExtra(VideoItemDetailFragment.STREAMING_SERVICE, -1);
|
||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingService);
|
||||
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY, false);
|
||||
}
|
||||
|
||||
} else {
|
||||
videoUrl = savedInstanceState.getString(VideoItemDetailFragment.VIDEO_URL);
|
||||
currentStreamingService = savedInstanceState.getInt(VideoItemDetailFragment.STREAMING_SERVICE);
|
||||
arguments = savedInstanceState;
|
||||
}
|
||||
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
fragment = new VideoItemDetailFragment();
|
||||
fragment.setArguments(arguments);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.videoitem_detail_container, fragment)
|
||||
.commit();
|
||||
initViews();
|
||||
initListeners();
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
App.checkStartTor(this);
|
||||
|
||||
// Currently only used for enable/disable related videos
|
||||
// but can be extended for other live settings change
|
||||
if (needUpdate) {
|
||||
if (relatedStreamsView != null) initRelatedVideos(currentStreamInfo);
|
||||
needUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
outState.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||
outState.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingService);
|
||||
outState.putBoolean(VideoItemDetailFragment.AUTO_PLAY, false);
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
try {
|
||||
NavStack.getInstance().navBack(this);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == RESULT_OK) {
|
||||
String videoUrl = getIntent().getStringExtra(NavStack.URL);
|
||||
NavStack.getInstance().openDetailActivity(this, videoUrl, serviceId);
|
||||
} else Log.e(TAG, "ReCaptcha failed");
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(getString(R.string.show_next_video_key))) {
|
||||
showRelatedStreams = sharedPreferences.getBoolean(key, true);
|
||||
needUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void initViews() {
|
||||
loadingProgressBar = (ProgressBar) findViewById(R.id.detail_loading_progress_bar);
|
||||
|
||||
parallaxScrollRootView = (ParallaxScrollView) findViewById(R.id.detail_main_content);
|
||||
|
||||
//thumbnailRootLayout = (RelativeLayout) findViewById(R.id.detail_thumbnail_root_layout);
|
||||
thumbnailBackgroundButton = (Button) findViewById(R.id.detail_stream_thumbnail_background_button);
|
||||
thumbnailImageView = (ImageView) findViewById(R.id.detail_thumbnail_image_view);
|
||||
thumbnailPlayButton = (ImageView) findViewById(R.id.detail_thumbnail_play_button);
|
||||
|
||||
contentRootLayout = (RelativeLayout) findViewById(R.id.detail_content_root_layout);
|
||||
|
||||
videoTitleRoot = findViewById(R.id.detail_title_root_layout);
|
||||
videoTitleTextView = (TextView) findViewById(R.id.detail_video_title_view);
|
||||
videoTitleToggleArrow = (ImageView) findViewById(R.id.detail_toggle_description_view);
|
||||
videoCountView = (TextView) findViewById(R.id.detail_view_count_view);
|
||||
|
||||
videoDescriptionRootLayout = (RelativeLayout) findViewById(R.id.detail_description_root_layout);
|
||||
videoUploadDateView = (TextView) findViewById(R.id.detail_upload_date_view);
|
||||
videoDescriptionView = (TextView) findViewById(R.id.detail_description_view);
|
||||
|
||||
//thumbsRootLayout = (LinearLayout) findViewById(R.id.detail_thumbs_root_layout);
|
||||
thumbsUpTextView = (TextView) findViewById(R.id.detail_thumbs_up_count_view);
|
||||
thumbsUpImageView = (ImageView) findViewById(R.id.detail_thumbs_up_img_view);
|
||||
thumbsDownTextView = (TextView) findViewById(R.id.detail_thumbs_down_count_view);
|
||||
thumbsDownImageView = (ImageView) findViewById(R.id.detail_thumbs_down_img_view);
|
||||
thumbsDisabledTextView = (TextView) findViewById(R.id.detail_thumbs_disabled_view);
|
||||
|
||||
//uploaderRootLayout = (FrameLayout) findViewById(R.id.detail_uploader_root_layout);
|
||||
uploaderButton = (Button) findViewById(R.id.detail_uploader_button);
|
||||
uploaderTextView = (TextView) findViewById(R.id.detail_uploader_text_view);
|
||||
uploaderThumb = (ImageView) findViewById(R.id.detail_uploader_thumbnail_view);
|
||||
|
||||
relatedStreamRootLayout = (RelativeLayout) findViewById(R.id.detail_related_streams_root_layout);
|
||||
nextStreamTitle = (TextView) findViewById(R.id.detail_next_stream_title);
|
||||
relatedStreamsView = (LinearLayout) findViewById(R.id.detail_related_streams_view);
|
||||
|
||||
actionBarHandler = new ActionBarHandler(this);
|
||||
actionBarHandler.setupNavMenu(this);
|
||||
videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
infoItemBuilder = new InfoItemBuilder(this, findViewById(android.R.id.content));
|
||||
|
||||
setHeightThumbnail();
|
||||
}
|
||||
|
||||
private void initListeners() {
|
||||
videoTitleRoot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
|
||||
videoTitleTextView.setMaxLines(1);
|
||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||
} else {
|
||||
videoTitleTextView.setMaxLines(10);
|
||||
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
|
||||
}
|
||||
}
|
||||
});
|
||||
thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!isLoading.get() && currentStreamInfo != null) playVideo(currentStreamInfo);
|
||||
}
|
||||
});
|
||||
|
||||
infoItemBuilder.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance().openDetailActivity(VideoItemDetailActivity.this, url, serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
uploaderButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavStack.getInstance().openChannelActivity(VideoItemDetailActivity.this, currentStreamInfo.channel_url, currentStreamInfo.service_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initThumbnailViews(StreamInfo info) {
|
||||
if (info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.thumbnail_url, thumbnailImageView,
|
||||
displayImageOptions, new SimpleImageLoadingListener() {
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
streamThumbnail = loadedImage;
|
||||
|
||||
if (streamThumbnail != null) {
|
||||
// TODO: Change the thumbnail implementation
|
||||
|
||||
// When the thumbnail is not loaded yet, it not passes to the service in time
|
||||
// so, I can notify the service through a broadcast, but the problem is
|
||||
// when I click in another video, another thumbnail will be load, and will
|
||||
// notify again, so I send the videoUrl and compare with the service's url
|
||||
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||
Intent intent = new Intent(AbstractPlayer.ACTION_UPDATE_THUMB);
|
||||
intent.putExtra(AbstractPlayer.VIDEO_URL, currentStreamInfo.webpage_url);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(VideoItemDetailActivity.this,
|
||||
failReason.getCause(), null, findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE,
|
||||
NewPipe.getNameOfService(currentStreamInfo.service_id), imageUri,
|
||||
R.string.could_not_load_thumbnails));
|
||||
}
|
||||
|
||||
});
|
||||
} else thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
|
||||
if (info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.uploader_thumbnail_url,
|
||||
uploaderThumb, displayImageOptions,
|
||||
new ImageErrorLoadingListener(this, findViewById(android.R.id.content), info.service_id));
|
||||
}
|
||||
}
|
||||
|
||||
private void initRelatedVideos(StreamInfo info) {
|
||||
if (relatedStreamsView.getChildCount() > 0) relatedStreamsView.removeAllViews();
|
||||
|
||||
if (info.next_video != null && showRelatedStreams) {
|
||||
nextStreamTitle.setVisibility(View.VISIBLE);
|
||||
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, info.next_video));
|
||||
relatedStreamsView.addView(getSeparatorView());
|
||||
relatedStreamsView.setVisibility(View.VISIBLE);
|
||||
} else nextStreamTitle.setVisibility(View.GONE);
|
||||
|
||||
if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) {
|
||||
for (InfoItem item : info.related_streams) {
|
||||
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
|
||||
}
|
||||
relatedStreamsView.setVisibility(View.VISIBLE);
|
||||
} else if (info.next_video == null) relatedStreamsView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
actionBarHandler.setupMenu(menu, getMenuInflater());
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == android.R.id.home) {
|
||||
// This ID represents the Home or Up button. In the case of this
|
||||
// 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, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
NavStack.getInstance().openMainActivity(this);
|
||||
return true;
|
||||
}
|
||||
return actionBarHandler.onItemSelected(item) || super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setupActionBarHandler(final StreamInfo info) {
|
||||
actionBarHandler.setupStreamList(info.video_streams);
|
||||
actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if (isLoading.get()) return;
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, VideoItemDetailActivity.this.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if (isLoading.get()) return;
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(info.webpage_url));
|
||||
startActivity(Intent.createChooser(intent, VideoItemDetailActivity.this.getString(R.string.choose_browser)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnOpenInPopupListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if (isLoading.get()) return;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(VideoItemDetailActivity.this)) {
|
||||
Toast.makeText(VideoItemDetailActivity.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||
|
||||
Intent i = new Intent(VideoItemDetailActivity.this, PopupVideoPlayer.class);
|
||||
Toast.makeText(VideoItemDetailActivity.this, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
i.putExtra(AbstractPlayer.VIDEO_TITLE, info.title)
|
||||
.putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader)
|
||||
.putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamId)
|
||||
.putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams));
|
||||
if (info.start_position > 0) i.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000);
|
||||
VideoItemDetailActivity.this.startService(i);
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if (isLoading.get()) return;
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setPackage(KORE_PACKET);
|
||||
intent.setData(Uri.parse(info.webpage_url.replace("https", "http")));
|
||||
VideoItemDetailActivity.this.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(VideoItemDetailActivity.this);
|
||||
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(VideoItemDetailActivity.this.getString(R.string.fdroid_kore_url)));
|
||||
VideoItemDetailActivity.this.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
|
||||
if (isLoading.get() || !PermissionHelper.checkStoragePermissions(VideoItemDetailActivity.this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
// Sometimes it may be that some information is not available due to changes fo the
|
||||
// website which was crawled. Then the ui has to understand this and act right.
|
||||
|
||||
if (info.audio_streams != null) {
|
||||
AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
|
||||
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
|
||||
args.putString(DownloadDialog.AUDIO_URL, audioStream.url);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
|
||||
}
|
||||
|
||||
if (info.video_streams != null) {
|
||||
VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
|
||||
String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
|
||||
args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url);
|
||||
}
|
||||
|
||||
args.putString(DownloadDialog.TITLE, info.title);
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(args);
|
||||
downloadDialog.show(VideoItemDetailActivity.this.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(VideoItemDetailActivity.this,
|
||||
R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (info.audio_streams == null) {
|
||||
actionBarHandler.showAudioAction(false);
|
||||
} else {
|
||||
return fragment.onOptionsItemSelected(item) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if (isLoading.get()) return;
|
||||
|
||||
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(VideoItemDetailActivity.this)
|
||||
.getBoolean(VideoItemDetailActivity.this.getString(R.string.use_external_audio_player_key), false);
|
||||
Intent intent;
|
||||
AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||
//internal music player: explicit intent
|
||||
if (!BackgroundPlayer.isRunning && streamThumbnail != null) {
|
||||
ActivityCommunicator.getCommunicator()
|
||||
.backgroundPlayerThumbnail = streamThumbnail;
|
||||
intent = new Intent(VideoItemDetailActivity.this, BackgroundPlayer.class);
|
||||
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
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, serviceId);
|
||||
intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader);
|
||||
VideoItemDetailActivity.this.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 !!!
|
||||
VideoItemDetailActivity.this.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(VideoItemDetailActivity.this);
|
||||
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(VideoItemDetailActivity.this.getString(R.string.fdroid_vlc_url)));
|
||||
VideoItemDetailActivity.this.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
serviceId = intent.getIntExtra(NavStack.SERVICE_ID, 0);
|
||||
videoUrl = intent.getStringExtra(NavStack.URL);
|
||||
autoPlayEnabled = intent.getBooleanExtra(AUTO_PLAY, false);
|
||||
selectVideo(videoUrl, serviceId);
|
||||
}
|
||||
|
||||
private void selectVideo(String url, int serviceId) {
|
||||
if (curExtractorThread != null && curExtractorThread.isRunning()) curExtractorThread.cancel();
|
||||
|
||||
animateView(contentRootLayout, false, 200, null);
|
||||
|
||||
thumbnailPlayButton.setVisibility(View.GONE);
|
||||
loadingProgressBar.setVisibility(View.VISIBLE);
|
||||
|
||||
imageLoader.cancelDisplayTask(thumbnailImageView);
|
||||
imageLoader.cancelDisplayTask(uploaderThumb);
|
||||
thumbnailImageView.setImageDrawable(null);
|
||||
|
||||
curExtractorThread = StreamExtractorWorker.startExtractorThread(serviceId, url, this, this);
|
||||
isLoading.set(true);
|
||||
}
|
||||
|
||||
public void playVideo(StreamInfo info) {
|
||||
// ----------- THE MAGIC MOMENT ---------------
|
||||
VideoStream selectedVideoStream = info.video_streams.get(actionBarHandler.getSelectedVideoStream());
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
|
||||
// External Player
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW)
|
||||
.setDataAndType(Uri.parse(selectedVideoStream.url), MediaFormat.getMimeById(selectedVideoStream.format))
|
||||
.putExtra(Intent.EXTRA_TITLE, info.title)
|
||||
.putExtra("title", info.title);
|
||||
this.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent()
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(getString(R.string.fdroid_vlc_url)));
|
||||
startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
} else {
|
||||
Intent intent;
|
||||
boolean useOldPlayer = PreferenceManager
|
||||
.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.use_old_player_key), false)
|
||||
|| (Build.VERSION.SDK_INT < 16);
|
||||
if (!useOldPlayer) {
|
||||
// ExoPlayer
|
||||
if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||
intent = new Intent(this, ExoPlayerActivity.class)
|
||||
.putExtra(AbstractPlayer.VIDEO_TITLE, info.title)
|
||||
.putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader)
|
||||
.putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, actionBarHandler.getSelectedVideoStream())
|
||||
.putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams));
|
||||
if (info.start_position > 0) intent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000);
|
||||
} else {
|
||||
// Internal Player
|
||||
intent = new Intent(this, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title)
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url)
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private int getPreferredAudioStreamId(final StreamInfo info) {
|
||||
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString(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 View getSeparatorView() {
|
||||
View separator = new View(this);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
|
||||
int m8 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
|
||||
int m5 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
|
||||
params.setMargins(m8, m5, m8, m5);
|
||||
separator.setLayoutParams(params);
|
||||
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(R.attr.separatorColor, typedValue, true);
|
||||
separator.setBackgroundColor(typedValue.data);
|
||||
return separator;
|
||||
}
|
||||
|
||||
private void setHeightThumbnail() {
|
||||
boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels;
|
||||
int height = isPortrait ? (int) (getResources().getDisplayMetrics().widthPixels / (16.0f / 9.0f))
|
||||
: (int) (getResources().getDisplayMetrics().heightPixels / 2f);
|
||||
thumbnailImageView.setScaleType(isPortrait ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.FIT_CENTER);
|
||||
thumbnailImageView.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
|
||||
thumbnailImageView.setMinimumHeight(height);
|
||||
thumbnailBackgroundButton.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
|
||||
thumbnailBackgroundButton.setMinimumHeight(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the view
|
||||
*
|
||||
* @param view view that will be animated
|
||||
* @param enterOrExit true to enter, false to exit
|
||||
* @param duration how long the animation will take, in milliseconds
|
||||
* @param execOnEnd runnable that will be executed when the animation ends
|
||||
*/
|
||||
public void animateView(final View view, final boolean enterOrExit, long duration, final Runnable execOnEnd) {
|
||||
if (view.getVisibility() == View.VISIBLE && enterOrExit) {
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.setAlpha(1f);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
return;
|
||||
} else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) && !enterOrExit) {
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.GONE);
|
||||
view.setAlpha(0f);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
return;
|
||||
}
|
||||
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
if (enterOrExit) {
|
||||
view.animate().alpha(1f).setDuration(duration)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
view.animate().alpha(0f)
|
||||
.setDuration(duration)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(View.GONE);
|
||||
if (execOnEnd != null) execOnEnd.run();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnStreamInfoReceivedListener callbacks
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
fragment.onCreateOptionsMenu(menu, getMenuInflater());
|
||||
return true;
|
||||
public void onReceive(StreamInfo info) {
|
||||
currentStreamInfo = info;
|
||||
|
||||
loadingProgressBar.setVisibility(View.GONE);
|
||||
thumbnailPlayButton.setVisibility(View.VISIBLE);
|
||||
relatedStreamRootLayout.setVisibility(showRelatedStreams ? View.VISIBLE : View.GONE);
|
||||
parallaxScrollRootView.scrollTo(0, 0);
|
||||
|
||||
// Since newpipe is designed to work even if certain information is not available,
|
||||
// the UI has to react on missing information.
|
||||
videoTitleTextView.setText(info.title);
|
||||
if (!info.uploader.isEmpty()) uploaderTextView.setText(info.uploader);
|
||||
uploaderTextView.setVisibility(!info.uploader.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
uploaderButton.setVisibility(!info.channel_url.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.buddy));
|
||||
|
||||
if (info.view_count >= 0) videoCountView.setText(Localization.localizeViewCount(info.view_count, this));
|
||||
videoCountView.setVisibility(info.view_count >= 0 ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (info.dislike_count == -1 && info.like_count == -1) {
|
||||
thumbsDownImageView.setVisibility(View.VISIBLE);
|
||||
thumbsUpImageView.setVisibility(View.VISIBLE);
|
||||
thumbsUpTextView.setVisibility(View.GONE);
|
||||
thumbsDownTextView.setVisibility(View.GONE);
|
||||
|
||||
thumbsDisabledTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
thumbsDisabledTextView.setVisibility(View.GONE);
|
||||
|
||||
if (info.dislike_count >= 0) thumbsDownTextView.setText(Localization.localizeNumber(info.dislike_count, this));
|
||||
thumbsDownTextView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE);
|
||||
thumbsDownImageView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (info.like_count >= 0) thumbsUpTextView.setText(Localization.localizeNumber(info.like_count, this));
|
||||
thumbsUpTextView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE);
|
||||
thumbsUpImageView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
if (!info.upload_date.isEmpty()) videoUploadDateView.setText(Localization.localizeDate(info.upload_date, this));
|
||||
videoUploadDateView.setVisibility(!info.upload_date.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (!info.description.isEmpty()) videoDescriptionView.setText(
|
||||
Build.VERSION.SDK_INT >= 24 ? Html.fromHtml(info.description, 0) : Html.fromHtml(info.description)
|
||||
);
|
||||
videoDescriptionView.setVisibility(!info.description.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||
|
||||
setupActionBarHandler(info);
|
||||
initRelatedVideos(info);
|
||||
initThumbnailViews(info);
|
||||
|
||||
animateView(contentRootLayout, true, 200, null);
|
||||
|
||||
isLoading.set(false);
|
||||
if (autoPlayEnabled) playVideo(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int messageId) {
|
||||
Toast.makeText(this, messageId, Toast.LENGTH_LONG).show();
|
||||
loadingProgressBar.setVisibility(View.GONE);
|
||||
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.not_available_monkey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaException() {
|
||||
Toast.makeText(this, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(new Intent(this, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedByGemaError() {
|
||||
loadingProgressBar.setVisibility(View.GONE);
|
||||
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.gruese_die_gema));
|
||||
thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(getString(R.string.c3s_url)));
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
Toast.makeText(this, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentErrorWithMessage(int messageId) {
|
||||
loadingProgressBar.setVisibility(View.GONE);
|
||||
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.not_available_monkey));
|
||||
Toast.makeText(this, messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentError() {
|
||||
loadingProgressBar.setVisibility(View.GONE);
|
||||
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.not_available_monkey));
|
||||
Toast.makeText(this, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,792 +0,0 @@
|
||||
package org.schabi.newpipe.detail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.ChannelActivity;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.Localization;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.AudioStream;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.VideoStream;
|
||||
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.ExoPlayerActivity;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoItemDetailFragment.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class VideoItemDetailFragment extends Fragment {
|
||||
|
||||
private static final String TAG = VideoItemDetailFragment.class.toString();
|
||||
private static final String KORE_PACKET = "org.xbmc.kore";
|
||||
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
public static final String STREAMING_SERVICE = "streaming_service";
|
||||
public static final String AUTO_PLAY = "auto_play";
|
||||
|
||||
private AppCompatActivity activity;
|
||||
private ActionBarHandler actionBarHandler;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private int streamingServiceId = -1;
|
||||
|
||||
private boolean autoPlayEnabled;
|
||||
private boolean showNextStreamItem;
|
||||
|
||||
private View thumbnailWindowLayout;
|
||||
//this only remains due to downwards compatibility
|
||||
private FloatingActionButton playVideoButton;
|
||||
private final Point initialThumbnailPos = new Point(0, 0);
|
||||
private View rootView = null;
|
||||
private Bitmap streamThumbnail = null;
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private InfoItemBuilder infoItemBuilder = null;
|
||||
|
||||
public interface OnInvokeCreateOptionsMenuListener {
|
||||
void createOptionsMenu();
|
||||
}
|
||||
|
||||
private OnInvokeCreateOptionsMenuListener onInvokeCreateOptionsMenuListener;
|
||||
|
||||
private void updateInfo(final StreamInfo info) {
|
||||
Activity a = getActivity();
|
||||
|
||||
RelativeLayout textContentLayout =
|
||||
(RelativeLayout) activity.findViewById(R.id.detail_text_content_layout);
|
||||
final TextView videoTitleView =
|
||||
(TextView) activity.findViewById(R.id.detail_video_title_view);
|
||||
TextView uploaderView = (TextView) activity.findViewById(R.id.detail_uploader_view);
|
||||
TextView viewCountView = (TextView) activity.findViewById(R.id.detail_view_count_view);
|
||||
TextView thumbsUpView = (TextView) activity.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
TextView thumbsDownView =
|
||||
(TextView) activity.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
TextView uploadDateView = (TextView) activity.findViewById(R.id.detail_upload_date_view);
|
||||
TextView descriptionView = (TextView) activity.findViewById(R.id.detail_description_view);
|
||||
RecyclerView nextStreamView =
|
||||
(RecyclerView) activity.findViewById(R.id.detail_next_stream_content);
|
||||
RelativeLayout nextVideoRootFrame =
|
||||
(RelativeLayout) activity.findViewById(R.id.detail_next_stream_root_layout);
|
||||
TextView similarTitle = (TextView) activity.findViewById(R.id.detail_similar_title);
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detail_stream_thumbnail_window_background_button);
|
||||
View topView = activity.findViewById(R.id.detailTopView);
|
||||
Button channelButton = (Button) activity.findViewById(R.id.channel_button);
|
||||
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if(info.next_video != null) {
|
||||
// todo: activate this function or remove it
|
||||
nextStreamView.setVisibility(View.GONE);
|
||||
} else {
|
||||
nextStreamView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
ImageView playArrowView = (ImageView) activity.findViewById(R.id.play_arrow_view);
|
||||
playArrowView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (!showNextStreamItem) {
|
||||
nextVideoRootFrame.setVisibility(View.GONE);
|
||||
similarTitle.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
videoTitleView.setText(info.title);
|
||||
|
||||
topView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == android.view.MotionEvent.ACTION_UP) {
|
||||
ImageView arrow = (ImageView) activity.findViewById(R.id.toggle_description_view);
|
||||
View extra = activity.findViewById(R.id.detailExtraView);
|
||||
if (extra.getVisibility() == View.VISIBLE) {
|
||||
extra.setVisibility(View.GONE);
|
||||
arrow.setImageResource(R.drawable.arrow_down);
|
||||
} else {
|
||||
extra.setVisibility(View.VISIBLE);
|
||||
arrow.setImageResource(R.drawable.arrow_up);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Since newpipe is designed to work even if certain information is not available,
|
||||
// the UI has to react on missing information.
|
||||
videoTitleView.setText(info.title);
|
||||
if(!info.uploader.isEmpty()) {
|
||||
uploaderView.setText(info.uploader);
|
||||
} else {
|
||||
activity.findViewById(R.id.detail_uploader_view).setVisibility(View.GONE);
|
||||
}
|
||||
if(info.view_count >= 0) {
|
||||
viewCountView.setText(Localization.localizeViewCount(info.view_count, a));
|
||||
} else {
|
||||
viewCountView.setVisibility(View.GONE);
|
||||
}
|
||||
if(info.dislike_count >= 0) {
|
||||
thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, a));
|
||||
} else {
|
||||
thumbsDownView.setVisibility(View.INVISIBLE);
|
||||
activity.findViewById(R.id.detail_thumbs_down_count_view).setVisibility(View.GONE);
|
||||
}
|
||||
if(info.like_count >= 0) {
|
||||
thumbsUpView.setText(Localization.localizeNumber(info.like_count, a));
|
||||
} else {
|
||||
thumbsUpView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detail_thumbs_up_img_view).setVisibility(View.GONE);
|
||||
thumbsDownView.setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detail_thumbs_down_img_view).setVisibility(View.GONE);
|
||||
}
|
||||
if(!info.upload_date.isEmpty()) {
|
||||
uploadDateView.setText(Localization.localizeDate(info.upload_date, a));
|
||||
} else {
|
||||
uploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
if(!info.description.isEmpty()) {
|
||||
descriptionView.setText(Html.fromHtml(info.description));
|
||||
} else {
|
||||
descriptionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
// parse streams
|
||||
Vector<VideoStream> streamsToUse = new Vector<>();
|
||||
for (VideoStream i : info.video_streams) {
|
||||
if (useStream(i, streamsToUse)) {
|
||||
streamsToUse.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
textContentLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
if(info.next_video == null) {
|
||||
activity.findViewById(R.id.detail_next_stream_title).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(info.related_streams != null && !info.related_streams.isEmpty()) {
|
||||
initSimilarVideos(info);
|
||||
} else {
|
||||
activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.similar_streams_view).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setupActionBarHandler(info);
|
||||
|
||||
if(autoPlayEnabled) {
|
||||
playVideo(info);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playVideo(info);
|
||||
}
|
||||
});
|
||||
|
||||
if(info.channel_url != null && info.channel_url != "") {
|
||||
channelButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent i = new Intent(activity, ChannelActivity.class);
|
||||
i.putExtra(ChannelActivity.CHANNEL_URL, info.channel_url);
|
||||
i.putExtra(ChannelActivity.SERVICE_ID, info.service_id);
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channelButton.setVisibility(Button.GONE);
|
||||
}
|
||||
|
||||
initThumbnailViews(info);
|
||||
}
|
||||
|
||||
private void initThumbnailViews(final StreamInfo info) {
|
||||
ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
ImageView uploaderThumb
|
||||
= (ImageView) activity.findViewById(R.id.detail_uploader_thumbnail_view);
|
||||
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.thumbnail_url, videoThumbnailView,
|
||||
displayImageOptions, new ImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingStarted(String imageUri, View view) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(getActivity(),
|
||||
failReason.getCause(), null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE,
|
||||
ServiceList.getNameOfService(info.service_id), imageUri,
|
||||
R.string.could_not_load_thumbnails));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
streamThumbnail = loadedImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
videoThumbnailView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
if(info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.uploader_thumbnail_url,
|
||||
uploaderThumb, displayImageOptions,
|
||||
new ImageErrorLoadingListener(activity, rootView, info.service_id));
|
||||
}
|
||||
}
|
||||
|
||||
private void setupActionBarHandler(final StreamInfo info) {
|
||||
actionBarHandler.setupStreamList(info.video_streams);
|
||||
|
||||
actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url);
|
||||
intent.setType("text/plain");
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(info.webpage_url));
|
||||
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser)));
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setPackage(KORE_PACKET);
|
||||
intent.setData(Uri.parse(info.webpage_url.replace("https", "http")));
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.kore_not_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_kore_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
try {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
// Sometimes it may be that some information is not available due to changes fo the
|
||||
// website which was crawled. Then the ui has to understand this and act right.
|
||||
|
||||
if (info.audio_streams != null) {
|
||||
AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
|
||||
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
|
||||
args.putString(DownloadDialog.AUDIO_URL, audioStream.url);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix);
|
||||
}
|
||||
|
||||
if (info.video_streams != null) {
|
||||
VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
|
||||
String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
|
||||
args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url);
|
||||
}
|
||||
|
||||
args.putString(DownloadDialog.TITLE, info.title);
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(args);
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(info.audio_streams == null) {
|
||||
actionBarHandler.showAudioAction(false);
|
||||
} else {
|
||||
actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
Intent intent;
|
||||
AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||
//internal music player: explicit intent
|
||||
if (!BackgroundPlayer.isRunning && streamThumbnail != null) {
|
||||
ActivityCommunicator.getCommunicator()
|
||||
.backgroundPlayerThumbnail = streamThumbnail;
|
||||
intent = new Intent(activity, BackgroundPlayer.class);
|
||||
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
Log.i(TAG, "audioStream is null:" + (audioStream == null));
|
||||
Log.i(TAG, "audioStream.url is null:" + (audioStream.url == null));
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
MediaFormat.getMimeById(audioStream.format));
|
||||
intent.putExtra(BackgroundPlayer.TITLE, info.title);
|
||||
intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url);
|
||||
intent.putExtra(BackgroundPlayer.SERVICE_ID, streamingServiceId);
|
||||
intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader);
|
||||
activity.startService(intent);
|
||||
}
|
||||
} else {
|
||||
intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(audioStream.url),
|
||||
MediaFormat.getMimeById(audioStream.format));
|
||||
intent.putExtra(Intent.EXTRA_TITLE, info.title);
|
||||
intent.putExtra("title", info.title);
|
||||
// HERE !!!
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Log.i(TAG, "You unlocked a secret unicorn.");
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private int getPreferredAudioStreamId(final StreamInfo info) {
|
||||
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getString(activity.getString(R.string.default_audio_format_key), "webm");
|
||||
|
||||
int preferredFormat = MediaFormat.WEBMA.id;
|
||||
switch(preferredFormatString) {
|
||||
case "webm":
|
||||
preferredFormat = MediaFormat.WEBMA.id;
|
||||
break;
|
||||
case "m4a":
|
||||
preferredFormat = MediaFormat.M4A.id;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for(int i = 0; i < info.audio_streams.size(); i++) {
|
||||
if(info.audio_streams.get(i).format == preferredFormat) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
//todo: make this a proper error
|
||||
Log.e(TAG, "FAILED to set audioStream value!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void initSimilarVideos(final StreamInfo info) {
|
||||
LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similar_streams_view);
|
||||
for (final StreamPreviewInfo item : info.related_streams) {
|
||||
similarLayout.addView(infoItemBuilder.buildView(similarLayout, item));
|
||||
}
|
||||
infoItemBuilder.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url) {
|
||||
openStreamUrl(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onErrorBlockedByGema() {
|
||||
Button backgroundButton = (Button)
|
||||
activity.findViewById(R.id.detail_stream_thumbnail_window_background_button);
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.gruese_die_gema));
|
||||
backgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(activity.getString(R.string.c3s_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void onNotSpecifiedContentError() {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.not_available_monkey));
|
||||
Toast.makeText(activity, R.string.content_not_available, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onNotSpecifiedContentErrorWithMessage(int resourceId) {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
thumbnailView.setImageBitmap(BitmapFactory.decodeResource(
|
||||
getResources(), R.drawable.not_available_monkey));
|
||||
Toast.makeText(activity, resourceId, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean useStream(VideoStream stream, Vector<VideoStream> streams) {
|
||||
for(VideoStream i : streams) {
|
||||
if(i.resolution.equals(stream.resolution)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
activity = (AppCompatActivity) getActivity();
|
||||
showNextStreamItem = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(activity.getString(R.string.show_next_video_key), true);
|
||||
|
||||
|
||||
StreamInfoWorker siw = StreamInfoWorker.getInstance();
|
||||
siw.setOnStreamInfoReceivedListener(new StreamInfoWorker.OnStreamInfoReceivedListener() {
|
||||
@Override
|
||||
public void onReceive(StreamInfo info) {
|
||||
updateInfo(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int messageId) {
|
||||
postNewErrorToast(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedByGemaError() {
|
||||
onErrorBlockedByGema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentErrorWithMessage(int messageId) {
|
||||
onNotSpecifiedContentErrorWithMessage(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentError() {
|
||||
onNotSpecifiedContentError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false);
|
||||
progressBar = (ProgressBar) rootView.findViewById(R.id.detail_progress_bar);
|
||||
|
||||
actionBarHandler = new ActionBarHandler(activity);
|
||||
actionBarHandler.setupNavMenu(activity);
|
||||
if(onInvokeCreateOptionsMenuListener != null) {
|
||||
onInvokeCreateOptionsMenuListener.createOptionsMenu();
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceBundle) {
|
||||
super.onActivityCreated(savedInstanceBundle);
|
||||
Activity a = getActivity();
|
||||
|
||||
infoItemBuilder = new InfoItemBuilder(a, a.findViewById(android.R.id.content));
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 18) {
|
||||
playVideoButton = (FloatingActionButton) a.findViewById(R.id.play_video_button);
|
||||
}
|
||||
thumbnailWindowLayout = a.findViewById(R.id.detail_stream_thumbnail_window_layout);
|
||||
Button backgroundButton = (Button)
|
||||
a.findViewById(R.id.detail_stream_thumbnail_window_background_button);
|
||||
|
||||
// Sometimes when this fragment is not visible it still gets initiated
|
||||
// then we must not try to access objects of this fragment.
|
||||
// Otherwise the applications would crash.
|
||||
if(backgroundButton != null) {
|
||||
streamingServiceId = getArguments().getInt(STREAMING_SERVICE);
|
||||
String videoUrl = getArguments().getString(VIDEO_URL);
|
||||
StreamInfoWorker siw = StreamInfoWorker.getInstance();
|
||||
siw.search(streamingServiceId, videoUrl, getActivity());
|
||||
|
||||
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 18) {
|
||||
ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view);
|
||||
thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
||||
// This is used to synchronize the thumbnailWindowButton and the playVideoButton
|
||||
// inside the ScrollView with the actual size of the thumbnail.
|
||||
//todo: onLayoutChage sometimes not triggered
|
||||
// background buttons area seem to overlap the thumbnail view
|
||||
// So although you just clicked slightly beneath the thumbnail the action still
|
||||
// gets triggered.
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
RelativeLayout.LayoutParams newWindowLayoutParams =
|
||||
(RelativeLayout.LayoutParams) thumbnailWindowLayout.getLayoutParams();
|
||||
newWindowLayoutParams.height = bottom - top;
|
||||
thumbnailWindowLayout.setLayoutParams(newWindowLayoutParams);
|
||||
|
||||
//noinspection SuspiciousNameCombination
|
||||
initialThumbnailPos.set(top, left);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void playVideo(final StreamInfo info) {
|
||||
// ----------- THE MAGIC MOMENT ---------------
|
||||
VideoStream selectedVideoStream =
|
||||
info.video_streams.get(actionBarHandler.getSelectedVideoStream());
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false)) {
|
||||
|
||||
// External Player
|
||||
Intent intent = new Intent();
|
||||
try {
|
||||
intent.setAction(Intent.ACTION_VIEW)
|
||||
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||
MediaFormat.getMimeById(selectedVideoStream.format))
|
||||
.putExtra(Intent.EXTRA_TITLE, info.title)
|
||||
.putExtra("title", info.title);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent()
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url)));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
} else {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
|
||||
|
||||
// exo player
|
||||
|
||||
if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) {
|
||||
// try dash
|
||||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||
.setData(Uri.parse(info.dashMpdUrl))
|
||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
||||
startActivity(intent);
|
||||
} else if((info.audio_streams != null && !info.audio_streams.isEmpty()) &&
|
||||
(info.video_only_streams != null && !info.video_only_streams.isEmpty())) {
|
||||
// try smooth streaming
|
||||
|
||||
} else {
|
||||
//default streaming
|
||||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||
MediaFormat.getMimeById(selectedVideoStream.format))
|
||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
}
|
||||
//-------------
|
||||
|
||||
} else {
|
||||
// Internal Player
|
||||
Intent intent = new Intent(activity, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title)
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url)
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url)
|
||||
.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
|
||||
activity.startActivity(intent); //also HERE !!!
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
actionBarHandler.setupMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
return actionBarHandler.onItemSelected(item);
|
||||
}
|
||||
|
||||
public void setOnInvokeCreateOptionsMenuListener(OnInvokeCreateOptionsMenuListener listener) {
|
||||
this.onInvokeCreateOptionsMenuListener = listener;
|
||||
}
|
||||
|
||||
private void postNewErrorToast(final int stringResource) {
|
||||
Toast.makeText(VideoItemDetailFragment.this.getActivity(),
|
||||
stringResource, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void openStreamUrl(String url) {
|
||||
Intent detailIntent = new Intent(activity, VideoItemDetailActivity.class);
|
||||
detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
|
||||
detailIntent.putExtra(
|
||||
VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
|
||||
activity.startActivity(detailIntent);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,10 @@ package org.schabi.newpipe.download;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.ActionBar;
|
||||
@@ -23,55 +20,39 @@ import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Vector;
|
||||
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.AllMissionsFragment;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
import us.shandian.giga.util.CrashHandler;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener{
|
||||
public class DownloadActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
|
||||
|
||||
public static final String INTENT_DOWNLOAD = "us.shandian.giga.intent.DOWNLOAD";
|
||||
|
||||
public static final String INTENT_LIST = "us.shandian.giga.intent.LIST";
|
||||
|
||||
private static final String TAG = MainActivity.class.toString();
|
||||
public static final String THREADS = "threads";
|
||||
|
||||
|
||||
private static final String TAG = DownloadActivity.class.toString();
|
||||
private MissionsFragment mFragment;
|
||||
private DownloadManager mManager;
|
||||
private DownloadManagerService.DMBinder mBinder;
|
||||
|
||||
|
||||
private String mPendingUrl;
|
||||
private SharedPreferences mPrefs;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName p1, IBinder binder) {
|
||||
mBinder = (DownloadManagerService.DMBinder) binder;
|
||||
mManager = mBinder.getDownloadManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName p1) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
@TargetApi(21)
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -82,15 +63,14 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte
|
||||
Intent i = new Intent();
|
||||
i.setClass(this, DownloadManagerService.class);
|
||||
startService(i);
|
||||
bindService(i, mConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
|
||||
//noinspection ConstantConditions
|
||||
|
||||
// its ok if this failes, we will catch that error later, and send it as report
|
||||
// its ok if this fails, we will catch that error later, and send it as report
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.downloads_title);
|
||||
@@ -150,6 +130,8 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte
|
||||
final TextView tCount = Utility.findViewById(v, R.id.threads_count);
|
||||
final SeekBar threads = Utility.findViewById(v, R.id.threads);
|
||||
final Toolbar toolbar = Utility.findViewById(v, R.id.toolbar);
|
||||
final RadioButton audioButton = (RadioButton) Utility.findViewById(v, R.id.audio_button);
|
||||
|
||||
|
||||
threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
|
||||
@@ -199,18 +181,24 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
|
||||
String location;
|
||||
if(audioButton.isChecked()) {
|
||||
location = NewPipeSettings.getAudioDownloadPath(DownloadActivity.this);
|
||||
} else {
|
||||
location = NewPipeSettings.getVideoDownloadPath(DownloadActivity.this);
|
||||
}
|
||||
|
||||
String fName = name.getText().toString().trim();
|
||||
|
||||
File f = new File(mManager.getLocation() + "/" + fName);
|
||||
|
||||
File f = new File(location, fName);
|
||||
if (f.exists()) {
|
||||
Toast.makeText(MainActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
|
||||
while (mBinder == null);
|
||||
|
||||
int res = mManager.startMission(getIntent().getData().toString(), fName, threads.getProgress() + 1);
|
||||
mBinder.onMissionAdded(mManager.getMission(res));
|
||||
DownloadManagerService.startMission(
|
||||
DownloadActivity.this,
|
||||
getIntent().getData().toString(), location, fName,
|
||||
audioButton.isChecked(), threads.getProgress() + 1);
|
||||
mFragment.notifyChange();
|
||||
|
||||
mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit();
|
||||
@@ -247,9 +235,7 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
@@ -257,17 +243,9 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_report_error: {
|
||||
ErrorActivity.reportError(MainActivity.this, new Vector<Throwable>(),
|
||||
null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.USER_REPORT,
|
||||
null,
|
||||
"user_report", R.string.user_report));
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return mFragment.onOptionsItemSelected(item) ||
|
||||
super.onOptionsItemSelected(item);
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,12 +26,15 @@ import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
|
||||
@@ -64,24 +67,6 @@ public class DownloadDialog extends DialogFragment {
|
||||
public static final String AUDIO_URL = "audio_url";
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
|
||||
private DownloadManager mManager;
|
||||
private DownloadManagerService.DMBinder mBinder;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName p1, IBinder binder) {
|
||||
mBinder = (DownloadManagerService.DMBinder) binder;
|
||||
mManager = mBinder.getDownloadManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName p1) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public DownloadDialog() {
|
||||
|
||||
}
|
||||
@@ -101,12 +86,6 @@ public class DownloadDialog extends DialogFragment {
|
||||
if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
|
||||
ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0);
|
||||
|
||||
Intent i = new Intent();
|
||||
i.setClass(getContext(), DownloadManagerService.class);
|
||||
getContext().startService(i);
|
||||
getContext().bindService(i, mConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
|
||||
return inflater.inflate(R.layout.dialog_url, container);
|
||||
}
|
||||
|
||||
@@ -218,26 +197,22 @@ public class DownloadDialog extends DialogFragment {
|
||||
|
||||
String fName = name.getText().toString().trim();
|
||||
|
||||
// todo: add timeout? would be bad if the thread gets locked dueto this.
|
||||
while (mBinder == null);
|
||||
|
||||
if(audioButton.isChecked()){
|
||||
int res = mManager.startMission(
|
||||
arguments.getString(AUDIO_URL),
|
||||
fName + arguments.getString(FILE_SUFFIX_AUDIO),
|
||||
threads.getProgress() + 1);
|
||||
mBinder.onMissionAdded(mManager.getMission(res));
|
||||
boolean isAudio = audioButton.isChecked();
|
||||
String url, location, filename;
|
||||
if(isAudio) {
|
||||
url = arguments.getString(AUDIO_URL);
|
||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||
filename = fName + arguments.getString(FILE_SUFFIX_AUDIO);
|
||||
} else {
|
||||
url = arguments.getString(VIDEO_URL);
|
||||
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
||||
filename = fName + arguments.getString(FILE_SUFFIX_VIDEO);
|
||||
}
|
||||
|
||||
if(videoButton.isChecked()){
|
||||
int res = mManager.startMission(
|
||||
arguments.getString(VIDEO_URL),
|
||||
fName + arguments.getString(FILE_SUFFIX_VIDEO),
|
||||
threads.getProgress() + 1);
|
||||
mBinder.onMissionAdded(mManager.getMission(res));
|
||||
}
|
||||
DownloadManagerService.startMission(getContext(), url, location, filename, isAudio,
|
||||
threads.getProgress() + 1);
|
||||
|
||||
getDialog().dismiss();
|
||||
|
||||
}
|
||||
|
||||
private void download(String url, String title,
|
||||
@@ -255,8 +230,8 @@ public class DownloadDialog extends DialogFragment {
|
||||
//we'll see later
|
||||
FileDownloader.downloadFile(getContext(), url, saveFilePath, title);
|
||||
} else {
|
||||
Intent intent = new Intent(getContext(), MainActivity.class);
|
||||
intent.setAction(MainActivity.INTENT_DOWNLOAD);
|
||||
Intent intent = new Intent(getContext(), DownloadActivity.class);
|
||||
intent.setAction(DownloadActivity.INTENT_DOWNLOAD);
|
||||
intent.setData(Uri.parse(url));
|
||||
intent.putExtra("fileName", createFileName(title) + fileSuffix);
|
||||
startActivity(intent);
|
||||
|
||||
@@ -42,7 +42,7 @@ import info.guardianproject.netcipher.NetCipher;
|
||||
*/
|
||||
|
||||
|
||||
// TODO: FOR HEVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!!
|
||||
// TODO: FOR HEAVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!!
|
||||
public class FileDownloader extends AsyncTask<Void, Integer, Void> {
|
||||
public static final String TAG = "FileDownloader";
|
||||
|
||||
|
||||
1
app/src/main/java/org/schabi/newpipe/extractor
Submodule
1
app/src/main/java/org/schabi/newpipe/extractor
Submodule
Submodule app/src/main/java/org/schabi/newpipe/extractor added at 6ab3dc876e
@@ -1,44 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* AbstractVideoInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Common properties between StreamInfo and StreamPreviewInfo.*/
|
||||
public abstract class AbstractVideoInfo {
|
||||
public static enum StreamType {
|
||||
NONE, // placeholder to check if stream type was checked or not
|
||||
VIDEO_STREAM,
|
||||
AUDIO_STREAM,
|
||||
LIVE_STREAM,
|
||||
AUDIO_LIVE_STREAM,
|
||||
FILE
|
||||
}
|
||||
|
||||
public StreamType stream_type;
|
||||
public int service_id = -1;
|
||||
public String id = "";
|
||||
public String title = "";
|
||||
public String uploader = "";
|
||||
public String thumbnail_url = "";
|
||||
public Bitmap thumbnail;
|
||||
public String webpage_url = "";
|
||||
public String upload_date = "";
|
||||
public long view_count = -1;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 04.03.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* AudioStream.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class AudioStream {
|
||||
public String url = "";
|
||||
public int format = -1;
|
||||
public int bandwidth = -1;
|
||||
public int sampling_rate = -1;
|
||||
|
||||
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
|
||||
this.url = url; this.format = format;
|
||||
this.bandwidth = bandwidth; this.sampling_rate = samplingRate;
|
||||
}
|
||||
|
||||
// reveals 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 cmp != null && equalStats(cmp)
|
||||
&& url == cmp.url;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 25.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class ChannelExtractor {
|
||||
private int serviceId;
|
||||
private String url;
|
||||
private UrlIdHandler urlIdHandler;
|
||||
private Downloader downloader;
|
||||
private StreamPreviewInfoCollector previewInfoCollector;
|
||||
private int page = -1;
|
||||
|
||||
public ChannelExtractor(UrlIdHandler urlIdHandler, String url, int page, Downloader dl, int serviceId)
|
||||
throws ExtractionException, IOException {
|
||||
this.url = url;
|
||||
this.page = page;
|
||||
this.downloader = dl;
|
||||
this.serviceId = serviceId;
|
||||
this.urlIdHandler = urlIdHandler;
|
||||
previewInfoCollector = new StreamPreviewInfoCollector(urlIdHandler, serviceId);
|
||||
}
|
||||
|
||||
public String getUrl() { return url; }
|
||||
public UrlIdHandler getUrlIdHandler() { return urlIdHandler; }
|
||||
public Downloader getDownloader() { return downloader; }
|
||||
public StreamPreviewInfoCollector getStreamPreviewInfoCollector() {
|
||||
return previewInfoCollector;
|
||||
}
|
||||
|
||||
public abstract String getChannelName() throws ParsingException;
|
||||
public abstract String getAvatarUrl() throws ParsingException;
|
||||
public abstract String getBannerUrl() throws ParsingException;
|
||||
public abstract String getFeedUrl() throws ParsingException;
|
||||
public abstract StreamPreviewInfoCollector getStreams() throws ParsingException;
|
||||
public abstract boolean hasNextPage() throws ParsingException;
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 31.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ChannelInfo {
|
||||
|
||||
|
||||
public void addException(Exception e) {
|
||||
errors.add(e);
|
||||
}
|
||||
|
||||
public static ChannelInfo getInfo(ChannelExtractor extractor, Downloader dl)
|
||||
throws ParsingException {
|
||||
ChannelInfo info = new ChannelInfo();
|
||||
|
||||
// importand data
|
||||
info.service_id = extractor.getServiceId();
|
||||
info.channel_name = extractor.getChannelName();
|
||||
info.hasNextPage = extractor.hasNextPage();
|
||||
|
||||
try {
|
||||
info.avatar_url = extractor.getAvatarUrl();
|
||||
} catch (Exception e) {
|
||||
info.errors.add(e);
|
||||
}
|
||||
try {
|
||||
info.banner_url = extractor.getBannerUrl();
|
||||
} catch (Exception e) {
|
||||
info.errors.add(e);
|
||||
}
|
||||
try {
|
||||
info.feed_url = extractor.getFeedUrl();
|
||||
} catch(Exception e) {
|
||||
info.errors.add(e);
|
||||
}
|
||||
try {
|
||||
StreamPreviewInfoCollector c = extractor.getStreams();
|
||||
info.related_streams = c.getItemList();
|
||||
info.errors.addAll(c.getErrors());
|
||||
} catch(Exception e) {
|
||||
info.errors.add(e);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public int service_id = -1;
|
||||
public String channel_name = "";
|
||||
public String avatar_url = "";
|
||||
public String banner_url = "";
|
||||
public String feed_url = "";
|
||||
public List<StreamPreviewInfo> related_streams = null;
|
||||
public boolean hasNextPage = false;
|
||||
|
||||
public List<Throwable> errors = new Vector<>();
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
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 {
|
||||
|
||||
private DashMpdParser() {
|
||||
}
|
||||
|
||||
static class DashMpdParsingException extends ParsingException {
|
||||
DashMpdParsingException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<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<AudioStream> audioStreams = new Vector<>();
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setInput(new StringReader(dashDoc));
|
||||
String tagName = "";
|
||||
String currentMimeType = "";
|
||||
int currentBandwidth = -1;
|
||||
int currentSamplingRate = -1;
|
||||
boolean currentTagIsBaseUrl = false;
|
||||
for(int eventType = parser.getEventType();
|
||||
eventType != XmlPullParser.END_DOCUMENT;
|
||||
eventType = parser.next() ) {
|
||||
switch(eventType) {
|
||||
case XmlPullParser.START_TAG:
|
||||
tagName = parser.getName();
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "mimeType");
|
||||
} else if(tagName.equals("Representation") && currentMimeType.contains("audio")) {
|
||||
currentBandwidth = Integer.parseInt(
|
||||
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "bandwidth"));
|
||||
currentSamplingRate = Integer.parseInt(
|
||||
parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "audioSamplingRate"));
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case XmlPullParser.TEXT:
|
||||
// actual stream tag
|
||||
if(currentTagIsBaseUrl &&
|
||||
(currentMimeType.contains("audio"))) {
|
||||
int format = -1;
|
||||
if(currentMimeType.equals(MediaFormat.WEBMA.mimeType)) {
|
||||
format = MediaFormat.WEBMA.id;
|
||||
} else if(currentMimeType.equals(MediaFormat.M4A.mimeType)) {
|
||||
format = MediaFormat.M4A.id;
|
||||
}
|
||||
audioStreams.add(new AudioStream(parser.getText(),
|
||||
format, currentBandwidth, currentSamplingRate));
|
||||
}
|
||||
break;
|
||||
|
||||
case XmlPullParser.END_TAG:
|
||||
if(tagName.equals("AdaptationSet")) {
|
||||
currentMimeType = "";
|
||||
} else if(tagName.equals("BaseURL")) {
|
||||
currentTagIsBaseUrl = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new DashMpdParsingException("Could not parse Dash mpd", e);
|
||||
}
|
||||
return audioStreams;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* Downloader.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public interface Downloader {
|
||||
|
||||
/**Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param language the language (usually a 2-character code) to set as the preferred language
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException*/
|
||||
String download(String siteUrl, String language) throws IOException;
|
||||
|
||||
/**Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param customProperties set request header properties
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException*/
|
||||
String download(String siteUrl, Map<String, String> customProperties) throws IOException;
|
||||
|
||||
/**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;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 30.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ExtractionException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ExtractionException extends Exception {
|
||||
public ExtractionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ExtractionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ExtractionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 12.09.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* FoundAdException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class FoundAdException extends ParsingException {
|
||||
public FoundAdException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public FoundAdException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
*
|
||||
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||
* and Adam Howard <achdisposable1@gmail.com> 2015
|
||||
*
|
||||
* MediaFormat.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Static data about various media formats support by Newpipe, eg mime type, extension*/
|
||||
|
||||
public enum MediaFormat {
|
||||
//video and audio combined formats
|
||||
// id name suffix mime type
|
||||
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
||||
v3GPP (0x1, "3GPP", "3gp", "video/3gpp"),
|
||||
WEBM (0x2, "WebM", "webm", "video/webm"),
|
||||
// audio formats
|
||||
M4A (0x3, "m4a", "m4a", "audio/mp4"),
|
||||
WEBMA (0x4, "WebM", "webm", "audio/webm");
|
||||
|
||||
public final int id;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public final String name;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public final String suffix;
|
||||
public final String mimeType;
|
||||
|
||||
MediaFormat(int id, String name, String suffix, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.suffix = suffix;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
/**Return the friendly name of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the friendly name of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getNameById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**Return the file extension of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the file extension of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getSuffixById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.suffix;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**Return the MIME type of the media format with the supplied id
|
||||
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the MIME type of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.*/
|
||||
public static String getMimeById(int ident) {
|
||||
for (MediaFormat vf : MediaFormat.values()) {
|
||||
if(vf.id == ident) return vf.mimeType;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* Parser.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** avoid using regex !!! */
|
||||
public class Parser {
|
||||
|
||||
private Parser() {
|
||||
}
|
||||
|
||||
public static class RegexException extends ParsingException {
|
||||
public RegexException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static String matchGroup1(String pattern, String input) throws RegexException {
|
||||
Pattern pat = Pattern.compile(pattern);
|
||||
Matcher mat = pat.matcher(input);
|
||||
boolean foundMatch = mat.find();
|
||||
if (foundMatch) {
|
||||
return mat.group(1);
|
||||
}
|
||||
else {
|
||||
//Log.e(TAG, "failed to find pattern \""+pattern+"\" inside of \""+input+"\"");
|
||||
throw new RegexException("failed to find pattern \""+pattern+" inside of "+input+"\"");
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, String> compatParseMap(final String input) throws UnsupportedEncodingException {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for(String arg : input.split("&")) {
|
||||
String[] splitArg = arg.split("=");
|
||||
if(splitArg.length > 1) {
|
||||
map.put(splitArg[0], URLDecoder.decode(splitArg[1], "UTF-8"));
|
||||
} else {
|
||||
map.put(splitArg[0], "");
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 31.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ParsingException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
public class ParsingException extends ExtractionException {
|
||||
public ParsingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public ParsingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 10.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* SearchEngine.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public abstract class SearchEngine {
|
||||
public static class NothingFoundException extends ExtractionException {
|
||||
public NothingFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private StreamPreviewInfoSearchCollector collector;
|
||||
|
||||
public SearchEngine(UrlIdHandler urlIdHandler, int serviceId) {
|
||||
collector = new StreamPreviewInfoSearchCollector(urlIdHandler, serviceId);
|
||||
}
|
||||
|
||||
protected StreamPreviewInfoSearchCollector getStreamPreviewInfoSearchCollector() {
|
||||
return collector;
|
||||
}
|
||||
|
||||
public abstract List<String> suggestionList(
|
||||
String query,String contentCountry, Downloader dl)
|
||||
throws ExtractionException, IOException;
|
||||
|
||||
//Result search(String query, int page);
|
||||
public abstract StreamPreviewInfoSearchCollector search(
|
||||
String query, int page, String contentCountry, Downloader dl)
|
||||
throws ExtractionException, IOException;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 29.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SearchResult.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SearchResult {
|
||||
public static SearchResult getSearchResult(SearchEngine engine, String query,
|
||||
int page, String languageCode, Downloader dl)
|
||||
throws ExtractionException, IOException {
|
||||
|
||||
SearchResult result = engine.search(query, page, languageCode, dl).getSearchResult();
|
||||
if(result.resultList.isEmpty()) {
|
||||
if(result.suggestion.isEmpty()) {
|
||||
throw new ExtractionException("Empty result despite no error");
|
||||
} else {
|
||||
// This is used as a fallback. Do not relay on it !!!
|
||||
throw new SearchEngine.NothingFoundException(result.suggestion);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String suggestion = "";
|
||||
public List<StreamPreviewInfo> resultList = new Vector<>();
|
||||
public List<Throwable> errors = new Vector<>();
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ServiceList.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Provides access to the video streaming services supported by NewPipe.
|
||||
* Currently only Youtube until the API becomes more stable.*/
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class ServiceList {
|
||||
|
||||
private ServiceList() {
|
||||
}
|
||||
|
||||
private static final String TAG = ServiceList.class.toString();
|
||||
private static final StreamingService[] services = {
|
||||
new YoutubeService(0)
|
||||
};
|
||||
public static StreamingService[] getServices() {
|
||||
return services;
|
||||
}
|
||||
public static StreamingService getService(int serviceId)throws ExtractionException {
|
||||
for(StreamingService s : services) {
|
||||
if(s.getServiceId() == serviceId) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
throw new ExtractionException("Service not known: " + Integer.toString(serviceId));
|
||||
}
|
||||
public static StreamingService getService(String serviceName) throws ExtractionException {
|
||||
return services[getIdOfService(serviceName)];
|
||||
}
|
||||
public static String getNameOfService(int id) {
|
||||
try {
|
||||
return getService(id).getServiceInfo().name;
|
||||
} catch (Exception e) {
|
||||
System.err.println("Service id not known");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
public static int getIdOfService(String serviceName) throws ExtractionException {
|
||||
for(int i = 0; i < services.length; i++) {
|
||||
if(services[i].getServiceInfo().name.equals(serviceName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw new ExtractionException("Error: Service " + serviceName + " not known.");
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 10.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**Scrapes information from a video streaming service (eg, YouTube).*/
|
||||
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public abstract class StreamExtractor {
|
||||
|
||||
private int serviceId;
|
||||
private String url;
|
||||
private UrlIdHandler urlIdHandler;
|
||||
private Downloader downloader;
|
||||
private StreamPreviewInfoCollector previewInfoCollector;
|
||||
|
||||
public class ExctractorInitException extends ExtractionException {
|
||||
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(String message) {
|
||||
super(message);
|
||||
}
|
||||
public ContentNotAvailableException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public StreamExtractor(UrlIdHandler urlIdHandler, String url, Downloader dl, int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
this.urlIdHandler = urlIdHandler;
|
||||
previewInfoCollector = new StreamPreviewInfoCollector(urlIdHandler, serviceId);
|
||||
}
|
||||
|
||||
protected StreamPreviewInfoCollector getStreamPreviewInfoCollector() {
|
||||
return previewInfoCollector;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public UrlIdHandler getUrlIdHandler() {
|
||||
return urlIdHandler;
|
||||
}
|
||||
|
||||
public Downloader getDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
public abstract int getTimeStamp() throws ParsingException;
|
||||
public abstract String getTitle() throws ParsingException;
|
||||
public abstract String getDescription() throws ParsingException;
|
||||
public abstract String getUploader() throws ParsingException;
|
||||
public abstract String getChannelUrl() throws ParsingException;
|
||||
public abstract int getLength() throws ParsingException;
|
||||
public abstract long getViewCount() throws ParsingException;
|
||||
public abstract String getUploadDate() throws ParsingException;
|
||||
public abstract String getThumbnailUrl() throws ParsingException;
|
||||
public abstract String getUploaderThumbnailUrl() throws ParsingException;
|
||||
public abstract List<AudioStream> getAudioStreams() throws ParsingException;
|
||||
public abstract List<VideoStream> getVideoStreams() throws ParsingException;
|
||||
public abstract List<VideoStream> getVideoOnlyStreams() throws ParsingException;
|
||||
public abstract String getDashMpdUrl() throws ParsingException;
|
||||
public abstract int getAgeLimit() throws ParsingException;
|
||||
public abstract String getAverageRating() throws ParsingException;
|
||||
public abstract int getLikeCount() throws ParsingException;
|
||||
public abstract int getDislikeCount() throws ParsingException;
|
||||
public abstract StreamPreviewInfoExtractor getNextVideo() throws ParsingException;
|
||||
public abstract StreamPreviewInfoCollector getRelatedVideos() throws ParsingException;
|
||||
public abstract String getPageUrl();
|
||||
public abstract StreamInfo.StreamType getStreamType() throws ParsingException;
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
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 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Info object for opened videos, ie the video ready to play.*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class StreamInfo extends AbstractVideoInfo {
|
||||
|
||||
public static class StreamExctractException extends ExtractionException {
|
||||
StreamExctractException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public StreamInfo() {}
|
||||
|
||||
/**Creates a new StreamInfo object from an existing AbstractVideoInfo.
|
||||
* All the shared properties are copied to the new StreamInfo.*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public StreamInfo(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 StreamPreviewInfo) {
|
||||
//shitty String to convert code
|
||||
/*
|
||||
String dur = ((StreamPreviewInfo)avi).duration;
|
||||
int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":")));
|
||||
int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length()));
|
||||
*/
|
||||
this.duration = ((StreamPreviewInfo)avi).duration;
|
||||
}
|
||||
}
|
||||
|
||||
public void addException(Exception e) {
|
||||
errors.add(e);
|
||||
}
|
||||
|
||||
/**Fills out the video info fields which are common to all services.
|
||||
* Probably needs to be overridden by subclasses*/
|
||||
public static StreamInfo getVideoInfo(StreamExtractor extractor, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
StreamInfo streamInfo = new StreamInfo();
|
||||
|
||||
streamInfo = extractImportantData(streamInfo, extractor, downloader);
|
||||
streamInfo = extractStreams(streamInfo, extractor, downloader);
|
||||
streamInfo = extractOptionalData(streamInfo, extractor, downloader);
|
||||
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
private static StreamInfo extractImportantData(
|
||||
StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader)
|
||||
throws ExtractionException, 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.
|
||||
|
||||
UrlIdHandler uiconv = extractor.getUrlIdHandler();
|
||||
|
||||
streamInfo.service_id = extractor.getServiceId();
|
||||
streamInfo.webpage_url = extractor.getPageUrl();
|
||||
streamInfo.stream_type = extractor.getStreamType();
|
||||
streamInfo.id = uiconv.getId(extractor.getPageUrl());
|
||||
streamInfo.title = extractor.getTitle();
|
||||
streamInfo.age_limit = extractor.getAgeLimit();
|
||||
|
||||
if((streamInfo.stream_type == StreamType.NONE)
|
||||
|| (streamInfo.webpage_url == null || streamInfo.webpage_url.isEmpty())
|
||||
|| (streamInfo.id == null || streamInfo.id.isEmpty())
|
||||
|| (streamInfo.title == null /* streamInfo.title can be empty of course */)
|
||||
|| (streamInfo.age_limit == -1)) {
|
||||
throw new ExtractionException("Some importand stream information was not given.");
|
||||
}
|
||||
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
private static StreamInfo extractStreams(
|
||||
StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
/* ---- stream extraction goes here ---- */
|
||||
// At least one type of stream has to be available,
|
||||
// otherwise an exception will be thrown directly into the frontend.
|
||||
|
||||
try {
|
||||
streamInfo.dashMpdUrl = extractor.getDashMpdUrl();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(new ExtractionException("Couldn't get Dash manifest", e));
|
||||
}
|
||||
|
||||
/* Load and extract audio */
|
||||
try {
|
||||
streamInfo.audio_streams = extractor.getAudioStreams();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(new ExtractionException("Couldn't get audio streams", e));
|
||||
}
|
||||
// also try to get streams from the dashMpd
|
||||
if(streamInfo.dashMpdUrl != null && !streamInfo.dashMpdUrl.isEmpty()) {
|
||||
if(streamInfo.audio_streams == null) {
|
||||
streamInfo.audio_streams = new Vector<>();
|
||||
}
|
||||
//todo: make this quick and dirty solution a real fallback
|
||||
// same as the quick and dirty aboth
|
||||
try {
|
||||
streamInfo.audio_streams.addAll(
|
||||
DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl, downloader));
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(
|
||||
new ExtractionException("Couldn't get audio streams from dash mpd", e));
|
||||
}
|
||||
}
|
||||
/* Extract video stream url*/
|
||||
try {
|
||||
streamInfo.video_streams = extractor.getVideoStreams();
|
||||
} catch (Exception e) {
|
||||
streamInfo.addException(
|
||||
new ExtractionException("Couldn't get video streams", e));
|
||||
}
|
||||
/* Extract video only stream url*/
|
||||
try {
|
||||
streamInfo.video_only_streams = extractor.getVideoOnlyStreams();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(
|
||||
new ExtractionException("Couldn't get video only streams", e));
|
||||
}
|
||||
|
||||
// either dash_mpd audio_only or video has to be available, otherwise we didn't get a stream,
|
||||
// and therefore failed. (Since video_only_streams are just optional they don't caunt).
|
||||
if((streamInfo.video_streams == null || streamInfo.video_streams.isEmpty())
|
||||
&& (streamInfo.audio_streams == null || streamInfo.audio_streams.isEmpty())
|
||||
&& (streamInfo.dashMpdUrl == null || streamInfo.dashMpdUrl.isEmpty())) {
|
||||
throw new StreamExctractException(
|
||||
"Could not get any stream. See error variable to get further details.");
|
||||
}
|
||||
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
private static StreamInfo extractOptionalData(
|
||||
StreamInfo streamInfo, StreamExtractor extractor, 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 {
|
||||
streamInfo.thumbnail_url = extractor.getThumbnailUrl();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.duration = extractor.getLength();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.uploader = extractor.getUploader();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.channel_url = extractor.getChannelUrl();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.description = extractor.getDescription();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.view_count = extractor.getViewCount();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.upload_date = extractor.getUploadDate();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.start_position = extractor.getTimeStamp();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.average_rating = extractor.getAverageRating();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.like_count = extractor.getLikeCount();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
streamInfo.dislike_count = extractor.getDislikeCount();
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
// get next video
|
||||
if(streamInfo.next_video != null)
|
||||
{
|
||||
StreamPreviewInfoCollector c = new StreamPreviewInfoCollector(
|
||||
extractor.getUrlIdHandler(), extractor.getServiceId());
|
||||
StreamPreviewInfoExtractor nextVideo = extractor.getNextVideo();
|
||||
c.commit(nextVideo);
|
||||
if(c.getItemList().size() != 0) {
|
||||
streamInfo.next_video = c.getItemList().get(0);
|
||||
}
|
||||
streamInfo.errors.addAll(c.getErrors());
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
// get related videos
|
||||
StreamPreviewInfoCollector c = extractor.getRelatedVideos();
|
||||
streamInfo.related_streams = c.getItemList();
|
||||
streamInfo.errors.addAll(c.getErrors());
|
||||
} catch(Exception e) {
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
public String uploader_thumbnail_url = "";
|
||||
public String channel_url = "";
|
||||
public String description = "";
|
||||
|
||||
public List<VideoStream> video_streams = null;
|
||||
public List<AudioStream> audio_streams = null;
|
||||
public List<VideoStream> video_only_streams = null;
|
||||
// video streams provided by the dash mpd do not need to be provided as VideoStream.
|
||||
// Later on this will also aplly to audio streams. Since dash mpd is standarized,
|
||||
// crawling such a file is not service dependent. Therefore getting audio only streams by yust
|
||||
// providing the dash mpd fille will be possible in the future.
|
||||
public String dashMpdUrl = "";
|
||||
public int duration = -1;
|
||||
|
||||
public int age_limit = -1;
|
||||
public int like_count = -1;
|
||||
public int dislike_count = -1;
|
||||
public String average_rating = "";
|
||||
public StreamPreviewInfo next_video = null;
|
||||
public List<StreamPreviewInfo> related_streams = null;
|
||||
//in seconds. some metadata is not passed using a StreamInfo object!
|
||||
public int start_position = 0;
|
||||
|
||||
public List<Throwable> errors = new Vector<>();
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Info object for previews of unopened videos, eg search results, related videos*/
|
||||
public class StreamPreviewInfo extends AbstractVideoInfo {
|
||||
public int duration;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamUrlIdHandler;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfoCollector.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class StreamPreviewInfoCollector {
|
||||
private List<StreamPreviewInfo> itemList = new Vector<>();
|
||||
private List<Throwable> errors = new Vector<>();
|
||||
private UrlIdHandler urlIdHandler;
|
||||
private int serviceId = -1;
|
||||
|
||||
public StreamPreviewInfoCollector(UrlIdHandler handler, int serviceId) {
|
||||
urlIdHandler = handler;
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public List<StreamPreviewInfo> getItemList() {
|
||||
return itemList;
|
||||
}
|
||||
|
||||
public List<Throwable> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
public void addError(Exception e) {
|
||||
errors.add(e);
|
||||
}
|
||||
|
||||
public void commit(StreamPreviewInfoExtractor extractor) throws ParsingException {
|
||||
try {
|
||||
StreamPreviewInfo resultItem = new StreamPreviewInfo();
|
||||
// importand information
|
||||
resultItem.service_id = serviceId;
|
||||
resultItem.webpage_url = extractor.getWebPageUrl();
|
||||
if (urlIdHandler == null) {
|
||||
throw new ParsingException("Error: UrlIdHandler not set");
|
||||
} else if(!resultItem.webpage_url.isEmpty()) {
|
||||
resultItem.id = (new YoutubeStreamUrlIdHandler()).getId(resultItem.webpage_url);
|
||||
}
|
||||
resultItem.title = extractor.getTitle();
|
||||
resultItem.stream_type = extractor.getStreamType();
|
||||
|
||||
// optional information
|
||||
try {
|
||||
resultItem.duration = extractor.getDuration();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.uploader = extractor.getUploader();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.upload_date = extractor.getUploadDate();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.view_count = extractor.getViewCount();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.thumbnail_url = extractor.getThumbnailUrl();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
itemList.add(resultItem);
|
||||
} catch(FoundAdException ae) {
|
||||
System.out.println("AD_WARNING: " + ae.getMessage());
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfoExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public interface StreamPreviewInfoExtractor {
|
||||
AbstractVideoInfo.StreamType getStreamType() throws ParsingException;
|
||||
String getWebPageUrl() throws ParsingException;
|
||||
String getTitle() throws ParsingException;
|
||||
int getDuration() throws ParsingException;
|
||||
String getUploader() throws ParsingException;
|
||||
String getUploadDate() throws ParsingException;
|
||||
long getViewCount() throws ParsingException;
|
||||
String getThumbnailUrl() throws ParsingException;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 11.05.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfoSearchCollector.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class StreamPreviewInfoSearchCollector extends StreamPreviewInfoCollector {
|
||||
|
||||
private String suggestion = "";
|
||||
|
||||
public StreamPreviewInfoSearchCollector(UrlIdHandler handler, int serviceId) {
|
||||
super(handler, serviceId);
|
||||
}
|
||||
|
||||
public void setSuggestion(String suggestion) {
|
||||
this.suggestion = suggestion;
|
||||
}
|
||||
|
||||
public SearchResult getSearchResult() {
|
||||
SearchResult result = new SearchResult();
|
||||
result.suggestion = suggestion;
|
||||
result.errors = getErrors();
|
||||
result.resultList = getItemList();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamingService.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class StreamingService {
|
||||
public class ServiceInfo {
|
||||
public String name = "";
|
||||
}
|
||||
|
||||
private int serviceId;
|
||||
|
||||
public StreamingService(int id) {
|
||||
serviceId = id;
|
||||
}
|
||||
|
||||
public abstract ServiceInfo getServiceInfo();
|
||||
|
||||
public abstract StreamExtractor getExtractorInstance(String url, Downloader downloader)
|
||||
throws IOException, ExtractionException;
|
||||
public abstract SearchEngine getSearchEngineInstance(Downloader downloader);
|
||||
public abstract UrlIdHandler getUrlIdHandlerInstance();
|
||||
public abstract UrlIdHandler getChannelUrlIdHandlerInstance();
|
||||
public abstract ChannelExtractor getChannelExtractorInstance(String url, int page, Downloader downloader)
|
||||
throws ExtractionException, IOException;
|
||||
|
||||
public final int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* UrlIdHandler.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public interface UrlIdHandler {
|
||||
String getUrl(String videoId);
|
||||
String getId(String siteUrl) throws ParsingException;
|
||||
String cleanUrl(String siteUrl) throws ParsingException;
|
||||
|
||||
/**When a VIEW_ACTION is caught this function will test if the url delivered within the calling
|
||||
Intent was meant to be watched with this Service.
|
||||
Return false if this service shall not allow to be called through ACTIONs.*/
|
||||
boolean acceptUrl(String videoUrl);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 04.03.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* VideoStream.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class VideoStream {
|
||||
//url of the stream
|
||||
public String url = "";
|
||||
public int format = -1;
|
||||
public String resolution = "";
|
||||
|
||||
public VideoStream(String url, int format, String res) {
|
||||
this.url = url; this.format = format; resolution = res;
|
||||
}
|
||||
|
||||
// reveals wether two streams are the same, but have diferent urls
|
||||
public boolean equalStats(VideoStream cmp) {
|
||||
return format == cmp.format
|
||||
&& resolution == cmp.resolution;
|
||||
}
|
||||
|
||||
// revelas wether two streams are equal
|
||||
public boolean equals(VideoStream cmp) {
|
||||
return cmp != null && equalStats(cmp)
|
||||
&& url == cmp.url;
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoCollector;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 25.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeChannelExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeChannelExtractor extends ChannelExtractor {
|
||||
|
||||
private static final String TAG = YoutubeChannelExtractor.class.toString();
|
||||
|
||||
// private CSSOMParser cssParser = new CSSOMParser(new SACParserCSS3());
|
||||
|
||||
private Downloader downloader;
|
||||
private Document doc = null;
|
||||
|
||||
private boolean isAjaxPage = false;
|
||||
private static String userUrl = "";
|
||||
private static String channelName = "";
|
||||
private static String avatarUrl = "";
|
||||
private static String bannerUrl = "";
|
||||
private static String feedUrl = "";
|
||||
// the fist page is html all other pages are ajax. Every new page can be requested by sending
|
||||
// this request url.
|
||||
private static String nextPageUrl = "";
|
||||
|
||||
public YoutubeChannelExtractor(UrlIdHandler urlIdHandler, String url, int page, Downloader dl, int serviceId)
|
||||
throws ExtractionException, IOException {
|
||||
super(urlIdHandler, url, page, dl, serviceId);
|
||||
|
||||
url = urlIdHandler.cleanUrl(url) ; //+ "/video?veiw=0&flow=list&sort=dd";
|
||||
downloader = dl;
|
||||
|
||||
if(page == 0) {
|
||||
if (isUserUrl(url)) {
|
||||
userUrl = url;
|
||||
} else {
|
||||
// we first need to get the user url. Otherwise we can't find videos
|
||||
String channelPageContent = downloader.download(url);
|
||||
Document channelDoc = Jsoup.parse(channelPageContent, url);
|
||||
userUrl = getUserUrl(channelDoc);
|
||||
}
|
||||
|
||||
userUrl = userUrl + "/videos?veiw=0&flow=list&sort=dd&live_view=10000";
|
||||
String pageContent = downloader.download(userUrl);
|
||||
doc = Jsoup.parse(pageContent, userUrl);
|
||||
nextPageUrl = getNextPageUrl(doc);
|
||||
isAjaxPage = false;
|
||||
} else {
|
||||
String ajaxDataRaw = downloader.download(nextPageUrl);
|
||||
JSONObject ajaxData;
|
||||
try {
|
||||
ajaxData = new JSONObject(ajaxDataRaw);
|
||||
String htmlDataRaw = ajaxData.getString("content_html");
|
||||
doc = Jsoup.parse(htmlDataRaw, nextPageUrl);
|
||||
|
||||
String nextPageHtmlDataRaw = ajaxData.getString("load_more_widget_html");
|
||||
if(!nextPageHtmlDataRaw.isEmpty()) {
|
||||
Document nextPageData = Jsoup.parse(nextPageHtmlDataRaw, nextPageUrl);
|
||||
nextPageUrl = getNextPageUrl(nextPageData);
|
||||
} else {
|
||||
nextPageUrl = "";
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not parse json data for next page", e);
|
||||
}
|
||||
isAjaxPage = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannelName() throws ParsingException {
|
||||
try {
|
||||
if(!isAjaxPage) {
|
||||
channelName = doc.select("span[class=\"qualified-channel-title-text\"]").first()
|
||||
.select("a").first().text();
|
||||
}
|
||||
return channelName;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get channel name");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarUrl() throws ParsingException {
|
||||
try {
|
||||
if(!isAjaxPage) {
|
||||
avatarUrl = doc.select("img[class=\"channel-header-profile-image\"]")
|
||||
.first().attr("abs:src");
|
||||
}
|
||||
return avatarUrl;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get avatar", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBannerUrl() throws ParsingException {
|
||||
try {
|
||||
if(!isAjaxPage) {
|
||||
Element el = doc.select("div[id=\"gh-banner\"]").first().select("style").first();
|
||||
String cssContent = el.html();
|
||||
String url = "https:" + Parser.matchGroup1("url\\((.*)\\)", cssContent);
|
||||
if (url.contains("s.ytimg.com")) {
|
||||
bannerUrl = null;
|
||||
} else {
|
||||
bannerUrl = url;
|
||||
}
|
||||
}
|
||||
return bannerUrl;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get Banner", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamPreviewInfoCollector getStreams() throws ParsingException {
|
||||
StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector();
|
||||
Element ul = null;
|
||||
if(isAjaxPage) {
|
||||
ul = doc.select("body").first();
|
||||
} else {
|
||||
ul = doc.select("ul[id=\"browse-items-primary\"]").first();
|
||||
}
|
||||
|
||||
for(final Element li : ul.children()) {
|
||||
if (li.select("div[class=\"feed-item-dismissable\"]").first() != null) {
|
||||
collector.commit(new StreamPreviewInfoExtractor() {
|
||||
@Override
|
||||
public AbstractVideoInfo.StreamType getStreamType() throws ParsingException {
|
||||
return AbstractVideoInfo.StreamType.VIDEO_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebPageUrl() throws ParsingException {
|
||||
try {
|
||||
Element el = li.select("div[class=\"feed-item-dismissable\"]").first();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
return dl.attr("abs:href");
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get web page url for the video", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() throws ParsingException {
|
||||
try {
|
||||
Element el = li.select("div[class=\"feed-item-dismissable\"]").first();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
return dl.text();
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get title", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() throws ParsingException {
|
||||
try {
|
||||
return YoutubeParsingHelper.parseDurationString(
|
||||
li.select("span[class=\"video-time\"]").first().text());
|
||||
} catch(Exception e) {
|
||||
if(isLiveStream(li)) {
|
||||
// -1 for no duration
|
||||
return -1;
|
||||
} else {
|
||||
throw new ParsingException("Could not get Duration: " + getTitle(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploader() throws ParsingException {
|
||||
return getChannelName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploadDate() throws ParsingException {
|
||||
try {
|
||||
return li.select("div[class=\"yt-lockup-meta\"]").first()
|
||||
.select("li").first()
|
||||
.text();
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get uplaod date", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
String output;
|
||||
String input;
|
||||
try {
|
||||
input = li.select("div[class=\"yt-lockup-meta\"]").first()
|
||||
.select("li").get(1)
|
||||
.text();
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
if(isLiveStream(li)) {
|
||||
// -1 for no view count
|
||||
return -1;
|
||||
} else {
|
||||
throw new ParsingException(
|
||||
"Could not parse yt-lockup-meta although available: " + getTitle(), e);
|
||||
}
|
||||
}
|
||||
|
||||
output = Parser.matchGroup1("([0-9,\\. ]*)", input)
|
||||
.replace(" ", "")
|
||||
.replace(".", "")
|
||||
.replace(",", "");
|
||||
|
||||
try {
|
||||
return Long.parseLong(output);
|
||||
} catch (NumberFormatException e) {
|
||||
// if this happens the video probably has no views
|
||||
if(!input.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
throw new ParsingException("Could not handle input: " + input, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
try {
|
||||
String url;
|
||||
Element te = li.select("span[class=\"yt-thumb-clip\"]").first()
|
||||
.select("img").first();
|
||||
url = te.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files which somehow seem to not exist
|
||||
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||
// to use that if we've caught such an item.
|
||||
if (url.contains(".gif")) {
|
||||
url = te.attr("abs:data-thumb");
|
||||
}
|
||||
return url;
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get thumbnail url", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLiveStream(Element item) {
|
||||
Element bla = item.select("span[class*=\"yt-badge-live\"]").first();
|
||||
|
||||
if(bla == null) {
|
||||
// sometimes livestreams dont have badges but sill are live streams
|
||||
// if video time is not available we most likly have an offline livestream
|
||||
if(item.select("span[class*=\"video-time\"]").first() == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return bla != null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return collector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeedUrl() throws ParsingException {
|
||||
try {
|
||||
if(userUrl.contains("channel")) {
|
||||
//channels don't have feeds in youtube, only user can provide such
|
||||
return "";
|
||||
}
|
||||
if(!isAjaxPage) {
|
||||
feedUrl = doc.select("link[title=\"RSS\"]").first().attr("abs:href");
|
||||
}
|
||||
return feedUrl;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get feed url", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNextPage() throws ParsingException {
|
||||
return !nextPageUrl.isEmpty();
|
||||
}
|
||||
|
||||
private String getUserUrl(Document d) throws ParsingException {
|
||||
return d.select("span[class=\"qualified-channel-title-text\"]").first()
|
||||
.select("a").first().attr("abs:href");
|
||||
}
|
||||
|
||||
private boolean isUserUrl(String url) throws ParsingException {
|
||||
return url.contains("/user/");
|
||||
}
|
||||
|
||||
private String getNextPageUrl(Document d) throws ParsingException {
|
||||
try {
|
||||
Element button = d.select("button[class*=\"yt-uix-load-more\"]").first();
|
||||
if(button != null) {
|
||||
return button.attr("abs:data-uix-load-more-href");
|
||||
} else {
|
||||
// sometimes channels are simply so small, they don't have a second/next4q page
|
||||
return "";
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("could not load next page url", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 25.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeChannelUrlIdHandler.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeChannelUrlIdHandler implements UrlIdHandler {
|
||||
|
||||
public String getUrl(String channelId) {
|
||||
return "https://www.youtube.com/" + channelId;
|
||||
}
|
||||
|
||||
public String getId(String siteUrl) throws ParsingException {
|
||||
return Parser.matchGroup1("/(user/[A-Za-z0-9_-]*|channel/[A-Za-z0-9_-]*)", siteUrl);
|
||||
}
|
||||
|
||||
public String cleanUrl(String siteUrl) throws ParsingException {
|
||||
return getUrl(getId(siteUrl));
|
||||
}
|
||||
|
||||
public boolean acceptUrl(String videoUrl) {
|
||||
return (videoUrl.contains("youtube") ||
|
||||
videoUrl.contains("youtu.be")) &&
|
||||
( videoUrl.contains("/user/") ||
|
||||
videoUrl.contains("/channel/"));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.03.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeParsingHelper.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeParsingHelper {
|
||||
|
||||
private YoutubeParsingHelper() {
|
||||
}
|
||||
|
||||
public static int parseDurationString(String input)
|
||||
throws ParsingException, NumberFormatException {
|
||||
String[] splitInput = input.split(":");
|
||||
String days = "0";
|
||||
String hours = "0";
|
||||
String minutes = "0";
|
||||
String seconds;
|
||||
|
||||
switch(splitInput.length) {
|
||||
case 4:
|
||||
days = splitInput[0];
|
||||
hours = splitInput[1];
|
||||
minutes = splitInput[2];
|
||||
seconds = splitInput[3];
|
||||
break;
|
||||
case 3:
|
||||
hours = splitInput[0];
|
||||
minutes = splitInput[1];
|
||||
seconds = splitInput[2];
|
||||
break;
|
||||
case 2:
|
||||
minutes = splitInput[0];
|
||||
seconds = splitInput[1];
|
||||
break;
|
||||
case 1:
|
||||
seconds = splitInput[0];
|
||||
break;
|
||||
default:
|
||||
throw new ParsingException("Error duration string with unknown format: " + input);
|
||||
}
|
||||
return ((((Integer.parseInt(days) * 24)
|
||||
+ Integer.parseInt(hours) * 60)
|
||||
+ Integer.parseInt(minutes)) * 60)
|
||||
+ Integer.parseInt(seconds);
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoCollector;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoSearchCollector;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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 extends SearchEngine {
|
||||
|
||||
private static final String TAG = YoutubeSearchEngine.class.toString();
|
||||
public static final String CHARSET_UTF_8 = "UTF-8";
|
||||
|
||||
public YoutubeSearchEngine(UrlIdHandler urlIdHandler, int serviceId) {
|
||||
super(urlIdHandler, serviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamPreviewInfoSearchCollector search(String query, int page, String languageCode, Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
StreamPreviewInfoSearchCollector collector = getStreamPreviewInfoSearchCollector();
|
||||
|
||||
/* Cant use Uri.Bilder since it's android code.
|
||||
// Android code is baned from the extractor side.
|
||||
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 = "https://www.youtube.com/results"
|
||||
+ "?search_query=" + URLEncoder.encode(query, CHARSET_UTF_8)
|
||||
+ "&page=" + Integer.toString(page)
|
||||
+ "&filters=" + "video";
|
||||
|
||||
String site;
|
||||
//String url = builder.build().toString();
|
||||
//if we've been passed a valid language code, append it to the URL
|
||||
if(!languageCode.isEmpty()) {
|
||||
//assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode);
|
||||
site = downloader.download(url, languageCode);
|
||||
}
|
||||
else {
|
||||
site = downloader.download(url);
|
||||
}
|
||||
|
||||
Document doc = Jsoup.parse(site, url);
|
||||
Element list = doc.select("ol[class=\"item-section\"]").first();
|
||||
|
||||
for (Element item : list.children()) {
|
||||
/* First we need to determine which kind of item we are working with.
|
||||
Youtube depicts five different kinds of items on its search result page. These are
|
||||
regular videos, playlists, channels, two types of video suggestions, and a "no video
|
||||
found" item. Since we only want videos, we need to filter out all the others.
|
||||
An example for this can be seen here:
|
||||
https://www.youtube.com/results?search_query=asdf&page=1
|
||||
|
||||
We already applied a filter to the url, so we don't need to care about channels and
|
||||
playlists now.
|
||||
*/
|
||||
|
||||
Element el;
|
||||
|
||||
// both types of spell correction item
|
||||
if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) {
|
||||
collector.setSuggestion(el.select("a").first().text());
|
||||
if(list.children().size() == 1) {
|
||||
throw new NothingFoundException("Did you mean: " + el.select("a").first().text());
|
||||
}
|
||||
// search message item
|
||||
} else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) {
|
||||
//result.errorMessage = el.text();
|
||||
throw new NothingFoundException(el.text());
|
||||
|
||||
// video item type
|
||||
} else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
|
||||
collector.commit(extractPreviewInfo(el));
|
||||
} else {
|
||||
//noinspection ConstantConditions
|
||||
collector.addError(new Exception("unexpected element found:\"" + el + "\""));
|
||||
}
|
||||
}
|
||||
|
||||
return collector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggestionList(String query, String contentCountry, Downloader dl)
|
||||
throws IOException, ParsingException {
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
|
||||
/* Cant use Uri.Bilder since it's android code.
|
||||
// Android code is baned from the extractor side.
|
||||
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("hl",contentCountry)
|
||||
.appendQueryParameter("q", query);
|
||||
*/
|
||||
String url = "https://suggestqueries.google.com/complete/search"
|
||||
+ "?client=" + ""
|
||||
+ "&output=" + "toolbar"
|
||||
+ "&ds=" + "yt"
|
||||
+ "&hl=" + URLEncoder.encode(contentCountry, CHARSET_UTF_8)
|
||||
+ "&q=" + URLEncoder.encode(query, CHARSET_UTF_8);
|
||||
|
||||
|
||||
String response = dl.download(url);
|
||||
|
||||
//TODO: Parse xml data using Jsoup not done
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder;
|
||||
org.w3c.dom.Document doc = null;
|
||||
|
||||
try {
|
||||
dBuilder = dbFactory.newDocumentBuilder();
|
||||
doc = dBuilder.parse(new InputSource(
|
||||
new ByteArrayInputStream(response.getBytes(CHARSET_UTF_8))));
|
||||
doc.getDocumentElement().normalize();
|
||||
} catch (ParserConfigurationException | SAXException | IOException e) {
|
||||
throw new ParsingException("Could not parse document.");
|
||||
}
|
||||
|
||||
try {
|
||||
NodeList nList = doc.getElementsByTagName("CompleteSuggestion");
|
||||
for (int temp = 0; temp < nList.getLength(); temp++) {
|
||||
|
||||
NodeList nList1 = doc.getElementsByTagName("suggestion");
|
||||
Node nNode1 = nList1.item(temp);
|
||||
if (nNode1.getNodeType() == Node.ELEMENT_NODE) {
|
||||
org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1;
|
||||
suggestions.add(eElement.getAttribute("data"));
|
||||
}
|
||||
}
|
||||
return suggestions;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get suggestions form document.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private StreamPreviewInfoExtractor extractPreviewInfo(final Element item) {
|
||||
return new YoutubeStreamPreviewInfoExtractor(item);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import org.schabi.newpipe.extractor.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeService.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeService extends StreamingService {
|
||||
|
||||
public YoutubeService(int id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceInfo getServiceInfo() {
|
||||
ServiceInfo serviceInfo = new ServiceInfo();
|
||||
serviceInfo.name = "Youtube";
|
||||
return serviceInfo;
|
||||
}
|
||||
@Override
|
||||
public StreamExtractor getExtractorInstance(String url, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
UrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler();
|
||||
if(urlIdHandler.acceptUrl(url)) {
|
||||
return new YoutubeStreamExtractor(urlIdHandler, url, downloader, getServiceId());
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("supplied String is not a valid Youtube URL");
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public SearchEngine getSearchEngineInstance(Downloader downloader) {
|
||||
return new YoutubeSearchEngine(getUrlIdHandlerInstance(), getServiceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UrlIdHandler getUrlIdHandlerInstance() {
|
||||
return new YoutubeStreamUrlIdHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UrlIdHandler getChannelUrlIdHandlerInstance() {
|
||||
return new YoutubeChannelUrlIdHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelExtractor getChannelExtractorInstance(String url, int page, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
return new YoutubeChannelExtractor(getChannelUrlIdHandlerInstance(), url, page, downloader, getServiceId());
|
||||
}
|
||||
}
|
||||
@@ -1,849 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.Function;
|
||||
import org.mozilla.javascript.ScriptableObject;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.AudioStream;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoCollector;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.VideoStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 06.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeStreamExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeStreamExtractor extends StreamExtractor {
|
||||
public static final String URL_ENCODED_FMT_STREAM_MAP = "url_encoded_fmt_stream_map";
|
||||
public static final String HTTPS = "https:";
|
||||
public static final String CONTENT = "content";
|
||||
public static final String REGEX_INT = "[^\\d]";
|
||||
|
||||
// exceptions
|
||||
|
||||
public class DecryptException extends ParsingException {
|
||||
DecryptException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
// special content not available exceptions
|
||||
|
||||
public class GemaException extends ContentNotAvailableException {
|
||||
GemaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public class LiveStreamException extends ContentNotAvailableException {
|
||||
LiveStreamException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
|
||||
// Sometimes if the html page of youtube is already downloaded, youtube web page will internally
|
||||
// download the /get_video_info page. Since a certain date dashmpd url is only available over
|
||||
// this /get_video_info page, so we always need to download this one to.
|
||||
// %%video_id%% will be replaced by the actual video id
|
||||
// $$el_type$$ will be replaced by the actual el_type (se the declarations below)
|
||||
private static final String GET_VIDEO_INFO_URL =
|
||||
"https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en";
|
||||
// eltype is 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;
|
||||
public int fps = -1;
|
||||
public int samplingRate = -1;
|
||||
public int bandWidth = -1;
|
||||
}
|
||||
|
||||
private static final ItagItem[] itagList = {
|
||||
// video streams
|
||||
// id, ItagType, MediaFormat, Resolution, fps
|
||||
new ItagItem(17, ItagType.VIDEO, MediaFormat.v3GPP, "144p", 12),
|
||||
new ItagItem(18, ItagType.VIDEO, MediaFormat.MPEG_4, "360p", 24),
|
||||
new ItagItem(22, ItagType.VIDEO, MediaFormat.MPEG_4, "720p", 24),
|
||||
new ItagItem(36, ItagType.VIDEO, MediaFormat.v3GPP, "240p", 24),
|
||||
new ItagItem(37, ItagType.VIDEO, MediaFormat.MPEG_4, "1080p", 24),
|
||||
new ItagItem(38, ItagType.VIDEO, MediaFormat.MPEG_4, "1080p", 24),
|
||||
new ItagItem(43, ItagType.VIDEO, MediaFormat.WEBM, "360p", 24),
|
||||
new ItagItem(44, ItagType.VIDEO, MediaFormat.WEBM, "480p", 24),
|
||||
new ItagItem(45, ItagType.VIDEO, MediaFormat.WEBM, "720p", 24),
|
||||
new ItagItem(46, ItagType.VIDEO, MediaFormat.WEBM, "1080p", 24),
|
||||
// audio streams
|
||||
// id, ItagType, MediaFormat, samplingR, bandwidth
|
||||
new ItagItem(249, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0), // bandwith/samplingR 0 because not known
|
||||
new ItagItem(250, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0),
|
||||
new ItagItem(171, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0),
|
||||
new ItagItem(140, ItagType.AUDIO, MediaFormat.M4A, 0, 0),
|
||||
new ItagItem(251, ItagType.AUDIO, MediaFormat.WEBMA, 0, 0),
|
||||
// video only streams
|
||||
new ItagItem(160, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "144p", 24),
|
||||
new ItagItem(133, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "240p", 24),
|
||||
new ItagItem(134, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "360p", 24),
|
||||
new ItagItem(135, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "480p", 24),
|
||||
new ItagItem(136, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "720p", 24),
|
||||
new ItagItem(137, ItagType.VIDEO_ONLY, MediaFormat.MPEG_4, "1080p", 24),
|
||||
};
|
||||
|
||||
/**These lists only contain itag formats that are supported by the common Android Video player.
|
||||
However if you are looking for a list showing all itag formats, look at
|
||||
https://github.com/rg3/youtube-dl/issues/1687 */
|
||||
|
||||
public static boolean itagIsSupported(int itag) {
|
||||
for(ItagItem item : itagList) {
|
||||
if(itag == item.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ItagItem getItagItem(int itag) throws ParsingException {
|
||||
for(ItagItem item : itagList) {
|
||||
if(itag == item.id) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
throw new ParsingException("itag=" + Integer.toString(itag) + " not supported");
|
||||
}
|
||||
|
||||
private static final String TAG = YoutubeStreamExtractor.class.toString();
|
||||
private final Document doc;
|
||||
private JSONObject playerArgs;
|
||||
private boolean isAgeRestricted;
|
||||
private Map<String, String> videoInfoPage;
|
||||
|
||||
// static values
|
||||
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
||||
|
||||
// cached values
|
||||
private static volatile String decryptionCode = "";
|
||||
|
||||
UrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler();
|
||||
String pageUrl = "";
|
||||
|
||||
private Downloader downloader;
|
||||
|
||||
public YoutubeStreamExtractor(UrlIdHandler urlIdHandler, String pageUrl,
|
||||
Downloader dl, int serviceId)
|
||||
throws ExtractionException, IOException {
|
||||
super(urlIdHandler ,pageUrl, dl, serviceId);
|
||||
//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);
|
||||
JSONObject ytPlayerConfig;
|
||||
String playerUrl;
|
||||
|
||||
// Check if the video is age restricted
|
||||
if (pageContent.contains("<meta property=\"og:restrictions:age")) {
|
||||
String videoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
|
||||
urlidhandler.getId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
|
||||
String videoInfoPageString = downloader.download(videoInfoUrl);
|
||||
videoInfoPage = Parser.compatParseMap(videoInfoPageString);
|
||||
playerUrl = getPlayerUrlFromRestrictedVideo(pageUrl);
|
||||
isAgeRestricted = true;
|
||||
} else {
|
||||
ytPlayerConfig = getPlayerConfig(pageContent);
|
||||
playerArgs = getPlayerArgs(ytPlayerConfig);
|
||||
playerUrl = getPlayerUrl(ytPlayerConfig);
|
||||
isAgeRestricted = false;
|
||||
}
|
||||
|
||||
if(decryptionCode.isEmpty()) {
|
||||
decryptionCode = loadDecryptionCode(playerUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getPlayerConfig(String pageContent) throws ParsingException {
|
||||
try {
|
||||
String ytPlayerConfigRaw =
|
||||
Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent);
|
||||
return new JSONObject(ytPlayerConfigRaw);
|
||||
} catch (Parser.RegexException e) {
|
||||
String errorReason = findErrorReason(doc);
|
||||
switch(errorReason) {
|
||||
case "GEMA":
|
||||
throw new GemaException(errorReason);
|
||||
case "":
|
||||
throw new ContentNotAvailableException("Content not available: player config empty", e);
|
||||
default:
|
||||
throw new ContentNotAvailableException("Content not available", e);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not parse yt player config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getPlayerArgs(JSONObject playerConfig) throws ParsingException {
|
||||
JSONObject playerArgs;
|
||||
|
||||
//attempt to load the youtube js player JSON arguments
|
||||
boolean isLiveStream = false; //used to determine if this is a livestream or not
|
||||
try {
|
||||
playerArgs = playerConfig.getJSONObject("args");
|
||||
|
||||
// check if we have a live stream. We need to filter it, since its not yet supported.
|
||||
if((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
|
||||
|| (playerArgs.get(URL_ENCODED_FMT_STREAM_MAP).toString().isEmpty())) {
|
||||
isLiveStream = true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not parse yt player config", e);
|
||||
}
|
||||
if (isLiveStream) {
|
||||
throw new LiveStreamException("This is a Life stream. Can't use those right now.");
|
||||
}
|
||||
|
||||
return playerArgs;
|
||||
}
|
||||
|
||||
private String getPlayerUrl(JSONObject playerConfig) throws ParsingException {
|
||||
try {
|
||||
// The Youtube service needs to be initialized by downloading the
|
||||
// js-Youtube-player. This is done in order to get the algorithm
|
||||
// for decrypting cryptic signatures inside certain stream urls.
|
||||
String playerUrl = "";
|
||||
|
||||
JSONObject ytAssets = playerConfig.getJSONObject("assets");
|
||||
playerUrl = ytAssets.getString("js");
|
||||
|
||||
if (playerUrl.startsWith("//")) {
|
||||
playerUrl = HTTPS + playerUrl;
|
||||
}
|
||||
return playerUrl;
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException(
|
||||
"Could not load decryption code for the Youtube service.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlayerUrlFromRestrictedVideo(String pageUrl) throws ParsingException {
|
||||
try {
|
||||
String playerUrl = "";
|
||||
String videoId = urlidhandler.getId(pageUrl);
|
||||
String embedUrl = "https://www.youtube.com/embed/" + videoId;
|
||||
String embedPageContent = downloader.download(embedUrl);
|
||||
//todo: find out if this can be reapaced by Parser.matchGroup1()
|
||||
Pattern assetsPattern = Pattern.compile("\"assets\":.+?\"js\":\\s*(\"[^\"]+\")");
|
||||
Matcher patternMatcher = assetsPattern.matcher(embedPageContent);
|
||||
while (patternMatcher.find()) {
|
||||
playerUrl = patternMatcher.group(1);
|
||||
}
|
||||
playerUrl = playerUrl.replace("\\", "").replace("\"", "");
|
||||
|
||||
if (playerUrl.startsWith("//")) {
|
||||
playerUrl = HTTPS + playerUrl;
|
||||
}
|
||||
return playerUrl;
|
||||
} catch (IOException e) {
|
||||
throw new ParsingException(
|
||||
"Could load decryption code form restricted video for the Youtube service.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() throws ParsingException {
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return videoInfoPage.get("title");
|
||||
}
|
||||
//json player args method
|
||||
return playerArgs.getString("title");
|
||||
} catch(JSONException je) {//html <meta> method
|
||||
je.printStackTrace();
|
||||
System.err.println("failed to load title from JSON args; trying to extract it from HTML");
|
||||
try { // fall through to fall-back
|
||||
return doc.select("meta[name=title]").attr(CONTENT);
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("failed permanently to load title.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() throws ParsingException {
|
||||
try {
|
||||
return doc.select("p[id=\"eow-description\"]").first().html();
|
||||
} catch (Exception e) {//todo: add fallback method <-- there is no ... as long as i know
|
||||
throw new ParsingException("failed to load description.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploader() throws ParsingException {
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return videoInfoPage.get("author");
|
||||
}
|
||||
//json player args method
|
||||
return playerArgs.getString("author");
|
||||
} catch(JSONException je) {
|
||||
je.printStackTrace();
|
||||
System.err.println(
|
||||
"failed to load uploader name from JSON args; trying to extract it from HTML");
|
||||
} try {//fall through to fallback HTML method
|
||||
return doc.select("div.yt-user-info").first().text();
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("failed permanently to load uploader name.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLength() throws ParsingException {
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return Integer.valueOf(videoInfoPage.get("length_seconds"));
|
||||
}
|
||||
return playerArgs.getInt("length_seconds");
|
||||
} catch (JSONException e) {//todo: find fallback method
|
||||
throw new ParsingException("failed to load video duration from JSON args", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
try {
|
||||
String viewCountString = doc.select("meta[itemprop=interactionCount]").attr(CONTENT);
|
||||
return Long.parseLong(viewCountString);
|
||||
} catch (Exception e) {//todo: find fallback method
|
||||
throw new ParsingException("failed to get number of views", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploadDate() throws ParsingException {
|
||||
try {
|
||||
return doc.select("meta[itemprop=datePublished]").attr(CONTENT);
|
||||
} catch (Exception e) {//todo: add fallback method
|
||||
throw new ParsingException("failed to get upload date.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
//first attempt getting a small image version
|
||||
//in the html extracting part we try to get a thumbnail with a higher resolution
|
||||
// Try to get high resolution thumbnail if it fails use low res from the player instead
|
||||
try {
|
||||
return doc.select("link[itemprop=\"thumbnailUrl\"]").first().attr("abs:href");
|
||||
} catch(Exception e) {
|
||||
System.err.println("Could not find high res Thumbnail. Using low res instead");
|
||||
}
|
||||
try { //fall through to fallback
|
||||
return playerArgs.getString("thumbnail_url");
|
||||
} catch (JSONException je) {
|
||||
throw new ParsingException(
|
||||
"failed to extract thumbnail URL from JSON args; trying to extract it from HTML", je);
|
||||
} catch (NullPointerException ne) {
|
||||
// Get from the video info page instead
|
||||
return videoInfoPage.get("thumbnail_url");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderThumbnailUrl() throws ParsingException {
|
||||
try {
|
||||
return doc.select("a[class*=\"yt-user-photo\"]").first()
|
||||
.select("img").first()
|
||||
.attr("abs:data-thumb");
|
||||
} catch (Exception e) {//todo: add fallback method
|
||||
throw new ParsingException("failed to get uploader thumbnail URL.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDashMpdUrl() throws ParsingException {
|
||||
/*
|
||||
try {
|
||||
String dashManifestUrl = videoInfoPage.get("dashmpd");
|
||||
if(!dashManifestUrl.contains("/signature/")) {
|
||||
String encryptedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifestUrl);
|
||||
String decryptedSig;
|
||||
|
||||
decryptedSig = decryptSignature(encryptedSig, decryptionCode);
|
||||
dashManifestUrl = dashManifestUrl.replace("/s/" + encryptedSig, "/signature/" + decryptedSig);
|
||||
}
|
||||
return dashManifestUrl;
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(
|
||||
"Could not get \"dashmpd\" maybe VideoInfoPage is broken.", e);
|
||||
}
|
||||
*/
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<AudioStream> getAudioStreams() throws ParsingException {
|
||||
Vector<AudioStream> audioStreams = new Vector<>();
|
||||
try{
|
||||
String encodedUrlMap;
|
||||
// playerArgs could be null if the video is age restricted
|
||||
if (playerArgs == null) {
|
||||
encodedUrlMap = videoInfoPage.get("adaptive_fmts");
|
||||
} else {
|
||||
encodedUrlMap = playerArgs.getString("adaptive_fmts");
|
||||
}
|
||||
for(String url_data_str : encodedUrlMap.split(",")) {
|
||||
// This loop iterates through multiple streams, therefor tags
|
||||
// is related to one and the same stream at a time.
|
||||
Map<String, String> tags = Parser.compatParseMap(
|
||||
org.jsoup.parser.Parser.unescapeEntities(url_data_str, true));
|
||||
|
||||
int itag = Integer.parseInt(tags.get("itag"));
|
||||
|
||||
if (itagIsSupported(itag)) {
|
||||
ItagItem itagItem = getItagItem(itag);
|
||||
if (itagItem.itagType == ItagType.AUDIO) {
|
||||
String streamUrl = tags.get("url");
|
||||
// if video has a signature: decrypt it and add it to the url
|
||||
if (tags.get("s") != null) {
|
||||
streamUrl = streamUrl + "&signature="
|
||||
+ decryptSignature(tags.get("s"), decryptionCode);
|
||||
}
|
||||
|
||||
audioStreams.add(new AudioStream(streamUrl,
|
||||
itagItem.mediaFormatId,
|
||||
itagItem.bandWidth,
|
||||
itagItem.samplingRate));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get audiostreams", e);
|
||||
}
|
||||
return audioStreams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoStreams() throws ParsingException {
|
||||
Vector<VideoStream> videoStreams = new Vector<>();
|
||||
|
||||
try{
|
||||
String encodedUrlMap;
|
||||
// playerArgs could be null if the video is age restricted
|
||||
if (playerArgs == null) {
|
||||
encodedUrlMap = videoInfoPage.get(URL_ENCODED_FMT_STREAM_MAP);
|
||||
} else {
|
||||
encodedUrlMap = playerArgs.getString(URL_ENCODED_FMT_STREAM_MAP);
|
||||
}
|
||||
for(String url_data_str : encodedUrlMap.split(",")) {
|
||||
try {
|
||||
// This loop iterates through multiple streams, therefor tags
|
||||
// is related to one and the same stream at a time.
|
||||
Map<String, String> tags = Parser.compatParseMap(
|
||||
org.jsoup.parser.Parser.unescapeEntities(url_data_str, true));
|
||||
|
||||
int itag = Integer.parseInt(tags.get("itag"));
|
||||
|
||||
if (itagIsSupported(itag)) {
|
||||
ItagItem itagItem = getItagItem(itag);
|
||||
if(itagItem.itagType == ItagType.VIDEO) {
|
||||
String streamUrl = tags.get("url");
|
||||
// if video has a signature: decrypt it and add it to the url
|
||||
if (tags.get("s") != null) {
|
||||
streamUrl = streamUrl + "&signature="
|
||||
+ decryptSignature(tags.get("s"), decryptionCode);
|
||||
}
|
||||
videoStreams.add(new VideoStream(
|
||||
streamUrl,
|
||||
itagItem.mediaFormatId,
|
||||
itagItem.resolutionString));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//todo: dont log throw an error
|
||||
System.err.println("Could not get Video stream.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Failed to get video streams", e);
|
||||
}
|
||||
|
||||
if(videoStreams.isEmpty()) {
|
||||
throw new ParsingException("Failed to get any video stream");
|
||||
}
|
||||
return videoStreams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoOnlyStreams() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**Attempts to parse (and return) the offset to start playing the video from.
|
||||
* @return the offset (in seconds), or 0 if no timestamp is found.*/
|
||||
@Override
|
||||
public int getTimeStamp() throws ParsingException {
|
||||
String timeStamp;
|
||||
try {
|
||||
timeStamp = Parser.matchGroup1("((#|&|\\?)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)", pageUrl);
|
||||
} catch (Parser.RegexException e) {
|
||||
// catch this instantly since an url does not necessarily have to have a time stamp
|
||||
|
||||
// -2 because well the testing system will then know its the regex that failed :/
|
||||
// not good i know
|
||||
return -2;
|
||||
}
|
||||
|
||||
if(!timeStamp.isEmpty()) {
|
||||
try {
|
||||
String secondsString = "";
|
||||
String minutesString = "";
|
||||
String hoursString = "";
|
||||
try {
|
||||
secondsString = Parser.matchGroup1("(\\d{1,3})s", timeStamp);
|
||||
minutesString = Parser.matchGroup1("(\\d{1,3})m", timeStamp);
|
||||
hoursString = Parser.matchGroup1("(\\d{1,3})h", timeStamp);
|
||||
} catch (Exception e) {
|
||||
//it could be that time is given in another method
|
||||
if (secondsString.isEmpty() //if nothing was got,
|
||||
&& minutesString.isEmpty()//treat as unlabelled seconds
|
||||
&& hoursString.isEmpty()) {
|
||||
secondsString = Parser.matchGroup1("t=(\\d+)", timeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString);
|
||||
int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString);
|
||||
int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString);
|
||||
|
||||
//don't trust BODMAS!
|
||||
return seconds + (60 * minutes) + (3600 * hours);
|
||||
//Log.d(TAG, "derived timestamp value:"+ret);
|
||||
//the ordering varies internationally
|
||||
} catch (ParsingException e) {
|
||||
throw new ParsingException("Could not get timestamp.", e);
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAgeLimit() throws ParsingException {
|
||||
if (!isAgeRestricted) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(doc.head()
|
||||
.getElementsByAttributeValue("property", "og:restrictions:age")
|
||||
.attr(CONTENT).replace("+", ""));
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get age restriction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAverageRating() throws ParsingException {
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return videoInfoPage.get("avg_rating");
|
||||
}
|
||||
return playerArgs.getString("avg_rating");
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not get Average rating", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLikeCount() throws ParsingException {
|
||||
String likesString = "";
|
||||
try {
|
||||
|
||||
Element button = doc.select("button.like-button-renderer-like-button").first();
|
||||
try {
|
||||
likesString = button.select("span.yt-uix-button-content").first().text();
|
||||
} catch (NullPointerException e) {
|
||||
//if this ckicks in our button has no content and thefore likes/dislikes are disabled
|
||||
return -1;
|
||||
}
|
||||
return Integer.parseInt(likesString.replaceAll(REGEX_INT, ""));
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new ParsingException(
|
||||
"failed to parse likesString \"" + likesString + "\" as integers", nfe);
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get like count", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDislikeCount() throws ParsingException {
|
||||
String dislikesString = "";
|
||||
try {
|
||||
Element button = doc.select("button.like-button-renderer-dislike-button").first();
|
||||
try {
|
||||
dislikesString = button.select("span.yt-uix-button-content").first().text();
|
||||
} catch (NullPointerException e) {
|
||||
//if this kicks in our button has no content and therefore likes/dislikes are disabled
|
||||
return -1;
|
||||
}
|
||||
return Integer.parseInt(dislikesString.replaceAll(REGEX_INT, ""));
|
||||
} catch(NumberFormatException nfe) {
|
||||
throw new ParsingException(
|
||||
"failed to parse dislikesString \"" + dislikesString + "\" as integers", nfe);
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get dislike count", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamPreviewInfoExtractor getNextVideo() throws ParsingException {
|
||||
try {
|
||||
return extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
||||
.select("li").first());
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get next video", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamPreviewInfoCollector getRelatedVideos() throws ParsingException {
|
||||
try {
|
||||
StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector();
|
||||
Element ul = doc.select("ul[id=\"watch-related\"]").first();
|
||||
if(ul != null) {
|
||||
for (Element li : ul.children()) {
|
||||
// first check if we have a playlist. If so leave them out
|
||||
if (li.select("a[class*=\"content-link\"]").first() != null) {
|
||||
collector.commit(extractVideoPreviewInfo(li));
|
||||
}
|
||||
}
|
||||
}
|
||||
return collector;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get related videos", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPageUrl() {
|
||||
return pageUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannelUrl() throws ParsingException {
|
||||
try {
|
||||
return doc.select("div[class=\"yt-user-info\"]").first().children()
|
||||
.select("a").first().attr("abs:href");
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get channel link", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamInfo.StreamType getStreamType() throws ParsingException {
|
||||
//todo: if implementing livestream support this value should be generated dynamically
|
||||
return StreamInfo.StreamType.VIDEO_STREAM;
|
||||
}
|
||||
|
||||
/**Provides information about links to other videos on the video page, such as related videos.
|
||||
* This is encapsulated in a StreamPreviewInfo object,
|
||||
* which is a subset of the fields in a full StreamInfo.*/
|
||||
private StreamPreviewInfoExtractor extractVideoPreviewInfo(final Element li) {
|
||||
return new StreamPreviewInfoExtractor() {
|
||||
@Override
|
||||
public AbstractVideoInfo.StreamType getStreamType() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebPageUrl() throws ParsingException {
|
||||
return li.select("a.content-link").first().attr("abs:href");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() throws ParsingException {
|
||||
//todo: check NullPointerException causing
|
||||
return li.select("span.title").first().text();
|
||||
//this page causes the NullPointerException, after finding it by searching for "tjvg":
|
||||
//https://www.youtube.com/watch?v=Uqg0aEhLFAg
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() throws ParsingException {
|
||||
return YoutubeParsingHelper.parseDurationString(
|
||||
li.select("span.video-time").first().text());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploader() throws ParsingException {
|
||||
return li.select("span.g-hovercard").first().text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploadDate() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
//this line is unused
|
||||
//String views = li.select("span.view-count").first().text();
|
||||
|
||||
//Log.i(TAG, "title:"+info.title);
|
||||
//Log.i(TAG, "view count:"+views);
|
||||
|
||||
try {
|
||||
return Long.parseLong(li.select("span.view-count")
|
||||
.first().text().replaceAll(REGEX_INT, ""));
|
||||
} catch (Exception e) {
|
||||
//related videos sometimes have no view count
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
Element img = li.select("img").first();
|
||||
String thumbnailUrl = img.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files which somehow seem to not exist
|
||||
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||
// to use that if we caught such an item.
|
||||
if (thumbnailUrl.contains(".gif")) {
|
||||
thumbnailUrl = img.attr("data-thumb");
|
||||
}
|
||||
if (thumbnailUrl.startsWith("//")) {
|
||||
thumbnailUrl = HTTPS + thumbnailUrl;
|
||||
}
|
||||
return thumbnailUrl;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private String loadDecryptionCode(String playerUrl) throws DecryptException {
|
||||
String decryptionFuncName;
|
||||
String decryptionFunc;
|
||||
String helperObjectName;
|
||||
String helperObject;
|
||||
String callerFunc = "function " + DECRYPTION_FUNC_NAME + "(a){return %%(a);}";
|
||||
String decryptionCode;
|
||||
|
||||
try {
|
||||
String playerCode = downloader.download(playerUrl);
|
||||
|
||||
decryptionFuncName =
|
||||
Parser.matchGroup1("\\.sig\\|\\|([a-zA-Z0-9$]+)\\(", playerCode);
|
||||
|
||||
String functionPattern = "("
|
||||
+ decryptionFuncName.replace("$", "\\$")
|
||||
+ "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})";
|
||||
decryptionFunc = "var " + Parser.matchGroup1(functionPattern, playerCode) + ";";
|
||||
|
||||
helperObjectName = Parser
|
||||
.matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(", decryptionFunc);
|
||||
|
||||
String helperPattern = "(var "
|
||||
+ helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)";
|
||||
helperObject = Parser.matchGroup1(helperPattern, playerCode);
|
||||
|
||||
|
||||
callerFunc = callerFunc.replace("%%", decryptionFuncName);
|
||||
decryptionCode = helperObject + decryptionFunc + callerFunc;
|
||||
} catch(IOException ioe) {
|
||||
throw new DecryptException("Could not load decrypt function", ioe);
|
||||
} catch(Exception e) {
|
||||
throw new DecryptException("Could not parse decrypt function ", e);
|
||||
}
|
||||
|
||||
return decryptionCode;
|
||||
}
|
||||
|
||||
private String decryptSignature(String encryptedSig, String decryptionCode)
|
||||
throws DecryptException{
|
||||
Context context = Context.enter();
|
||||
context.setOptimizationLevel(-1);
|
||||
Object result = null;
|
||||
try {
|
||||
ScriptableObject scope = context.initStandardObjects();
|
||||
context.evaluateString(scope, decryptionCode, "decryptionCode", 1, null);
|
||||
Function decryptionFunc = (Function) scope.get("decrypt", scope);
|
||||
result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig});
|
||||
} catch (Exception e) {
|
||||
throw new DecryptException("could not get decrypt signature", e);
|
||||
} finally {
|
||||
Context.exit();
|
||||
}
|
||||
return result == null ? "" : result.toString();
|
||||
}
|
||||
|
||||
private String findErrorReason(Document doc) {
|
||||
String errorMessage = doc.select("h1[id=\"unavailable-message\"]").first().text();
|
||||
if(errorMessage.contains("GEMA")) {
|
||||
// Gema sometimes blocks youtube music content in germany:
|
||||
// https://www.gema.de/en/
|
||||
// Detailed description:
|
||||
// https://en.wikipedia.org/wiki/GEMA_%28German_organization%29
|
||||
return "GEMA";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor;
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeStreamPreviewInfoExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeStreamPreviewInfoExtractor implements StreamPreviewInfoExtractor {
|
||||
|
||||
private final Element item;
|
||||
|
||||
public YoutubeStreamPreviewInfoExtractor(Element item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebPageUrl() throws ParsingException {
|
||||
try {
|
||||
Element el = item.select("div[class*=\"yt-lockup-video\"").first();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
return dl.attr("abs:href");
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get web page url for the video", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() throws ParsingException {
|
||||
try {
|
||||
Element el = item.select("div[class*=\"yt-lockup-video\"").first();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
return dl.text();
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get title", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() throws ParsingException {
|
||||
try {
|
||||
return YoutubeParsingHelper.parseDurationString(
|
||||
item.select("span[class=\"video-time\"]").first().text());
|
||||
} catch(Exception e) {
|
||||
if(isLiveStream(item)) {
|
||||
// -1 for no duration
|
||||
return -1;
|
||||
} else {
|
||||
throw new ParsingException("Could not get Duration: " + getTitle(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploader() throws ParsingException {
|
||||
try {
|
||||
return item.select("div[class=\"yt-lockup-byline\"]").first()
|
||||
.select("a").first()
|
||||
.text();
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get uploader", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploadDate() throws ParsingException {
|
||||
try {
|
||||
return item.select("div[class=\"yt-lockup-meta\"]").first()
|
||||
.select("li").first()
|
||||
.text();
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not get uplaod date", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
String output;
|
||||
String input;
|
||||
try {
|
||||
input = item.select("div[class=\"yt-lockup-meta\"]").first()
|
||||
.select("li").get(1)
|
||||
.text();
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
if(isLiveStream(item)) {
|
||||
// -1 for no view count
|
||||
return -1;
|
||||
} else {
|
||||
throw new ParsingException(
|
||||
"Could not parse yt-lockup-meta although available: " + getTitle(), e);
|
||||
}
|
||||
}
|
||||
|
||||
output = Parser.matchGroup1("([0-9,\\. ]*)", input)
|
||||
.replace(" ", "")
|
||||
.replace(".", "")
|
||||
.replace(",", "");
|
||||
|
||||
try {
|
||||
return Long.parseLong(output);
|
||||
} catch (NumberFormatException e) {
|
||||
// if this happens the video probably has no views
|
||||
if(!input.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
throw new ParsingException("Could not handle input: " + input, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
try {
|
||||
String url;
|
||||
Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first()
|
||||
.select("img").first();
|
||||
url = te.attr("abs:src");
|
||||
// Sometimes youtube sends links to gif files which somehow seem to not exist
|
||||
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||
// to use that if we've caught such an item.
|
||||
if (url.contains(".gif")) {
|
||||
url = te.attr("abs:data-thumb");
|
||||
}
|
||||
return url;
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get thumbnail url", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractVideoInfo.StreamType getStreamType() {
|
||||
if(isLiveStream(item)) {
|
||||
return AbstractVideoInfo.StreamType.LIVE_STREAM;
|
||||
} else {
|
||||
return AbstractVideoInfo.StreamType.VIDEO_STREAM;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLiveStream(Element item) {
|
||||
Element bla = item.select("span[class*=\"yt-badge-live\"]").first();
|
||||
|
||||
if(bla == null) {
|
||||
// sometimes livestreams dont have badges but sill are live streams
|
||||
// if video time is not available we most likly have an offline livestream
|
||||
if(item.select("span[class*=\"video-time\"]").first() == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return bla != null;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import org.schabi.newpipe.extractor.FoundAdException;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
|
||||
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>
|
||||
* YoutubeStreamUrlIdHandler.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeStreamUrlIdHandler implements UrlIdHandler {
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Override
|
||||
public String getUrl(String videoId) {
|
||||
return "https://www.youtube.com/watch?v=" + videoId;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Override
|
||||
public String getId(String url) throws ParsingException, IllegalArgumentException {
|
||||
if(url.isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("The url parameter should not be empty");
|
||||
}
|
||||
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 if(url.contains("vnd.youtube"))
|
||||
{
|
||||
id = Parser.matchGroup1("([\\-a-zA-Z0-9_]{11}).*", url);
|
||||
} else if(url.contains("googleads")) {
|
||||
throw new FoundAdException("Error found add: " + url);
|
||||
} else {
|
||||
id = Parser.matchGroup1("[?&]v=([\\-a-zA-Z0-9_]{11})", url);
|
||||
}
|
||||
}
|
||||
else if(url.contains("youtu.be")) {
|
||||
if(url.contains("v=")) {
|
||||
id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", url);
|
||||
} else {
|
||||
id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new ParsingException("Error no suitable url: " + url);
|
||||
}
|
||||
|
||||
|
||||
if(!id.isEmpty()){
|
||||
return id;
|
||||
} else {
|
||||
throw new ParsingException("Error could not parse url: " + url);
|
||||
}
|
||||
}
|
||||
|
||||
public String cleanUrl(String complexUrl) throws ParsingException {
|
||||
return getUrl(getId(complexUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptUrl(String videoUrl) {
|
||||
return videoUrl.contains("youtube") ||
|
||||
videoUrl.contains("youtu.be");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItemHolder .java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ChannelInfoItemHolder extends InfoItemHolder {
|
||||
public final CircleImageView itemThumbnailView;
|
||||
public final TextView itemChannelTitleView;
|
||||
public final TextView itemSubscriberCountView;
|
||||
public final TextView itemVideoCountView;
|
||||
public final TextView itemChannelDescriptionView;
|
||||
public final Button itemButton;
|
||||
|
||||
ChannelInfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView);
|
||||
itemSubscriberCountView = (TextView) v.findViewById(R.id.itemSubscriberCountView);
|
||||
itemVideoCountView = (TextView) v.findViewById(R.id.itemVideoCountView);
|
||||
itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView);
|
||||
itemButton = (Button) v.findViewById(R.id.item_button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItem.InfoType infoType() {
|
||||
return InfoItem.InfoType.CHANNEL;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.AbstractStreamInfo;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.09.16.
|
||||
@@ -35,8 +39,17 @@ import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
|
||||
public class InfoItemBuilder {
|
||||
|
||||
public interface OnItemSelectedListener {
|
||||
void selected(String url);
|
||||
final String viewsS;
|
||||
final String videosS;
|
||||
final String subsS;
|
||||
|
||||
final String thousand;
|
||||
final String million;
|
||||
final String billion;
|
||||
|
||||
private static final String TAG = InfoItemBuilder.class.toString();
|
||||
public interface OnInfoItemSelectedListener {
|
||||
void selected(String url, int serviceId);
|
||||
}
|
||||
|
||||
private Activity activity = null;
|
||||
@@ -44,18 +57,75 @@ public class InfoItemBuilder {
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
private OnItemSelectedListener onItemSelectedListener;
|
||||
private OnInfoItemSelectedListener onStreamInfoItemSelectedListener;
|
||||
private OnInfoItemSelectedListener onChannelInfoItemSelectedListener;
|
||||
|
||||
public InfoItemBuilder(Activity a, View rootView) {
|
||||
activity = a;
|
||||
this.rootView = rootView;
|
||||
viewsS = a.getString(R.string.views);
|
||||
videosS = a.getString(R.string.videos);
|
||||
subsS = a.getString(R.string.subscriber);
|
||||
thousand = a.getString(R.string.short_thousand);
|
||||
million = a.getString(R.string.short_million);
|
||||
billion = a.getString(R.string.short_billion);
|
||||
}
|
||||
|
||||
public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
|
||||
this.onItemSelectedListener = onItemSelectedListener;
|
||||
public void setOnStreamInfoItemSelectedListener(
|
||||
OnInfoItemSelectedListener listener) {
|
||||
this.onStreamInfoItemSelectedListener = listener;
|
||||
}
|
||||
|
||||
public void buildByHolder(InfoItemHolder holder, final StreamPreviewInfo info) {
|
||||
public void setOnChannelInfoItemSelectedListener(
|
||||
OnInfoItemSelectedListener listener) {
|
||||
this.onChannelInfoItemSelectedListener = listener;
|
||||
}
|
||||
|
||||
public void buildByHolder(InfoItemHolder holder, final InfoItem i) {
|
||||
if(i.infoType() != holder.infoType())
|
||||
return;
|
||||
switch(i.infoType()) {
|
||||
case STREAM:
|
||||
buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i);
|
||||
break;
|
||||
case CHANNEL:
|
||||
buildChannelInfoItem((ChannelInfoItemHolder) holder, (ChannelInfoItem) i);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "Not yet implemented");
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
}
|
||||
}
|
||||
|
||||
public View buildView(ViewGroup parent, final InfoItem info) {
|
||||
View itemView = null;
|
||||
InfoItemHolder holder = null;
|
||||
switch(info.infoType()) {
|
||||
case STREAM:
|
||||
itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.stream_item, parent, false);
|
||||
holder = new StreamInfoItemHolder(itemView);
|
||||
break;
|
||||
case CHANNEL:
|
||||
itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.channel_item, parent, false);
|
||||
holder = new ChannelInfoItemHolder(itemView);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "Not yet implemented");
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
}
|
||||
buildByHolder(holder, info);
|
||||
return itemView;
|
||||
}
|
||||
|
||||
private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) {
|
||||
if(info.infoType() != InfoItem.InfoType.STREAM) {
|
||||
Log.e("InfoItemBuilder", "Info type not yet supported");
|
||||
}
|
||||
// fill holder with information
|
||||
holder.itemVideoTitleView.setText(info.title);
|
||||
if(info.uploader != null && !info.uploader.isEmpty()) {
|
||||
@@ -66,7 +136,7 @@ public class InfoItemBuilder {
|
||||
if(info.duration > 0) {
|
||||
holder.itemDurationView.setText(getDurationString(info.duration));
|
||||
} else {
|
||||
if(info.stream_type == AbstractVideoInfo.StreamType.LIVE_STREAM) {
|
||||
if(info.stream_type == AbstractStreamInfo.StreamType.LIVE_STREAM) {
|
||||
holder.itemDurationView.setText(R.string.duration_live);
|
||||
} else {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
@@ -92,29 +162,55 @@ public class InfoItemBuilder {
|
||||
holder.itemButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onItemSelectedListener.selected(info.webpage_url);
|
||||
onStreamInfoItemSelectedListener.selected(info.webpage_url, info.service_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public View buildView(ViewGroup parent, final StreamPreviewInfo info) {
|
||||
View streamPreviewView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.video_item, parent, false);
|
||||
InfoItemHolder holder = new InfoItemHolder(streamPreviewView);
|
||||
buildByHolder(holder, info);
|
||||
return streamPreviewView;
|
||||
private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) {
|
||||
holder.itemChannelTitleView.setText(info.getTitle());
|
||||
holder.itemSubscriberCountView.setText(shortSubscriber(info.subscriberCount) + " • ");
|
||||
holder.itemVideoCountView.setText(info.videoAmount + " " + videosS);
|
||||
holder.itemChannelDescriptionView.setText(info.description);
|
||||
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.buddy_channel_item);
|
||||
if(info.thumbnailUrl != null && !info.thumbnailUrl.isEmpty()) {
|
||||
imageLoader.displayImage(info.thumbnailUrl,
|
||||
holder.itemThumbnailView,
|
||||
displayImageOptions,
|
||||
new ImageErrorLoadingListener(activity, rootView, info.serviceId));
|
||||
}
|
||||
|
||||
holder.itemButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onChannelInfoItemSelectedListener.selected(info.getLink(), info.serviceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static String shortViewCount(Long viewCount){
|
||||
public String shortViewCount(Long viewCount){
|
||||
if(viewCount >= 1000000000){
|
||||
return Long.toString(viewCount/1000000000)+"B views";
|
||||
return Long.toString(viewCount/1000000000)+ billion + " " + viewsS;
|
||||
}else if(viewCount>=1000000){
|
||||
return Long.toString(viewCount/1000000)+"M views";
|
||||
return Long.toString(viewCount/1000000)+ million + " " + viewsS;
|
||||
}else if(viewCount>=1000){
|
||||
return Long.toString(viewCount/1000)+"K views";
|
||||
return Long.toString(viewCount/1000)+ thousand + " " + viewsS;
|
||||
}else {
|
||||
return Long.toString(viewCount)+" views";
|
||||
return Long.toString(viewCount)+ " " + viewsS;
|
||||
}
|
||||
}
|
||||
|
||||
public String shortSubscriber(Long count){
|
||||
if(count >= 1000000000){
|
||||
return Long.toString(count/1000000000)+ billion + " " + subsS;
|
||||
}else if(count>=1000000){
|
||||
return Long.toString(count/1000000)+ million + " " + subsS;
|
||||
}else if(count>=1000){
|
||||
return Long.toString(count/1000)+ thousand + " " + subsS;
|
||||
}else {
|
||||
return Long.toString(count)+ " " + subsS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,11 @@ package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemHolder.java is part of NewPipe.
|
||||
@@ -28,24 +25,9 @@ import org.schabi.newpipe.R;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public ImageView itemThumbnailView;
|
||||
public TextView itemVideoTitleView,
|
||||
itemUploaderView,
|
||||
itemDurationView,
|
||||
itemUploadDateView,
|
||||
itemViewCountView;
|
||||
public Button itemButton;
|
||||
|
||||
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||
public InfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
|
||||
itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView);
|
||||
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
|
||||
itemButton = (Button) v.findViewById(R.id.item_button);
|
||||
}
|
||||
public abstract InfoItem.InfoType infoType();
|
||||
}
|
||||
|
||||
@@ -2,17 +2,13 @@ package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.AbstractVideoInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
@@ -37,47 +33,131 @@ import java.util.Vector;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class InfoListAdapter extends RecyclerView.Adapter<InfoItemHolder> {
|
||||
public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private static final String TAG = InfoListAdapter.class.toString();
|
||||
|
||||
InfoItemBuilder infoItemBuilder = null;
|
||||
List<StreamPreviewInfo> streamList = new Vector<>();
|
||||
private final InfoItemBuilder infoItemBuilder;
|
||||
private final List<InfoItem> infoItemList;
|
||||
private boolean showFooter = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
public class HFHolder extends RecyclerView.ViewHolder {
|
||||
public HFHolder(View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
public View view;
|
||||
}
|
||||
|
||||
public void showFooter(boolean show) {
|
||||
showFooter = show;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public InfoListAdapter(Activity a, View rootView) {
|
||||
infoItemBuilder = new InfoItemBuilder(a, rootView);
|
||||
infoItemList = new Vector<>();
|
||||
}
|
||||
|
||||
public void setOnItemSelectedListener
|
||||
(InfoItemBuilder.OnItemSelectedListener onItemSelectedListener) {
|
||||
infoItemBuilder.setOnItemSelectedListener(onItemSelectedListener);
|
||||
public void setOnStreamInfoItemSelectedListener
|
||||
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
|
||||
infoItemBuilder.setOnStreamInfoItemSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void addStreamItemList(List<StreamPreviewInfo> videos) {
|
||||
if(videos!= null) {
|
||||
streamList.addAll(videos);
|
||||
public void setOnChannelInfoItemSelectedListener
|
||||
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
|
||||
infoItemBuilder.setOnChannelInfoItemSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void addInfoItemList(List<InfoItem> data) {
|
||||
if(data != null) {
|
||||
infoItemList.addAll(data);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSteamItemList() {
|
||||
streamList = new Vector<>();
|
||||
infoItemList.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setHeader(View header) {
|
||||
this.header = header;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setFooter(View view) {
|
||||
this.footer = view;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return streamList.size();
|
||||
int cound = infoItemList.size();
|
||||
if(header != null) cound++;
|
||||
if(footer != null && showFooter) cound++;
|
||||
return cound;
|
||||
}
|
||||
|
||||
// don't ask why we have to do that this way... it's android accept it -.-
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if(header != null && position == 0) {
|
||||
return 0;
|
||||
} else if(header != null) {
|
||||
position--;
|
||||
}
|
||||
if(footer != null && position == infoItemList.size() && showFooter) {
|
||||
return 1;
|
||||
}
|
||||
switch(infoItemList.get(position).infoType()) {
|
||||
case STREAM:
|
||||
return 2;
|
||||
case CHANNEL:
|
||||
return 3;
|
||||
case PLAYLIST:
|
||||
return 4;
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemHolder onCreateViewHolder(ViewGroup parent, int i) {
|
||||
View itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.video_item, parent, false);
|
||||
|
||||
return new InfoItemHolder(itemView);
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||
switch(type) {
|
||||
case 0:
|
||||
return new HFHolder(header);
|
||||
case 1:
|
||||
return new HFHolder(footer);
|
||||
case 2:
|
||||
return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.stream_item, parent, false));
|
||||
case 3:
|
||||
return new ChannelInfoItemHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.channel_item, parent, false));
|
||||
case 4:
|
||||
Log.e(TAG, "Playlist is not yet implemented");
|
||||
return null;
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(InfoItemHolder holder, int i) {
|
||||
infoItemBuilder.buildByHolder(holder, streamList.get(i));
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
|
||||
//god damen f*** ANDROID SH**
|
||||
if(holder instanceof InfoItemHolder) {
|
||||
if(header != null) {
|
||||
i--;
|
||||
}
|
||||
infoItemBuilder.buildByHolder((InfoItemHolder) holder, infoItemList.get(i));
|
||||
} else if(holder instanceof HFHolder && i == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if(holder instanceof HFHolder && i == infoItemList.size() && footer != null && showFooter) {
|
||||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.icu.text.IDNA;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoItemHolder.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class StreamInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
public final ImageView itemThumbnailView;
|
||||
public final TextView itemVideoTitleView,
|
||||
itemUploaderView,
|
||||
itemDurationView,
|
||||
itemUploadDateView,
|
||||
itemViewCountView;
|
||||
public final Button itemButton;
|
||||
|
||||
public StreamInfoItemHolder(View v) {
|
||||
super(v);
|
||||
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
|
||||
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
|
||||
itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView);
|
||||
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
|
||||
itemButton = (Button) v.findViewById(R.id.item_button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItem.InfoType infoType() {
|
||||
return InfoItem.InfoType.STREAM;
|
||||
}
|
||||
}
|
||||
1104
app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java
Normal file
1104
app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@ import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
@@ -24,9 +26,10 @@ import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
@@ -51,10 +54,13 @@ import java.io.IOException;
|
||||
/**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";
|
||||
private static final String ACTION_REWIND = TAG + ".REWIND";
|
||||
private static final String TAG = "BackgroundPlayer";
|
||||
private static final String CLASSNAME = "org.schabi.newpipe.player.BackgroundPlayer";
|
||||
private static final String ACTION_STOP = CLASSNAME + ".STOP";
|
||||
private static final String ACTION_PLAYPAUSE = CLASSNAME + ".PLAYPAUSE";
|
||||
private static final String ACTION_REWIND = CLASSNAME + ".REWIND";
|
||||
private static final String ACTION_PLAYBACK_STATE = CLASSNAME + ".PLAYBACK_STATE";
|
||||
private static final String EXTRA_PLAYBACK_STATE = CLASSNAME + ".extras.EXTRA_PLAYBACK_STATE";
|
||||
|
||||
// Extra intent arguments
|
||||
public static final String TITLE = "title";
|
||||
@@ -114,6 +120,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
|
||||
private class PlayerThread extends Thread {
|
||||
MediaPlayer mediaPlayer;
|
||||
private String source;
|
||||
@@ -123,8 +130,8 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
private NotificationManager noteMgr;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
private Bitmap videoThumbnail;
|
||||
private NotificationCompat.Builder noteBuilder;
|
||||
private Notification note;
|
||||
private NoteBuilder noteBuilder;
|
||||
private volatile boolean donePlaying = false;
|
||||
|
||||
public PlayerThread(String src, String title, BackgroundPlayer owner) {
|
||||
this.source = src;
|
||||
@@ -134,6 +141,45 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||
}
|
||||
|
||||
public boolean isDonePlaying() {
|
||||
return donePlaying;
|
||||
}
|
||||
|
||||
private boolean isPlaying() {
|
||||
try {
|
||||
return mediaPlayer.isPlaying();
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Unable to retrieve playing state", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setDonePlaying() {
|
||||
donePlaying = true;
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized PlaybackState getPlaybackState() {
|
||||
try {
|
||||
return new PlaybackState(mediaPlayer.getDuration(), mediaPlayer.getCurrentPosition(), isPlaying());
|
||||
} catch (IllegalStateException e) {
|
||||
// This isn't that nice way to handle this.
|
||||
// maybe there is a better way
|
||||
Log.w(TAG, this + ": Got illegal state exception while creating playback state", e);
|
||||
return PlaybackState.UNPREPARED;
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastState() {
|
||||
PlaybackState state = getPlaybackState();
|
||||
if(state == null) return;
|
||||
Intent intent = new Intent(ACTION_PLAYBACK_STATE);
|
||||
intent.putExtra(EXTRA_PLAYBACK_STATE, state);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock
|
||||
@@ -181,27 +227,29 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
filter.addAction(ACTION_PLAYPAUSE);
|
||||
filter.addAction(ACTION_STOP);
|
||||
filter.addAction(ACTION_REWIND);
|
||||
filter.addAction(ACTION_PLAYBACK_STATE);
|
||||
registerReceiver(broadcastReceiver, filter);
|
||||
|
||||
note = buildNotification();
|
||||
|
||||
startForeground(noteID, note);
|
||||
initNotificationBuilder();
|
||||
startForeground(noteID, noteBuilder.build());
|
||||
|
||||
//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());
|
||||
int vidLength = mediaPlayer.getDuration();
|
||||
int sleepTime = Math.min(2000, (int)(vidLength / 4));
|
||||
while(!isDonePlaying()) {
|
||||
broadcastState();
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
synchronized (this) {
|
||||
wait(sleepTime);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "sleep failure");
|
||||
Log.e(TAG, "sleep failure", e);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
/**Handles button presses from the notification. */
|
||||
@@ -210,39 +258,50 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
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);
|
||||
switch (action) {
|
||||
case ACTION_PLAYPAUSE: {
|
||||
boolean isPlaying = mediaPlayer.isPlaying();
|
||||
if(isPlaying) {
|
||||
mediaPlayer.pause();
|
||||
} else {
|
||||
//reacquire CPU lock after auto-releasing it on pause
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mediaPlayer.start();
|
||||
}
|
||||
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);
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
noteMgr.notify(noteID, note);
|
||||
break;
|
||||
}
|
||||
case ACTION_REWIND:
|
||||
mediaPlayer.seekTo(0);
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
break;
|
||||
case ACTION_STOP:
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
break;
|
||||
case ACTION_PLAYBACK_STATE: {
|
||||
PlaybackState playbackState = intent.getParcelableExtra(EXTRA_PLAYBACK_STATE);
|
||||
if(!playbackState.equals(PlaybackState.UNPREPARED)) {
|
||||
noteBuilder.setProgress(playbackState.getDuration(), playbackState.getPlayedTime(), false);
|
||||
noteBuilder.setIsPlaying(playbackState.isPlaying());
|
||||
} else {
|
||||
noteBuilder.setProgress(0, 0, true);
|
||||
}
|
||||
noteMgr.notify(noteID, noteBuilder.build());
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(action.equals(ACTION_REWIND)) {
|
||||
mediaPlayer.seekTo(0);
|
||||
// noteMgr.notify(noteID, note);
|
||||
}
|
||||
else if(action.equals(ACTION_STOP)) {
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void afterPlayCleanup() {
|
||||
// Notify thread to stop
|
||||
setDonePlaying();
|
||||
//remove progress bar
|
||||
//noteBuilder.setProgress(0, 0, false);
|
||||
|
||||
@@ -256,7 +315,12 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
wifiLock.release();
|
||||
//remove foreground status of service; make BackgroundPlayer killable
|
||||
stopForeground(true);
|
||||
|
||||
try {
|
||||
// Wait for thread to stop
|
||||
PlayerThread.this.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "unable to join player thread", e);
|
||||
}
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -272,10 +336,14 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
private void initNotificationBuilder() {
|
||||
Notification note;
|
||||
Resources res = getApplicationContext().getResources();
|
||||
noteBuilder = new NotificationCompat.Builder(owner);
|
||||
|
||||
/*
|
||||
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
|
||||
(R.drawable.ic_pause_white, "Pause", playPI).build();
|
||||
*/
|
||||
|
||||
PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
@@ -283,71 +351,235 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent rewindPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_REWIND), 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.putExtra(NavStack.SERVICE_ID, serviceId);
|
||||
openDetailViewIntent.putExtra(NavStack.URL, webUrl);
|
||||
openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID,
|
||||
openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
noteBuilder = new NoteBuilder(owner, playPI, stopPI, rewindPI, openDetailView);
|
||||
noteBuilder
|
||||
.setTitle(title)
|
||||
.setArtist(channelName)
|
||||
.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))
|
||||
.setContentIntent(openDetailView);
|
||||
.setContentIntent(openDetailView)
|
||||
.setCategory(Notification.CATEGORY_TRANSPORT)
|
||||
//Make notification appear on lockscreen
|
||||
.setVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
|
||||
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.notificationRewind, rewindPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
/**
|
||||
* Notification builder which works like the real builder but uses a custom view.
|
||||
*/
|
||||
class NoteBuilder extends NotificationCompat.Builder {
|
||||
|
||||
//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.notificationRewind, rewindPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationContent, 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;
|
||||
/**
|
||||
* @param context
|
||||
* @inheritDoc
|
||||
*/
|
||||
public NoteBuilder(Context context, PendingIntent playPI, PendingIntent stopPI,
|
||||
PendingIntent rewindPI, PendingIntent openDetailView) {
|
||||
super(context);
|
||||
setCustomContentView(createCustomContentView(playPI, stopPI, rewindPI, openDetailView));
|
||||
setCustomBigContentView(createCustomBigContentView(playPI, stopPI, rewindPI, openDetailView));
|
||||
}
|
||||
|
||||
return note;
|
||||
private RemoteViews createCustomBigContentView(PendingIntent playPI,
|
||||
PendingIntent stopPI,
|
||||
PendingIntent rewindPI,
|
||||
PendingIntent 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.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
return expandedView;
|
||||
}
|
||||
|
||||
private RemoteViews createCustomContentView(PendingIntent playPI, PendingIntent stopPI,
|
||||
PendingIntent rewindPI,
|
||||
PendingIntent openDetailView) {
|
||||
RemoteViews view = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
|
||||
view.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
view.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the stream
|
||||
* @param title the title of the stream
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
NoteBuilder setTitle(String title) {
|
||||
setContentTitle(title);
|
||||
getContentView().setTextViewText(R.id.notificationSongName, title);
|
||||
getBigContentView().setTextViewText(R.id.notificationSongName, title);
|
||||
setTicker(String.format(getBaseContext().getString(
|
||||
R.string.background_player_time_text), title));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the artist of the stream
|
||||
* @param artist the artist of the stream
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
NoteBuilder setArtist(String artist) {
|
||||
setSubText(artist);
|
||||
getContentView().setTextViewText(R.id.notificationArtist, artist);
|
||||
getBigContentView().setTextViewText(R.id.notificationArtist, artist);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.support.v4.app.NotificationCompat.Builder setProgress(int max, int progress, boolean indeterminate) {
|
||||
super.setProgress(max, progress, indeterminate);
|
||||
getBigContentView().setProgressBar(R.id.playbackProgress, max, progress, indeterminate);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the isPlaying state
|
||||
* @param isPlaying the is playing state
|
||||
*/
|
||||
public void setIsPlaying(boolean isPlaying) {
|
||||
RemoteViews views = getContentView(), bigViews = getBigContentView();
|
||||
int imageSrc;
|
||||
if(isPlaying) {
|
||||
imageSrc = R.drawable.ic_pause_white;
|
||||
} else {
|
||||
imageSrc = R.drawable.ic_play_circle_filled_white_24dp;
|
||||
}
|
||||
views.setImageViewResource(R.id.notificationPlayPause, imageSrc);
|
||||
bigViews.setImageViewResource(R.id.notificationPlayPause, imageSrc);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state of the player.
|
||||
*/
|
||||
public static class PlaybackState implements Parcelable {
|
||||
|
||||
private static final int INDEX_IS_PLAYING = 0;
|
||||
private static final int INDEX_IS_PREPARED= 1;
|
||||
private static final int INDEX_HAS_ERROR = 2;
|
||||
private final int duration;
|
||||
private final int played;
|
||||
private final boolean[] booleanValues = new boolean[3];
|
||||
|
||||
static final PlaybackState UNPREPARED = new PlaybackState(false, false, false);
|
||||
static final PlaybackState FAILED = new PlaybackState(false, false, true);
|
||||
|
||||
|
||||
PlaybackState(Parcel in) {
|
||||
duration = in.readInt();
|
||||
played = in.readInt();
|
||||
in.readBooleanArray(booleanValues);
|
||||
}
|
||||
|
||||
PlaybackState(int duration, int played, boolean isPlaying) {
|
||||
this.played = played;
|
||||
this.duration = duration;
|
||||
this.booleanValues[INDEX_IS_PLAYING] = isPlaying;
|
||||
this.booleanValues[INDEX_IS_PREPARED] = true;
|
||||
this.booleanValues[INDEX_HAS_ERROR] = false;
|
||||
}
|
||||
|
||||
private PlaybackState(boolean isPlaying, boolean isPrepared, boolean hasErrors) {
|
||||
this.played = 0;
|
||||
this.duration = 0;
|
||||
this.booleanValues[INDEX_IS_PLAYING] = isPlaying;
|
||||
this.booleanValues[INDEX_IS_PREPARED] = isPrepared;
|
||||
this.booleanValues[INDEX_HAS_ERROR] = hasErrors;
|
||||
}
|
||||
|
||||
int getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
int getPlayedTime() {
|
||||
return played;
|
||||
}
|
||||
|
||||
boolean isPlaying() {
|
||||
return booleanValues[INDEX_IS_PLAYING];
|
||||
}
|
||||
|
||||
boolean isPrepared() {
|
||||
return booleanValues[INDEX_IS_PREPARED];
|
||||
}
|
||||
|
||||
boolean hasErrors() {
|
||||
return booleanValues[INDEX_HAS_ERROR];
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(duration);
|
||||
dest.writeInt(played);
|
||||
dest.writeBooleanArray(booleanValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<PlaybackState> CREATOR = new Creator<PlaybackState>() {
|
||||
@Override
|
||||
public PlaybackState createFromParcel(Parcel in) {
|
||||
return new PlaybackState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackState[] newArray(int size) {
|
||||
return new PlaybackState[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PlaybackState that = (PlaybackState) o;
|
||||
|
||||
if (duration != that.duration) return false;
|
||||
if (played != that.played) return false;
|
||||
return Arrays.equals(booleanValues, that.booleanValues);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if(this == UNPREPARED) return 1;
|
||||
if(this == FAILED) return 2;
|
||||
int result = duration;
|
||||
result = 31 * result + played;
|
||||
result = 31 * result + Arrays.hashCode(booleanValues);
|
||||
return result + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,8 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -28,7 +27,6 @@ import android.widget.MediaController;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Service Popup Player implementing AbstractPlayer
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public class PopupVideoPlayer extends Service {
|
||||
private static final String TAG = ".PopupVideoPlayer";
|
||||
private static final boolean DEBUG = AbstractPlayer.DEBUG;
|
||||
|
||||
private static final int NOTIFICATION_ID = 40028922;
|
||||
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE";
|
||||
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE";
|
||||
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL";
|
||||
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT";
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams windowLayoutParams;
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private float screenWidth, screenHeight;
|
||||
private float popupWidth, popupHeight;
|
||||
private float currentPopupHeight = 110.0f * Resources.getSystem().getDisplayMetrics().density;
|
||||
//private float minimumHeight = 100; // TODO: Use it when implementing the resize of the popup
|
||||
|
||||
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notBuilder;
|
||||
private RemoteViews notRemoteView;
|
||||
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private AbstractPlayerImpl playerImpl;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
|
||||
initReceiver();
|
||||
|
||||
playerImpl = new AbstractPlayerImpl();
|
||||
ThemeHelper.setTheme(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (playerImpl.getPlayer() == null) initPopup();
|
||||
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
|
||||
|
||||
if (imageLoader != null) imageLoader.clearMemoryCache();
|
||||
if (intent.getStringExtra(NavStack.URL) != null) {
|
||||
playerImpl.setStartedFromNewPipe(false);
|
||||
Thread fetcher = new Thread(new FetcherRunnable(intent));
|
||||
fetcher.start();
|
||||
} else {
|
||||
playerImpl.setStartedFromNewPipe(true);
|
||||
playerImpl.handleIntent(intent);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
updateScreenSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
stopForeground(true);
|
||||
if (playerImpl != null) {
|
||||
playerImpl.destroy();
|
||||
if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView());
|
||||
}
|
||||
if (imageLoader != null) imageLoader.clearMemoryCache();
|
||||
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
|
||||
if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initReceiver() {
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]");
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_CLOSE:
|
||||
onVideoClose();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
playerImpl.onVideoPlayPause();
|
||||
break;
|
||||
case ACTION_OPEN_DETAIL:
|
||||
onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl());
|
||||
break;
|
||||
case ACTION_REPEAT:
|
||||
playerImpl.onRepeatClicked();
|
||||
break;
|
||||
case AbstractPlayer.ACTION_UPDATE_THUMB:
|
||||
playerImpl.onUpdateThumbnail(intent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ACTION_CLOSE);
|
||||
intentFilter.addAction(ACTION_PLAY_PAUSE);
|
||||
intentFilter.addAction(ACTION_OPEN_DETAIL);
|
||||
intentFilter.addAction(ACTION_REPEAT);
|
||||
intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void initPopup() {
|
||||
if (DEBUG) Log.d(TAG, "initPopup() called");
|
||||
View rootView = View.inflate(this, R.layout.player_popup, null);
|
||||
|
||||
playerImpl.setup(rootView);
|
||||
|
||||
updateScreenSize();
|
||||
windowLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) getMinimumVideoWidth(currentPopupHeight), (int) currentPopupHeight,
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
|
||||
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
|
||||
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
|
||||
gestureDetector = new GestureDetector(this, listener);
|
||||
gestureDetector.setIsLongpressEnabled(false);
|
||||
rootView.setOnTouchListener(listener);
|
||||
playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
|
||||
playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height);
|
||||
windowManager.addView(rootView, windowLayoutParams);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification);
|
||||
|
||||
if (playerImpl.getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail());
|
||||
else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
|
||||
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName());
|
||||
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationStop,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationContent,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
switch (playerImpl.getCurrentRepeatMode()) {
|
||||
case REPEAT_DISABLED:
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_play_arrow_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContent(notRemoteView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the notification, and the play/pause button in it.
|
||||
* Used for changes on the remoteView
|
||||
*
|
||||
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
|
||||
*/
|
||||
private void updateNotification(int drawableId) {
|
||||
if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
|
||||
if (notBuilder == null || notRemoteView == null) return;
|
||||
if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Misc
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void onVideoClose() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoClose() called");
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
public void onOpenDetail(Context context, String videoUrl) {
|
||||
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
|
||||
Intent i = new Intent(context, VideoItemDetailActivity.class);
|
||||
i.putExtra(NavStack.SERVICE_ID, 0)
|
||||
.putExtra(NavStack.URL, videoUrl)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private float getMinimumVideoWidth(float height) {
|
||||
float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||
if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width);
|
||||
return width;
|
||||
}
|
||||
|
||||
private void updateScreenSize() {
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
windowManager.getDefaultDisplay().getMetrics(metrics);
|
||||
|
||||
screenWidth = metrics.widthPixels;
|
||||
screenHeight = metrics.heightPixels;
|
||||
if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private class AbstractPlayerImpl extends AbstractPlayer {
|
||||
AbstractPlayerImpl() {
|
||||
super("AbstractPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playVideo(Uri videoURI, boolean autoPlay) {
|
||||
super.playVideo(videoURI, autoPlay);
|
||||
|
||||
windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight);
|
||||
windowManager.updateViewLayout(getRootView(), windowLayoutParams);
|
||||
|
||||
notBuilder = createNotification();
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullScreenButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
|
||||
Intent intent;
|
||||
//if (getSharedPreferences().getBoolean(getResources().getString(R.string.use_exoplayer_key), false)) {
|
||||
// TODO: Remove this check when ExoPlayer is the default
|
||||
// For now just disable the non-exoplayer player
|
||||
//noinspection ConstantConditions,ConstantIfStatement
|
||||
if (true) {
|
||||
intent = new Intent(PopupVideoPlayer.this, ExoPlayerActivity.class)
|
||||
.putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle())
|
||||
.putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl())
|
||||
.putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName())
|
||||
.putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream())
|
||||
.putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList())
|
||||
.putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition()));
|
||||
if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(AbstractPlayer.STARTED_FROM_NEWPIPE, false);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
} else {
|
||||
intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle())
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedStreamUri().toString())
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl())
|
||||
.putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatClicked() {
|
||||
super.onRepeatClicked();
|
||||
switch (getCurrentRepeatMode()) {
|
||||
case REPEAT_DISABLED:
|
||||
// Drawable didn't work on low API :/
|
||||
//notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white);
|
||||
// Set the icon to 30% opacity - 255 (max) * .3
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77);
|
||||
break;
|
||||
case REPEAT_ONE:
|
||||
notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
break;
|
||||
case REPEAT_ALL:
|
||||
// Waiting :)
|
||||
break;
|
||||
}
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateThumbnail(Intent intent) {
|
||||
super.onUpdateThumbnail(intent);
|
||||
if (getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, getVideoThumbnail());
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(PopupMenu menu) {
|
||||
super.onDismiss(menu);
|
||||
if (isPlaying()) animateView(getControlsRoot(), false, 500, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onLoading() {
|
||||
super.onLoading();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
super.onBuffering();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
showAndAnimateControl(R.drawable.ic_play_arrow_white, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
showAndAnimateControl(R.drawable.ic_replay_white, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private int initialPopupX, initialPopupY;
|
||||
private boolean isMoving;
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (!playerImpl.isPlaying()) return false;
|
||||
if (e.getX() > popupWidth / 2) playerImpl.onFastForward();
|
||||
else playerImpl.onFastRewind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (playerImpl.getPlayer() == null) return false;
|
||||
playerImpl.onVideoPlayPause();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
|
||||
initialPopupX = windowLayoutParams.x;
|
||||
initialPopupY = windowLayoutParams.y;
|
||||
popupWidth = playerImpl.getRootView().getWidth();
|
||||
popupHeight = playerImpl.getRootView().getHeight();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f) playerImpl.animateView(playerImpl.getControlsRoot(), true, 30, 0);
|
||||
isMoving = true;
|
||||
float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX);
|
||||
float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY);
|
||||
|
||||
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
|
||||
else if (posX < 0) posX = 0;
|
||||
|
||||
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
|
||||
else if (posY < 0) posY = 0;
|
||||
|
||||
windowLayoutParams.x = (int) posX;
|
||||
windowLayoutParams.y = (int) posY;
|
||||
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
|
||||
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
|
||||
", distanceXy = [" + distanceX + ", " + distanceY + "]" +
|
||||
", posXy = [" + posX + ", " + posY + "]" +
|
||||
", popupWh rootView.get wh = [" + popupWidth + " x " + popupHeight + "]");
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) {
|
||||
playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
gestureDetector.onTouchEvent(event);
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && isMoving) {
|
||||
isMoving = false;
|
||||
onScrollEnd();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetcher used if open by a link out of NewPipe
|
||||
*/
|
||||
private class FetcherRunnable implements Runnable {
|
||||
private final Intent intent;
|
||||
private final Handler mainHandler;
|
||||
private final boolean printStreams = true;
|
||||
|
||||
|
||||
FetcherRunnable(Intent intent) {
|
||||
this.intent = intent;
|
||||
this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
StreamExtractor streamExtractor;
|
||||
try {
|
||||
StreamingService service = NewPipe.getService(0);
|
||||
if (service == null) return;
|
||||
streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL));
|
||||
StreamInfo info = StreamInfo.getVideoInfo(streamExtractor);
|
||||
String defaultResolution = playerImpl.getSharedPreferences().getString(
|
||||
getResources().getString(R.string.default_resolution_key),
|
||||
getResources().getString(R.string.default_resolution_value));
|
||||
|
||||
VideoStream chosen = null, secondary = null, fallback = null;
|
||||
playerImpl.setVideoStreamsList(info.video_streams instanceof ArrayList
|
||||
? (ArrayList<VideoStream>) info.video_streams
|
||||
: new ArrayList<>(info.video_streams));
|
||||
|
||||
for (VideoStream item : info.video_streams) {
|
||||
if (DEBUG && printStreams) {
|
||||
Log.d(TAG, "FetcherRunnable.StreamExtractor: current Item"
|
||||
+ ", item.resolution = " + item.resolution
|
||||
+ ", item.format = " + item.format
|
||||
+ ", item.url = " + item.url);
|
||||
}
|
||||
if (defaultResolution.equals(item.resolution)) {
|
||||
if (item.format == MediaFormat.MPEG_4.id) {
|
||||
chosen = item;
|
||||
if (DEBUG) Log.d(TAG, "FetcherRunnable.StreamExtractor: CHOSEN item, item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url);
|
||||
} else if (item.format == 2) secondary = item;
|
||||
else fallback = item;
|
||||
}
|
||||
}
|
||||
|
||||
int selectedIndexStream;
|
||||
|
||||
if (chosen != null) selectedIndexStream = info.video_streams.indexOf(chosen);
|
||||
else if (secondary != null) selectedIndexStream = info.video_streams.indexOf(secondary);
|
||||
else if (fallback != null) selectedIndexStream = info.video_streams.indexOf(fallback);
|
||||
else selectedIndexStream = 0;
|
||||
|
||||
playerImpl.setSelectedIndexStream(selectedIndexStream);
|
||||
|
||||
if (DEBUG && printStreams) Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + chosen
|
||||
+ "\n, secondary = " + secondary
|
||||
+ "\n, fallback = " + fallback
|
||||
+ "\n, info.video_streams.get(0).url = " + info.video_streams.get(0).url);
|
||||
|
||||
|
||||
playerImpl.setVideoUrl(info.webpage_url);
|
||||
playerImpl.setVideoTitle(info.title);
|
||||
playerImpl.setChannelName(info.uploader);
|
||||
if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000);
|
||||
else playerImpl.setVideoStartPos(-1);
|
||||
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playerImpl.playVideo(playerImpl.getSelectedStreamUri(), true);
|
||||
}
|
||||
});
|
||||
imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playerImpl.setVideoThumbnail(loadedImage);
|
||||
if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
updateNotification(-1);
|
||||
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (IOException ie) {
|
||||
if (DEBUG) ie.printStackTrace();
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(PopupVideoPlayer.this, R.string.network_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
} catch (Exception e) {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(PopupVideoPlayer.this, R.string.content_not_available, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
public interface StateInterface {
|
||||
int STATE_LOADING = 123;
|
||||
int STATE_PLAYING = 124;
|
||||
int STATE_BUFFERING = 125;
|
||||
int STATE_PAUSED = 126;
|
||||
int STATE_PAUSED_SEEK = 127;
|
||||
int STATE_COMPLETED = 128;
|
||||
|
||||
void changeState(int state);
|
||||
|
||||
void onLoading();
|
||||
void onPlaying();
|
||||
void onBuffering();
|
||||
void onPaused();
|
||||
void onPausedSeek();
|
||||
void onCompleted();
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.dash.DefaultDashTrackSelector;
|
||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||
import com.google.android.exoplayer.dash.mpd.Period;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElement;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback;
|
||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for DASH.
|
||||
*/
|
||||
public class DashRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final String TAG = "DashRendererBuilder";
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||
|
||||
private static final int SECURITY_LEVEL_UNKNOWN = -1;
|
||||
private static final int SECURITY_LEVEL_1 = 1;
|
||||
private static final int SECURITY_LEVEL_3 = 3;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public DashRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
this.drmCallback = drmCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder
|
||||
implements ManifestFetcher.ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||
private final UriDataSource manifestDataSource;
|
||||
|
||||
private boolean canceled;
|
||||
private MediaPresentationDescription manifest;
|
||||
private long elapsedRealtimeOffset;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.drmCallback = drmCallback;
|
||||
this.player = player;
|
||||
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||
manifestDataSource = new DefaultUriDataSource(context, userAgent);
|
||||
manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(MediaPresentationDescription manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manifest = manifest;
|
||||
if (manifest.dynamic && manifest.utcTiming != null) {
|
||||
UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming,
|
||||
manifestFetcher.getManifestLoadCompleteTimestamp(), this);
|
||||
} else {
|
||||
buildRenderers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||
buildRenderers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
|
||||
// Be optimistic and continue in the hope that the device clock is correct.
|
||||
buildRenderers();
|
||||
}
|
||||
|
||||
private void buildRenderers() {
|
||||
Period period = manifest.getPeriod(0);
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
||||
boolean hasContentProtection = false;
|
||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||
if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) {
|
||||
hasContentProtection |= adaptationSet.hasContentProtection();
|
||||
}
|
||||
}
|
||||
|
||||
// Check drm support if necessary.
|
||||
boolean filterHdContent = false;
|
||||
StreamingDrmSessionManager drmSessionManager = null;
|
||||
if (hasContentProtection) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
player.onRenderersError(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(
|
||||
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||
filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1;
|
||||
} catch (UnsupportedDrmException e) {
|
||||
player.onRenderersError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent),
|
||||
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_VIDEO);
|
||||
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
drmSessionManager, true, mainHandler, player, 50);
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO);
|
||||
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_AUDIO);
|
||||
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Build the text renderer.
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource textChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_TEXT);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||
mainHandler.getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
|
||||
String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
|
||||
return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
|
||||
.equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.audio.AudioTrack;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.util.VerboseLogUtil;
|
||||
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Logs player events using {@link Log}.
|
||||
*/
|
||||
public class EventLogger implements NPExoPlayer.Listener, NPExoPlayer.InfoListener,
|
||||
NPExoPlayer.InternalErrorListener {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final NumberFormat TIME_FORMAT;
|
||||
static {
|
||||
TIME_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
TIME_FORMAT.setMinimumFractionDigits(2);
|
||||
TIME_FORMAT.setMaximumFractionDigits(2);
|
||||
}
|
||||
|
||||
private long sessionStartTimeMs;
|
||||
private long[] loadStartTimeMs;
|
||||
private long[] availableRangeValuesUs;
|
||||
|
||||
public EventLogger() {
|
||||
loadStartTimeMs = new long[NPExoPlayer.RENDERER_COUNT];
|
||||
}
|
||||
|
||||
public void startSession() {
|
||||
sessionStartTimeMs = SystemClock.elapsedRealtime();
|
||||
Log.d(TAG, "start [0]");
|
||||
}
|
||||
|
||||
public void endSession() {
|
||||
Log.d(TAG, "end [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.Listener
|
||||
|
||||
@Override
|
||||
public void onStateChanged(boolean playWhenReady, int state) {
|
||||
Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
|
||||
+ getStateString(state) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees
|
||||
+ ", " + pixelWidthHeightRatio + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.InfoListener
|
||||
|
||||
@Override
|
||||
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||
Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", "
|
||||
+ getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
|
||||
+ ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
|
||||
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs) {
|
||||
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
|
||||
+ Integer.toString(trigger) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs) {
|
||||
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
|
||||
+ Integer.toString(trigger) + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.InternalErrorListener
|
||||
|
||||
@Override
|
||||
public void onLoadError(int sourceId, IOException e) {
|
||||
printInternalError("loadError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRendererInitializationError(Exception e) {
|
||||
printInternalError("rendererInitError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
printInternalError("drmSessionManagerError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||
printInternalError("decoderInitializationError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||
printInternalError("audioTrackInitializationError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||
printInternalError("audioTrackWriteError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
|
||||
+ elapsedSinceLastFeedMs + "]", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoError(CryptoException e) {
|
||||
printInternalError("cryptoError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs) {
|
||||
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||
availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
|
||||
Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0]
|
||||
+ ", " + availableRangeValuesUs[1] + "]");
|
||||
}
|
||||
|
||||
private void printInternalError(String type, Exception e) {
|
||||
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
|
||||
}
|
||||
|
||||
private String getStateString(int state) {
|
||||
switch (state) {
|
||||
case ExoPlayer.STATE_BUFFERING:
|
||||
return "B";
|
||||
case ExoPlayer.STATE_ENDED:
|
||||
return "E";
|
||||
case ExoPlayer.STATE_IDLE:
|
||||
return "I";
|
||||
case ExoPlayer.STATE_PREPARING:
|
||||
return "P";
|
||||
case ExoPlayer.STATE_READY:
|
||||
return "R";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
private String getSessionTimeString() {
|
||||
return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs);
|
||||
}
|
||||
|
||||
private String getTimeString(long timeMs) {
|
||||
return TIME_FORMAT.format((timeMs) / 1000f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.extractor.Extractor;
|
||||
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.Allocator;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for streams that can be read using an {@link Extractor}.
|
||||
*/
|
||||
public class ExtractorRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int BUFFER_SEGMENT_COUNT = 256;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final Uri uri;
|
||||
|
||||
public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
|
||||
|
||||
// Build the video and audio renderers.
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(),
|
||||
null);
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
|
||||
BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE);
|
||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
player.getMainHandler(), player, 50);
|
||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player,
|
||||
player.getMainHandler().getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
|
||||
import com.google.android.exoplayer.hls.HlsChunkSource;
|
||||
import com.google.android.exoplayer.hls.HlsMasterPlaylist;
|
||||
import com.google.android.exoplayer.hls.HlsPlaylist;
|
||||
import com.google.android.exoplayer.hls.HlsPlaylistParser;
|
||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
|
||||
import com.google.android.exoplayer.metadata.Id3Parser;
|
||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for HLS.
|
||||
*/
|
||||
public class HlsRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int MAIN_BUFFER_SEGMENTS = 256;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public HlsRendererBuilder(Context context, String userAgent, String url) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder implements ManifestCallback<HlsPlaylist> {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<HlsPlaylist> playlistFetcher;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
this.player = player;
|
||||
HlsPlaylistParser parser = new HlsPlaylistParser();
|
||||
playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent),
|
||||
parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(HlsPlaylist manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
|
||||
|
||||
// Build the video/audio/metadata renderers.
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
|
||||
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
|
||||
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
||||
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
||||
5000, mainHandler, player, 50);
|
||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
|
||||
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
|
||||
|
||||
// Build the text renderer, preferring Webvtt where available.
|
||||
boolean preferWebvtt = false;
|
||||
if (manifest instanceof HlsMasterPlaylist) {
|
||||
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
|
||||
}
|
||||
TrackRenderer textRenderer;
|
||||
if (preferWebvtt) {
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
|
||||
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
|
||||
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper());
|
||||
} else {
|
||||
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
|
||||
}
|
||||
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,599 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.CodecCounters;
|
||||
import com.google.android.exoplayer.DummyTrackRenderer;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioTrack;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer;
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.text.TextRenderer;
|
||||
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.util.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer.util.PlayerControl;
|
||||
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
|
||||
* with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
|
||||
* SmoothStreaming and so on).
|
||||
*/
|
||||
public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
||||
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
|
||||
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
|
||||
StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
|
||||
MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {
|
||||
|
||||
/**
|
||||
* Builds renderers for the player.
|
||||
*/
|
||||
public interface RendererBuilder {
|
||||
/**
|
||||
* Builds renderers for playback.
|
||||
*
|
||||
* @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers}
|
||||
* should be invoked once the renderers have been built. If building fails,
|
||||
* {@link NPExoPlayer#onRenderersError} should be invoked.
|
||||
*/
|
||||
void buildRenderers(NPExoPlayer player);
|
||||
/**
|
||||
* Cancels the current build operation, if there is one. Else does nothing.
|
||||
* <p>
|
||||
* A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or
|
||||
* {@link NPExoPlayer#onRenderersError} on the player, which may have been released.
|
||||
*/
|
||||
void cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for core events.
|
||||
*/
|
||||
public interface Listener {
|
||||
void onStateChanged(boolean playWhenReady, int playbackState);
|
||||
void onError(Exception e);
|
||||
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for internal errors.
|
||||
* <p>
|
||||
* These errors are not visible to the user, and hence this listener is provided for
|
||||
* informational purposes only. Note however that an internal error may cause a fatal
|
||||
* error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
|
||||
* will be invoked.
|
||||
*/
|
||||
public interface InternalErrorListener {
|
||||
void onRendererInitializationError(Exception e);
|
||||
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
|
||||
void onAudioTrackWriteError(AudioTrack.WriteException e);
|
||||
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
||||
void onDecoderInitializationError(DecoderInitializationException e);
|
||||
void onCryptoError(CryptoException e);
|
||||
void onLoadError(int sourceId, IOException e);
|
||||
void onDrmSessionManagerError(Exception e);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for debugging information.
|
||||
*/
|
||||
public interface InfoListener {
|
||||
void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||
void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||
void onDroppedFrames(int count, long elapsed);
|
||||
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
|
||||
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs);
|
||||
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
|
||||
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs);
|
||||
void onAvailableRangeChanged(int sourceId, TimeRange availableRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for receiving notifications of timed text.
|
||||
*/
|
||||
public interface CaptionListener {
|
||||
void onCues(List<Cue> cues);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for receiving ID3 metadata parsed from the media stream.
|
||||
*/
|
||||
public interface Id3MetadataListener {
|
||||
void onId3Metadata(Map<String, Object> metadata);
|
||||
}
|
||||
|
||||
// Constants pulled into this class for convenience.
|
||||
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
|
||||
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
|
||||
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
|
||||
public static final int STATE_READY = ExoPlayer.STATE_READY;
|
||||
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
|
||||
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
|
||||
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
|
||||
|
||||
public static final int RENDERER_COUNT = 4;
|
||||
public static final int TYPE_VIDEO = 0;
|
||||
public static final int TYPE_AUDIO = 1;
|
||||
public static final int TYPE_TEXT = 2;
|
||||
public static final int TYPE_METADATA = 3;
|
||||
|
||||
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
|
||||
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
|
||||
private static final int RENDERER_BUILDING_STATE_BUILT = 3;
|
||||
|
||||
private final RendererBuilder rendererBuilder;
|
||||
private final ExoPlayer player;
|
||||
private final PlayerControl playerControl;
|
||||
private final Handler mainHandler;
|
||||
private final CopyOnWriteArrayList<Listener> listeners;
|
||||
|
||||
private int rendererBuildingState;
|
||||
private int lastReportedPlaybackState;
|
||||
private boolean lastReportedPlayWhenReady;
|
||||
|
||||
private Surface surface;
|
||||
private TrackRenderer videoRenderer;
|
||||
private CodecCounters codecCounters;
|
||||
private Format videoFormat;
|
||||
private int videoTrackToRestore;
|
||||
|
||||
private BandwidthMeter bandwidthMeter;
|
||||
private boolean backgrounded;
|
||||
|
||||
private CaptionListener captionListener;
|
||||
private Id3MetadataListener id3MetadataListener;
|
||||
private InternalErrorListener internalErrorListener;
|
||||
private InfoListener infoListener;
|
||||
|
||||
public NPExoPlayer(RendererBuilder rendererBuilder) {
|
||||
this.rendererBuilder = rendererBuilder;
|
||||
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
|
||||
player.addListener(this);
|
||||
playerControl = new PlayerControl(player);
|
||||
mainHandler = new Handler();
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
lastReportedPlaybackState = STATE_IDLE;
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
// Disable text initially.
|
||||
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
|
||||
}
|
||||
|
||||
public PlayerControl getPlayerControl() {
|
||||
return playerControl;
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void setInternalErrorListener(InternalErrorListener listener) {
|
||||
internalErrorListener = listener;
|
||||
}
|
||||
|
||||
public void setInfoListener(InfoListener listener) {
|
||||
infoListener = listener;
|
||||
}
|
||||
|
||||
public void setCaptionListener(CaptionListener listener) {
|
||||
captionListener = listener;
|
||||
}
|
||||
|
||||
public void setMetadataListener(Id3MetadataListener listener) {
|
||||
id3MetadataListener = listener;
|
||||
}
|
||||
|
||||
public void setSurface(Surface surface) {
|
||||
this.surface = surface;
|
||||
pushSurface(false);
|
||||
}
|
||||
|
||||
public Surface getSurface() {
|
||||
return surface;
|
||||
}
|
||||
|
||||
public void blockingClearSurface() {
|
||||
surface = null;
|
||||
pushSurface(true);
|
||||
}
|
||||
|
||||
public int getTrackCount(int type) {
|
||||
return player.getTrackCount(type);
|
||||
}
|
||||
|
||||
public MediaFormat getTrackFormat(int type, int index) {
|
||||
return player.getTrackFormat(type, index);
|
||||
}
|
||||
|
||||
public int getSelectedTrack(int type) {
|
||||
return player.getSelectedTrack(type);
|
||||
}
|
||||
|
||||
public void setSelectedTrack(int type, int index) {
|
||||
player.setSelectedTrack(type, index);
|
||||
if (type == TYPE_TEXT && index < 0 && captionListener != null) {
|
||||
captionListener.onCues(Collections.<Cue>emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getBackgrounded() {
|
||||
return backgrounded;
|
||||
}
|
||||
|
||||
public void setBackgrounded(boolean backgrounded) {
|
||||
if (this.backgrounded == backgrounded) {
|
||||
return;
|
||||
}
|
||||
this.backgrounded = backgrounded;
|
||||
if (backgrounded) {
|
||||
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
|
||||
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
|
||||
blockingClearSurface();
|
||||
} else {
|
||||
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
|
||||
player.stop();
|
||||
}
|
||||
rendererBuilder.cancel();
|
||||
videoFormat = null;
|
||||
videoRenderer = null;
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
|
||||
maybeReportPlayerState();
|
||||
rendererBuilder.buildRenderers(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked with the results from a {@link RendererBuilder}.
|
||||
*
|
||||
* @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual
|
||||
* element may be null if there do not exist tracks of the corresponding type.
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
|
||||
*/
|
||||
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
|
||||
for (int i = 0; i < RENDERER_COUNT; i++) {
|
||||
if (renderers[i] == null) {
|
||||
// Convert a null renderer to a dummy renderer.
|
||||
renderers[i] = new DummyTrackRenderer();
|
||||
}
|
||||
}
|
||||
// Complete preparation.
|
||||
this.videoRenderer = renderers[TYPE_VIDEO];
|
||||
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
|
||||
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
|
||||
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
|
||||
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
pushSurface(false);
|
||||
player.prepare(renderers);
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked if a {@link RendererBuilder} encounters an error.
|
||||
*
|
||||
* @param e Describes the error.
|
||||
*/
|
||||
/* package */ void onRenderersError(Exception e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onRendererInitializationError(e);
|
||||
}
|
||||
for (Listener listener : listeners) {
|
||||
listener.onError(e);
|
||||
}
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
maybeReportPlayerState();
|
||||
}
|
||||
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
player.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
|
||||
public void seekTo(long positionMs) {
|
||||
player.seekTo(positionMs);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
rendererBuilder.cancel();
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
surface = null;
|
||||
player.release();
|
||||
}
|
||||
|
||||
public int getPlaybackState() {
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
|
||||
return STATE_PREPARING;
|
||||
}
|
||||
int playerState = player.getPlaybackState();
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
|
||||
// This is an edge case where the renderers are built, but are still being passed to the
|
||||
// player's playback thread.
|
||||
return STATE_PREPARING;
|
||||
}
|
||||
return playerState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format getFormat() {
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BandwidthMeter getBandwidthMeter() {
|
||||
return bandwidthMeter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodecCounters getCodecCounters() {
|
||||
return codecCounters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return player.getCurrentPosition();
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return player.getDuration();
|
||||
}
|
||||
|
||||
public int getBufferedPercentage() {
|
||||
return player.getBufferedPercentage();
|
||||
}
|
||||
|
||||
public boolean getPlayWhenReady() {
|
||||
return player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
/* package */ Looper getPlaybackLooper() {
|
||||
return player.getPlaybackLooper();
|
||||
}
|
||||
|
||||
/* package */ Handler getMainHandler() {
|
||||
return mainHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int state) {
|
||||
maybeReportPlayerState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException exception) {
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
for (Listener listener : listeners) {
|
||||
listener.onError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onDroppedFrames(count, elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
|
||||
long mediaTimeMs) {
|
||||
if (infoListener == null) {
|
||||
return;
|
||||
}
|
||||
if (sourceId == TYPE_VIDEO) {
|
||||
videoFormat = format;
|
||||
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
|
||||
} else if (sourceId == TYPE_AUDIO) {
|
||||
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysLoaded() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onDrmSessionManagerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onDecoderInitializationError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackInitializationError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackWriteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoError(CryptoException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onCryptoError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(int sourceId, IOException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onLoadError(sourceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
|
||||
captionListener.onCues(cues);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadata(Map<String, Object> metadata) {
|
||||
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
|
||||
id3MetadataListener.onId3Metadata(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onAvailableRangeChanged(sourceId, availableRange);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyCommitted() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawnToSurface(Surface surface) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
|
||||
mediaEndTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
|
||||
mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(int sourceId, long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void maybeReportPlayerState() {
|
||||
boolean playWhenReady = player.getPlayWhenReady();
|
||||
int playbackState = getPlaybackState();
|
||||
if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
lastReportedPlayWhenReady = playWhenReady;
|
||||
lastReportedPlaybackState = playbackState;
|
||||
}
|
||||
}
|
||||
|
||||
private void pushSurface(boolean blockForSurfacePush) {
|
||||
if (videoRenderer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockForSurfacePush) {
|
||||
player.blockingSendMessage(
|
||||
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||
} else {
|
||||
player.sendMessage(
|
||||
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for SmoothStreaming.
|
||||
*/
|
||||
public class SmoothStreamingRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest";
|
||||
this.drmCallback = drmCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder
|
||||
implements ManifestFetcher.ManifestCallback<SmoothStreamingManifest> {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.drmCallback = drmCallback;
|
||||
this.player = player;
|
||||
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||
manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null),
|
||||
parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException exception) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(SmoothStreamingManifest manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
||||
// Check drm support if necessary.
|
||||
DrmSessionManager drmSessionManager = null;
|
||||
if (manifest.protectionElement != null) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
player.onRenderersError(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
|
||||
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
player.onRenderersError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false),
|
||||
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_VIDEO);
|
||||
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
drmSessionManager, true, mainHandler, player, 50);
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newAudioInstance(),
|
||||
audioDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_AUDIO);
|
||||
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Build the text renderer.
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newTextInstance(),
|
||||
textDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_TEXT);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||
mainHandler.getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
package org.schabi.newpipe.report;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -8,6 +6,8 @@ import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
@@ -15,8 +15,6 @@ import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -36,6 +34,7 @@ import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
@@ -66,64 +65,11 @@ import java.util.Vector;
|
||||
*/
|
||||
|
||||
public class ErrorActivity extends AppCompatActivity {
|
||||
public static class ErrorInfo implements Parcelable {
|
||||
public int userAction;
|
||||
public String request;
|
||||
public String serviceName;
|
||||
public int message;
|
||||
|
||||
public static ErrorInfo make(int userAction, String serviceName, String request, int message) {
|
||||
ErrorInfo info = new ErrorInfo();
|
||||
info.userAction = userAction;
|
||||
info.serviceName = serviceName;
|
||||
info.request = request;
|
||||
info.message = message;
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(this.userAction);
|
||||
dest.writeString(this.request);
|
||||
dest.writeString(this.serviceName);
|
||||
dest.writeInt(this.message);
|
||||
}
|
||||
|
||||
public ErrorInfo() {
|
||||
}
|
||||
|
||||
protected ErrorInfo(Parcel in) {
|
||||
this.userAction = in.readInt();
|
||||
this.request = in.readString();
|
||||
this.serviceName = in.readString();
|
||||
this.message = in.readInt();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<ErrorInfo> CREATOR = new Parcelable.Creator<ErrorInfo>() {
|
||||
@Override
|
||||
public ErrorInfo createFromParcel(Parcel source) {
|
||||
return new ErrorInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorInfo[] newArray(int size) {
|
||||
return new ErrorInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// LOG TAGS
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
|
||||
// BUNDLE TAGS
|
||||
public static final String ERROR_INFO = "error_info";
|
||||
public static final String ERROR_LIST = "error_list";
|
||||
|
||||
// MESSAGE ID
|
||||
public static final int SEARCHED = 0;
|
||||
public static final int REQUESTED_STREAM = 1;
|
||||
@@ -133,7 +79,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
public static final int LOAD_IMAGE = 5;
|
||||
public static final int UI_ERROR = 6;
|
||||
public static final int REQUESTED_CHANNEL = 7;
|
||||
|
||||
// MESSAGE STRING
|
||||
public static final String SEARCHED_STRING = "searched";
|
||||
public static final String REQUESTED_STREAM_STRING = "requested stream";
|
||||
@@ -143,17 +88,14 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
public static final String LOAD_IMAGE_STRING = "load image";
|
||||
public static final String UI_ERROR_STRING = "ui error";
|
||||
public static final String REQUESTED_CHANNEL_STRING = "requested channel";
|
||||
|
||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME;
|
||||
|
||||
Thread globIpRangeThread;
|
||||
private String[] errorList;
|
||||
private ErrorInfo errorInfo;
|
||||
private Class returnActivity;
|
||||
private String currentTimeStamp;
|
||||
private String globIpRange;
|
||||
Thread globIpRangeThread;
|
||||
|
||||
// views
|
||||
private TextView errorView;
|
||||
private EditText userCommentBox;
|
||||
@@ -161,6 +103,10 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
private TextView infoView;
|
||||
private TextView errorMessageView;
|
||||
|
||||
public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
|
||||
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
public static void reportError(final Context context, final List<Throwable> el,
|
||||
final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) {
|
||||
|
||||
@@ -238,9 +184,26 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static String getStackTrace(final Throwable throwable) {
|
||||
final StringWriter sw = new StringWriter();
|
||||
final PrintWriter pw = new PrintWriter(sw, true);
|
||||
throwable.printStackTrace(pw);
|
||||
return sw.getBuffer().toString();
|
||||
}
|
||||
|
||||
// errorList to StringList
|
||||
private static String[] elToSl(List<Throwable> stackTraces) {
|
||||
String[] out = new String[stackTraces.size()];
|
||||
for (int i = 0; i < stackTraces.size(); i++) {
|
||||
out[i] = getStackTrace(stackTraces.get(i));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, true);
|
||||
setContentView(R.layout.activity_error);
|
||||
|
||||
Intent intent = getIntent();
|
||||
@@ -297,6 +260,11 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
errorView.setText(formErrorText(errorList));
|
||||
|
||||
//print stack trace once again for debugging:
|
||||
for(String e : errorList) {
|
||||
Log.e(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -325,13 +293,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getStackTrace(final Throwable throwable) {
|
||||
final StringWriter sw = new StringWriter();
|
||||
final PrintWriter pw = new PrintWriter(sw, true);
|
||||
throwable.printStackTrace(pw);
|
||||
return sw.getBuffer().toString();
|
||||
}
|
||||
|
||||
private String formErrorText(String[] el) {
|
||||
String text = "";
|
||||
if(el != null) {
|
||||
@@ -468,13 +429,63 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
public static class ErrorInfo implements Parcelable {
|
||||
public static final Parcelable.Creator<ErrorInfo> CREATOR = new Parcelable.Creator<ErrorInfo>() {
|
||||
@Override
|
||||
public ErrorInfo createFromParcel(Parcel source) {
|
||||
return new ErrorInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorInfo[] newArray(int size) {
|
||||
return new ErrorInfo[size];
|
||||
}
|
||||
};
|
||||
public int userAction;
|
||||
public String request;
|
||||
public String serviceName;
|
||||
public int message;
|
||||
|
||||
public ErrorInfo() {
|
||||
}
|
||||
|
||||
protected ErrorInfo(Parcel in) {
|
||||
this.userAction = in.readInt();
|
||||
this.request = in.readString();
|
||||
this.serviceName = in.readString();
|
||||
this.message = in.readInt();
|
||||
}
|
||||
|
||||
public static ErrorInfo make(int userAction, String serviceName, String request, int message) {
|
||||
ErrorInfo info = new ErrorInfo();
|
||||
info.userAction = userAction;
|
||||
info.serviceName = serviceName;
|
||||
info.request = request;
|
||||
info.message = message;
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(this.userAction);
|
||||
dest.writeString(this.request);
|
||||
dest.writeString(this.serviceName);
|
||||
dest.writeInt(this.message);
|
||||
}
|
||||
}
|
||||
|
||||
private class IpRagneRequester implements Runnable {
|
||||
Handler h = new Handler();
|
||||
public void run() {
|
||||
String ipRange = "none";
|
||||
try {
|
||||
Downloader dl = new Downloader();
|
||||
String ip = dl.download("https://ifcfg.me/ip");
|
||||
Downloader dl = Downloader.getInstance();
|
||||
String ip = dl.download("https://ipv4.icanhazip.com");
|
||||
|
||||
ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip)
|
||||
+ "0.0";
|
||||
@@ -487,8 +498,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class IpRageReturnRunnable implements Runnable {
|
||||
String ipRange;
|
||||
public IpRageReturnRunnable(String ipRange) {
|
||||
@@ -504,13 +513,4 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorList to StringList
|
||||
private static String[] elToSl(List<Throwable> stackTraces) {
|
||||
String[] out = new String[stackTraces.size()];
|
||||
for(int i = 0; i < stackTraces.size(); i++) {
|
||||
out[i] = getStackTrace(stackTraces.get(i));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -18,14 +19,20 @@ import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.extractor.SearchResult;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
@@ -51,13 +58,18 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
|
||||
private static final String TAG = SearchInfoItemFragment.class.toString();
|
||||
|
||||
private EnumSet<SearchEngine.Filter> filter =
|
||||
EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM);
|
||||
|
||||
/**
|
||||
* Listener for search queries
|
||||
*/
|
||||
public class SearchQueryListener implements SearchView.OnQueryTextListener {
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
Activity a = getActivity();
|
||||
try {
|
||||
searchQuery = query;
|
||||
search(query);
|
||||
|
||||
// hide virtual keyboard
|
||||
@@ -67,12 +79,12 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
//noinspection ConstantConditions
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
a.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
} catch(NullPointerException e) {
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(a, e, null,
|
||||
a.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
ServiceList.getNameOfService(streamingServiceId),
|
||||
NewPipe.getNameOfService(streamingServiceId),
|
||||
"Could not get widget with focus", R.string.general_error));
|
||||
}
|
||||
// clear focus
|
||||
@@ -81,17 +93,15 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
// onQueryTextSubmit to trigger twice when focus is not cleared.
|
||||
// See: http://stackoverflow.com/questions/17874951/searchview-onquerytextsubmit-runs-twice-while-i-pressed-once
|
||||
a.getCurrentFocus().clearFocus();
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
View bg = a.findViewById(R.id.mainBG);
|
||||
bg.setVisibility(View.GONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
if(!newText.isEmpty()) {
|
||||
if (!newText.isEmpty()) {
|
||||
searchSuggestions(newText);
|
||||
}
|
||||
return true;
|
||||
@@ -103,12 +113,10 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
private boolean isLoading = false;
|
||||
|
||||
private ProgressBar loadingIndicator = null;
|
||||
private SearchView searchView = null;
|
||||
private int pageNumber = 0;
|
||||
private SuggestionListAdapter suggestionListAdapter = null;
|
||||
private InfoListAdapter infoListAdapter = null;
|
||||
private LinearLayoutManager streamInfoListLayoutManager = null;
|
||||
private RecyclerView recyclerView = null;
|
||||
|
||||
// savedInstanceBundle arguments
|
||||
private static final String QUERY = "query";
|
||||
@@ -121,40 +129,50 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
public SearchInfoItemFragment() {
|
||||
}
|
||||
|
||||
// TODO: Customize parameter initialization
|
||||
@SuppressWarnings("unused")
|
||||
public static SearchInfoItemFragment newInstance(int columnCount) {
|
||||
public static SearchInfoItemFragment newInstance(int streamingServiceId, String searchQuery) {
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(STREAMING_SERVICE, streamingServiceId);
|
||||
args.putString(QUERY, searchQuery);
|
||||
SearchInfoItemFragment fragment = new SearchInfoItemFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState != null) {
|
||||
searchQuery = "";
|
||||
if (savedInstanceState != null) {
|
||||
searchQuery = savedInstanceState.getString(QUERY);
|
||||
streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
|
||||
} else {
|
||||
try {
|
||||
streamingServiceId = ServiceList.getIdOfService("Youtube");
|
||||
} catch(Exception e) {
|
||||
Bundle args = getArguments();
|
||||
if(args != null) {
|
||||
searchQuery = args.getString(QUERY);
|
||||
streamingServiceId = args.getInt(STREAMING_SERVICE);
|
||||
} else {
|
||||
streamingServiceId = NewPipe.getIdOfService("Youtube");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(getActivity(), e, null,
|
||||
getActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
ServiceList.getNameOfService(streamingServiceId),
|
||||
NewPipe.getNameOfService(streamingServiceId),
|
||||
"", R.string.general_error));
|
||||
}
|
||||
}
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
SearchWorker sw = SearchWorker.getInstance();
|
||||
sw.setSearchWorkerResultListner(new SearchWorker.SearchWorkerResultListner() {
|
||||
sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() {
|
||||
@Override
|
||||
public void onResult(SearchResult result) {
|
||||
infoListAdapter.addStreamItemList(result.resultList);
|
||||
isLoading = false;
|
||||
loadingIndicator.setVisibility(View.GONE);
|
||||
infoListAdapter.addInfoItemList(result.resultList);
|
||||
setDoneLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -162,8 +180,7 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
//setListShown(true);
|
||||
Toast.makeText(getActivity(), getString(stringResource),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
isLoading = false;
|
||||
loadingIndicator.setVisibility(View.GONE);
|
||||
setDoneLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,8 +188,18 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
//setListShown(true);
|
||||
Toast.makeText(getActivity(), message,
|
||||
Toast.LENGTH_LONG).show();
|
||||
isLoading = false;
|
||||
loadingIndicator.setVisibility(View.GONE);
|
||||
setDoneLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaChallenge() {
|
||||
Toast.makeText(getActivity(), "ReCaptcha Challenge requested",
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(
|
||||
new Intent(getActivity(), ReCaptchaActivity.class),
|
||||
RECAPTCHA_REQUEST);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -184,36 +211,43 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
|
||||
Context context = view.getContext();
|
||||
loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar);
|
||||
recyclerView = (RecyclerView) view.findViewById(R.id.list);
|
||||
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list);
|
||||
streamInfoListLayoutManager = new LinearLayoutManager(context);
|
||||
recyclerView.setLayoutManager(streamInfoListLayoutManager);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(getActivity(),
|
||||
getActivity().findViewById(android.R.id.content));
|
||||
infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
|
||||
infoListAdapter.setFooter(inflater.inflate(R.layout.pignate_footer, recyclerView, false));
|
||||
infoListAdapter.showFooter(false);
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(
|
||||
new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url) {
|
||||
Intent i = new Intent(getActivity(), VideoItemDetailActivity.class);
|
||||
i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
|
||||
i.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
|
||||
getActivity().startActivity(i);
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openDetailActivity(getContext(), url, serviceId);
|
||||
}
|
||||
});
|
||||
infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(String url, int serviceId) {
|
||||
NavStack.getInstance()
|
||||
.openChannelActivity(getContext(), url, serviceId);
|
||||
}
|
||||
});
|
||||
recyclerView.setAdapter(infoListAdapter);
|
||||
|
||||
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
recyclerView.clearOnScrollListeners();
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
int pastVisiblesItems, visibleItemCount, totalItemCount;
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if(dy > 0) //check for scroll down
|
||||
if (dy > 0) //check for scroll down
|
||||
{
|
||||
visibleItemCount = streamInfoListLayoutManager.getChildCount();
|
||||
totalItemCount = streamInfoListLayoutManager.getItemCount();
|
||||
pastVisiblesItems = streamInfoListLayoutManager.findFirstVisibleItemPosition();
|
||||
|
||||
if ( (visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading)
|
||||
{
|
||||
if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading) {
|
||||
pageNumber++;
|
||||
search(searchQuery, pageNumber);
|
||||
}
|
||||
@@ -225,13 +259,18 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if(!searchQuery.isEmpty()) {
|
||||
search(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(QUERY, searchQuery);
|
||||
outState.putInt(STREAMING_SERVICE, streamingServiceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,13 +279,34 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
inflater.inflate(R.menu.search_menu, menu);
|
||||
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
searchView = (SearchView) searchItem.getActionView();
|
||||
SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
setupSearchView(searchView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
return super.onOptionsItemSelected(item);
|
||||
switch(item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
case R.id.menu_filter_video:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM));
|
||||
return true;
|
||||
case R.id.menu_filter_channel:
|
||||
changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void changeFilter(MenuItem item, EnumSet<SearchEngine.Filter> filter) {
|
||||
this.filter = filter;
|
||||
item.setChecked(true);
|
||||
if(searchQuery != null && !searchQuery.isEmpty()) {
|
||||
Log.d(TAG, "Fuck+ " + searchQuery);
|
||||
search(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSearchView(SearchView searchView) {
|
||||
@@ -254,7 +314,7 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
searchView.setSuggestionsAdapter(suggestionListAdapter);
|
||||
searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView, suggestionListAdapter));
|
||||
searchView.setOnQueryTextListener(new SearchQueryListener());
|
||||
if(searchQuery != null && !searchQuery.isEmpty()) {
|
||||
if (searchQuery != null && !searchQuery.isEmpty()) {
|
||||
searchView.setQuery(searchQuery, false);
|
||||
searchView.setIconifiedByDefault(false);
|
||||
}
|
||||
@@ -262,15 +322,37 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
|
||||
private void search(String query) {
|
||||
infoListAdapter.clearSteamItemList();
|
||||
infoListAdapter.showFooter(false);
|
||||
pageNumber = 0;
|
||||
searchQuery = query;
|
||||
search(query, pageNumber);
|
||||
hideBackground();
|
||||
loadingIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void search(String query, int page) {
|
||||
isLoading = true;
|
||||
SearchWorker sw = SearchWorker.getInstance();
|
||||
sw.search(streamingServiceId, query, page, getActivity());
|
||||
sw.search(streamingServiceId,
|
||||
query,
|
||||
page,
|
||||
getActivity(),
|
||||
filter);
|
||||
}
|
||||
|
||||
private void setDoneLoading() {
|
||||
this.isLoading = false;
|
||||
loadingIndicator.setVisibility(View.GONE);
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the "dummy" background when no results are shown
|
||||
*/
|
||||
private void hideBackground() {
|
||||
View view = getView();
|
||||
if(view == null) return;
|
||||
view.findViewById(R.id.mainBG).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void searchSuggestions(String query) {
|
||||
@@ -279,4 +361,23 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
Thread suggestionThread = new Thread(suggestionSearchRunnable);
|
||||
suggestionThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case RECAPTCHA_REQUEST:
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (searchQuery.length() != 0) {
|
||||
search(searchQuery);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import android.support.v7.widget.SearchView;
|
||||
|
||||
public class SearchSuggestionListener implements SearchView.OnSuggestionListener{
|
||||
|
||||
private SearchView searchView;
|
||||
private SuggestionListAdapter adapter;
|
||||
private final SearchView searchView;
|
||||
private final SuggestionListAdapter adapter;
|
||||
|
||||
public SearchSuggestionListener(SearchView searchView, SuggestionListAdapter adapter) {
|
||||
this.searchView = searchView;
|
||||
|
||||
@@ -7,15 +7,16 @@ import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.SearchResult;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
@@ -41,10 +42,11 @@ import java.io.IOException;
|
||||
public class SearchWorker {
|
||||
private static final String TAG = SearchWorker.class.toString();
|
||||
|
||||
public interface SearchWorkerResultListner {
|
||||
public interface SearchWorkerResultListener {
|
||||
void onResult(SearchResult result);
|
||||
void onNothingFound(final int stringResource);
|
||||
void onError(String message);
|
||||
void onReCaptchaChallenge();
|
||||
}
|
||||
|
||||
private class ResultRunnable implements Runnable {
|
||||
@@ -57,7 +59,7 @@ public class SearchWorker {
|
||||
@Override
|
||||
public void run() {
|
||||
if(this.requestId == SearchWorker.this.requestId) {
|
||||
searchWorkerResultListner.onResult(result);
|
||||
searchWorkerResultListener.onResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,14 +68,21 @@ public class SearchWorker {
|
||||
public static final String YOUTUBE = "Youtube";
|
||||
private final String query;
|
||||
private final int page;
|
||||
private final EnumSet<SearchEngine.Filter> filter;
|
||||
final Handler h = new Handler();
|
||||
private volatile boolean runs = true;
|
||||
private Activity a = null;
|
||||
private int serviceId = -1;
|
||||
public SearchRunnable(int serviceId, String query, int page, Activity activity, int requestId) {
|
||||
public SearchRunnable(int serviceId,
|
||||
String query,
|
||||
int page,
|
||||
EnumSet<SearchEngine.Filter> filter,
|
||||
Activity activity,
|
||||
int requestId) {
|
||||
this.serviceId = serviceId;
|
||||
this.query = query;
|
||||
this.page = page;
|
||||
this.filter = filter;
|
||||
this.a = activity;
|
||||
}
|
||||
void terminate() {
|
||||
@@ -81,12 +90,13 @@ public class SearchWorker {
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
final String serviceName = NewPipe.getNameOfService(serviceId);
|
||||
SearchResult result = null;
|
||||
SearchEngine engine = null;
|
||||
|
||||
try {
|
||||
engine = ServiceList.getService(serviceId)
|
||||
.getSearchEngineInstance(new Downloader());
|
||||
engine = NewPipe.getService(serviceId)
|
||||
.getSearchEngineInstance();
|
||||
} catch(ExtractionException e) {
|
||||
ErrorActivity.reportError(h, a, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
@@ -100,14 +110,14 @@ public class SearchWorker {
|
||||
String searchLanguage = sp.getString(searchLanguageKey,
|
||||
a.getString(R.string.default_language_value));
|
||||
result = SearchResult
|
||||
.getSearchResult(engine, query, page, searchLanguage, new Downloader());
|
||||
|
||||
.getSearchResult(engine, query, page, searchLanguage, filter);
|
||||
if(runs) {
|
||||
h.post(new ResultRunnable(result, requestId));
|
||||
}
|
||||
|
||||
// look for errors during extraction
|
||||
// soft errors:
|
||||
View rootView = a.findViewById(android.R.id.content);
|
||||
if(result != null &&
|
||||
!result.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:");
|
||||
@@ -116,18 +126,31 @@ public class SearchWorker {
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
View rootView = a.findViewById(android.R.id.content);
|
||||
ErrorActivity.reportError(h, a, result.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.light_parsing_error));
|
||||
|
||||
if(result.resultList.isEmpty()&& !result.errors.isEmpty()) {
|
||||
// if it compleatly failes dont show snackbar, instead show error directlry
|
||||
ErrorActivity.reportError(h, a, result.errors, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
serviceName, query, R.string.parsing_error));
|
||||
} else {
|
||||
// if it partly show snackbar
|
||||
ErrorActivity.reportError(h, a, result.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
serviceName, query, R.string.light_parsing_error));
|
||||
}
|
||||
}
|
||||
// hard errors:
|
||||
} catch (ReCaptchaException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
searchWorkerResultListener.onReCaptchaChallenge();
|
||||
}
|
||||
});
|
||||
} catch(IOException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
searchWorkerResultListner.onNothingFound(R.string.network_error);
|
||||
searchWorkerResultListener.onNothingFound(R.string.network_error);
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
@@ -135,14 +158,13 @@ public class SearchWorker {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
searchWorkerResultListner.onError(e.getMessage());
|
||||
searchWorkerResultListener.onError(e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch(ExtractionException e) {
|
||||
ErrorActivity.reportError(h, a, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */
|
||||
YOUTUBE, query, R.string.parsing_error));
|
||||
serviceName, query, R.string.parsing_error));
|
||||
//postNewErrorToast(h, R.string.parsing_error);
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -157,7 +179,7 @@ public class SearchWorker {
|
||||
}
|
||||
|
||||
private static SearchWorker searchWorker = null;
|
||||
private SearchWorkerResultListner searchWorkerResultListner = null;
|
||||
private SearchWorkerResultListener searchWorkerResultListener = null;
|
||||
private SearchRunnable runnable = null;
|
||||
private int requestId = 0; //prevents running requests that have already ben expired
|
||||
|
||||
@@ -165,19 +187,23 @@ public class SearchWorker {
|
||||
return searchWorker == null ? (searchWorker = new SearchWorker()) : searchWorker;
|
||||
}
|
||||
|
||||
public void setSearchWorkerResultListner(SearchWorkerResultListner listener) {
|
||||
searchWorkerResultListner = listener;
|
||||
public void setSearchWorkerResultListener(SearchWorkerResultListener listener) {
|
||||
searchWorkerResultListener = listener;
|
||||
}
|
||||
|
||||
private SearchWorker() {
|
||||
|
||||
}
|
||||
|
||||
public void search(int serviceId, String query, int page, Activity a) {
|
||||
public void search(int serviceId,
|
||||
String query,
|
||||
int page,
|
||||
Activity a,
|
||||
EnumSet<SearchEngine.Filter> filter) {
|
||||
if(runnable != null) {
|
||||
terminate();
|
||||
}
|
||||
runnable = new SearchRunnable(serviceId, query, page, a, requestId);
|
||||
runnable = new SearchRunnable(serviceId, query, page, filter, a, requestId);
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package org.schabi.newpipe.search_fragment;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.support.v4.widget.ResourceCursorAdapter;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
@@ -31,52 +29,56 @@ import java.util.List;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SuggestionListAdapter extends CursorAdapter {
|
||||
/**
|
||||
* {@link ResourceCursorAdapter} to display suggestions.
|
||||
*/
|
||||
public class SuggestionListAdapter extends ResourceCursorAdapter {
|
||||
|
||||
private static final String[] columns = new String[]{"_id", "title"};
|
||||
private static final int INDEX_ID = 0;
|
||||
private static final int INDEX_TITLE = 1;
|
||||
|
||||
private String[] columns = new String[]{"_id", "title"};
|
||||
|
||||
public SuggestionListAdapter(Context context) {
|
||||
super(context, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
ViewHolder viewHolder;
|
||||
|
||||
View view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
viewHolder = new ViewHolder();
|
||||
viewHolder.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
|
||||
view.setTag(viewHolder);
|
||||
|
||||
|
||||
return view;
|
||||
super(context, android.R.layout.simple_list_item_1, null, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
ViewHolder viewHolder = (ViewHolder) view.getTag();
|
||||
viewHolder.suggestionTitle.setText(cursor.getString(1));
|
||||
ViewHolder viewHolder = new ViewHolder(view);
|
||||
viewHolder.suggestionTitle.setText(cursor.getString(INDEX_TITLE));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the suggestion list
|
||||
* @param suggestions the list of suggestions
|
||||
*/
|
||||
public void updateAdapter(List<String> suggestions) {
|
||||
MatrixCursor cursor = new MatrixCursor(columns);
|
||||
MatrixCursor cursor = new MatrixCursor(columns, suggestions.size());
|
||||
int i = 0;
|
||||
for (String s : suggestions) {
|
||||
String[] temp = new String[2];
|
||||
temp[0] = Integer.toString(i);
|
||||
temp[1] = s;
|
||||
for (String suggestion : suggestions) {
|
||||
String[] columnValues = new String[columns.length];
|
||||
columnValues[INDEX_TITLE] = suggestion;
|
||||
columnValues[INDEX_ID] = Integer.toString(i);
|
||||
cursor.addRow(columnValues);
|
||||
i++;
|
||||
cursor.addRow(temp);
|
||||
}
|
||||
changeCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the suggestion for a position
|
||||
* @param position the position of the suggestion
|
||||
* @return the suggestion
|
||||
*/
|
||||
public String getSuggestion(int position) {
|
||||
return ((Cursor) getItem(position)).getString(1);
|
||||
return ((Cursor) getItem(position)).getString(INDEX_TITLE);
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
public TextView suggestionTitle;
|
||||
private final TextView suggestionTitle;
|
||||
private ViewHolder(View view) {
|
||||
this.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,11 @@ import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -38,14 +37,15 @@ import java.util.List;
|
||||
|
||||
public class SuggestionSearchRunnable implements Runnable{
|
||||
|
||||
/**
|
||||
* Runnable to update a {@link SuggestionListAdapter}
|
||||
*/
|
||||
private class SuggestionResultRunnable implements Runnable{
|
||||
|
||||
private List<String> suggestions;
|
||||
private SuggestionListAdapter adapter;
|
||||
private final List<String> suggestions;
|
||||
|
||||
private SuggestionResultRunnable(List<String> suggestions, SuggestionListAdapter adapter) {
|
||||
private SuggestionResultRunnable(List<String> suggestions) {
|
||||
this.suggestions = suggestions;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,9 +56,9 @@ public class SuggestionSearchRunnable implements Runnable{
|
||||
|
||||
private final int serviceId;
|
||||
private final String query;
|
||||
final Handler h = new Handler();
|
||||
private Activity a = null;
|
||||
private SuggestionListAdapter adapter;
|
||||
private final Handler h = new Handler();
|
||||
private final Activity a;
|
||||
private final SuggestionListAdapter adapter;
|
||||
public SuggestionSearchRunnable(int serviceId, String query,
|
||||
Activity activity, SuggestionListAdapter adapter) {
|
||||
this.serviceId = serviceId;
|
||||
@@ -70,18 +70,18 @@ public class SuggestionSearchRunnable implements Runnable{
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
SearchEngine engine =
|
||||
ServiceList.getService(serviceId).getSearchEngineInstance(new Downloader());
|
||||
SuggestionExtractor se =
|
||||
NewPipe.getService(serviceId).getSuggestionExtractorInstance();
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(a);
|
||||
String searchLanguageKey = a.getString(R.string.search_language_key);
|
||||
String searchLanguage = sp.getString(searchLanguageKey,
|
||||
a.getString(R.string.default_language_value));
|
||||
List<String> suggestions = engine.suggestionList(query,searchLanguage,new Downloader());
|
||||
h.post(new SuggestionResultRunnable(suggestions, adapter));
|
||||
List<String> suggestions = se.suggestionList(query, searchLanguage);
|
||||
h.post(new SuggestionResultRunnable(suggestions));
|
||||
} catch (ExtractionException e) {
|
||||
ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
ServiceList.getNameOfService(serviceId), query, R.string.parsing_error));
|
||||
NewPipe.getNameOfService(serviceId), query, R.string.parsing_error));
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
@@ -89,7 +89,7 @@ public class SuggestionSearchRunnable implements Runnable{
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
ServiceList.getNameOfService(serviceId), query, R.string.general_error));
|
||||
NewPipe.getNameOfService(serviceId), query, R.string.general_error));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,14 +89,6 @@ public class NewPipeSettings {
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
public static String getDownloadPath(Context context, String fileName)
|
||||
{
|
||||
if(Utility.isVideoFile(fileName)) {
|
||||
return NewPipeSettings.getVideoDownloadPath(context);
|
||||
}
|
||||
return NewPipeSettings.getAudioDownloadPath(context);
|
||||
}
|
||||
|
||||
private static File getFolder(Context context, int keyID, String defaultDirectoryName) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(keyID);
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
||||
/**
|
||||
@@ -38,11 +39,16 @@ import org.schabi.newpipe.R;
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
private AppCompatDelegate mDelegate = null;
|
||||
SettingsFragment f = new SettingsFragment();
|
||||
private AppCompatDelegate mDelegate = null;
|
||||
|
||||
public static void initSettings(Context context) {
|
||||
NewPipeSettings.initSettings(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceBundle) {
|
||||
ThemeHelper.setTheme(this, true);
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceBundle);
|
||||
super.onCreate(savedInstanceBundle);
|
||||
@@ -148,8 +154,4 @@ public class SettingsActivity extends PreferenceActivity {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void initSettings(Context context) {
|
||||
NewPipeSettings.initSettings(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,27 @@ package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
|
||||
@@ -46,28 +49,28 @@ import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
public class SettingsFragment extends PreferenceFragment
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
public static final int REQUEST_INSTALL_ORBOT = 0x1234;
|
||||
SharedPreferences.OnSharedPreferenceChangeListener prefListener;
|
||||
|
||||
// get keys
|
||||
String DEFAULT_RESOLUTION_PREFERENCE;
|
||||
String PREFERRED_VIDEO_FORMAT_PREFERENCE;
|
||||
String DEFAULT_AUDIO_FORMAT_PREFERENCE;
|
||||
String SEARCH_LANGUAGE_PREFERENCE;
|
||||
String DOWNLOAD_PATH_PREFERENCE;
|
||||
String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
String USE_TOR_KEY;
|
||||
|
||||
public static final int REQUEST_INSTALL_ORBOT = 0x1234;
|
||||
|
||||
String THEME;
|
||||
private ListPreference defaultResolutionPreference;
|
||||
private ListPreference preferredVideoFormatPreference;
|
||||
private ListPreference defaultAudioFormatPreference;
|
||||
private ListPreference searchLanguagePreference;
|
||||
private Preference downloadPathPreference;
|
||||
private Preference downloadPathAudioPreference;
|
||||
private Preference themePreference;
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.settings);
|
||||
|
||||
@@ -77,21 +80,28 @@ public class SettingsFragment extends PreferenceFragment
|
||||
|
||||
// get keys
|
||||
DEFAULT_RESOLUTION_PREFERENCE = getString(R.string.default_resolution_key);
|
||||
PREFERRED_VIDEO_FORMAT_PREFERENCE = getString(R.string.preferred_video_format_key);
|
||||
DEFAULT_AUDIO_FORMAT_PREFERENCE = getString(R.string.default_audio_format_key);
|
||||
SEARCH_LANGUAGE_PREFERENCE = getString(R.string.search_language_key);
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
THEME = getString(R.string.theme_key);
|
||||
USE_TOR_KEY = getString(R.string.use_tor_key);
|
||||
|
||||
// get pref objects
|
||||
defaultResolutionPreference =
|
||||
(ListPreference) findPreference(DEFAULT_RESOLUTION_PREFERENCE);
|
||||
preferredVideoFormatPreference =
|
||||
(ListPreference) findPreference(PREFERRED_VIDEO_FORMAT_PREFERENCE);
|
||||
defaultAudioFormatPreference =
|
||||
(ListPreference) findPreference(DEFAULT_AUDIO_FORMAT_PREFERENCE);
|
||||
searchLanguagePreference =
|
||||
(ListPreference) findPreference(SEARCH_LANGUAGE_PREFERENCE);
|
||||
downloadPathPreference = findPreference(DOWNLOAD_PATH_PREFERENCE);
|
||||
downloadPathAudioPreference = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||
themePreference = findPreference(THEME);
|
||||
|
||||
final String currentTheme = defaultPreferences.getString(THEME, "Light");
|
||||
|
||||
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
@@ -132,6 +142,30 @@ public class SettingsFragment extends PreferenceFragment
|
||||
downloadPathAudioPreference
|
||||
.setSummary(downloadPath);
|
||||
}
|
||||
else if (key == THEME)
|
||||
{
|
||||
String selectedTheme = sharedPreferences.getString(THEME, "Light");
|
||||
themePreference.setSummary(selectedTheme);
|
||||
|
||||
if(!selectedTheme.equals(currentTheme)) { // If it's not the current theme
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.restart_title)
|
||||
.setMessage(R.string.msg_restart)
|
||||
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intentToMain = new Intent(activity, MainActivity.class);
|
||||
intentToMain.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
activity.startActivity(intentToMain);
|
||||
|
||||
activity.finish();
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.later, null)
|
||||
.create().show();
|
||||
}
|
||||
}
|
||||
updateSummary();
|
||||
}
|
||||
};
|
||||
@@ -161,7 +195,6 @@ public class SettingsFragment extends PreferenceFragment
|
||||
activity.startActivityForResult(i, R.string.download_path_audio_key);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preferenceScreen, preference);
|
||||
}
|
||||
|
||||
@@ -216,8 +249,8 @@ public class SettingsFragment extends PreferenceFragment
|
||||
// installing the app does not necessarily return RESULT_OK
|
||||
App.configureTor(requestCode == REQUEST_INSTALL_ORBOT
|
||||
&& OrbotHelper.requestStartTor(a));
|
||||
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
@@ -227,6 +260,9 @@ public class SettingsFragment extends PreferenceFragment
|
||||
defaultResolutionPreference.setSummary(
|
||||
defaultPreferences.getString(DEFAULT_RESOLUTION_PREFERENCE,
|
||||
getString(R.string.default_resolution_value)));
|
||||
preferredVideoFormatPreference.setSummary(
|
||||
defaultPreferences.getString(PREFERRED_VIDEO_FORMAT_PREFERENCE,
|
||||
getString(R.string.preferred_video_format_default)));
|
||||
defaultAudioFormatPreference.setSummary(
|
||||
defaultPreferences.getString(DEFAULT_AUDIO_FORMAT_PREFERENCE,
|
||||
getString(R.string.default_audio_format_value)));
|
||||
@@ -239,6 +275,9 @@ public class SettingsFragment extends PreferenceFragment
|
||||
downloadPathAudioPreference.setSummary(
|
||||
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE,
|
||||
getString(R.string.download_path_audio_summary)));
|
||||
themePreference.setSummary(
|
||||
defaultPreferences.getString(THEME,
|
||||
getString(R.string.light_theme_title)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
148
app/src/main/java/org/schabi/newpipe/util/NavStack.java
Normal file
148
app/src/main/java/org/schabi/newpipe/util/NavStack.java
Normal file
@@ -0,0 +1,148 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
|
||||
import org.schabi.newpipe.ChannelActivity;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 16.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* NavStack.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* class helps to navigate within the app
|
||||
* IMPORTAND: the top of the stack is the current activity !!!
|
||||
*/
|
||||
public class NavStack {
|
||||
private static final String TAG = NavStack.class.toString();
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String URL = "url";
|
||||
|
||||
private static final String NAV_STACK="nav_stack";
|
||||
|
||||
private enum ActivityId {
|
||||
CHANNEL,
|
||||
DETAIL
|
||||
}
|
||||
|
||||
private class NavEntry {
|
||||
public NavEntry(String url, int serviceId) {
|
||||
this.url = url;
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
public String url;
|
||||
public int serviceId;
|
||||
}
|
||||
|
||||
private static NavStack instance = new NavStack();
|
||||
private Stack<NavEntry> stack = new Stack<NavEntry>();
|
||||
|
||||
private NavStack() {
|
||||
}
|
||||
|
||||
public static NavStack getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void navBack(Activity activity) throws Exception {
|
||||
if(stack.size() == 0) { // if stack is already empty here, activity was probably called
|
||||
// from another app
|
||||
activity.finish();
|
||||
return;
|
||||
}
|
||||
stack.pop(); // remove curent activty, since we dont want to return to itself
|
||||
if (stack.size() == 0) {
|
||||
openMainActivity(activity); // if no more page is on the stack this means we are home
|
||||
return;
|
||||
}
|
||||
NavEntry entry = stack.pop(); // this element will reapear, since by calling the old page
|
||||
// this element will be pushed on top again
|
||||
try {
|
||||
StreamingService service = NewPipe.getService(entry.serviceId);
|
||||
switch (service.getLinkTypeByUrl(entry.url)) {
|
||||
case STREAM:
|
||||
openDetailActivity(activity, entry.url, entry.serviceId);
|
||||
break;
|
||||
case CHANNEL:
|
||||
openChannelActivity(activity, entry.url, entry.serviceId);
|
||||
break;
|
||||
case NONE:
|
||||
throw new Exception("Url not known to service. service="
|
||||
+ Integer.toString(entry.serviceId) + " url=" + entry.url);
|
||||
default:
|
||||
openMainActivity(activity);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void openChannelActivity(Context context, String url, int serviceId) {
|
||||
openActivity(context, url, serviceId, ChannelActivity.class);
|
||||
}
|
||||
|
||||
public void openDetailActivity(Context context, String url, int serviceId) {
|
||||
openActivity(context, url, serviceId, VideoItemDetailActivity.class);
|
||||
}
|
||||
|
||||
private void openActivity(Context context, String url, int serviceId, Class acitivtyClass) {
|
||||
//if last element has the same url do not push to stack again
|
||||
if(stack.isEmpty() || !stack.peek().url.equals(url)) {
|
||||
stack.push(new NavEntry(url, serviceId));
|
||||
}
|
||||
Intent i = new Intent(context, acitivtyClass);
|
||||
i.putExtra(SERVICE_ID, serviceId);
|
||||
i.putExtra(URL, url);
|
||||
context.startActivity(i);
|
||||
}
|
||||
|
||||
public void openMainActivity(Activity a) {
|
||||
stack.clear();
|
||||
Intent i = new Intent(a, MainActivity.class);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(a, i);
|
||||
}
|
||||
|
||||
public void onSaveInstanceState(Bundle state) {
|
||||
ArrayList<String> sa = new ArrayList<>();
|
||||
for(NavEntry entry : stack) {
|
||||
sa.add(entry.url);
|
||||
}
|
||||
state.putStringArrayList(NAV_STACK, sa);
|
||||
}
|
||||
|
||||
public void restoreSavedInstanceState(Bundle state) {
|
||||
ArrayList<String> sa = state.getStringArrayList(NAV_STACK);
|
||||
stack.clear();
|
||||
for(String url : sa) {
|
||||
stack.push(new NavEntry(url, NewPipe.getServiceByUrl(url).getServiceId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
public class PermissionHelper {
|
||||
public static final int PERMISSION_WRITE_STORAGE = 778;
|
||||
public static final int PERMISSION_READ_STORAGE = 777;
|
||||
public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779;
|
||||
|
||||
|
||||
public static boolean checkStoragePermissions(Activity activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
if(!checkReadStoragePermissions(activity)) return false;
|
||||
}
|
||||
return checkWriteStoragePermissions(activity);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
public static boolean checkReadStoragePermissions(Activity activity) {
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
PERMISSION_READ_STORAGE);
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public static boolean checkWriteStoragePermissions(Activity activity) {
|
||||
// Here, thisActivity is the current activity
|
||||
if (ContextCompat.checkSelfPermission(activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
// Should we show an explanation?
|
||||
/*if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
|
||||
// Show an explanation to the user *asynchronously* -- don't block
|
||||
// this thread waiting for the user's response! After the user
|
||||
// sees the explanation, try again to request the permission.
|
||||
} else {*/
|
||||
|
||||
// No explanation needed, we can request the permission.
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
PERMISSION_WRITE_STORAGE);
|
||||
|
||||
// PERMISSION_WRITE_STORAGE is an
|
||||
// app-defined int constant. The callback method gets the
|
||||
// result of the request.
|
||||
/*}*/
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted.
|
||||
* <p>
|
||||
* On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest),
|
||||
* on > 23, however, it have to start a activity asking the user if he agree.
|
||||
* <p>
|
||||
* This method just return if canDraw over other apps, if it doesn't, try to get the permission,
|
||||
* it does not get the result of the startActivityForResult, if the user accept, the next time that he tries to open
|
||||
* it will return true.
|
||||
*
|
||||
* @param activity context to startActivityForResult
|
||||
* @return returns {@link Settings#canDrawOverlays(Context)}
|
||||
**/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean checkSystemAlertWindowPermission(Activity activity) {
|
||||
if (!Settings.canDrawOverlays(activity)) {
|
||||
Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(i, PERMISSION_SYSTEM_ALERT_WINDOW);
|
||||
return false;
|
||||
}else return true;
|
||||
}
|
||||
}
|
||||
34
app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
Normal file
34
app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class ThemeHelper {
|
||||
|
||||
/**
|
||||
* Apply the selected theme (on NewPipe settings) in the context
|
||||
*
|
||||
* @param context context that the theme will be applied
|
||||
* @param useActionbarTheme whether to use an action bar theme or not
|
||||
*/
|
||||
public static void setTheme(Context context, boolean useActionbarTheme) {
|
||||
String themeKey = context.getString(R.string.theme_key);
|
||||
String darkTheme = context.getResources().getString(R.string.dark_theme_title);
|
||||
String blackTheme = context.getResources().getString(R.string.black_theme_title);
|
||||
|
||||
String sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(themeKey, context.getResources().getString(R.string.light_theme_title));
|
||||
|
||||
if (useActionbarTheme) {
|
||||
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme);
|
||||
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme);
|
||||
else context.setTheme(R.style.AppTheme);
|
||||
} else {
|
||||
if (sp.equals(darkTheme)) context.setTheme(R.style.DarkTheme_NoActionBar);
|
||||
else if (sp.equals(blackTheme)) context.setTheme(R.style.BlackTheme_NoActionBar);
|
||||
else context.setTheme(R.style.AppTheme_NoActionBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides access to the storage of {@link DownloadMission}s
|
||||
*/
|
||||
public interface DownloadDataSource {
|
||||
|
||||
/**
|
||||
* Load all missions
|
||||
* @return a list of download missions
|
||||
*/
|
||||
List<DownloadMission> loadMissions();
|
||||
|
||||
/**
|
||||
* Add a downlaod mission to the storage
|
||||
* @param downloadMission the download mission to add
|
||||
* @return the identifier of the mission
|
||||
*/
|
||||
void addMission(DownloadMission downloadMission);
|
||||
|
||||
/**
|
||||
* Update a download mission which exists in the storage
|
||||
* @param downloadMission the download mission to update
|
||||
* @throws IllegalArgumentException if the mission was not added to storage
|
||||
*/
|
||||
void updateMission(DownloadMission downloadMission);
|
||||
|
||||
|
||||
/**
|
||||
* Delete a download mission
|
||||
* @param downloadMission the mission to delete
|
||||
*/
|
||||
void deleteMission(DownloadMission downloadMission);
|
||||
}
|
||||
@@ -3,12 +3,46 @@ package us.shandian.giga.get;
|
||||
public interface DownloadManager
|
||||
{
|
||||
int BLOCK_SIZE = 512 * 1024;
|
||||
|
||||
int startMission(String url, String name, int threads);
|
||||
|
||||
/**
|
||||
* Start a new download mission
|
||||
* @param url the url to download
|
||||
* @param location the location
|
||||
* @param name the name of the file to create
|
||||
* @param isAudio true if the download is an audio file
|
||||
* @param threads the number of threads maximal used to download chunks of the file. @return the identifier of the mission.
|
||||
*/
|
||||
int startMission(String url, String location, String name, boolean isAudio, int threads);
|
||||
|
||||
/**
|
||||
* Resume the execution of a download mission.
|
||||
* @param id the identifier of the mission to resume.
|
||||
*/
|
||||
void resumeMission(int id);
|
||||
|
||||
/**
|
||||
* Pause the execution of a download mission.
|
||||
* @param id the identifier of the mission to pause.
|
||||
*/
|
||||
void pauseMission(int id);
|
||||
|
||||
/**
|
||||
* Deletes the mission from the downloaded list but keeps the downloaded file.
|
||||
* @param id The mission identifier
|
||||
*/
|
||||
void deleteMission(int id);
|
||||
|
||||
/**
|
||||
* Get the download mission by its identifier
|
||||
* @param id the identifier of the download mission
|
||||
* @return the download mission or null if the mission doesn't exist
|
||||
*/
|
||||
DownloadMission getMission(int id);
|
||||
|
||||
/**
|
||||
* Get the number of download missions.
|
||||
* @return the number of download missions.
|
||||
*/
|
||||
int getCount();
|
||||
String getLocation();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
@@ -19,29 +23,48 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
public class DownloadManagerImpl implements DownloadManager
|
||||
{
|
||||
private static final String TAG = DownloadManagerImpl.class.getSimpleName();
|
||||
|
||||
private Context mContext;
|
||||
private String mLocation;
|
||||
protected ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
|
||||
|
||||
public DownloadManagerImpl(Context context, String location) {
|
||||
mContext = context;
|
||||
mLocation = location;
|
||||
loadMissions();
|
||||
private final DownloadDataSource mDownloadDataSource;
|
||||
|
||||
private final ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
|
||||
|
||||
/**
|
||||
* Create a new instance
|
||||
* @param searchLocations the directories to search for unfinished downloads
|
||||
* @param downloadDataSource the data source for finished downloads
|
||||
*/
|
||||
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) {
|
||||
mDownloadDataSource = downloadDataSource;
|
||||
loadMissions(searchLocations);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int startMission(String url, String name, int threads) {
|
||||
DownloadMission mission = new DownloadMission();
|
||||
mission.url = url;
|
||||
mission.name = name;
|
||||
mission.location = NewPipeSettings.getDownloadPath(mContext, name);
|
||||
public int startMission(String url, String location, String name, boolean isAudio, int threads) {
|
||||
DownloadMission existingMission = getMissionByLocation(location, name);
|
||||
if(existingMission != null) {
|
||||
// Already downloaded or downloading
|
||||
if(existingMission.finished) {
|
||||
// Overwrite mission
|
||||
deleteMission(mMissions.indexOf(existingMission));
|
||||
} else {
|
||||
// Rename file (?)
|
||||
try {
|
||||
name = generateUniqueName(location, name);
|
||||
}catch (Exception e) {
|
||||
Log.e(TAG, "Unable to generate unique name", e);
|
||||
name = System.currentTimeMillis() + name ;
|
||||
Log.i(TAG, "Using " + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DownloadMission mission = new DownloadMission(name, url, location);
|
||||
mission.timestamp = System.currentTimeMillis();
|
||||
mission.threadCount = threads;
|
||||
new Initializer(mContext, mission).start();
|
||||
mission.addListener(new MissionListener(mission));
|
||||
new Initializer(mission).start();
|
||||
return insertMission(mission);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void resumeMission(int i) {
|
||||
DownloadMission d = getMission(i);
|
||||
@@ -49,7 +72,7 @@ public class DownloadManagerImpl implements DownloadManager
|
||||
d.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void pauseMission(int i) {
|
||||
DownloadMission d = getMission(i);
|
||||
@@ -57,55 +80,94 @@ public class DownloadManagerImpl implements DownloadManager
|
||||
d.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void deleteMission(int i) {
|
||||
getMission(i).delete();
|
||||
DownloadMission mission = getMission(i);
|
||||
if(mission.finished) {
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
}
|
||||
mission.delete();
|
||||
mMissions.remove(i);
|
||||
}
|
||||
|
||||
private void loadMissions() {
|
||||
File f = new File(mLocation);
|
||||
|
||||
private void loadMissions(Iterable<String> searchLocations) {
|
||||
mMissions.clear();
|
||||
loadFinishedMissions();
|
||||
for(String location: searchLocations) {
|
||||
loadMissions(location);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads finished missions from the data source
|
||||
*/
|
||||
private void loadFinishedMissions() {
|
||||
List<DownloadMission> finishedMissions = mDownloadDataSource.loadMissions();
|
||||
if(finishedMissions == null) {
|
||||
finishedMissions = new ArrayList<>();
|
||||
}
|
||||
// Ensure its sorted
|
||||
Collections.sort(finishedMissions, new Comparator<DownloadMission>() {
|
||||
@Override
|
||||
public int compare(DownloadMission o1, DownloadMission o2) {
|
||||
return (int) (o1.timestamp - o2.timestamp);
|
||||
}
|
||||
});
|
||||
mMissions.ensureCapacity(mMissions.size() + finishedMissions.size());
|
||||
for(DownloadMission mission: finishedMissions) {
|
||||
File downloadedFile = mission.getDownloadedFile();
|
||||
if(!downloadedFile.isFile()) {
|
||||
if(DEBUG) {
|
||||
Log.d(TAG, "downloaded file removed: " + downloadedFile.getAbsolutePath());
|
||||
}
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
} else {
|
||||
mission.length = downloadedFile.length();
|
||||
mission.finished = true;
|
||||
mission.running = false;
|
||||
mMissions.add(mission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMissions(String location) {
|
||||
|
||||
File f = new File(location);
|
||||
|
||||
if (f.exists() && f.isDirectory()) {
|
||||
File[] subs = f.listFiles();
|
||||
|
||||
|
||||
if(subs == null) {
|
||||
Log.e(TAG, "listFiles() returned null");
|
||||
return;
|
||||
}
|
||||
|
||||
for (File sub : subs) {
|
||||
if (sub.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sub.getName().endsWith(".giga")) {
|
||||
if (sub.isFile() && sub.getName().endsWith(".giga")) {
|
||||
String str = Utility.readFromFile(sub.getAbsolutePath());
|
||||
if (str != null && !str.trim().equals("")) {
|
||||
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loading mission " + sub.getName());
|
||||
Log.d(TAG, str);
|
||||
}
|
||||
|
||||
|
||||
DownloadMission mis = new Gson().fromJson(str, DownloadMission.class);
|
||||
|
||||
|
||||
if (mis.finished) {
|
||||
sub.delete();
|
||||
if(!sub.delete()) {
|
||||
Log.w(TAG, "Unable to delete .giga file: " + sub.getPath());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
mis.running = false;
|
||||
mis.recovered = true;
|
||||
insertMission(mis);
|
||||
}
|
||||
} else if (!sub.getName().startsWith(".") && !new File(sub.getPath() + ".giga").exists()) {
|
||||
// Add a dummy mission for downloaded files
|
||||
DownloadMission mis = new DownloadMission();
|
||||
mis.length = sub.length();
|
||||
mis.done = mis.length;
|
||||
mis.finished = true;
|
||||
mis.running = false;
|
||||
mis.name = sub.getName();
|
||||
mis.location = mLocation;
|
||||
mis.timestamp = sub.lastModified();
|
||||
insertMission(mis);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,18 +202,81 @@ public class DownloadManagerImpl implements DownloadManager
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocation() {
|
||||
return mLocation;
|
||||
|
||||
/**
|
||||
* Get a mission by its location and name
|
||||
* @param location the location
|
||||
* @param name the name
|
||||
* @return the mission or null if no such mission exists
|
||||
*/
|
||||
private @Nullable DownloadMission getMissionByLocation(String location, String name) {
|
||||
for(DownloadMission mission: mMissions) {
|
||||
if(location.equals(mission.location) && name.equals(mission.name)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Splits the filename into name and extension
|
||||
*
|
||||
* Dots are ignored if they appear: not at all, at the beginning of the file,
|
||||
* at the end of the file
|
||||
*
|
||||
* @param name the name to split
|
||||
* @return a string array with a length of 2 containing the name and the extension
|
||||
*/
|
||||
private static String[] splitName(String name) {
|
||||
int dotIndex = name.lastIndexOf('.');
|
||||
if(dotIndex <= 0 || (dotIndex == name.length() - 1)) {
|
||||
return new String[]{name, ""};
|
||||
} else {
|
||||
return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique file name.
|
||||
*
|
||||
* e.g. "myname (1).txt" if the name "myname.txt" exists.
|
||||
* @param location the location (to check for existing files)
|
||||
* @param name the name of the file
|
||||
* @return the unique file name
|
||||
* @throws IllegalArgumentException if the location is not a directory
|
||||
* @throws SecurityException if the location is not readable
|
||||
*/
|
||||
private static String generateUniqueName(String location, String name) {
|
||||
if(location == null) throw new NullPointerException("location is null");
|
||||
if(name == null) throw new NullPointerException("name is null");
|
||||
File destination = new File(location);
|
||||
if(!destination.isDirectory()) {
|
||||
throw new IllegalArgumentException("location is not a directory: " + location);
|
||||
}
|
||||
final String[] nameParts = splitName(name);
|
||||
String[] existingName = destination.list(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.startsWith(nameParts[0]);
|
||||
}
|
||||
});
|
||||
Arrays.sort(existingName);
|
||||
String newName;
|
||||
int downloadIndex = 0;
|
||||
do {
|
||||
newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1];
|
||||
++downloadIndex;
|
||||
if(downloadIndex == 1000) { // Probably an error on our side
|
||||
throw new RuntimeException("Too many existing files");
|
||||
}
|
||||
} while (Arrays.binarySearch(existingName, newName) >= 0);
|
||||
return newName;
|
||||
}
|
||||
|
||||
private class Initializer extends Thread {
|
||||
private Context context;
|
||||
private DownloadMission mission;
|
||||
|
||||
public Initializer(Context context, DownloadMission mission) {
|
||||
this.context = context;
|
||||
public Initializer(DownloadMission mission) {
|
||||
this.mission = mission;
|
||||
}
|
||||
|
||||
@@ -213,4 +338,30 @@ public class DownloadManagerImpl implements DownloadManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for mission to finish to add it to the {@link #mDownloadDataSource}
|
||||
*/
|
||||
private class MissionListener implements DownloadMission.MissionListener {
|
||||
private final DownloadMission mMission;
|
||||
|
||||
private MissionListener(DownloadMission mission) {
|
||||
if(mission == null) throw new NullPointerException("mission is null");
|
||||
// Could the mission be passed in onFinish()?
|
||||
mMission = mission;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(DownloadMission downloadMission) {
|
||||
mDownloadDataSource.addMission(mMission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(DownloadMission downloadMission, int errCode) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package us.shandian.giga.get;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
@@ -10,39 +9,63 @@ import com.google.gson.Gson;
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadMission
|
||||
{
|
||||
private static final String TAG = DownloadMission.class.getSimpleName();
|
||||
|
||||
|
||||
public interface MissionListener {
|
||||
HashMap<MissionListener, Handler> handlerStore = new HashMap<>();
|
||||
|
||||
void onProgressUpdate(long done, long total);
|
||||
void onFinish();
|
||||
void onError(int errCode);
|
||||
void onProgressUpdate(DownloadMission downloadMission, long done, long total);
|
||||
void onFinish(DownloadMission downloadMission);
|
||||
void onError(DownloadMission downloadMission, int errCode);
|
||||
}
|
||||
|
||||
public static final int ERROR_SERVER_UNSUPPORTED = 206;
|
||||
public static final int ERROR_UNKNOWN = 233;
|
||||
|
||||
public String name = "";
|
||||
public String url = "";
|
||||
public String location = "";
|
||||
|
||||
/**
|
||||
* The filename
|
||||
*/
|
||||
public String name;
|
||||
|
||||
/**
|
||||
* The url of the file to download
|
||||
*/
|
||||
public String url;
|
||||
|
||||
/**
|
||||
* The directory to store the download
|
||||
*/
|
||||
public String location;
|
||||
|
||||
/**
|
||||
* Number of blocks the size of {@link DownloadManager#BLOCK_SIZE}
|
||||
*/
|
||||
public long blocks;
|
||||
|
||||
/**
|
||||
* Number of bytes
|
||||
*/
|
||||
public long length;
|
||||
|
||||
/**
|
||||
* Number of bytes downloaded
|
||||
*/
|
||||
public long done;
|
||||
public int threadCount = 3;
|
||||
public int finishCount;
|
||||
public List<Long> threadPositions = new ArrayList<Long>();
|
||||
public Map<Long, Boolean> blockState = new HashMap<Long, Boolean>();
|
||||
private List<Long> threadPositions = new ArrayList<Long>();
|
||||
public final Map<Long, Boolean> blockState = new HashMap<Long, Boolean>();
|
||||
public boolean running;
|
||||
public boolean finished;
|
||||
public boolean fallback;
|
||||
@@ -53,23 +76,65 @@ public class DownloadMission
|
||||
|
||||
private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>();
|
||||
private transient boolean mWritingToFile;
|
||||
|
||||
|
||||
private static final int NO_IDENTIFIER = -1;
|
||||
private long db_identifier = NO_IDENTIFIER;
|
||||
|
||||
public DownloadMission() {
|
||||
}
|
||||
|
||||
public DownloadMission(String name, String url, String location) {
|
||||
if(name == null) throw new NullPointerException("name is null");
|
||||
if(name.isEmpty()) throw new IllegalArgumentException("name is empty");
|
||||
if(url == null) throw new NullPointerException("url is null");
|
||||
if(url.isEmpty()) throw new IllegalArgumentException("url is empty");
|
||||
if(location == null) throw new NullPointerException("location is null");
|
||||
if(location.isEmpty()) throw new IllegalArgumentException("location is empty");
|
||||
this.url = url;
|
||||
this.name = name;
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
|
||||
private void checkBlock(long block) {
|
||||
if(block < 0 || block >= blocks) {
|
||||
throw new IllegalArgumentException("illegal block identifier");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is reserved
|
||||
* @param block the block identifier
|
||||
* @return true if the block is reserved and false if otherwise
|
||||
*/
|
||||
public boolean isBlockPreserved(long block) {
|
||||
checkBlock(block);
|
||||
return blockState.containsKey(block) ? blockState.get(block) : false;
|
||||
}
|
||||
|
||||
public void preserveBlock(long block) {
|
||||
checkBlock(block);
|
||||
synchronized (blockState) {
|
||||
blockState.put(block, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPosition(int id, long position) {
|
||||
threadPositions.set(id, position);
|
||||
|
||||
/**
|
||||
* Set the download position of the file
|
||||
* @param threadId the identifier of the thread
|
||||
* @param position the download position of the thread
|
||||
*/
|
||||
public void setPosition(int threadId, long position) {
|
||||
threadPositions.set(threadId, position);
|
||||
}
|
||||
|
||||
public long getPosition(int id) {
|
||||
return threadPositions.get(id);
|
||||
|
||||
/**
|
||||
* Get the position of a thread
|
||||
* @param threadId the identifier of the thread
|
||||
* @return the position for the thread
|
||||
*/
|
||||
public long getPosition(int threadId) {
|
||||
return threadPositions.get(threadId);
|
||||
}
|
||||
|
||||
public synchronized void notifyProgress(long deltaLen) {
|
||||
@@ -95,13 +160,16 @@ public class DownloadMission
|
||||
MissionListener.handlerStore.get(listener).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onProgressUpdate(done, length);
|
||||
listener.onProgressUpdate(DownloadMission.this, done, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called by a download thread when it finished.
|
||||
*/
|
||||
public synchronized void notifyFinished() {
|
||||
if (errCode > 0) return;
|
||||
|
||||
@@ -111,7 +179,10 @@ public class DownloadMission
|
||||
onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when all parts are downloaded
|
||||
*/
|
||||
private void onFinish() {
|
||||
if (errCode > 0) return;
|
||||
|
||||
@@ -130,7 +201,7 @@ public class DownloadMission
|
||||
MissionListener.handlerStore.get(listener).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onFinish();
|
||||
listener.onFinish(DownloadMission.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -147,7 +218,7 @@ public class DownloadMission
|
||||
MissionListener.handlerStore.get(listener).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onError(errCode);
|
||||
listener.onError(DownloadMission.this, errCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -169,7 +240,10 @@ public class DownloadMission
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start downloading with multiple threads.
|
||||
*/
|
||||
public void start() {
|
||||
if (!running && !finished) {
|
||||
running = true;
|
||||
@@ -200,12 +274,19 @@ public class DownloadMission
|
||||
// if (err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the file and the meta file
|
||||
*/
|
||||
public void delete() {
|
||||
deleteThisFromFile();
|
||||
new File(location + "/" + name).delete();
|
||||
new File(location, name).delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write this {@link DownloadMission} to the meta file asynchronously
|
||||
* if no thread is already running.
|
||||
*/
|
||||
public void writeThisToFile() {
|
||||
if (!mWritingToFile) {
|
||||
mWritingToFile = true;
|
||||
@@ -218,14 +299,30 @@ public class DownloadMission
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write this {@link DownloadMission} to the meta file.
|
||||
*/
|
||||
private void doWriteThisToFile() {
|
||||
synchronized (blockState) {
|
||||
Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this));
|
||||
Utility.writeToFile(getMetaFilename(), new Gson().toJson(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteThisFromFile() {
|
||||
new File(location + "/" + name + ".giga").delete();
|
||||
new File(getMetaFilename()).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the meta file
|
||||
* @return the path to the meta file
|
||||
*/
|
||||
private String getMetaFilename() {
|
||||
return location + "/" + name + ".giga";
|
||||
}
|
||||
|
||||
public File getDownloadedFile() {
|
||||
return new File(location, name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,14 +9,19 @@ import java.net.URL;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
/**
|
||||
* Runnable to download blocks of a file until the file is completely downloaded,
|
||||
* an error occurs or the process is stopped.
|
||||
*/
|
||||
public class DownloadRunnable implements Runnable
|
||||
{
|
||||
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
||||
|
||||
private DownloadMission mMission;
|
||||
private int mId;
|
||||
private final DownloadMission mMission;
|
||||
private final int mId;
|
||||
|
||||
public DownloadRunnable(DownloadMission mission, int id) {
|
||||
if(mission == null) throw new NullPointerException("mission is null");
|
||||
mMission = mission;
|
||||
mId = id;
|
||||
}
|
||||
@@ -86,7 +91,7 @@ public class DownloadRunnable implements Runnable
|
||||
Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
|
||||
}
|
||||
|
||||
// A server may be ignoring the range requet
|
||||
// A server may be ignoring the range request
|
||||
if (conn.getResponseCode() != 206) {
|
||||
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
|
||||
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
|
||||
@@ -131,7 +136,7 @@ public class DownloadRunnable implements Runnable
|
||||
notifyProgress(-total);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":position " + position + " retrying");
|
||||
Log.d(TAG, mId + ":position " + position + " retrying", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import java.net.URL;
|
||||
// Single-threaded fallback mode
|
||||
public class DownloadRunnableFallback implements Runnable
|
||||
{
|
||||
private DownloadMission mMission;
|
||||
private final DownloadMission mMission;
|
||||
//private int mId;
|
||||
|
||||
public DownloadRunnableFallback(DownloadMission mission) {
|
||||
if(mission == null) throw new NullPointerException("mission is null");
|
||||
//mId = id;
|
||||
mMission = mission;
|
||||
}
|
||||
@@ -35,7 +36,7 @@ public class DownloadRunnableFallback implements Runnable
|
||||
f.write(buf, 0, len);
|
||||
notifyProgress(len);
|
||||
|
||||
if (Thread.currentThread().interrupted()) {
|
||||
if (Thread.interrupted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package us.shandian.giga.get.sqlite;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* SqliteHelper to store {@link us.shandian.giga.get.DownloadMission}
|
||||
*/
|
||||
public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper {
|
||||
|
||||
|
||||
private final String TAG = "DownloadMissionHelper";
|
||||
|
||||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||
private static final String DATABASE_NAME = "downloads.db";
|
||||
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
/**
|
||||
* The table name of download missions
|
||||
*/
|
||||
static final String MISSIONS_TABLE_NAME = "download_missions";
|
||||
|
||||
/**
|
||||
* The key to the directory location of the mission
|
||||
*/
|
||||
static final String KEY_LOCATION = "location";
|
||||
/**
|
||||
* The key to the url of a mission
|
||||
*/
|
||||
static final String KEY_URL = "url";
|
||||
/**
|
||||
* The key to the name of a mission
|
||||
*/
|
||||
static final String KEY_NAME = "name";
|
||||
|
||||
/**
|
||||
* The key to the done.
|
||||
*/
|
||||
static final String KEY_DONE = "bytes_downloaded";
|
||||
|
||||
static final String KEY_TIMESTAMP = "timestamp";
|
||||
|
||||
/**
|
||||
* The statement to create the table
|
||||
*/
|
||||
private static final String MISSIONS_CREATE_TABLE =
|
||||
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
|
||||
KEY_LOCATION + " TEXT NOT NULL, " +
|
||||
KEY_NAME + " TEXT NOT NULL, " +
|
||||
KEY_URL + " TEXT NOT NULL, " +
|
||||
KEY_DONE + " INTEGER NOT NULL, " +
|
||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
|
||||
|
||||
|
||||
DownloadMissionSQLiteHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values of the download mission as ContentValues.
|
||||
* @param downloadMission the download mission
|
||||
* @return the content values
|
||||
*/
|
||||
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_URL, downloadMission.url);
|
||||
values.put(KEY_LOCATION, downloadMission.location);
|
||||
values.put(KEY_NAME, downloadMission.name);
|
||||
values.put(KEY_DONE, downloadMission.done);
|
||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// Currently nothing to do
|
||||
}
|
||||
|
||||
public static DownloadMission getMissionFromCursor(Cursor cursor) {
|
||||
if(cursor == null) throw new NullPointerException("cursor is null");
|
||||
int pos;
|
||||
String name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
|
||||
String location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
|
||||
String url = cursor.getString(cursor.getColumnIndexOrThrow(KEY_URL));
|
||||
DownloadMission mission = new DownloadMission(name, url, location);
|
||||
mission.done = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||
mission.finished = true;
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package us.shandian.giga.get.sqlite;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadDataSource;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_LOCATION;
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_NAME;
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.MISSIONS_TABLE_NAME;
|
||||
|
||||
|
||||
/**
|
||||
* Non-thread-safe implementation of {@link DownloadDataSource}
|
||||
*/
|
||||
public class SQLiteDownloadDataSource implements DownloadDataSource {
|
||||
|
||||
private static final String TAG = "DownloadDataSourceImpl";
|
||||
private final DownloadMissionSQLiteHelper downloadMissionSQLiteHelper;
|
||||
|
||||
public SQLiteDownloadDataSource(Context context) {
|
||||
downloadMissionSQLiteHelper = new DownloadMissionSQLiteHelper(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DownloadMission> loadMissions() {
|
||||
ArrayList<DownloadMission> result;
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getReadableDatabase();
|
||||
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
|
||||
null, null, null, DownloadMissionSQLiteHelper.KEY_TIMESTAMP);
|
||||
|
||||
int count = cursor.getCount();
|
||||
if(count == 0) return new ArrayList<>();
|
||||
result = new ArrayList<>(count);
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(DownloadMissionSQLiteHelper.getMissionFromCursor(cursor));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMission(DownloadMission downloadMission) {
|
||||
if(downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
|
||||
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
|
||||
database.insert(MISSIONS_TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMission(DownloadMission downloadMission) {
|
||||
if(downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
|
||||
ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission);
|
||||
String whereClause = KEY_LOCATION+ " = ? AND " +
|
||||
KEY_NAME + " = ?";
|
||||
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
|
||||
whereClause, new String[]{downloadMission.location, downloadMission.name});
|
||||
if(rowsAffected != 1) {
|
||||
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMission(DownloadMission downloadMission) {
|
||||
if(downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase();
|
||||
database.delete(MISSIONS_TABLE_NAME,
|
||||
KEY_LOCATION + " = ? AND " +
|
||||
KEY_NAME + " = ?",
|
||||
new String[]{downloadMission.location, downloadMission.name});
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,99 @@
|
||||
package us.shandian.giga.service;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.NotificationCompat.Builder;
|
||||
import android.support.v4.content.PermissionChecker;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadDataSource;
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadManagerImpl;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import org.schabi.newpipe.download.MainActivity;
|
||||
import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadManagerService extends Service implements DownloadMission.MissionListener
|
||||
public class DownloadManagerService extends Service
|
||||
{
|
||||
|
||||
|
||||
private static final String TAG = DownloadManagerService.class.getSimpleName();
|
||||
|
||||
|
||||
/**
|
||||
* Message code of update messages stored as {@link Message#what}.
|
||||
*/
|
||||
private static final int UPDATE_MESSAGE = 0;
|
||||
private static final int NOTIFICATION_ID = 1000;
|
||||
private static final String EXTRA_NAME = "DownloadManagerService.extra.name";
|
||||
private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location";
|
||||
private static final String EXTRA_IS_AUDIO = "DownloadManagerService.extra.is_audio";
|
||||
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
|
||||
|
||||
|
||||
private DMBinder mBinder;
|
||||
private DownloadManager mManager;
|
||||
private Notification mNotification;
|
||||
private Handler mHandler;
|
||||
private long mLastTimeStamp = System.currentTimeMillis();
|
||||
private DownloadDataSource mDataSource;
|
||||
|
||||
|
||||
|
||||
private MissionListener missionListener = new MissionListener();
|
||||
|
||||
|
||||
private void notifyMediaScanner(DownloadMission mission) {
|
||||
Uri uri = Uri.parse("file://" + mission.location + "/" + mission.name);
|
||||
// notify media scanner on downloaded media file ...
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate");
|
||||
}
|
||||
|
||||
|
||||
mBinder = new DMBinder();
|
||||
if(mDataSource == null) {
|
||||
mDataSource = new SQLiteDownloadDataSource(this);
|
||||
}
|
||||
if (mManager == null) {
|
||||
String path = NewPipeSettings.getVideoDownloadPath(this);
|
||||
mManager = new DownloadManagerImpl(this, path);
|
||||
ArrayList<String> paths = new ArrayList<>(2);
|
||||
paths.add(NewPipeSettings.getVideoDownloadPath(this));
|
||||
paths.add(NewPipeSettings.getAudioDownloadPath(this));
|
||||
mManager = new DownloadManagerImpl(paths, mDataSource);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mManager == null");
|
||||
Log.d(TAG, "Download directory: " + path);
|
||||
Log.d(TAG, "Download directory: " + paths);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Intent i = new Intent();
|
||||
i.setAction(Intent.ACTION_MAIN);
|
||||
i.setClass(this, MainActivity.class);
|
||||
i.setClass(this, DownloadActivity.class);
|
||||
|
||||
Drawable icon = this.getResources().getDrawable(R.mipmap.ic_launcher);
|
||||
|
||||
@@ -68,8 +108,8 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
new Intent(this, MainActivity.class)
|
||||
.setAction(MainActivity.INTENT_LIST),
|
||||
new Intent(this, DownloadActivity.class)
|
||||
.setAction(DownloadActivity.INTENT_LIST),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
|
||||
@@ -83,28 +123,50 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||
mHandler = new Handler(thread.getLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == 0) {
|
||||
int runningCount = 0;
|
||||
|
||||
for (int i = 0; i < mManager.getCount(); i++) {
|
||||
if (mManager.getMission(i).running) {
|
||||
runningCount++;
|
||||
switch (msg.what) {
|
||||
case UPDATE_MESSAGE: {
|
||||
int runningCount = 0;
|
||||
|
||||
for (int i = 0; i < mManager.getCount(); i++) {
|
||||
if (mManager.getMission(i).running) {
|
||||
runningCount++;
|
||||
}
|
||||
}
|
||||
updateState(runningCount);
|
||||
break;
|
||||
}
|
||||
|
||||
updateState(runningCount);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private void startMissionAsync(final String url, final String location, final String name,
|
||||
final boolean isAudio, final int threads) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int missionId = mManager.startMission(url, location, name, isAudio, threads);
|
||||
mBinder.onMissionAdded(mManager.getMission(missionId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Starting");
|
||||
}
|
||||
|
||||
Log.i(TAG, "Got intent: " + intent);
|
||||
String action = intent.getAction();
|
||||
if(action != null && action.equals(Intent.ACTION_RUN)) {
|
||||
String name = intent.getStringExtra(EXTRA_NAME);
|
||||
String location = intent.getStringExtra(EXTRA_LOCATION);
|
||||
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||
boolean isAudio = intent.getBooleanExtra(EXTRA_IS_AUDIO, false);
|
||||
String url = intent.getDataString();
|
||||
startMissionAsync(url, location, name, isAudio, threads);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@@ -119,52 +181,76 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||
for (int i = 0; i < mManager.getCount(); i++) {
|
||||
mManager.pauseMission(i);
|
||||
}
|
||||
|
||||
|
||||
stopForeground(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
int permissionCheck;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(long done, long total) {
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
long delta = now - mLastTimeStamp;
|
||||
|
||||
if (delta > 2000) {
|
||||
postUpdateMessage();
|
||||
mLastTimeStamp = now;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
postUpdateMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errCode) {
|
||||
postUpdateMessage();
|
||||
}
|
||||
|
||||
private void postUpdateMessage() {
|
||||
mHandler.sendEmptyMessage(0);
|
||||
mHandler.sendEmptyMessage(UPDATE_MESSAGE);
|
||||
}
|
||||
|
||||
private void updateState(int runningCount) {
|
||||
if (runningCount == 0) {
|
||||
stopForeground(true);
|
||||
} else {
|
||||
startForeground(1000, mNotification);
|
||||
startForeground(NOTIFICATION_ID, mNotification);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void startMission(Context context, String url, String location, String name, boolean isAudio, int threads) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.setData(Uri.parse(url));
|
||||
intent.putExtra(EXTRA_NAME, name);
|
||||
intent.putExtra(EXTRA_LOCATION, location);
|
||||
intent.putExtra(EXTRA_IS_AUDIO, isAudio);
|
||||
intent.putExtra(EXTRA_THREADS, threads);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
|
||||
class MissionListener implements DownloadMission.MissionListener {
|
||||
@Override
|
||||
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
|
||||
long now = System.currentTimeMillis();
|
||||
long delta = now - mLastTimeStamp;
|
||||
if (delta > 2000) {
|
||||
postUpdateMessage();
|
||||
mLastTimeStamp = now;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(DownloadMission downloadMission) {
|
||||
postUpdateMessage();
|
||||
notifyMediaScanner(downloadMission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(DownloadMission downloadMission, int errCode) {
|
||||
postUpdateMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wrapper of DownloadManager
|
||||
public class DMBinder extends Binder {
|
||||
public DownloadManager getDownloadManager() {
|
||||
@@ -172,15 +258,13 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||
}
|
||||
|
||||
public void onMissionAdded(DownloadMission mission) {
|
||||
mission.addListener(DownloadManagerService.this);
|
||||
mission.addListener(missionListener);
|
||||
postUpdateMessage();
|
||||
}
|
||||
|
||||
public void onMissionRemoved(DownloadMission mission) {
|
||||
mission.removeListener(DownloadManagerService.this);
|
||||
mission.removeListener(missionListener);
|
||||
postUpdateMessage();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user