├── 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: [![JitPack](https://jitpack.io/v/HaarigerHarald/android-youtubeExtractor.svg)](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 | --------------------------------------------------------------------------------