├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── Screenshot_2015-04-26-17-04-382.png
├── Screenshot_2015-04-27-17-05-50.png
├── youtubeExtractor
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── at
│ │ │ └── huber
│ │ │ └── youtubeExtractor
│ │ │ ├── YouTubeUriExtractor.java
│ │ │ ├── YtFile.java
│ │ │ ├── VideoMeta.java
│ │ │ ├── Format.java
│ │ │ └── YouTubeExtractor.java
│ └── androidTest
│ │ └── java
│ │ └── at
│ │ └── huber
│ │ └── youtubeExtractor
│ │ ├── ExtractorTestSuite.java
│ │ └── ExtractorTestCases.java
├── .gitignore
└── build.gradle
├── sampleApp
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ ├── values-v21
│ │ │ └── styles.xml
│ │ └── layout
│ │ │ └── activity_sample_download.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── at
│ │ └── huber
│ │ └── sampleDownload
│ │ └── SampleDownloadActivity.java
├── build.gradle
└── .gitignore
├── advancedDownloader
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── values-v21
│ │ │ └── styles.xml
│ │ └── layout
│ │ │ └── activity_sample_download.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── at
│ │ └── huber
│ │ └── youtubeDownloader
│ │ ├── DownloadActivity.java
│ │ └── DownloadFinishedReceiver.java
├── .gitignore
├── build.gradle
└── proguard.cfg
├── .gitignore
├── gradlew.bat
├── LICENSE
├── README.md
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sampleApp', ':youtubeExtractor', ':advancedDownloader'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Screenshot_2015-04-26-17-04-382.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/Screenshot_2015-04-26-17-04-382.png
--------------------------------------------------------------------------------
/Screenshot_2015-04-27-17-05-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/Screenshot_2015-04-27-17-05-50.png
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | youtubeExtractor
3 |
4 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/sampleApp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/sampleApp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/sampleApp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleApp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/sampleApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/advancedDownloader/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/advancedDownloader/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/advancedDownloader/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubs/android-youtubeExtractor/master/advancedDownloader/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleApp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | YouTube Download
5 | Not a valid YouTube link!
6 |
7 |
8 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Apr 18 17:50:00 CEST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/androidTest/java/at/huber/youtubeExtractor/ExtractorTestSuite.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | import org.junit.runner.RunWith;
4 | import org.junit.runners.Suite;
5 |
6 | @RunWith(Suite.class)
7 | @Suite.SuiteClasses({ExtractorTestCases.class})
8 | public class ExtractorTestSuite {}
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/sampleApp/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 |
6 | defaultConfig {
7 | applicationId "at.huber.sampleDownload"
8 | minSdkVersion 14
9 | targetSdkVersion 22
10 | versionCode 1
11 | versionName "1.0"
12 | }
13 | }
14 |
15 | dependencies {
16 | implementation project(':youtubeExtractor')
17 | }
18 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | YouTube Download
5 | Not a valid YouTube link!
6 | Couldn\'t extract URLs you may need an update:
7 | latest GitHub release
8 |
9 |
10 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/java/at/huber/youtubeExtractor/YouTubeUriExtractor.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | import android.content.Context;
4 | import android.util.SparseArray;
5 |
6 | @Deprecated
7 | public abstract class YouTubeUriExtractor extends YouTubeExtractor {
8 |
9 | public YouTubeUriExtractor(Context con) {
10 | super(con);
11 | }
12 |
13 | @Override
14 | protected void onExtractionComplete(SparseArray ytFiles, VideoMeta videoMeta) {
15 | onUrisAvailable(videoMeta.getVideoId(), videoMeta.getTitle(), ytFiles);
16 | }
17 |
18 | public abstract void onUrisAvailable(String videoId, String videoTitle, SparseArray ytFiles);
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 | out/
15 | build/
16 |
17 | # Local configuration file (sdk path, etc)
18 | local.properties
19 |
20 | # Eclipse project files
21 | .classpath
22 | .project
23 |
24 | # Windows thumbnail db
25 | .DS_Store
26 |
27 | # IDEA/Android Studio project files, because
28 | # the project can be imported from settings.gradle
29 | .idea
30 | *.iml
31 |
32 | # Old-style IDEA project files
33 | *.ipr
34 | *.iws
35 |
36 | # Local IDEA workspace
37 | .idea/workspace.xml
38 |
39 | # Gradle cache
40 | .gradle
41 |
42 | # Sandbox stuff
43 | _sandbox
44 |
45 |
--------------------------------------------------------------------------------
/sampleApp/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 | out/
15 | build/
16 |
17 | # Local configuration file (sdk path, etc)
18 | local.properties
19 |
20 | # Eclipse project files
21 | .classpath
22 | .project
23 |
24 | # Windows thumbnail db
25 | .DS_Store
26 |
27 | # IDEA/Android Studio project files, because
28 | # the project can be imported from settings.gradle
29 | .idea
30 | *.iml
31 |
32 | # Old-style IDEA project files
33 | *.ipr
34 | *.iws
35 |
36 | # Local IDEA workspace
37 | .idea/workspace.xml
38 |
39 | # Gradle cache
40 | .gradle
41 |
42 | # Sandbox stuff
43 | _sandbox
44 |
45 |
--------------------------------------------------------------------------------
/youtubeExtractor/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 | out/
15 | build/
16 |
17 | # Local configuration file (sdk path, etc)
18 | local.properties
19 |
20 | # Eclipse project files
21 | .classpath
22 | .project
23 |
24 | # Windows thumbnail db
25 | .DS_Store
26 |
27 | # IDEA/Android Studio project files, because
28 | # the project can be imported from settings.gradle
29 | .idea
30 | *.iml
31 |
32 | # Old-style IDEA project files
33 | *.ipr
34 | *.iws
35 |
36 | # Local IDEA workspace
37 | .idea/workspace.xml
38 |
39 | # Gradle cache
40 | .gradle
41 |
42 | # Sandbox stuff
43 | _sandbox
44 |
45 |
--------------------------------------------------------------------------------
/advancedDownloader/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 | out/
15 | build/
16 |
17 | # Local configuration file (sdk path, etc)
18 | local.properties
19 |
20 | # Eclipse project files
21 | .classpath
22 | .project
23 |
24 | # Windows thumbnail db
25 | .DS_Store
26 |
27 | # IDEA/Android Studio project files, because
28 | # the project can be imported from settings.gradle
29 | .idea
30 | *.iml
31 |
32 | # Old-style IDEA project files
33 | *.ipr
34 | *.iws
35 |
36 | # Local IDEA workspace
37 | .idea/workspace.xml
38 |
39 | # Gradle cache
40 | .gradle
41 |
42 | # Sandbox stuff
43 | _sandbox
44 |
45 |
--------------------------------------------------------------------------------
/youtubeExtractor/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 28
5 |
6 | defaultConfig {
7 | minSdkVersion 14
8 | targetSdkVersion 28
9 | versionCode 1
10 | versionName "1.0"
11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
12 | }
13 | }
14 |
15 | dependencies {
16 | implementation('com.github.evgenyneu:js-evaluator-for-android:v4.0.0') {
17 | exclude module: 'appcompat-v7'
18 | }
19 | implementation 'com.android.support:support-annotations:28.0.0'
20 |
21 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
22 | androidTestImplementation 'com.android.support.test:rules:1.0.2'
23 | }
24 |
--------------------------------------------------------------------------------
/advancedDownloader/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 |
6 | defaultConfig {
7 | applicationId "at.huber.youtubeDownloader"
8 | minSdkVersion 14
9 | targetSdkVersion 22
10 | versionCode 4
11 | versionName "1.9"
12 | }
13 | buildTypes {
14 | release {
15 | proguardFile 'proguard.cfg'
16 | minifyEnabled true
17 | }
18 | }
19 | lintOptions {
20 | checkReleaseBuilds false
21 | }
22 | }
23 |
24 | repositories {
25 | mavenCentral()
26 | }
27 |
28 | dependencies {
29 | implementation project(':youtubeExtractor')
30 | implementation 'org.aspectj:aspectjrt:1.8.13'
31 | implementation 'com.googlecode.mp4parser:isoparser:1.1.22'
32 | }
33 |
--------------------------------------------------------------------------------
/sampleApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/java/at/huber/youtubeExtractor/YtFile.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | public class YtFile {
4 |
5 | private Format format;
6 | private String url = "";
7 |
8 | YtFile(Format format, String url) {
9 | this.format = format;
10 | this.url = url;
11 | }
12 |
13 | /**
14 | * The url to download the file.
15 | */
16 | public String getUrl() {
17 | return url;
18 | }
19 |
20 | /**
21 | * Format data for the specific file.
22 | */
23 | public Format getFormat() {
24 | return format;
25 | }
26 |
27 | /**
28 | * Format data for the specific file.
29 | */
30 | @Deprecated
31 | public Format getMeta() {
32 | return format;
33 | }
34 |
35 | @Override
36 | public boolean equals(Object o) {
37 | if (this == o) return true;
38 | if (o == null || getClass() != o.getClass()) return false;
39 |
40 | YtFile ytFile = (YtFile) o;
41 |
42 | if (format != null ? !format.equals(ytFile.format) : ytFile.format != null) return false;
43 | return url != null ? url.equals(ytFile.url) : ytFile.url == null;
44 | }
45 |
46 | @Override
47 | public int hashCode() {
48 | int result = format != null ? format.hashCode() : 0;
49 | result = 31 * result + (url != null ? url.hashCode() : 0);
50 | return result;
51 | }
52 |
53 | @Override
54 | public String toString() {
55 | return "YtFile{" +
56 | "format=" + format +
57 | ", url='" + url + '\'' +
58 | '}';
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/sampleApp/src/main/res/layout/activity_sample_download.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
18 |
19 |
26 |
27 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/res/layout/activity_sample_download.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
18 |
19 |
27 |
28 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Benjamin Huber
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | a.) Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | b.) Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 | c.) Neither the name of the copyright holder nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
30 | This software includes work that was released under the following license:
31 |
32 | MIT License
33 |
34 | Copyright (c) 2014 Evgenii Neumerzhitckii
35 |
36 | Permission is hereby granted, free of charge, to any person obtaining
37 | a copy of this software and associated documentation files (the
38 | "Software"), to deal in the Software without restriction, including
39 | without limitation the rights to use, copy, modify, merge, publish,
40 | distribute, sublicense, and/or sell copies of the Software, and to
41 | permit persons to whom the Software is furnished to do so, subject to
42 | the following conditions:
43 |
44 | The above copyright notice and this permission notice shall be
45 | included in all copies or substantial portions of the Software.
46 |
47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
48 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
49 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
50 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
51 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
52 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
53 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
54 |
--------------------------------------------------------------------------------
/advancedDownloader/proguard.cfg:
--------------------------------------------------------------------------------
1 | # This is a configuration file for ProGuard.
2 | # http://proguard.sourceforge.net/index.html#manual/usage.html
3 |
4 | # Optimizations: If you don't want to optimize, use the
5 | # proguard-android.txt configuration file instead of this one, which
6 | # turns off the optimization flags. Adding optimization introduces
7 | # certain risks, since for example not all optimizations performed by
8 | # ProGuard works on all versions of Dalvik. The following flags turn
9 | # off various optimizations known to have issues, but the list may not
10 | # be complete or up to date. (The "arithmetic" optimization can be
11 | # used if you are only targeting Android 2.0 or later.) Make sure you
12 | # test thoroughly if you go this route.
13 | -optimizations !code/allocation/variable,!code/simplification/cast,!field/*,!class/merging/*
14 | -optimizationpasses 5
15 | -allowaccessmodification
16 | -dontpreverify
17 | -dontobfuscate
18 |
19 | # The remainder of this file is identical to the non-optimized version
20 | # of the Proguard configuration file (except that the other file has
21 | # flags to turn off optimization).
22 |
23 | -dontusemixedcaseclassnames
24 | -dontskipnonpubliclibraryclasses
25 | -verbose
26 |
27 | -keepattributes *Annotation*
28 | -keep public class com.google.vending.licensing.ILicensingService
29 | -keep public class com.android.vending.licensing.ILicensingService
30 | -keep class * implements com.coremedia.iso.boxes.Box { *; }
31 |
32 | # js-evaluator-for-android
33 | -keepattributes JavascriptInterface
34 | -keepclassmembers class * {
35 | @android.webkit.JavascriptInterface ;
36 | }
37 |
38 | -assumenosideeffects class android.util.Log {
39 | public static *** d(...);
40 | public static *** v(...);
41 | public static *** w(...);
42 | public static *** i(...);
43 | }
44 |
45 | # ADDED
46 | -keep class com.google.zxing.client.android.camera.open.**
47 | -keep class com.google.zxing.client.android.camera.exposure.**
48 | -keep class com.google.zxing.client.android.common.executor.**
49 |
50 | # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
51 | -keepclasseswithmembernames class * {
52 | native ;
53 | }
54 |
55 | # keep setters in Views so that animations can still work.
56 | # see http://proguard.sourceforge.net/manual/examples.html#beans
57 | -keepclassmembers public class * extends android.view.View {
58 | void set*(***);
59 | *** get*();
60 | }
61 |
62 | # We want to keep methods in Activity that could be used in the XML attribute onClick
63 | -keepclassmembers class * extends android.app.Activity {
64 | public void *(android.view.View);
65 | }
66 |
67 | # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
68 | -keepclassmembers enum * {
69 | public static **[] values();
70 | public static ** valueOf(java.lang.String);
71 | }
72 |
73 | -keep class * implements android.os.Parcelable {
74 | public static final android.os.Parcelable$Creator *;
75 | }
76 |
77 | -keepclassmembers class **.R$* {
78 | public static ;
79 | }
80 |
81 | # The support library contains references to newer platform versions.
82 | # Don't warn about those in case this app is linking against an older
83 | # platform version. We know about them, and they are safe.
84 | -dontwarn android.support.**
85 | -dontwarn com.googlecode.mp4parser.authoring.tracks.mjpeg.**
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Android based YouTube url extractor
2 | =======================================================
3 |
4 | These are the urls to the YouTube video or audio files, so you can stream or download them.
5 | It features an age verification circumvention and a signature deciphering method (mainly for vevo videos).
6 |
7 | * Builds: [](https://jitpack.io/#HaarigerHarald/android-youtubeExtractor)
8 | * Dependency: [js-evaluator-for-android](https://github.com/evgenyneu/js-evaluator-for-android)
9 |
10 | ## Gradle
11 |
12 | To always build from the latest commit with all updates. Add the JitPack repository:
13 |
14 | ```java
15 | repositories {
16 | maven { url "https://jitpack.io" }
17 | }
18 | ```
19 |
20 | And the dependency:
21 |
22 | ```java
23 | implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
24 | ```
25 |
26 | ## Usage
27 |
28 | It's build around an AsyncTask. Called from an Activity you can write:
29 |
30 | ```java
31 | String youtubeLink = "http://youtube.com/watch?v=xxxx";
32 |
33 | new YouTubeExtractor(this) {
34 | @Override
35 | public void onExtractionComplete(SparseArray ytFiles, VideoMeta vMeta) {
36 | if (ytFiles != null) {
37 | int itag = 22;
38 | String downloadUrl = ytFiles.get(itag).getUrl();
39 | }
40 | }
41 | }.extract(youtubeLink, true, true);
42 | ```
43 |
44 | The ytFiles SparseArray is a map of available media files for one YouTube video, accessible by their itag
45 | value. For further infos about itags and their associated formats refer to: [Wikipedia - YouTube Quality and formats](http://en.wikipedia.org/wiki/YouTube#Quality_and_formats).
46 |
47 | ## Configuration
48 |
49 | There are 2 configuration options set via extract:
50 |
51 | ```java
52 | extract(youtubeLink, /*parseDashManifest*/ true, /*includeWebm*/ true);
53 | ```
54 |
55 | **parseDashManifest**
56 |
57 | The dash manifest contains dash streams and usually additionally the higher quality audio formats.
58 | But the main difference is that dash streams from the dash manifest seem to not get throttled by the YouTube servers.
59 | If you don't use the dash streams at all leave it deactivated since it needs to download additional files for extraction.
60 |
61 | Known issue: the dash manifest can't be parsed for signature enciphered videos
62 |
63 |
64 | **includeWebm**
65 |
66 | If set to false it excludes the webm container format streams from the result.
67 |
68 | ## Requirements
69 |
70 | Android **4.0** (API version 14) and up for Webview Javascript execution see: [js-evaluator-for-android](https://github.com/evgenyneu/js-evaluator-for-android).
71 | Not signature enciphered Videos may work on lower Android versions (untested).
72 |
73 | ## Limitations
74 |
75 | Those videos aren't working:
76 |
77 | * Everything private (private videos, bought movies, ...)
78 | * Unavailable in your country
79 | * RTMPE urls (very rare)
80 |
81 |
82 | ## Modules
83 |
84 | * **youtubeExtractor:** The extractor android library.
85 |
86 | * **sampleApp:** A simple example downloader App.
87 |
88 | * **advancedDownloader:** A more sophisticated App using the [mp4parser](https://github.com/sannies/mp4parser) library to mux dash audio and video files together and add metadata to audio files after downloading. [youtubeDownloader.apk](https://github.com/HaarigerHarald/android-youtubeExtractor/releases/latest)
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | ## License
97 |
98 | Modified BSD license see [LICENSE](LICENSE) and 3rd party licenses depending on what you need
99 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/java/at/huber/youtubeExtractor/VideoMeta.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | public class VideoMeta {
4 |
5 | private static final String IMAGE_BASE_URL = "http://i.ytimg.com/vi/";
6 |
7 | private String videoId;
8 | private String title;
9 |
10 | private String author;
11 | private String channelId;
12 |
13 | private long videoLength;
14 | private long viewCount;
15 |
16 | private boolean isLiveStream;
17 |
18 | protected VideoMeta(String videoId, String title, String author, String channelId, long videoLength, long viewCount, boolean isLiveStream) {
19 | this.videoId = videoId;
20 | this.title = title;
21 | this.author = author;
22 | this.channelId = channelId;
23 | this.videoLength = videoLength;
24 | this.viewCount = viewCount;
25 | this.isLiveStream = isLiveStream;
26 | }
27 |
28 |
29 | // 120 x 90
30 | public String getThumbUrl() {
31 | return IMAGE_BASE_URL + videoId + "/default.jpg";
32 | }
33 |
34 | // 320 x 180
35 | public String getMqImageUrl() {
36 | return IMAGE_BASE_URL + videoId + "/mqdefault.jpg";
37 | }
38 |
39 | // 480 x 360
40 | public String getHqImageUrl() {
41 | return IMAGE_BASE_URL + videoId + "/hqdefault.jpg";
42 | }
43 |
44 | // 640 x 480
45 | public String getSdImageUrl() {
46 | return IMAGE_BASE_URL + videoId + "/sddefault.jpg";
47 | }
48 |
49 | // Max Res
50 | public String getMaxResImageUrl() {
51 | return IMAGE_BASE_URL + videoId + "/maxresdefault.jpg";
52 | }
53 |
54 | public String getVideoId() {
55 | return videoId;
56 | }
57 |
58 | public String getTitle() {
59 | return title;
60 | }
61 |
62 | public String getAuthor() {
63 | return author;
64 | }
65 |
66 | public String getChannelId() {
67 | return channelId;
68 | }
69 |
70 | public boolean isLiveStream() {
71 | return isLiveStream;
72 | }
73 |
74 | /**
75 | * The video length in seconds.
76 | */
77 | public long getVideoLength() {
78 | return videoLength;
79 | }
80 |
81 | public long getViewCount() {
82 | return viewCount;
83 | }
84 |
85 | @Override
86 | public boolean equals(Object o) {
87 | if (this == o) return true;
88 | if (o == null || getClass() != o.getClass()) return false;
89 |
90 | VideoMeta videoMeta = (VideoMeta) o;
91 |
92 | if (videoLength != videoMeta.videoLength) return false;
93 | if (viewCount != videoMeta.viewCount) return false;
94 | if (isLiveStream != videoMeta.isLiveStream) return false;
95 | if (videoId != null ? !videoId.equals(videoMeta.videoId) : videoMeta.videoId != null)
96 | return false;
97 | if (title != null ? !title.equals(videoMeta.title) : videoMeta.title != null) return false;
98 | if (author != null ? !author.equals(videoMeta.author) : videoMeta.author != null)
99 | return false;
100 | return channelId != null ? channelId.equals(videoMeta.channelId) : videoMeta.channelId == null;
101 |
102 | }
103 |
104 | @Override
105 | public int hashCode() {
106 | int result = videoId != null ? videoId.hashCode() : 0;
107 | result = 31 * result + (title != null ? title.hashCode() : 0);
108 | result = 31 * result + (author != null ? author.hashCode() : 0);
109 | result = 31 * result + (channelId != null ? channelId.hashCode() : 0);
110 | result = 31 * result + (int) (videoLength ^ (videoLength >>> 32));
111 | result = 31 * result + (int) (viewCount ^ (viewCount >>> 32));
112 | result = 31 * result + (isLiveStream ? 1 : 0);
113 | return result;
114 | }
115 |
116 | @Override
117 | public String toString() {
118 | return "VideoMeta{" +
119 | "videoId='" + videoId + '\'' +
120 | ", title='" + title + '\'' +
121 | ", author='" + author + '\'' +
122 | ", channelId='" + channelId + '\'' +
123 | ", videoLength=" + videoLength +
124 | ", viewCount=" + viewCount +
125 | ", isLiveStream=" + isLiveStream +
126 | '}';
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/sampleApp/src/main/java/at/huber/sampleDownload/SampleDownloadActivity.java:
--------------------------------------------------------------------------------
1 | package at.huber.sampleDownload;
2 |
3 | import android.app.Activity;
4 | import android.app.DownloadManager;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.net.Uri;
8 | import android.os.Bundle;
9 | import android.os.Environment;
10 | import android.util.SparseArray;
11 | import android.view.View;
12 | import android.view.View.OnClickListener;
13 | import android.widget.Button;
14 | import android.widget.LinearLayout;
15 | import android.widget.ProgressBar;
16 | import android.widget.Toast;
17 |
18 | import at.huber.youtubeExtractor.VideoMeta;
19 | import at.huber.youtubeExtractor.YouTubeExtractor;
20 | import at.huber.youtubeExtractor.YtFile;
21 |
22 | public class SampleDownloadActivity extends Activity {
23 |
24 | private static String youtubeLink;
25 |
26 | private LinearLayout mainLayout;
27 | private ProgressBar mainProgressBar;
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 |
33 | setContentView(R.layout.activity_sample_download);
34 | mainLayout = (LinearLayout) findViewById(R.id.main_layout);
35 | mainProgressBar = (ProgressBar) findViewById(R.id.prgrBar);
36 |
37 | // Check how it was started and if we can get the youtube link
38 | if (savedInstanceState == null && Intent.ACTION_SEND.equals(getIntent().getAction())
39 | && getIntent().getType() != null && "text/plain".equals(getIntent().getType())) {
40 |
41 | String ytLink = getIntent().getStringExtra(Intent.EXTRA_TEXT);
42 |
43 | if (ytLink != null
44 | && (ytLink.contains("://youtu.be/") || ytLink.contains("youtube.com/watch?v="))) {
45 | youtubeLink = ytLink;
46 | // We have a valid link
47 | getYoutubeDownloadUrl(youtubeLink);
48 | } else {
49 | Toast.makeText(this, R.string.error_no_yt_link, Toast.LENGTH_LONG).show();
50 | finish();
51 | }
52 | } else if (savedInstanceState != null && youtubeLink != null) {
53 | getYoutubeDownloadUrl(youtubeLink);
54 | } else {
55 | finish();
56 | }
57 | }
58 |
59 | private void getYoutubeDownloadUrl(String youtubeLink) {
60 | new YouTubeExtractor(this) {
61 |
62 | @Override
63 | public void onExtractionComplete(SparseArray ytFiles, VideoMeta vMeta) {
64 | mainProgressBar.setVisibility(View.GONE);
65 |
66 | if (ytFiles == null) {
67 | // Something went wrong we got no urls. Always check this.
68 | finish();
69 | return;
70 | }
71 | // Iterate over itags
72 | for (int i = 0, itag; i < ytFiles.size(); i++) {
73 | itag = ytFiles.keyAt(i);
74 | // ytFile represents one file with its url and meta data
75 | YtFile ytFile = ytFiles.get(itag);
76 |
77 | // Just add videos in a decent format => height -1 = audio
78 | if (ytFile.getFormat().getHeight() == -1 || ytFile.getFormat().getHeight() >= 360) {
79 | addButtonToMainLayout(vMeta.getTitle(), ytFile);
80 | }
81 | }
82 | }
83 | }.extract(youtubeLink, true, false);
84 | }
85 |
86 | private void addButtonToMainLayout(final String videoTitle, final YtFile ytfile) {
87 | // Display some buttons and let the user choose the format
88 | String btnText = (ytfile.getFormat().getHeight() == -1) ? "Audio " +
89 | ytfile.getFormat().getAudioBitrate() + " kbit/s" :
90 | ytfile.getFormat().getHeight() + "p";
91 | btnText += (ytfile.getFormat().isDashContainer()) ? " dash" : "";
92 | Button btn = new Button(this);
93 | btn.setText(btnText);
94 | btn.setOnClickListener(new OnClickListener() {
95 |
96 | @Override
97 | public void onClick(View v) {
98 | String filename;
99 | if (videoTitle.length() > 55) {
100 | filename = videoTitle.substring(0, 55) + "." + ytfile.getFormat().getExt();
101 | } else {
102 | filename = videoTitle + "." + ytfile.getFormat().getExt();
103 | }
104 | filename = filename.replaceAll("[\\\\><\"|*?%:#/]", "");
105 | downloadFromUrl(ytfile.getUrl(), videoTitle, filename);
106 | finish();
107 | }
108 | });
109 | mainLayout.addView(btn);
110 | }
111 |
112 | private void downloadFromUrl(String youtubeDlUrl, String downloadTitle, String fileName) {
113 | Uri uri = Uri.parse(youtubeDlUrl);
114 | DownloadManager.Request request = new DownloadManager.Request(uri);
115 | request.setTitle(downloadTitle);
116 |
117 | request.allowScanningByMediaScanner();
118 | request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
119 | request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
120 |
121 | DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
122 | manager.enqueue(request);
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/java/at/huber/youtubeExtractor/Format.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | public class Format {
4 |
5 | public enum VCodec {
6 | H263, H264, MPEG4, VP8, VP9, NONE
7 | }
8 |
9 | public enum ACodec {
10 | MP3, AAC, VORBIS, OPUS, NONE
11 | }
12 |
13 | private int itag;
14 | private String ext;
15 | private int height;
16 | private int fps;
17 | private VCodec vCodec;
18 | private ACodec aCodec;
19 | private int audioBitrate;
20 | private boolean isDashContainer;
21 | private boolean isHlsContent;
22 |
23 | Format(int itag, String ext, int height, VCodec vCodec, ACodec aCodec, boolean isDashContainer) {
24 | this.itag = itag;
25 | this.ext = ext;
26 | this.height = height;
27 | this.fps = 30;
28 | this.audioBitrate = -1;
29 | this.isDashContainer = isDashContainer;
30 | this.isHlsContent = false;
31 | }
32 |
33 | Format(int itag, String ext, VCodec vCodec, ACodec aCodec, int audioBitrate, boolean isDashContainer) {
34 | this.itag = itag;
35 | this.ext = ext;
36 | this.height = -1;
37 | this.fps = 30;
38 | this.audioBitrate = audioBitrate;
39 | this.isDashContainer = isDashContainer;
40 | this.isHlsContent = false;
41 | }
42 |
43 | Format(int itag, String ext, int height, VCodec vCodec, ACodec aCodec, int audioBitrate,
44 | boolean isDashContainer) {
45 | this.itag = itag;
46 | this.ext = ext;
47 | this.height = height;
48 | this.fps = 30;
49 | this.audioBitrate = audioBitrate;
50 | this.isDashContainer = isDashContainer;
51 | this.isHlsContent = false;
52 | }
53 |
54 | Format(int itag, String ext, int height, VCodec vCodec, ACodec aCodec, int audioBitrate,
55 | boolean isDashContainer, boolean isHlsContent) {
56 | this.itag = itag;
57 | this.ext = ext;
58 | this.height = height;
59 | this.fps = 30;
60 | this.audioBitrate = audioBitrate;
61 | this.isDashContainer = isDashContainer;
62 | this.isHlsContent = isHlsContent;
63 | }
64 |
65 | Format(int itag, String ext, int height, VCodec vCodec, int fps, ACodec aCodec, boolean isDashContainer) {
66 | this.itag = itag;
67 | this.ext = ext;
68 | this.height = height;
69 | this.audioBitrate = -1;
70 | this.fps = fps;
71 | this.isDashContainer = isDashContainer;
72 | this.isHlsContent = false;
73 | }
74 |
75 | /**
76 | * Get the frames per second
77 | */
78 | public int getFps() {
79 | return fps;
80 | }
81 |
82 | /**
83 | * Audio bitrate in kbit/s or -1 if there is no audio track.
84 | */
85 | public int getAudioBitrate() {
86 | return audioBitrate;
87 | }
88 |
89 | /**
90 | * An identifier used by youtube for different formats.
91 | */
92 | public int getItag() {
93 | return itag;
94 | }
95 |
96 | /**
97 | * The file extension and conainer format like "mp4"
98 | */
99 | public String getExt() {
100 | return ext;
101 | }
102 |
103 | public boolean isDashContainer() {
104 | return isDashContainer;
105 | }
106 |
107 | public ACodec getAudioCodec() {
108 | return aCodec;
109 | }
110 |
111 | public VCodec getVideoCodec() {
112 | return vCodec;
113 | }
114 |
115 | public boolean isHlsContent() {
116 | return isHlsContent;
117 | }
118 |
119 | /**
120 | * The pixel height of the video stream or -1 for audio files.
121 | */
122 | public int getHeight() {
123 | return height;
124 | }
125 |
126 | @Override
127 | public boolean equals(Object o) {
128 | if (this == o) return true;
129 | if (o == null || getClass() != o.getClass()) return false;
130 |
131 | Format format = (Format) o;
132 |
133 | if (itag != format.itag) return false;
134 | if (height != format.height) return false;
135 | if (fps != format.fps) return false;
136 | if (audioBitrate != format.audioBitrate) return false;
137 | if (isDashContainer != format.isDashContainer) return false;
138 | if (isHlsContent != format.isHlsContent) return false;
139 | if (ext != null ? !ext.equals(format.ext) : format.ext != null) return false;
140 | if (vCodec != format.vCodec) return false;
141 | return aCodec == format.aCodec;
142 |
143 | }
144 |
145 | @Override
146 | public int hashCode() {
147 | int result = itag;
148 | result = 31 * result + (ext != null ? ext.hashCode() : 0);
149 | result = 31 * result + height;
150 | result = 31 * result + fps;
151 | result = 31 * result + (vCodec != null ? vCodec.hashCode() : 0);
152 | result = 31 * result + (aCodec != null ? aCodec.hashCode() : 0);
153 | result = 31 * result + audioBitrate;
154 | result = 31 * result + (isDashContainer ? 1 : 0);
155 | result = 31 * result + (isHlsContent ? 1 : 0);
156 | return result;
157 | }
158 |
159 | @Override
160 | public String toString() {
161 | return "Format{" +
162 | "itag=" + itag +
163 | ", ext='" + ext + '\'' +
164 | ", height=" + height +
165 | ", fps=" + fps +
166 | ", vCodec=" + vCodec +
167 | ", aCodec=" + aCodec +
168 | ", audioBitrate=" + audioBitrate +
169 | ", isDashContainer=" + isDashContainer +
170 | ", isHlsContent=" + isHlsContent +
171 | '}';
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/androidTest/java/at/huber/youtubeExtractor/ExtractorTestCases.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 | import android.support.test.InstrumentationRegistry;
6 | import android.support.test.filters.FlakyTest;
7 | import android.support.test.runner.AndroidJUnit4;
8 | import android.util.Log;
9 | import android.util.SparseArray;
10 |
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 |
14 | import java.net.HttpURLConnection;
15 | import java.net.URL;
16 | import java.util.Random;
17 | import java.util.concurrent.CountDownLatch;
18 | import java.util.concurrent.TimeUnit;
19 |
20 | import static android.support.test.InstrumentationRegistry.getInstrumentation;
21 | import static junit.framework.TestCase.assertEquals;
22 | import static junit.framework.TestCase.assertNotNull;
23 | import static junit.framework.TestCase.assertNotSame;
24 |
25 | @RunWith(AndroidJUnit4.class)
26 | @FlakyTest
27 | public class ExtractorTestCases {
28 |
29 | private static final String EXTRACTOR_TEST_TAG = "Extractor Test";
30 |
31 | private String testUrl;
32 |
33 | @Test
34 | public void testUsualVideo() throws Throwable {
35 | VideoMeta expMeta = new VideoMeta("YE7VzlLtp-4", "Big Buck Bunny", "Blender",
36 | "UCSMOQeBJ2RAnuFungnQOxLg", 597, 0, false);
37 | extractorTest("http://youtube.com/watch?v=YE7VzlLtp-4", expMeta);
38 | extractorTestDashManifest("http://youtube.com/watch?v=YE7VzlLtp-4");
39 | }
40 |
41 | @Test
42 | public void testUnembeddable() throws Throwable {
43 | VideoMeta expMeta = new VideoMeta("QH4VHl2uQ9o", "Match Chain Reaction Amazing Fire Art - real ghost rider", "BLACKHAND",
44 | "UCl9nsRuGenStMDZfD95w85A", 331, 0, false);
45 | extractorTest("https://www.youtube.com/watch?v=QH4VHl2uQ9o", expMeta);
46 | extractorTestDashManifest("https://www.youtube.com/watch?v=QH4VHl2uQ9o");
47 | }
48 |
49 | @Test
50 | public void testEncipheredVideo() throws Throwable {
51 | VideoMeta expMeta = new VideoMeta("e8X3ACToii0", "Rise Against - Savior (Official Video)", "RiseAgainstVEVO",
52 | "UChMKB2AHNpeuWhalpRYhUaw", 243, 0, false);
53 | extractorTest("https://www.youtube.com/watch?v=e8X3ACToii0", expMeta);
54 | }
55 |
56 | @Test
57 | public void testAgeRestrictVideo() throws Throwable {
58 | VideoMeta expMeta = new VideoMeta("61Ev-YvBw2c", "Test video for age-restriction",
59 | "jpdemoA", "UC95NqtFsDZKlmzOJmZi_g6Q", 14, 0, false);
60 | extractorTest("http://www.youtube.com/watch?v=61Ev-YvBw2c", expMeta);
61 | // extractorTestDashManifest("http://www.youtube.com/watch?v=61Ev-YvBw2c");
62 | }
63 |
64 | // public void testLiveStream() throws Throwable {
65 | // VideoMeta expMeta = new VideoMeta("ddFvjfvPnqk", "NASA Live Stream - Earth From Space (Full Screen) | ISS LIVE FEED - Debunk Flat Earth",
66 | // "Space Videos", "UCakgsb0w7QB0VHdnCc-OVEA", 0, 0, true);
67 | // extractorTest("http://www.youtube.com/watch?v=ddFvjfvPnqk", expMeta);
68 | // }
69 |
70 |
71 | private void extractorTestDashManifest(final String youtubeLink)
72 | throws Throwable {
73 | final CountDownLatch signal = new CountDownLatch(1);
74 | YouTubeExtractor.LOGGING = true;
75 |
76 | testUrl = null;
77 |
78 | new Handler(Looper.getMainLooper()).post(new Runnable() {
79 |
80 | @Override
81 | public void run() {
82 | final YouTubeExtractor ytEx = new YouTubeExtractor(InstrumentationRegistry.getContext()) {
83 | @Override
84 | public void onExtractionComplete(SparseArray ytFiles, VideoMeta videoMeta) {
85 | assertNotNull(ytFiles);
86 | int numNotDash = 0;
87 | int itag;
88 | for (int i = 0; i < ytFiles.size(); i++) {
89 | itag = ytFiles.keyAt(i);
90 | if (ytFiles.get(itag).getFormat().isDashContainer()) {
91 | numNotDash = i;
92 | break;
93 | }
94 | }
95 | itag = ytFiles.keyAt(new Random().nextInt(ytFiles.size() - numNotDash) + numNotDash);
96 | testUrl = ytFiles.get(itag).getUrl();
97 | Log.d(EXTRACTOR_TEST_TAG, "Testing itag: " + itag +", url:" + testUrl);
98 | signal.countDown();
99 | }
100 | };
101 | ytEx.extract(youtubeLink, true, true);
102 | }
103 | });
104 |
105 | signal.await(10, TimeUnit.SECONDS);
106 |
107 | assertNotNull(testUrl);
108 |
109 | final URL url = new URL(testUrl);
110 |
111 | HttpURLConnection con = (HttpURLConnection) url.openConnection();
112 | int code = con.getResponseCode();
113 | con.getInputStream().close();
114 | con.disconnect();
115 | assertEquals(200, code);
116 | }
117 |
118 |
119 | private void extractorTest(final String youtubeLink, final VideoMeta expMeta)
120 | throws Throwable {
121 | final CountDownLatch signal = new CountDownLatch(1);
122 | YouTubeExtractor.LOGGING = true;
123 |
124 | testUrl = null;
125 |
126 | new Handler(Looper.getMainLooper()).post(new Runnable() {
127 |
128 | @Override
129 | public void run() {
130 | final YouTubeExtractor ytEx = new YouTubeExtractor(getInstrumentation()
131 | .getTargetContext()) {
132 | @Override
133 | public void onExtractionComplete(SparseArray ytFiles, VideoMeta videoMeta) {
134 | assertEquals(expMeta.getVideoId(), videoMeta.getVideoId());
135 | assertEquals(expMeta.getTitle(),videoMeta.getTitle());
136 | assertEquals(expMeta.getAuthor(), videoMeta.getAuthor());
137 | assertEquals(expMeta.getChannelId(), videoMeta.getChannelId());
138 | assertEquals(expMeta.getVideoLength(), videoMeta.getVideoLength());
139 | assertNotSame(0, videoMeta.getViewCount());
140 | assertNotNull(ytFiles);
141 | int itag = ytFiles.keyAt(new Random().nextInt(ytFiles.size()));
142 | testUrl = ytFiles.get(itag).getUrl();
143 | Log.d(EXTRACTOR_TEST_TAG, "Testing itag: " + itag +", url:" + testUrl);
144 | signal.countDown();
145 | }
146 | };
147 | ytEx.extract(youtubeLink, false, true);
148 | }
149 | });
150 |
151 | signal.await(10, TimeUnit.SECONDS);
152 |
153 | assertNotNull(testUrl);
154 |
155 | final URL url = new URL(testUrl);
156 |
157 | HttpURLConnection con = (HttpURLConnection) url.openConnection();
158 | int code = con.getResponseCode();
159 | con.getInputStream().close();
160 | con.disconnect();
161 | assertEquals(200, code);
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/java/at/huber/youtubeDownloader/DownloadActivity.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeDownloader;
2 |
3 | import android.app.Activity;
4 | import android.app.DownloadManager;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.net.Uri;
8 | import android.os.Bundle;
9 | import android.os.Environment;
10 | import android.text.method.LinkMovementMethod;
11 | import android.util.SparseArray;
12 | import android.view.View;
13 | import android.view.View.OnClickListener;
14 | import android.widget.Button;
15 | import android.widget.LinearLayout;
16 | import android.widget.ProgressBar;
17 | import android.widget.TextView;
18 | import android.widget.Toast;
19 |
20 | import java.io.File;
21 | import java.io.IOException;
22 | import java.util.ArrayList;
23 | import java.util.Collections;
24 | import java.util.Comparator;
25 | import java.util.List;
26 |
27 | import at.huber.youtubeExtractor.VideoMeta;
28 | import at.huber.youtubeExtractor.YouTubeExtractor;
29 | import at.huber.youtubeExtractor.YtFile;
30 |
31 | public class DownloadActivity extends Activity {
32 |
33 | private static final int ITAG_FOR_AUDIO = 140;
34 |
35 | private static String youtubeLink;
36 |
37 | private LinearLayout mainLayout;
38 | private ProgressBar mainProgressBar;
39 | private List formatsToShowList;
40 |
41 | @Override
42 | protected void onCreate(Bundle savedInstanceState) {
43 | super.onCreate(savedInstanceState);
44 |
45 | setContentView(R.layout.activity_sample_download);
46 | mainLayout = (LinearLayout) findViewById(R.id.main_layout);
47 | mainProgressBar = (ProgressBar) findViewById(R.id.prgrBar);
48 |
49 | // Check how it was started and if we can get the youtube link
50 | if (savedInstanceState == null && Intent.ACTION_SEND.equals(getIntent().getAction())
51 | && getIntent().getType() != null && "text/plain".equals(getIntent().getType())) {
52 |
53 | String ytLink = getIntent().getStringExtra(Intent.EXTRA_TEXT);
54 |
55 | if (ytLink != null
56 | && (ytLink.contains("://youtu.be/") || ytLink.contains("youtube.com/watch?v="))) {
57 | youtubeLink = ytLink;
58 | // We have a valid link
59 | getYoutubeDownloadUrl(youtubeLink);
60 | } else {
61 | Toast.makeText(this, R.string.error_no_yt_link, Toast.LENGTH_LONG).show();
62 | finish();
63 | }
64 | } else if (savedInstanceState != null && youtubeLink != null) {
65 | getYoutubeDownloadUrl(youtubeLink);
66 | } else {
67 | finish();
68 | }
69 | }
70 |
71 | private void getYoutubeDownloadUrl(String youtubeLink) {
72 | new YouTubeExtractor(this) {
73 |
74 | @Override
75 | public void onExtractionComplete(SparseArray ytFiles, VideoMeta vMeta) {
76 | mainProgressBar.setVisibility(View.GONE);
77 | if (ytFiles == null) {
78 | TextView tv = new TextView(DownloadActivity.this);
79 | tv.setText(R.string.app_update);
80 | tv.setMovementMethod(LinkMovementMethod.getInstance());
81 | mainLayout.addView(tv);
82 | return;
83 | }
84 | formatsToShowList = new ArrayList<>();
85 | for (int i = 0, itag; i < ytFiles.size(); i++) {
86 | itag = ytFiles.keyAt(i);
87 | YtFile ytFile = ytFiles.get(itag);
88 |
89 | if (ytFile.getFormat().getHeight() == -1 || ytFile.getFormat().getHeight() >= 360) {
90 | addFormatToList(ytFile, ytFiles);
91 | }
92 | }
93 | Collections.sort(formatsToShowList, new Comparator() {
94 | @Override
95 | public int compare(YtFragmentedVideo lhs, YtFragmentedVideo rhs) {
96 | return lhs.height - rhs.height;
97 | }
98 | });
99 | for (YtFragmentedVideo files : formatsToShowList) {
100 | addButtonToMainLayout(vMeta.getTitle(), files);
101 | }
102 | }
103 | }.extract(youtubeLink, true, false);
104 | }
105 |
106 | private void addFormatToList(YtFile ytFile, SparseArray ytFiles) {
107 | int height = ytFile.getFormat().getHeight();
108 | if (height != -1) {
109 | for (YtFragmentedVideo frVideo : formatsToShowList) {
110 | if (frVideo.height == height && (frVideo.videoFile == null ||
111 | frVideo.videoFile.getFormat().getFps() == ytFile.getFormat().getFps())) {
112 | return;
113 | }
114 | }
115 | }
116 | YtFragmentedVideo frVideo = new YtFragmentedVideo();
117 | frVideo.height = height;
118 | if (ytFile.getFormat().isDashContainer()) {
119 | if (height > 0) {
120 | frVideo.videoFile = ytFile;
121 | frVideo.audioFile = ytFiles.get(ITAG_FOR_AUDIO);
122 | } else {
123 | frVideo.audioFile = ytFile;
124 | }
125 | } else {
126 | frVideo.videoFile = ytFile;
127 | }
128 | formatsToShowList.add(frVideo);
129 | }
130 |
131 |
132 | private void addButtonToMainLayout(final String videoTitle, final YtFragmentedVideo ytFrVideo) {
133 | // Display some buttons and let the user choose the format
134 | String btnText;
135 | if (ytFrVideo.height == -1)
136 | btnText = "Audio " + ytFrVideo.audioFile.getFormat().getAudioBitrate() + " kbit/s";
137 | else
138 | btnText = (ytFrVideo.videoFile.getFormat().getFps() == 60) ? ytFrVideo.height + "p60" :
139 | ytFrVideo.height + "p";
140 | Button btn = new Button(this);
141 | btn.setText(btnText);
142 | btn.setOnClickListener(new OnClickListener() {
143 |
144 | @Override
145 | public void onClick(View v) {
146 | String filename;
147 | if (videoTitle.length() > 55) {
148 | filename = videoTitle.substring(0, 55);
149 | } else {
150 | filename = videoTitle;
151 | }
152 | filename = filename.replaceAll("[\\\\><\"|*?%:#/]", "");
153 | filename += (ytFrVideo.height == -1) ? "" : "-" + ytFrVideo.height + "p";
154 | String downloadIds = "";
155 | boolean hideAudioDownloadNotification = false;
156 | if (ytFrVideo.videoFile != null) {
157 | downloadIds += downloadFromUrl(ytFrVideo.videoFile.getUrl(), videoTitle,
158 | filename + "." + ytFrVideo.videoFile.getFormat().getExt(), false);
159 | downloadIds += "-";
160 | hideAudioDownloadNotification = true;
161 | }
162 | if (ytFrVideo.audioFile != null) {
163 | downloadIds += downloadFromUrl(ytFrVideo.audioFile.getUrl(), videoTitle,
164 | filename + "." + ytFrVideo.audioFile.getFormat().getExt(), hideAudioDownloadNotification);
165 | }
166 | if (ytFrVideo.audioFile != null)
167 | cacheDownloadIds(downloadIds);
168 | finish();
169 | }
170 | });
171 | mainLayout.addView(btn);
172 | }
173 |
174 | private long downloadFromUrl(String youtubeDlUrl, String downloadTitle, String fileName, boolean hide) {
175 | Uri uri = Uri.parse(youtubeDlUrl);
176 | DownloadManager.Request request = new DownloadManager.Request(uri);
177 | request.setTitle(downloadTitle);
178 | if (hide) {
179 | request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
180 | request.setVisibleInDownloadsUi(false);
181 | } else
182 | request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
183 |
184 | request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
185 |
186 | DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
187 | return manager.enqueue(request);
188 | }
189 |
190 | private void cacheDownloadIds(String downloadIds) {
191 | File dlCacheFile = new File(this.getCacheDir().getAbsolutePath() + "/" + downloadIds);
192 | try {
193 | dlCacheFile.createNewFile();
194 | } catch (IOException e) {
195 | e.printStackTrace();
196 | }
197 | }
198 |
199 | private class YtFragmentedVideo {
200 | int height;
201 | YtFile audioFile;
202 | YtFile videoFile;
203 | }
204 |
205 | }
206 |
--------------------------------------------------------------------------------
/advancedDownloader/src/main/java/at/huber/youtubeDownloader/DownloadFinishedReceiver.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeDownloader;
2 |
3 | import android.app.DownloadManager;
4 | import android.content.BroadcastReceiver;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.database.Cursor;
8 | import android.net.Uri;
9 | import android.os.Bundle;
10 |
11 | import com.coremedia.iso.boxes.Box;
12 | import com.coremedia.iso.boxes.ChunkOffsetBox;
13 | import com.coremedia.iso.boxes.Container;
14 | import com.coremedia.iso.boxes.HandlerBox;
15 | import com.coremedia.iso.boxes.MetaBox;
16 | import com.coremedia.iso.boxes.MovieBox;
17 | import com.coremedia.iso.boxes.StaticChunkOffsetBox;
18 | import com.coremedia.iso.boxes.UserDataBox;
19 | import com.coremedia.iso.boxes.apple.AppleItemListBox;
20 | import com.googlecode.mp4parser.authoring.Movie;
21 | import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
22 | import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
23 | import com.googlecode.mp4parser.boxes.apple.AppleArtist2Box;
24 | import com.googlecode.mp4parser.boxes.apple.AppleArtistBox;
25 | import com.googlecode.mp4parser.boxes.apple.AppleNameBox;
26 | import com.googlecode.mp4parser.util.Path;
27 |
28 | import java.io.BufferedReader;
29 | import java.io.BufferedWriter;
30 | import java.io.File;
31 | import java.io.FileInputStream;
32 | import java.io.FileOutputStream;
33 | import java.io.IOException;
34 | import java.io.InputStreamReader;
35 | import java.io.OutputStreamWriter;
36 | import java.util.LinkedList;
37 | import java.util.List;
38 | import java.util.regex.Matcher;
39 | import java.util.regex.Pattern;
40 |
41 | public class DownloadFinishedReceiver extends BroadcastReceiver {
42 |
43 |
44 | private static final String TEMP_FILE_NAME = "tmp-";
45 | private static final Pattern ARTIST_TITLE_PATTERN =
46 | Pattern.compile("(.+?)(\\s*?)-(\\s*?)(\"|)(\\S(.+?))\\s*?([&\\*+,-/:;<=>@_\\|]+?\\s*?|)(\\z|\"|\\(|\\[|lyric|official)",
47 | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
48 |
49 | @Override
50 | public void onReceive(final Context context, Intent intent) {
51 | String action = intent.getAction();
52 | if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
53 | Bundle extras = intent.getExtras();
54 | DownloadManager.Query q = new DownloadManager.Query();
55 | long downloadId = extras.getLong(DownloadManager.EXTRA_DOWNLOAD_ID);
56 | q.setFilterById(downloadId);
57 | Cursor c = ((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)).query(q);
58 | if (c.moveToFirst()) {
59 | int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
60 | if (status == DownloadManager.STATUS_SUCCESSFUL) {
61 | String inPath = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
62 | String dlTitle = c.getString(c.getColumnIndex(DownloadManager.COLUMN_TITLE));
63 | c.close();
64 | DownloadStatus dlStatus = getMultiFileDlStatus(context, downloadId, inPath);
65 | if (dlStatus != null && dlStatus.readyToMerge) {
66 | if (!dlStatus.hasVideo) {
67 | String artist = null;
68 | String title = null;
69 | Matcher mat = ARTIST_TITLE_PATTERN.matcher(dlTitle);
70 | if (mat.find()) {
71 | artist = mat.group(1);
72 | title = mat.group(5);
73 | }
74 | convertM4a(inPath, title, artist);
75 | scanFile(inPath, context);
76 | } else {
77 | if (inPath.endsWith(".mp4")) {
78 | mergeMp4(dlStatus.otherFilePath, inPath);
79 | } else if (inPath.endsWith(".m4a")) {
80 | mergeMp4(inPath, dlStatus.otherFilePath);
81 | }
82 | }
83 | }
84 | } else if (status == DownloadManager.STATUS_FAILED) {
85 | removeTempOnFailure(context, downloadId);
86 | }
87 | }
88 |
89 | }
90 | }
91 |
92 | private void removeTempOnFailure(Context con, long downloadId) {
93 | File cacheFileDir = new File(con.getCacheDir().getAbsolutePath());
94 | for (File f : cacheFileDir.listFiles()) {
95 | if (f.getName().contains(downloadId + "")) {
96 | f.delete();
97 | break;
98 | }
99 | }
100 | }
101 |
102 | private DownloadStatus getMultiFileDlStatus(Context con, long downloadId, String filePath) {
103 | File cacheFileDir = new File(con.getCacheDir().getAbsolutePath());
104 | File cacheFile = null;
105 | for (File f : cacheFileDir.listFiles()) {
106 | if (f.getName().contains(downloadId + "")) {
107 | cacheFile = f;
108 | break;
109 | }
110 | }
111 | if (cacheFile != null && cacheFile.exists()) {
112 | DownloadStatus dlStatus = new DownloadStatus();
113 | dlStatus.hasVideo = cacheFile.getName().contains("-");
114 | BufferedReader reader = null;
115 | BufferedWriter writer = null;
116 | try {
117 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(cacheFile), "UTF-8"));
118 | dlStatus.otherFilePath = reader.readLine();
119 | reader.close();
120 | if (dlStatus.otherFilePath != null || !dlStatus.hasVideo) {
121 | cacheFile.delete();
122 | dlStatus.readyToMerge = true;
123 | } else {
124 | writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cacheFile)));
125 | writer.write(filePath);
126 | }
127 | return dlStatus;
128 | } catch (Exception e) {
129 | e.printStackTrace();
130 | } finally {
131 | if (reader != null) {
132 | try {
133 | reader.close();
134 | } catch (IOException e) {
135 | e.printStackTrace();
136 | }
137 | }
138 | if (writer != null) {
139 | try {
140 | writer.close();
141 | } catch (IOException e) {
142 | e.printStackTrace();
143 | }
144 | }
145 | }
146 | }
147 | return null;
148 | }
149 |
150 | private void convertM4a(String inFilePath, String title, String artist) {
151 | String path = inFilePath.substring(0, inFilePath.lastIndexOf("/"));
152 | try {
153 | Movie inAudio = MovieCreator.build(inFilePath);
154 | Container out = new DefaultMp4Builder().build(inAudio);
155 |
156 | if (title != null && artist != null) {
157 | writeMetaData(out, artist, title);
158 | }
159 | long currentMillis = System.currentTimeMillis();
160 | FileOutputStream fos = new FileOutputStream(new File(path + TEMP_FILE_NAME + currentMillis + ".m4a"));
161 | out.writeContainer(fos.getChannel());
162 | fos.close();
163 | File inFile = new File(inFilePath);
164 | if (inFile.delete()) {
165 | File tempOutFile = new File(path + TEMP_FILE_NAME + currentMillis + ".m4a");
166 | tempOutFile.renameTo(inFile);
167 | }
168 | } catch (IOException e) {
169 | e.printStackTrace();
170 | }
171 | }
172 |
173 | private void mergeMp4(String inFilePathAudio, String inFilePathVideo) {
174 | String path = inFilePathVideo.substring(0, inFilePathVideo.lastIndexOf("/"));
175 | try {
176 | Movie video = MovieCreator.build(inFilePathVideo);
177 | Movie audio = MovieCreator.build(inFilePathAudio);
178 | video.addTrack(audio.getTracks().get(0));
179 | Container out = new DefaultMp4Builder().build(video);
180 | long currentMillis = System.currentTimeMillis();
181 | FileOutputStream fos = new FileOutputStream(new File(path + TEMP_FILE_NAME + currentMillis + ".mp4"));
182 | out.writeContainer(fos.getChannel());
183 | fos.close();
184 | File inAudioFile = new File(inFilePathAudio);
185 | inAudioFile.delete();
186 | File inVideoFile = new File(inFilePathVideo);
187 | if (inVideoFile.delete()) {
188 | File tempOutFile = new File(path + TEMP_FILE_NAME + currentMillis + ".mp4");
189 | tempOutFile.renameTo(inVideoFile);
190 | }
191 | } catch (IOException e) {
192 | e.printStackTrace();
193 | }
194 | }
195 |
196 | private void writeMetaData(Container out, String artist, String title) {
197 | MovieBox mBox = null;
198 | for (Box box : out.getBoxes()) {
199 | if (box.getType().contains("moov")) {
200 | mBox = (MovieBox) box;
201 | break;
202 | }
203 | }
204 | if (mBox != null) {
205 | UserDataBox userDataBox = new UserDataBox();
206 | mBox.addBox(userDataBox);
207 | MetaBox metaBox = new MetaBox();
208 | userDataBox.addBox(metaBox);
209 | HandlerBox hBox = new HandlerBox();
210 | hBox.setName(null);
211 | hBox.setHandlerType("mdir");
212 | metaBox.addBox(hBox);
213 | AppleItemListBox listBox = new AppleItemListBox();
214 | metaBox.addBox(listBox);
215 | AppleNameBox titleBox = new AppleNameBox();
216 | titleBox.setValue(title);
217 | listBox.addBox(titleBox);
218 | AppleArtistBox artistBox = new AppleArtistBox();
219 | artistBox.setValue(artist);
220 | listBox.addBox(artistBox);
221 | AppleArtist2Box artist2Box = new AppleArtist2Box();
222 | artist2Box.setValue(artist);
223 | listBox.addBox(artist2Box);
224 | correctChunkOffsets(out, userDataBox.getSize());
225 | }
226 | }
227 |
228 | // From the mp4parser metadata example
229 | private void correctChunkOffsets(Container container, long correction) {
230 | List chunkOffsetBoxes = Path.getPaths(container, "/moov[0]/trak/mdia[0]/minf[0]/stbl[0]/stco[0]");
231 | for (Box chunkOffsetBox : chunkOffsetBoxes) {
232 |
233 | LinkedList stblChildren = new LinkedList<>(chunkOffsetBox.getParent().getBoxes());
234 | stblChildren.remove(chunkOffsetBox);
235 |
236 | long[] cOffsets = ((ChunkOffsetBox) chunkOffsetBox).getChunkOffsets();
237 | for (int i = 0; i < cOffsets.length; i++) {
238 | cOffsets[i] += correction;
239 | }
240 |
241 | StaticChunkOffsetBox cob = new StaticChunkOffsetBox();
242 | cob.setChunkOffsets(cOffsets);
243 | stblChildren.add(cob);
244 | chunkOffsetBox.getParent().setBoxes(stblChildren);
245 | }
246 | }
247 |
248 | private void scanFile(String path, Context con) {
249 | File file = new File(path);
250 | Intent scanFileIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file));
251 | con.sendBroadcast(scanFileIntent);
252 | }
253 |
254 | private class DownloadStatus {
255 | String otherFilePath;
256 | boolean readyToMerge = false;
257 | boolean hasVideo;
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/youtubeExtractor/src/main/java/at/huber/youtubeExtractor/YouTubeExtractor.java:
--------------------------------------------------------------------------------
1 | package at.huber.youtubeExtractor;
2 |
3 | import android.content.Context;
4 | import android.os.AsyncTask;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.support.annotation.NonNull;
8 | import android.util.Log;
9 | import android.util.SparseArray;
10 |
11 | import com.evgenii.jsevaluator.JsEvaluator;
12 | import com.evgenii.jsevaluator.interfaces.JsCallback;
13 |
14 | import java.io.BufferedReader;
15 | import java.io.BufferedWriter;
16 | import java.io.File;
17 | import java.io.FileInputStream;
18 | import java.io.FileOutputStream;
19 | import java.io.IOException;
20 | import java.io.InputStreamReader;
21 | import java.io.OutputStreamWriter;
22 | import java.io.UnsupportedEncodingException;
23 | import java.lang.ref.WeakReference;
24 | import java.net.HttpURLConnection;
25 | import java.net.URL;
26 | import java.net.URLDecoder;
27 | import java.net.URLEncoder;
28 | import java.util.concurrent.TimeUnit;
29 | import java.util.concurrent.locks.Condition;
30 | import java.util.concurrent.locks.Lock;
31 | import java.util.concurrent.locks.ReentrantLock;
32 | import java.util.regex.Matcher;
33 | import java.util.regex.Pattern;
34 |
35 | public abstract class YouTubeExtractor extends AsyncTask> {
36 |
37 | private final static boolean CACHING = true;
38 |
39 | static boolean LOGGING = false;
40 |
41 | private final static String LOG_TAG = "YouTubeExtractor";
42 | private final static String CACHE_FILE_NAME = "decipher_js_funct";
43 | private final static int DASH_PARSE_RETRIES = 5;
44 |
45 | private WeakReference refContext;
46 | private String videoID;
47 | private VideoMeta videoMeta;
48 | private boolean includeWebM = true;
49 | private boolean useHttp = false;
50 | private boolean parseDashManifest = false;
51 | private String cacheDirPath;
52 |
53 | private volatile String decipheredSignature;
54 |
55 | private static String decipherJsFileName;
56 | private static String decipherFunctions;
57 | private static String decipherFunctionName;
58 |
59 | private final Lock lock = new ReentrantLock();
60 | private final Condition jsExecuting = lock.newCondition();
61 |
62 | private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36";
63 | private static final String STREAM_MAP_STRING = "url_encoded_fmt_stream_map";
64 |
65 | private static final Pattern patYouTubePageLink = Pattern.compile("(http|https)://(www\\.|m.|)youtube\\.com/watch\\?v=(.+?)( |\\z|&)");
66 | private static final Pattern patYouTubeShortLink = Pattern.compile("(http|https)://(www\\.|)youtu.be/(.+?)( |\\z|&)");
67 |
68 | private static final Pattern patDashManifest1 = Pattern.compile("dashmpd=(.+?)(&|\\z)");
69 | private static final Pattern patDashManifest2 = Pattern.compile("\"dashmpd\":\"(.+?)\"");
70 | private static final Pattern patDashManifestEncSig = Pattern.compile("/s/([0-9A-F|.]{10,}?)(/|\\z)");
71 |
72 | private static final Pattern patTitle = Pattern.compile("title%22%3A%22(.*?)(%22|\\z)");
73 | private static final Pattern patAuthor = Pattern.compile("author%22%3A%22(.+?)(%22|\\z)");
74 | private static final Pattern patChannelId = Pattern.compile("channelId%22%3A%22(.+?)(%22|\\z)");
75 | private static final Pattern patLength = Pattern.compile("lengthSeconds%22%3A%22(\\d+?)(%22|\\z)");
76 | private static final Pattern patViewCount = Pattern.compile("viewCount%22%3A%22(\\d+?)(%22|\\z)");
77 | private static final Pattern patStatusOk = Pattern.compile("status=ok(&|,|\\z)");
78 |
79 | private static final Pattern patHlsvp = Pattern.compile("hlsvp=(.+?)(&|\\z)");
80 | private static final Pattern patHlsItag = Pattern.compile("/itag/(\\d+?)/");
81 |
82 | private static final Pattern patItag = Pattern.compile("itag=([0-9]+?)([&,])");
83 | private static final Pattern patEncSig = Pattern.compile("s=([0-9A-F|.]{10,}?)([&,\"])");
84 | private static final Pattern patIsSigEnc = Pattern.compile("s%3D([0-9A-F|.]{10,}?)(%26|%2C)");
85 | private static final Pattern patEncSig2 = Pattern.compile("(\\A|&|\")s=([0-9A-Za-z\\-_=%]{10,}?)([&,\"])");
86 | private static final Pattern patIsSigEnc2 = Pattern.compile("(%26|%3F|%2C)s%3D([0-9A-Za-z\\-_=%]{10,}?)(%26|%2C|\\z)");
87 | private static final Pattern patUrl = Pattern.compile("url=(.+?)([&,\"\\\\])");
88 |
89 | private static final Pattern patVariableFunction = Pattern.compile("([{; =])([a-zA-Z$][a-zA-Z0-9$]{0,2})\\.([a-zA-Z$][a-zA-Z0-9$]{0,2})\\(");
90 | private static final Pattern patFunction = Pattern.compile("([{; =])([a-zA-Z$_][a-zA-Z0-9$]{0,2})\\(");
91 |
92 | private static final Pattern patDecryptionJsFile = Pattern.compile("jsbin\\\\/(player(_ias)?-(.+?).js)");
93 | private static final Pattern patSignatureDecFunction = Pattern.compile("([\\w$]+)\\s*=\\s*function\\(([\\w$]+)\\).\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;");
94 |
95 | private static final SparseArray FORMAT_MAP = new SparseArray<>();
96 |
97 | static {
98 | // http://en.wikipedia.org/wiki/YouTube#Quality_and_formats
99 |
100 | // Video and Audio
101 | FORMAT_MAP.put(17, new Format(17, "3gp", 144, Format.VCodec.MPEG4, Format.ACodec.AAC, 24, false));
102 | FORMAT_MAP.put(36, new Format(36, "3gp", 240, Format.VCodec.MPEG4, Format.ACodec.AAC, 32, false));
103 | FORMAT_MAP.put(5, new Format(5, "flv", 240, Format.VCodec.H263, Format.ACodec.MP3, 64, false));
104 | FORMAT_MAP.put(43, new Format(43, "webm", 360, Format.VCodec.VP8, Format.ACodec.VORBIS, 128, false));
105 | FORMAT_MAP.put(18, new Format(18, "mp4", 360, Format.VCodec.H264, Format.ACodec.AAC, 96, false));
106 | FORMAT_MAP.put(22, new Format(22, "mp4", 720, Format.VCodec.H264, Format.ACodec.AAC, 192, false));
107 |
108 | // Dash Video
109 | FORMAT_MAP.put(160, new Format(160, "mp4", 144, Format.VCodec.H264, Format.ACodec.NONE, true));
110 | FORMAT_MAP.put(133, new Format(133, "mp4", 240, Format.VCodec.H264, Format.ACodec.NONE, true));
111 | FORMAT_MAP.put(134, new Format(134, "mp4", 360, Format.VCodec.H264, Format.ACodec.NONE, true));
112 | FORMAT_MAP.put(135, new Format(135, "mp4", 480, Format.VCodec.H264, Format.ACodec.NONE, true));
113 | FORMAT_MAP.put(136, new Format(136, "mp4", 720, Format.VCodec.H264, Format.ACodec.NONE, true));
114 | FORMAT_MAP.put(137, new Format(137, "mp4", 1080, Format.VCodec.H264, Format.ACodec.NONE, true));
115 | FORMAT_MAP.put(264, new Format(264, "mp4", 1440, Format.VCodec.H264, Format.ACodec.NONE, true));
116 | FORMAT_MAP.put(266, new Format(266, "mp4", 2160, Format.VCodec.H264, Format.ACodec.NONE, true));
117 |
118 | FORMAT_MAP.put(298, new Format(298, "mp4", 720, Format.VCodec.H264, 60, Format.ACodec.NONE, true));
119 | FORMAT_MAP.put(299, new Format(299, "mp4", 1080, Format.VCodec.H264, 60, Format.ACodec.NONE, true));
120 |
121 | // Dash Audio
122 | FORMAT_MAP.put(140, new Format(140, "m4a", Format.VCodec.NONE, Format.ACodec.AAC, 128, true));
123 | FORMAT_MAP.put(141, new Format(141, "m4a", Format.VCodec.NONE, Format.ACodec.AAC, 256, true));
124 |
125 | // WEBM Dash Video
126 | FORMAT_MAP.put(278, new Format(278, "webm", 144, Format.VCodec.VP9, Format.ACodec.NONE, true));
127 | FORMAT_MAP.put(242, new Format(242, "webm", 240, Format.VCodec.VP9, Format.ACodec.NONE, true));
128 | FORMAT_MAP.put(243, new Format(243, "webm", 360, Format.VCodec.VP9, Format.ACodec.NONE, true));
129 | FORMAT_MAP.put(244, new Format(244, "webm", 480, Format.VCodec.VP9, Format.ACodec.NONE, true));
130 | FORMAT_MAP.put(247, new Format(247, "webm", 720, Format.VCodec.VP9, Format.ACodec.NONE, true));
131 | FORMAT_MAP.put(248, new Format(248, "webm", 1080, Format.VCodec.VP9, Format.ACodec.NONE, true));
132 | FORMAT_MAP.put(271, new Format(271, "webm", 1440, Format.VCodec.VP9, Format.ACodec.NONE, true));
133 | FORMAT_MAP.put(313, new Format(313, "webm", 2160, Format.VCodec.VP9, Format.ACodec.NONE, true));
134 |
135 | FORMAT_MAP.put(302, new Format(302, "webm", 720, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
136 | FORMAT_MAP.put(308, new Format(308, "webm", 1440, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
137 | FORMAT_MAP.put(303, new Format(303, "webm", 1080, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
138 | FORMAT_MAP.put(315, new Format(315, "webm", 2160, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
139 |
140 | // WEBM Dash Audio
141 | FORMAT_MAP.put(171, new Format(171, "webm", Format.VCodec.NONE, Format.ACodec.VORBIS, 128, true));
142 |
143 | FORMAT_MAP.put(249, new Format(249, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 48, true));
144 | FORMAT_MAP.put(250, new Format(250, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 64, true));
145 | FORMAT_MAP.put(251, new Format(251, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 160, true));
146 |
147 | // HLS Live Stream
148 | FORMAT_MAP.put(91, new Format(91, "mp4", 144 ,Format.VCodec.H264, Format.ACodec.AAC, 48, false, true));
149 | FORMAT_MAP.put(92, new Format(92, "mp4", 240 ,Format.VCodec.H264, Format.ACodec.AAC, 48, false, true));
150 | FORMAT_MAP.put(93, new Format(93, "mp4", 360 ,Format.VCodec.H264, Format.ACodec.AAC, 128, false, true));
151 | FORMAT_MAP.put(94, new Format(94, "mp4", 480 ,Format.VCodec.H264, Format.ACodec.AAC, 128, false, true));
152 | FORMAT_MAP.put(95, new Format(95, "mp4", 720 ,Format.VCodec.H264, Format.ACodec.AAC, 256, false, true));
153 | FORMAT_MAP.put(96, new Format(96, "mp4", 1080 ,Format.VCodec.H264, Format.ACodec.AAC, 256, false, true));
154 | }
155 |
156 | public YouTubeExtractor(@NonNull Context con) {
157 | refContext = new WeakReference<>(con);
158 | cacheDirPath = con.getCacheDir().getAbsolutePath();
159 | }
160 |
161 | @Override
162 | protected void onPostExecute(SparseArray ytFiles) {
163 | onExtractionComplete(ytFiles, videoMeta);
164 | }
165 |
166 |
167 | /**
168 | * Start the extraction.
169 | *
170 | * @param youtubeLink the youtube page link or video id
171 | * @param parseDashManifest true if the dash manifest should be downloaded and parsed
172 | * @param includeWebM true if WebM streams should be extracted
173 | */
174 | public void extract(String youtubeLink, boolean parseDashManifest, boolean includeWebM) {
175 | this.parseDashManifest = parseDashManifest;
176 | this.includeWebM = includeWebM;
177 | this.execute(youtubeLink);
178 | }
179 |
180 | protected abstract void onExtractionComplete(SparseArray ytFiles, VideoMeta videoMeta);
181 |
182 | @Override
183 | protected SparseArray doInBackground(String... params) {
184 | videoID = null;
185 | String ytUrl = params[0];
186 | if (ytUrl == null) {
187 | return null;
188 | }
189 | Matcher mat = patYouTubePageLink.matcher(ytUrl);
190 | if (mat.find()) {
191 | videoID = mat.group(3);
192 | } else {
193 | mat = patYouTubeShortLink.matcher(ytUrl);
194 | if (mat.find()) {
195 | videoID = mat.group(3);
196 | } else if (ytUrl.matches("\\p{Graph}+?")) {
197 | videoID = ytUrl;
198 | }
199 | }
200 | if (videoID != null) {
201 | try {
202 | return getStreamUrls();
203 | } catch (Exception e) {
204 | e.printStackTrace();
205 | }
206 | } else {
207 | Log.e(LOG_TAG, "Wrong YouTube link format");
208 | }
209 | return null;
210 | }
211 |
212 | private SparseArray getStreamUrls() throws IOException, InterruptedException {
213 |
214 | String ytInfoUrl = (useHttp) ? "http://" : "https://";
215 | ytInfoUrl += "www.youtube.com/get_video_info?video_id=" + videoID + "&eurl="
216 | + URLEncoder.encode("https://youtube.googleapis.com/v/" + videoID, "UTF-8");
217 |
218 | String dashMpdUrl = null;
219 | String streamMap;
220 | BufferedReader reader = null;
221 | URL getUrl = new URL(ytInfoUrl);
222 | if(LOGGING)
223 | Log.d(LOG_TAG, "infoUrl: " + ytInfoUrl);
224 | HttpURLConnection urlConnection = (HttpURLConnection) getUrl.openConnection();
225 | urlConnection.setRequestProperty("User-Agent", USER_AGENT);
226 | try {
227 | reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
228 | streamMap = reader.readLine();
229 |
230 | } finally {
231 | if (reader != null)
232 | reader.close();
233 | urlConnection.disconnect();
234 | }
235 | Matcher mat;
236 | String curJsFileName = null;
237 | String[] streams;
238 | SparseArray encSignatures = null;
239 |
240 | parseVideoMeta(streamMap);
241 |
242 | if(videoMeta.isLiveStream()){
243 | mat = patHlsvp.matcher(streamMap);
244 | if(mat.find()) {
245 | String hlsvp = URLDecoder.decode(mat.group(1), "UTF-8");
246 | SparseArray ytFiles = new SparseArray<>();
247 |
248 | getUrl = new URL(hlsvp);
249 | urlConnection = (HttpURLConnection) getUrl.openConnection();
250 | urlConnection.setRequestProperty("User-Agent", USER_AGENT);
251 | try {
252 | reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
253 | String line;
254 | while ((line = reader.readLine()) != null) {
255 | if(line.startsWith("https://") || line.startsWith("http://")){
256 | mat = patHlsItag.matcher(line);
257 | if(mat.find()){
258 | int itag = Integer.parseInt(mat.group(1));
259 | YtFile newFile = new YtFile(FORMAT_MAP.get(itag), line);
260 | ytFiles.put(itag, newFile);
261 | }
262 | }
263 | }
264 | } finally {
265 | if (reader != null)
266 | reader.close();
267 | urlConnection.disconnect();
268 | }
269 |
270 | if (ytFiles.size() == 0) {
271 | if (LOGGING)
272 | Log.d(LOG_TAG, streamMap);
273 | return null;
274 | }
275 | return ytFiles;
276 | }
277 | return null;
278 | }
279 |
280 | // "use_cipher_signature" disappeared, we check whether at least one ciphered signature
281 | // exists int the stream_map.
282 | boolean sigEnc = true, statusFail = false;
283 | if(streamMap != null && streamMap.contains(STREAM_MAP_STRING)){
284 |
285 | if(!patIsSigEnc2.matcher(streamMap).find() && !patIsSigEnc.matcher(streamMap).find()) {
286 | sigEnc = false;
287 |
288 | if (!patStatusOk.matcher(streamMap).find())
289 | statusFail = true;
290 | }
291 | }
292 |
293 | // Some videos are using a ciphered signature we need to get the
294 | // deciphering js-file from the youtubepage.
295 | if (sigEnc || statusFail) {
296 | // Get the video directly from the youtubepage
297 | if (CACHING
298 | && (decipherJsFileName == null || decipherFunctions == null || decipherFunctionName == null)) {
299 | readDecipherFunctFromCache();
300 | }
301 | if (LOGGING)
302 | Log.d(LOG_TAG, "Get from youtube page");
303 |
304 | getUrl = new URL("https://youtube.com/watch?v=" + videoID);
305 | urlConnection = (HttpURLConnection) getUrl.openConnection();
306 | urlConnection.setRequestProperty("User-Agent", USER_AGENT);
307 | try {
308 | reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
309 | String line;
310 | while ((line = reader.readLine()) != null) {
311 | // Log.d("line", line);
312 | if (line.contains(STREAM_MAP_STRING)) {
313 | streamMap = line.replace("\\u0026", "&");
314 | break;
315 | }
316 | }
317 | } finally {
318 | if (reader != null)
319 | reader.close();
320 | urlConnection.disconnect();
321 | }
322 | encSignatures = new SparseArray<>();
323 |
324 | mat = patDecryptionJsFile.matcher(streamMap);
325 | if (mat.find()) {
326 | curJsFileName = mat.group(1).replace("\\/", "/");
327 | if (mat.group(2) != null)
328 | curJsFileName.replace(mat.group(2), "");
329 | if (decipherJsFileName == null || !decipherJsFileName.equals(curJsFileName)) {
330 | decipherFunctions = null;
331 | decipherFunctionName = null;
332 | }
333 | decipherJsFileName = curJsFileName;
334 | }
335 |
336 | if (parseDashManifest) {
337 | mat = patDashManifest2.matcher(streamMap);
338 | if (mat.find()) {
339 | dashMpdUrl = mat.group(1).replace("\\/", "/");
340 | mat = patDashManifestEncSig.matcher(dashMpdUrl);
341 | if (mat.find()) {
342 | encSignatures.append(0, mat.group(1));
343 | } else {
344 | dashMpdUrl = null;
345 | }
346 | }
347 | }
348 | } else {
349 | if (parseDashManifest) {
350 | mat = patDashManifest1.matcher(streamMap);
351 | if (mat.find()) {
352 | dashMpdUrl = URLDecoder.decode(mat.group(1), "UTF-8");
353 | }
354 | }
355 | streamMap = URLDecoder.decode(streamMap, "UTF-8");
356 | }
357 |
358 | boolean oldSignature = false;
359 | streams = streamMap.split(",|"+STREAM_MAP_STRING+"|&adaptive_fmts=");
360 | SparseArray ytFiles = new SparseArray<>();
361 | for (String encStream : streams) {
362 | encStream = encStream + ",";
363 | if (!encStream.contains("itag%3D")) {
364 | continue;
365 | }
366 | String stream;
367 | stream = URLDecoder.decode(encStream, "UTF-8");
368 |
369 | mat = patItag.matcher(stream);
370 | int itag;
371 | if (mat.find()) {
372 | itag = Integer.parseInt(mat.group(1));
373 | if (LOGGING)
374 | Log.d(LOG_TAG, "Itag found:" + itag);
375 | if (FORMAT_MAP.get(itag) == null) {
376 | if (LOGGING)
377 | Log.d(LOG_TAG, "Itag not in list:" + itag);
378 | continue;
379 | } else if (!includeWebM && FORMAT_MAP.get(itag).getExt().equals("webm")) {
380 | continue;
381 | }
382 | } else {
383 | continue;
384 | }
385 |
386 | if (curJsFileName != null) {
387 | mat = patEncSig2.matcher(stream);
388 | if (mat.find()) {
389 | encSignatures.append(itag, URLDecoder.decode(mat.group(2), "UTF-8"));
390 | } else {
391 | mat = patEncSig.matcher(stream);
392 | if (mat.find())
393 | {
394 | encSignatures.append(itag, mat.group(1));
395 | oldSignature = true;
396 | }
397 | }
398 | }
399 | mat = patUrl.matcher(encStream);
400 | String url = null;
401 | if (mat.find()) {
402 | url = mat.group(1);
403 | }
404 |
405 | if (url != null) {
406 | Format format = FORMAT_MAP.get(itag);
407 | String finalUrl = URLDecoder.decode(url, "UTF-8");
408 | YtFile newVideo = new YtFile(format, finalUrl);
409 | ytFiles.put(itag, newVideo);
410 | }
411 | }
412 |
413 | if (encSignatures != null) {
414 |
415 | if (LOGGING)
416 | Log.d(LOG_TAG, "Decipher signatures: " + encSignatures.size()+ ", videos: " + ytFiles.size());
417 | String signature;
418 | decipheredSignature = null;
419 | if (decipherSignature(encSignatures)) {
420 | lock.lock();
421 | try {
422 | jsExecuting.await(7, TimeUnit.SECONDS);
423 | } finally {
424 | lock.unlock();
425 | }
426 | }
427 | signature = decipheredSignature;
428 | if (signature == null) {
429 | return null;
430 | } else {
431 | String[] sigs = signature.split("\n");
432 | for (int i = 0; i < encSignatures.size() && i < sigs.length; i++) {
433 | int key = encSignatures.keyAt(i);
434 | if (key == 0) {
435 | dashMpdUrl = dashMpdUrl.replace("/s/" + encSignatures.get(key), "/signature/" + sigs[i]);
436 | } else {
437 | String url = ytFiles.get(key).getUrl();
438 | url += (oldSignature ? "&signature=" : "&sig=") + sigs[i];
439 | YtFile newFile = new YtFile(FORMAT_MAP.get(key), url);
440 | ytFiles.put(key, newFile);
441 | }
442 | }
443 | }
444 | }
445 |
446 | if (parseDashManifest && dashMpdUrl != null) {
447 | for (int i = 0; i < DASH_PARSE_RETRIES; i++) {
448 | try {
449 | // It sometimes fails to connect for no apparent reason. We just retry.
450 | parseDashManifest(dashMpdUrl, ytFiles);
451 | break;
452 | } catch (IOException io) {
453 | Thread.sleep(5);
454 | if (LOGGING)
455 | Log.d(LOG_TAG, "Failed to parse dash manifest " + (i + 1));
456 | }
457 | }
458 | }
459 |
460 | if (ytFiles.size() == 0) {
461 | if (LOGGING)
462 | Log.d(LOG_TAG, streamMap);
463 | return null;
464 | }
465 | return ytFiles;
466 | }
467 |
468 | private boolean decipherSignature(final SparseArray encSignatures) throws IOException {
469 | // Assume the functions don't change that much
470 | if (decipherFunctionName == null || decipherFunctions == null) {
471 | String decipherFunctUrl = "https://s.ytimg.com/yts/jsbin/" + decipherJsFileName;
472 |
473 | BufferedReader reader = null;
474 | String javascriptFile;
475 | URL url = new URL(decipherFunctUrl);
476 | HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
477 | urlConnection.setRequestProperty("User-Agent", USER_AGENT);
478 | try {
479 | reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
480 | StringBuilder sb = new StringBuilder("");
481 | String line;
482 | while ((line = reader.readLine()) != null) {
483 | sb.append(line);
484 | sb.append(" ");
485 | }
486 | javascriptFile = sb.toString();
487 | } finally {
488 | if (reader != null)
489 | reader.close();
490 | urlConnection.disconnect();
491 | }
492 |
493 | if (LOGGING)
494 | Log.d(LOG_TAG, "Decipher FunctURL: " + decipherFunctUrl);
495 | Matcher mat = patSignatureDecFunction.matcher(javascriptFile);
496 | if (mat.find()) {
497 | decipherFunctionName = mat.group(1);
498 | if (LOGGING)
499 | Log.d(LOG_TAG, "Decipher Functname: " + decipherFunctionName);
500 |
501 | Pattern patMainVariable = Pattern.compile("(var |\\s|,|;)" + decipherFunctionName.replace("$", "\\$") +
502 | "(=function\\((.{1,3})\\)\\{)");
503 |
504 | String mainDecipherFunct;
505 |
506 | mat = patMainVariable.matcher(javascriptFile);
507 | if (mat.find()) {
508 | mainDecipherFunct = "var " + decipherFunctionName + mat.group(2);
509 | } else {
510 | Pattern patMainFunction = Pattern.compile("function " + decipherFunctionName.replace("$", "\\$") +
511 | "(\\((.{1,3})\\)\\{)");
512 | mat = patMainFunction.matcher(javascriptFile);
513 | if (!mat.find())
514 | return false;
515 | mainDecipherFunct = "function " + decipherFunctionName + mat.group(2);
516 | }
517 |
518 | int startIndex = mat.end();
519 |
520 | for (int braces = 1, i = startIndex; i < javascriptFile.length(); i++) {
521 | if (braces == 0 && startIndex + 5 < i) {
522 | mainDecipherFunct += javascriptFile.substring(startIndex, i) + ";";
523 | break;
524 | }
525 | if (javascriptFile.charAt(i) == '{')
526 | braces++;
527 | else if (javascriptFile.charAt(i) == '}')
528 | braces--;
529 | }
530 | decipherFunctions = mainDecipherFunct;
531 | // Search the main function for extra functions and variables
532 | // needed for deciphering
533 | // Search for variables
534 | mat = patVariableFunction.matcher(mainDecipherFunct);
535 | while (mat.find()) {
536 | String variableDef = "var " + mat.group(2) + "={";
537 | if (decipherFunctions.contains(variableDef)) {
538 | continue;
539 | }
540 | startIndex = javascriptFile.indexOf(variableDef) + variableDef.length();
541 | for (int braces = 1, i = startIndex; i < javascriptFile.length(); i++) {
542 | if (braces == 0) {
543 | decipherFunctions += variableDef + javascriptFile.substring(startIndex, i) + ";";
544 | break;
545 | }
546 | if (javascriptFile.charAt(i) == '{')
547 | braces++;
548 | else if (javascriptFile.charAt(i) == '}')
549 | braces--;
550 | }
551 | }
552 | // Search for functions
553 | mat = patFunction.matcher(mainDecipherFunct);
554 | while (mat.find()) {
555 | String functionDef = "function " + mat.group(2) + "(";
556 | if (decipherFunctions.contains(functionDef)) {
557 | continue;
558 | }
559 | startIndex = javascriptFile.indexOf(functionDef) + functionDef.length();
560 | for (int braces = 0, i = startIndex; i < javascriptFile.length(); i++) {
561 | if (braces == 0 && startIndex + 5 < i) {
562 | decipherFunctions += functionDef + javascriptFile.substring(startIndex, i) + ";";
563 | break;
564 | }
565 | if (javascriptFile.charAt(i) == '{')
566 | braces++;
567 | else if (javascriptFile.charAt(i) == '}')
568 | braces--;
569 | }
570 | }
571 |
572 | if (LOGGING)
573 | Log.d(LOG_TAG, "Decipher Function: " + decipherFunctions);
574 | decipherViaWebView(encSignatures);
575 | if (CACHING) {
576 | writeDeciperFunctToChache();
577 | }
578 | } else {
579 | return false;
580 | }
581 | } else {
582 | decipherViaWebView(encSignatures);
583 | }
584 | return true;
585 | }
586 |
587 | private void parseDashManifest(String dashMpdUrl, SparseArray ytFiles) throws IOException {
588 | Pattern patBaseUrl = Pattern.compile("<\\s*BaseURL(.*?)>(.+?)<\\s*/BaseURL\\s*>");
589 | Pattern patDashItag = Pattern.compile("itag/([0-9]+?)/");
590 | String dashManifest;
591 | BufferedReader reader = null;
592 | URL getUrl = new URL(dashMpdUrl);
593 | HttpURLConnection urlConnection = (HttpURLConnection) getUrl.openConnection();
594 | urlConnection.setRequestProperty("User-Agent", USER_AGENT);
595 | try {
596 | reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
597 | reader.readLine();
598 | dashManifest = reader.readLine();
599 |
600 | } finally {
601 | if (reader != null)
602 | reader.close();
603 | urlConnection.disconnect();
604 | }
605 | if (dashManifest == null)
606 | return;
607 | Matcher mat = patBaseUrl.matcher(dashManifest);
608 | while (mat.find()) {
609 | int itag;
610 | String url = mat.group(2);
611 | Matcher mat2 = patDashItag.matcher(url);
612 | if (mat2.find()) {
613 | itag = Integer.parseInt(mat2.group(1));
614 | if (FORMAT_MAP.get(itag) == null)
615 | continue;
616 | if (!includeWebM && FORMAT_MAP.get(itag).getExt().equals("webm"))
617 | continue;
618 | } else {
619 | continue;
620 | }
621 | YtFile yf = new YtFile(FORMAT_MAP.get(itag), url);
622 | ytFiles.append(itag, yf);
623 | }
624 | }
625 |
626 | private void parseVideoMeta(String getVideoInfo) throws UnsupportedEncodingException {
627 | boolean isLiveStream = false;
628 | String title = null, author = null, channelId = null;
629 | long viewCount = 0, length = 0;
630 | Matcher mat = patTitle.matcher(getVideoInfo);
631 | if (mat.find()) {
632 | title = URLDecoder.decode(mat.group(1), "UTF-8");
633 | }
634 |
635 | mat = patHlsvp.matcher(getVideoInfo);
636 | if(mat.find())
637 | isLiveStream = true;
638 |
639 | mat = patAuthor.matcher(getVideoInfo);
640 | if (mat.find()) {
641 | author = URLDecoder.decode(mat.group(1), "UTF-8");
642 | }
643 | mat = patChannelId.matcher(getVideoInfo);
644 | if (mat.find()) {
645 | channelId = mat.group(1);
646 | }
647 | mat = patLength.matcher(getVideoInfo);
648 | if (mat.find()) {
649 | length = Long.parseLong(mat.group(1));
650 | }
651 | mat = patViewCount.matcher(getVideoInfo);
652 | if (mat.find()) {
653 | viewCount = Long.parseLong(mat.group(1));
654 | }
655 | videoMeta = new VideoMeta(videoID, title, author, channelId, length, viewCount, isLiveStream);
656 |
657 | }
658 |
659 | private void readDecipherFunctFromCache() {
660 | File cacheFile = new File(cacheDirPath + "/" + CACHE_FILE_NAME);
661 | // The cached functions are valid for 2 weeks
662 | if (cacheFile.exists() && (System.currentTimeMillis() - cacheFile.lastModified()) < 1209600000) {
663 | BufferedReader reader = null;
664 | try {
665 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(cacheFile), "UTF-8"));
666 | decipherJsFileName = reader.readLine();
667 | decipherFunctionName = reader.readLine();
668 | decipherFunctions = reader.readLine();
669 | } catch (Exception e) {
670 | e.printStackTrace();
671 | } finally {
672 | if (reader != null) {
673 | try {
674 | reader.close();
675 | } catch (IOException e) {
676 | e.printStackTrace();
677 | }
678 | }
679 | }
680 | }
681 | }
682 |
683 | /**
684 | * Parse the dash manifest for different dash streams and high quality audio. Default: false
685 | */
686 | public void setParseDashManifest(boolean parseDashManifest) {
687 | this.parseDashManifest = parseDashManifest;
688 | }
689 |
690 |
691 | /**
692 | * Include the webm format files into the result. Default: true
693 | */
694 | public void setIncludeWebM(boolean includeWebM) {
695 | this.includeWebM = includeWebM;
696 | }
697 |
698 |
699 | /**
700 | * Set default protocol of the returned urls to HTTP instead of HTTPS.
701 | * HTTP may be blocked in some regions so HTTPS is the default value.
702 | *
703 | * Note: Enciphered videos require HTTPS so they are not affected by
704 | * this.
705 | */
706 | public void setDefaultHttpProtocol(boolean useHttp) {
707 | this.useHttp = useHttp;
708 | }
709 |
710 | private void writeDeciperFunctToChache() {
711 | File cacheFile = new File(cacheDirPath + "/" + CACHE_FILE_NAME);
712 | BufferedWriter writer = null;
713 | try {
714 | writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cacheFile), "UTF-8"));
715 | writer.write(decipherJsFileName + "\n");
716 | writer.write(decipherFunctionName + "\n");
717 | writer.write(decipherFunctions);
718 | } catch (Exception e) {
719 | e.printStackTrace();
720 | } finally {
721 | if (writer != null) {
722 | try {
723 | writer.close();
724 | } catch (IOException e) {
725 | e.printStackTrace();
726 | }
727 | }
728 | }
729 | }
730 |
731 | private void decipherViaWebView(final SparseArray encSignatures) {
732 | final Context context = refContext.get();
733 | if (context == null)
734 | {
735 | return;
736 | }
737 |
738 | final StringBuilder stb = new StringBuilder(decipherFunctions + " function decipher(");
739 | stb.append("){return ");
740 | for (int i = 0; i < encSignatures.size(); i++) {
741 | int key = encSignatures.keyAt(i);
742 | if (i < encSignatures.size() - 1)
743 | stb.append(decipherFunctionName).append("('").append(encSignatures.get(key)).
744 | append("')+\"\\n\"+");
745 | else
746 | stb.append(decipherFunctionName).append("('").append(encSignatures.get(key)).
747 | append("')");
748 | }
749 | stb.append("};decipher();");
750 |
751 | new Handler(Looper.getMainLooper()).post(new Runnable() {
752 |
753 | @Override
754 | public void run() {
755 | new JsEvaluator(context).evaluate(stb.toString(), new JsCallback() {
756 | @Override
757 | public void onResult(String result) {
758 | lock.lock();
759 | try {
760 | decipheredSignature = result;
761 | jsExecuting.signal();
762 | } finally {
763 | lock.unlock();
764 | }
765 | }
766 |
767 | @Override
768 | public void onError(String errorMessage) {
769 | lock.lock();
770 | try {
771 | if(LOGGING)
772 | Log.e(LOG_TAG, errorMessage);
773 | jsExecuting.signal();
774 | } finally {
775 | lock.unlock();
776 | }
777 | }
778 | });
779 | }
780 | });
781 | }
782 |
783 | }
784 |
--------------------------------------------------------------------------------