├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── io │ └── github │ └── gaeqs │ └── javayoutubedownloader │ ├── JavaYoutubeDownloader.java │ ├── decoder │ ├── Decoder.java │ ├── DecoderManager.java │ ├── EmbeddedDecoder.java │ ├── HTMLDecoder.java │ └── MultipleDecoderMethod.java │ ├── decrypt │ ├── Decrypt.java │ ├── DecryptScript.java │ └── HTML5SignatureDecrypt.java │ ├── exception │ ├── DownloadException.java │ ├── EmbeddedExtractionException.java │ ├── HTMLExtractionException.java │ ├── InvalidYoutubeURL.java │ └── StreamEncodedException.java │ ├── stream │ ├── EncodedStream.java │ ├── StreamOption.java │ ├── YoutubeVideo.java │ └── download │ │ ├── DownloadStatus.java │ │ ├── StreamDownloader.java │ │ └── StreamDownloaderNotifier.java │ ├── tag │ ├── AudioQuality.java │ ├── Container.java │ ├── Encoding.java │ ├── FPS.java │ ├── FormatNote.java │ ├── ITagMap.java │ ├── StreamType.java │ └── VideoQuality.java │ └── util │ ├── EncodedStreamUtils.java │ ├── HTMLUtils.java │ ├── IdExtractor.java │ ├── NumericUtils.java │ ├── PlayerResponseUtils.java │ └── Validate.java └── test └── io └── github └── gaeqs └── javayoutubedownloader └── JavaYoutubeDownloaderTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Created by https://www.gitignore.io/api/phpstorm 7 | 8 | ### PhpStorm ### 9 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 10 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 11 | 12 | # User-specific stuff: 13 | .idea/**/workspace.xml 14 | .idea/**/tasks.xml 15 | .idea/dictionaries 16 | .idea/workspace.xml 17 | 18 | # Sensitive or high-churn files: 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.xml 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | 27 | # Gradle: 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # CMake 32 | cmake-build-debug/ 33 | 34 | # Mongo Explorer plugin: 35 | .idea/**/mongoSettings.xml 36 | 37 | ## File-based project format: 38 | *.iws 39 | 40 | ## Plugin-specific files: 41 | 42 | # IntelliJ 43 | /out/ 44 | 45 | # mpeltonen/sbt-idea plugin 46 | .idea_modules/ 47 | 48 | # JIRA plugin 49 | atlassian-ide-plugin.xml 50 | 51 | # Cursive Clojure plugin 52 | .idea/replstate.xml 53 | 54 | # Crashlytics plugin (for Android Studio and IntelliJ) 55 | com_crashlytics_export_strings.xml 56 | crashlytics.properties 57 | crashlytics-build.properties 58 | fabric.properties 59 | 60 | ### PhpStorm Patch ### 61 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 62 | 63 | # *.iml 64 | # modules.xml 65 | # .idea/misc.xml 66 | # *.ipr 67 | 68 | # Sonarlint plugin 69 | .idea/sonarlint 70 | 71 | # End of https://www.gitignore.io/api/phpstorm 72 | 73 | # Mac 74 | .DS_Store 75 | 76 | # Maven 77 | log/ 78 | target/ 79 | .idea/ 80 | /.idea/workspace.xml 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gael Rial Costas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaYoutubeDownloader 2 | A simple but powerful Youtube Download API for Java. 3 | 4 |

What is JYD?

5 | JavaYoutubeDownloader is a small and simple Youtube Stream downloader 6 | that allows you to download or use any video on the platform in a 7 | few lines. 8 | 9 |

Installation

10 | 11 | You can easily install JYD using maven: 12 | 13 | ```xml 14 | 15 | 16 | io.github.gaeqs 17 | JavaYoutubeDownloader 18 | LATEST 19 | 20 | 21 | ``` 22 | 23 |

Usage

24 | 25 | Using JYD is very easy! This is an example of a method that downloads a video and saves the option with 26 | the best video quality into a file: 27 | 28 | ```java 29 | public static boolean download(String url, File folder) { 30 | //Extracts and decodes all streams. 31 | YoutubeVideo video = JavaYoutubeDownloader.decodeOrNull(url, MultipleDecoderMethod.AND, "html", "embedded"); 32 | //Gets the option with the greatest quality that has video and audio. 33 | StreamOption option = video.getStreamOptions().stream() 34 | .filter(target -> target.getType().hasVideo() && target.getType().hasAudio()) 35 | .min(Comparator.comparingInt(o -> o.getType().getVideoQuality().ordinal())).orElse(null); 36 | //If there is no option, returns false. 37 | if (option == null) return false; 38 | //Prints the option type. 39 | System.out.println(option.getType()); 40 | //Creates the file. folder/title.extension 41 | File file = new File(folder, video.getTitle() + "." + option.getType().getContainer().toString().toLowerCase()); 42 | //Creates the downloader. 43 | StreamDownloader downloader = new StreamDownloader(option, file, null); 44 | //Runs the downloader. 45 | new Thread(downloader).start(); 46 | return true; 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.gaeqs 8 | JavaYoutubeDownloader 9 | 1.2.3 10 | jar 11 | 12 | ${project.groupId}:${project.artifactId} 13 | A simple but powerful Youtube Download API for Java 14 | https://github.com/gaeqs/JavaYoutubeDownloader 15 | 16 | 17 | 18 | MIT License 19 | https://raw.githubusercontent.com/gaeqs/JavaYoutubeDownloader/master/LICENSE 20 | 21 | 22 | 23 | 24 | 25 | Gael Rial Costas 26 | gael.rial.costas@gmail.com 27 | 28 | 29 | 30 | 31 | scm:git:git://github.com/gaeqs/JavaYoutubeDownloader.git 32 | scm:git:ssh://github.com/gaeqs/JavaYoutubeDownloader.git 33 | https://github.com/gaeqs/JavaYoutubeDownloader/tree/master 34 | 35 | 36 | 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-resources-plugin 41 | 3.2.0 42 | 43 | UTF-8 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-compiler-plugin 49 | 3.8.1 50 | 51 | 8 52 | 8 53 | UTF-8 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-source-plugin 59 | 2.2.1 60 | 61 | 62 | attach-sources 63 | 64 | jar-no-fork 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-javadoc-plugin 72 | 2.9.1 73 | 74 | UTF-8 75 | 76 | 77 | 78 | attach-javadocs 79 | 80 | jar 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-gpg-plugin 88 | 1.6 89 | 90 | 91 | sign-artifacts 92 | verify 93 | 94 | sign 95 | 96 | 97 | 98 | 99 | 100 | org.sonatype.plugins 101 | nexus-staging-maven-plugin 102 | 1.6.7 103 | true 104 | 105 | ossrh 106 | https://oss.sonatype.org/ 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ossrh 116 | https://oss.sonatype.org/content/repositories/snapshots 117 | 118 | 119 | 120 | 121 | 122 | com.alibaba 123 | fastjson 124 | 1.2.83 125 | 126 | 127 | org.junit.jupiter 128 | junit-jupiter 129 | 5.8.2 130 | test 131 | 132 | 133 | junit 134 | junit 135 | 4.13.2 136 | test 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/JavaYoutubeDownloader.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.decoder.Decoder; 4 | import io.github.gaeqs.javayoutubedownloader.decoder.DecoderManager; 5 | import io.github.gaeqs.javayoutubedownloader.decoder.MultipleDecoderMethod; 6 | import io.github.gaeqs.javayoutubedownloader.stream.StreamOption; 7 | import io.github.gaeqs.javayoutubedownloader.stream.YoutubeVideo; 8 | import io.github.gaeqs.javayoutubedownloader.tag.ITagMap; 9 | import io.github.gaeqs.javayoutubedownloader.tag.StreamType; 10 | import io.github.gaeqs.javayoutubedownloader.util.Validate; 11 | 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.util.Optional; 15 | 16 | /** 17 | * The main class of the API. Here you can access to the {@link ITagMap} or the {@link DecoderManager}, and 18 | * execute multi-decoder stream extractions. 19 | */ 20 | public class JavaYoutubeDownloader { 21 | 22 | private static DecoderManager decoderManager = new DecoderManager(); 23 | 24 | /** 25 | * Returns the {@link ITagMap}. This map is used to get the {@link StreamType} 26 | * associated to a given iTag. 27 | * 28 | * @return the {@link ITagMap}. 29 | * @see ITagMap 30 | * @see StreamType 31 | */ 32 | public static ITagMap getITagMap() { 33 | return ITagMap.MAP; 34 | } 35 | 36 | /** 37 | * Returns the {@link DecoderManager}. With it you can get or add {@link Decoder}s. 38 | * 39 | * @return the {@link DecoderManager}. 40 | * @see Decoder 41 | * @see DecoderManager 42 | */ 43 | public static DecoderManager getDecoderManager() { 44 | return decoderManager; 45 | } 46 | 47 | /** 48 | * It does the same as {@link #decode(String, MultipleDecoderMethod, String...)}, but it returns {@code null} 49 | * if a exception is thrown. 50 | * 51 | * @param url the url. 52 | * @param method the method to use. 53 | * @param decoders the decoders to use. 54 | * @return the video, or null of an exception is thrown. 55 | * @see #decode(URL, MultipleDecoderMethod, String...) 56 | */ 57 | public static YoutubeVideo decodeOrNull(String url, MultipleDecoderMethod method, String... decoders) { 58 | try { 59 | return decode(url, method, decoders); 60 | } catch (Exception ex) { 61 | ex.printStackTrace(); 62 | return null; 63 | } 64 | } 65 | 66 | /** 67 | * It does the same as {@link #decode(URL, MultipleDecoderMethod, String...)}, but it returns {@code null} 68 | * if a exception is thrown. 69 | * 70 | * @param url the url. 71 | * @param method the method to use. 72 | * @param decoders the decoders to use. 73 | * @return the video, or null of an exception is thrown. 74 | * @see #decode(URL, MultipleDecoderMethod, String...) 75 | */ 76 | public static YoutubeVideo decodeOrNull(URL url, MultipleDecoderMethod method, String... decoders) { 77 | try { 78 | return decode(url, method, decoders); 79 | } catch (Exception ex) { 80 | ex.printStackTrace(); 81 | return null; 82 | } 83 | } 84 | 85 | /** 86 | * It does the same as {@link #decode(URL, MultipleDecoderMethod, String...)}, but it parses the given url 87 | * to an {@link URL} instance before. 88 | * 89 | * @param url the url. 90 | * @param method the method to use. 91 | * @param decoders the decoders to use. 92 | * @return the video, or null of an exception is thrown. 93 | * @throws MalformedURLException whether the url is malformed. 94 | * @see #decode(URL, MultipleDecoderMethod, String...) 95 | */ 96 | public static YoutubeVideo decode(String url, MultipleDecoderMethod method, String... decoders) throws MalformedURLException { 97 | Validate.notNull(url, "url cannot be null!"); 98 | return decode(new URL(url), method, decoders); 99 | } 100 | 101 | /** 102 | * Creates a {@link YoutubeVideo} using several decoders. 103 | *

104 | * Decoders are given by their name in the {@link DecoderManager}. If a decoder is not found it will be 105 | * ignored. 106 | * If no decoders are defined in the parameter an {@link IllegalArgumentException} is thrown. 107 | *

108 | * If none of the decoders are able to create a {@link YoutubeVideo} instance, an {@link IllegalStateException} is thrown. 109 | * The {@link YoutubeVideo} instance may exists, even if it has no {@link StreamOption}s. 110 | * This indicates that at least one of the decoders was executed successfully, but it wasn't able to find any stream. 111 | *

112 | * The given {@link MultipleDecoderMethod} modifies the behaviour of the algorithm. If the {@link MultipleDecoderMethod} is 113 | * {@link MultipleDecoderMethod#AND} all decoders will be executed. If the {@link MultipleDecoderMethod} is 114 | * {@link MultipleDecoderMethod#OR} the decoders will be executed in order until a non-empty {@link YoutubeVideo} 115 | * instance is created. 116 | * 117 | * @param url the video's {@link URL} 118 | * @param method the {@link MultipleDecoderMethod}. 119 | * @param decoders the {@link Decoder}s. 120 | * @return the {@link YoutubeVideo} instance. 121 | * @throws IllegalArgumentException if any of the arguments is null or if the decoder list is empty. 122 | * @throws IllegalStateException if none of the decoders was able to create a {@link YoutubeVideo} instance. 123 | */ 124 | public static YoutubeVideo decode(URL url, MultipleDecoderMethod method, String... decoders) { 125 | Validate.notNull(url, "url cannot be null!"); 126 | Validate.notNull(method, "method cannot be null!"); 127 | Validate.notNull(decoders, "decoders cannot be null!"); 128 | if (decoders.length == 0) throw new IllegalArgumentException("There are no decoders defined!"); 129 | YoutubeVideo video = null; 130 | YoutubeVideo current; 131 | 132 | Optional decoder; 133 | for (String string : decoders) { 134 | decoder = decoderManager.getDecoder(string); 135 | if (!decoder.isPresent()) continue; 136 | try { 137 | current = decoder.get().extractVideo(url); 138 | } catch (Exception ex) { 139 | ex.printStackTrace(); 140 | continue; 141 | } 142 | if (video == null) { 143 | video = current; 144 | } else { 145 | video.merge(current); 146 | } 147 | if (!video.getStreamOptions().isEmpty() && method == MultipleDecoderMethod.OR) return video; 148 | } 149 | if (video == null) throw new IllegalStateException("Couldn't get any video instance"); 150 | return video; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decoder/Decoder.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decoder; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.JavaYoutubeDownloader; 4 | import io.github.gaeqs.javayoutubedownloader.exception.HTMLExtractionException; 5 | import io.github.gaeqs.javayoutubedownloader.stream.StreamOption; 6 | import io.github.gaeqs.javayoutubedownloader.stream.YoutubeVideo; 7 | 8 | import java.io.IOException; 9 | import java.net.URL; 10 | 11 | /** 12 | * Represents a decoder. The decoder allows to extract all stream options from a URL using it's own decode algorithm. 13 | * Default decoders: 14 | * 15 | * @see EmbeddedDecoder 16 | * @see HTMLDecoder 17 | *

18 | * You can access to the default instance of a decoder using {@link JavaYoutubeDownloader#getDecoderManager()}. 19 | */ 20 | public interface Decoder { 21 | 22 | /** 23 | * Extracts and decodes all {@link StreamOption}s for the given URL. 24 | * 25 | * @param url the url. 26 | * @return the decoded {@link YoutubeVideo} with all the {@link StreamOption} inside. 27 | * @throws IOException whether any IO exception occurs. 28 | * @throws HTMLExtractionException whether the decoder is using an HTML algorithm and an exception is thrown. 29 | */ 30 | YoutubeVideo extractVideo(URL url) throws IOException; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decoder/DecoderManager.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decoder; 2 | 3 | import java.util.Map; 4 | import java.util.Optional; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | 8 | /** 9 | * Represents a decoder manager. This class is used to store all decoders implemented in the API. 10 | * Default decoders (HTML and Embedded) are automatically added when a instance is created. 11 | * To add your own decoders you can use the method {@link #addDecoder(String, Decoder)}. 12 | * 13 | * @see Decoder 14 | */ 15 | public class DecoderManager { 16 | 17 | private static final String DEFAULT_ENCODING = "UTF-8"; 18 | 19 | private Map decoders; 20 | 21 | public DecoderManager() { 22 | decoders = new ConcurrentHashMap<>(); 23 | loadDefaults(); 24 | } 25 | 26 | private void loadDefaults() { 27 | decoders.put("html", new HTMLDecoder(DEFAULT_ENCODING)); 28 | decoders.put("embedded", new EmbeddedDecoder(DEFAULT_ENCODING)); 29 | } 30 | 31 | /** 32 | * Returns the decoder associated to the given name. 33 | * 34 | * @param name the name of the decoder. 35 | * @return the decoder, of {@link Optional#empty()} if not present. 36 | */ 37 | public Optional getDecoder(String name) { 38 | return Optional.ofNullable(decoders.get(name)); 39 | } 40 | 41 | /** 42 | * Adds a decoder to the manager. 43 | * 44 | * @param name the decoder's name. 45 | * @param decoder the decoder. 46 | */ 47 | public void addDecoder(String name, Decoder decoder) { 48 | decoders.put(name, decoder); 49 | } 50 | 51 | /** 52 | * Returns a mutable {@link Map} with all decoders and their names. 53 | * 54 | * @return the map. 55 | */ 56 | public Map getDecoders() { 57 | return decoders; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decoder/EmbeddedDecoder.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decoder; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.exception.EmbeddedExtractionException; 4 | import io.github.gaeqs.javayoutubedownloader.stream.EncodedStream; 5 | import io.github.gaeqs.javayoutubedownloader.stream.YoutubeVideo; 6 | import io.github.gaeqs.javayoutubedownloader.util.EncodedStreamUtils; 7 | import io.github.gaeqs.javayoutubedownloader.util.HTMLUtils; 8 | import io.github.gaeqs.javayoutubedownloader.util.IdExtractor; 9 | import io.github.gaeqs.javayoutubedownloader.util.PlayerResponseUtils; 10 | 11 | import java.io.IOException; 12 | import java.io.UnsupportedEncodingException; 13 | import java.net.URL; 14 | import java.net.URLDecoder; 15 | import java.util.HashMap; 16 | import java.util.LinkedList; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * This class represents a decoder that uses the Youtube's embedded API to decode stream options. 22 | * Protected videos usually have their embedded API disabled, so if you use this decoder with a protected video 23 | * a exception will probably be thrown. 24 | *

25 | * The embedded protocol bypasses age restrictions and, if the video is compatible, it gives more 26 | * options than the {@link HTMLDecoder}. 27 | *

28 | * Its default name in the {@link DecoderManager} is "embedded". 29 | */ 30 | public class EmbeddedDecoder implements Decoder { 31 | 32 | public static final String DEFAULT_GET_VIDEO_URL = "https://www.youtube.com/get_video_info?video_id=%s"; 33 | public static final String TITLE_PARAMETER = "title"; 34 | public static final String AUTHOR_PARAMETER = "author"; 35 | public static final String MUXED_STREAM_LIST_PARAMETER = "url_encoded_fmt_stream_map"; 36 | public static final String ADAPTIVE_STREAM_LIST_PARAMETER = "adaptive_fmts"; 37 | 38 | public static final String PLAYER_RESPONSE_LIST_PARAMETER = "player_response"; 39 | 40 | private String urlEncoding; 41 | private String getVideoUrl; 42 | 43 | public EmbeddedDecoder(String urlEncoding) { 44 | this.urlEncoding = urlEncoding; 45 | this.getVideoUrl = DEFAULT_GET_VIDEO_URL; 46 | } 47 | 48 | public EmbeddedDecoder(String urlEncoding, String getVideoUrl) { 49 | this.urlEncoding = urlEncoding; 50 | this.getVideoUrl = getVideoUrl; 51 | } 52 | 53 | public String getUrlEncoding() { 54 | return urlEncoding; 55 | } 56 | 57 | public void setUrlEncoding(String urlEncoding) { 58 | this.urlEncoding = urlEncoding; 59 | } 60 | 61 | public String getGetVideoUrl() { 62 | return getVideoUrl; 63 | } 64 | 65 | public void setGetVideoUrl(String getVideoUrl) { 66 | this.getVideoUrl = getVideoUrl; 67 | } 68 | 69 | public YoutubeVideo extractVideo(URL url) throws IOException { 70 | URL embeddedUrl = new URL(String.format(getVideoUrl, IdExtractor.extractId(url.toExternalForm()))); 71 | String query = HTMLUtils.readAll(embeddedUrl); 72 | Map queryData = getQueryMap(query); 73 | checkExceptions(queryData); 74 | 75 | String title = queryData.containsKey(TITLE_PARAMETER) ? decode(queryData.get(TITLE_PARAMETER)) : "null"; 76 | String author = queryData.containsKey(AUTHOR_PARAMETER) ? decode(queryData.get(AUTHOR_PARAMETER)) : "null"; 77 | YoutubeVideo video = new YoutubeVideo(title, author); 78 | 79 | List encodedStreams = new LinkedList<>(); 80 | 81 | if (queryData.containsKey(PLAYER_RESPONSE_LIST_PARAMETER)) { 82 | PlayerResponseUtils.addPlayerResponseStreams(decode(queryData.get(PLAYER_RESPONSE_LIST_PARAMETER)), 83 | encodedStreams, urlEncoding); 84 | } 85 | 86 | //Muxed stream data. 87 | if (queryData.containsKey(MUXED_STREAM_LIST_PARAMETER)) { 88 | String encodedMuxedStreamList = decode(queryData.get(MUXED_STREAM_LIST_PARAMETER)); 89 | EncodedStreamUtils.addEncodedStreams(encodedMuxedStreamList, encodedStreams, urlEncoding); 90 | } 91 | 92 | //Adaptive stream data. 93 | if (queryData.containsKey(ADAPTIVE_STREAM_LIST_PARAMETER)) { 94 | String encodedAdaptiveStreamList = decode(queryData.get(ADAPTIVE_STREAM_LIST_PARAMETER)); 95 | EncodedStreamUtils.addEncodedStreams(encodedAdaptiveStreamList, encodedStreams, urlEncoding); 96 | } 97 | 98 | encodedStreams.removeIf(target -> !target.decode(null, true)); 99 | encodedStreams.forEach(target -> video.getStreamOptions().add(target.getDecodedStream())); 100 | return video; 101 | } 102 | 103 | private Map getQueryMap(String query) { 104 | HashMap map = new HashMap<>(); 105 | query = query.trim(); 106 | String[] pairs = query.split("&"); 107 | for (String pair : pairs) { 108 | int idx = pair.indexOf("="); 109 | map.put(decode(pair.substring(0, idx)), pair.substring(idx + 1)); 110 | } 111 | return map; 112 | } 113 | 114 | private String checkExceptions(Map queryMap) { 115 | String status = queryMap.get("status"); 116 | if (status.equals("fail")) { 117 | String error = queryMap.get("errorcode"); 118 | String reason = queryMap.get("reason"); 119 | if (error.equals("150")) 120 | throw new EmbeddedExtractionException("Embedding is disabled. Error code " + error + ". Reason: " + reason); 121 | if (error.equals("100")) 122 | throw new EmbeddedExtractionException("Video has been deleted. Error code " + error + ". Reason: " + reason); 123 | throw new EmbeddedExtractionException("Error code " + error + ". Reason: " + reason); 124 | } 125 | return status; 126 | } 127 | 128 | private String decode(String string) { 129 | try { 130 | return URLDecoder.decode(string, urlEncoding); 131 | } catch (UnsupportedEncodingException | NullPointerException e) { 132 | System.err.println("Error while decoding string " + string); 133 | e.printStackTrace(); 134 | return string; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decoder/HTMLDecoder.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decoder; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import io.github.gaeqs.javayoutubedownloader.stream.EncodedStream; 6 | import io.github.gaeqs.javayoutubedownloader.stream.YoutubeVideo; 7 | import io.github.gaeqs.javayoutubedownloader.util.EncodedStreamUtils; 8 | import io.github.gaeqs.javayoutubedownloader.util.HTMLUtils; 9 | 10 | import java.io.IOException; 11 | import java.io.UnsupportedEncodingException; 12 | import java.net.URL; 13 | import java.util.Collection; 14 | import java.util.HashSet; 15 | import java.util.NoSuchElementException; 16 | import java.util.Set; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | /** 21 | * Represents a decoder that uses the HTML5 web of youtube to decode stream options. This decoder is the most 22 | * safe to use, giving solid results. 23 | *

24 | * This protocol won't work if the video has an age restriction, or if it's not accessible in the country 25 | * of the running machine. 26 | *

27 | * Its default name in the {@link DecoderManager} is "html". 28 | */ 29 | public class HTMLDecoder implements Decoder { 30 | 31 | private static final String YOUTUBE_URL = "https://youtube.com"; 32 | 33 | private static final Pattern YT_PLAYER_RESPONSE = Pattern.compile("var ytInitialPlayerResponse = (\\{.*?});"); 34 | private static final Pattern YT_PLAYER_JS_URL = Pattern.compile("\"jsUrl\":\\s*\"(.*?)\""); 35 | 36 | private static final String KEY_STREAMING_DATA = "streamingData"; 37 | private static final String KEY_VIDEO_DETAILS = "videoDetails"; 38 | 39 | private static final String KEY_FORMATS = "formats"; 40 | private static final String KEY_ADAPTIVE_FORMATS = "adaptiveFormats"; 41 | private static final String KEY_TITLE = "title"; 42 | private static final String KEY_AUTHOR = "author"; 43 | 44 | private String urlEncoding; 45 | 46 | public HTMLDecoder(String urlEncoding) { 47 | this.urlEncoding = urlEncoding; 48 | } 49 | 50 | public String getUrlEncoding() { 51 | return urlEncoding; 52 | } 53 | 54 | public void setUrlEncoding(String urlEncoding) { 55 | this.urlEncoding = urlEncoding; 56 | } 57 | 58 | @Override 59 | public YoutubeVideo extractVideo(URL url) throws IOException { 60 | String html = HTMLUtils.readAll(url); 61 | String rawResponse = matchAndGet(YT_PLAYER_RESPONSE, html); 62 | 63 | JSONObject response = JSON.parseObject(rawResponse); 64 | JSONObject streamingData = response.getJSONObject(KEY_STREAMING_DATA); 65 | JSONObject details = response.getJSONObject(KEY_VIDEO_DETAILS); 66 | 67 | String jsUrl = YOUTUBE_URL + matchAndGet(YT_PLAYER_JS_URL, html); 68 | 69 | Set encodedStreams = new HashSet<>(); 70 | 71 | if (streamingData.containsKey(KEY_FORMATS)) { 72 | streamingData.getJSONArray(KEY_FORMATS).forEach(o -> parseFormat(o, encodedStreams)); 73 | } 74 | if (streamingData.containsKey(KEY_ADAPTIVE_FORMATS)) { 75 | streamingData.getJSONArray(KEY_ADAPTIVE_FORMATS).forEach(o -> parseFormat(o, encodedStreams)); 76 | } 77 | 78 | YoutubeVideo video = new YoutubeVideo(details.getString(KEY_TITLE), details.getString(KEY_AUTHOR), null); 79 | 80 | encodedStreams.removeIf(target -> !target.decode(jsUrl, false)); 81 | encodedStreams.forEach(target -> video.getStreamOptions().add(target.getDecodedStream())); 82 | 83 | return video; 84 | } 85 | 86 | private void parseFormat(Object object, Collection collection) { 87 | if (object instanceof JSONObject) { 88 | try { 89 | EncodedStreamUtils.addEncodedStreams((JSONObject) object, collection, urlEncoding); 90 | } catch (UnsupportedEncodingException e) { 91 | System.err.println("Error while parsing URL."); 92 | e.printStackTrace(); 93 | } 94 | } 95 | } 96 | 97 | private String matchAndGet(Pattern pattern, String data) { 98 | Matcher matcher = pattern.matcher(data); 99 | if (!matcher.find()) { 100 | throw new NoSuchElementException("Match not found!"); 101 | } 102 | return matcher.group(1); 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decoder/MultipleDecoderMethod.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decoder; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.JavaYoutubeDownloader; 4 | 5 | /** 6 | * This enum is used in the method {@link JavaYoutubeDownloader#decode(String, MultipleDecoderMethod, String...)}. 7 | * If you use the option {@link #AND} all decoders will be executed, adding all their results to a common list. 8 | * If you use the option {@link #OR} the method will only return the first non-empty result of the multi-decoder. 9 | */ 10 | public enum MultipleDecoderMethod { 11 | 12 | OR, AND 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decrypt/Decrypt.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decrypt; 2 | 3 | /** 4 | * Represents a decrypt algorithm. The Decrypt implementations are used to decrypt the signature code of protected streams. 5 | */ 6 | public interface Decrypt { 7 | 8 | /** 9 | * Decrypts the signature code inside the instance. 10 | * 11 | * @param jsUrl the url of the js file containing the decrypt method. 12 | * @param signature the signature to decrypt. 13 | * @return the decrypted code. 14 | */ 15 | String decrypt(String jsUrl, String signature); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decrypt/DecryptScript.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decrypt; 2 | 3 | import javax.script.Invocable; 4 | 5 | public class DecryptScript { 6 | 7 | private String name; 8 | private Invocable invocable; 9 | 10 | public DecryptScript(String name, Invocable invocable) { 11 | this.name = name; 12 | this.invocable = invocable; 13 | } 14 | 15 | public String getFunctionName() { 16 | return name; 17 | } 18 | 19 | public Invocable getInvocable() { 20 | return invocable; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/decrypt/HTML5SignatureDecrypt.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.decrypt; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.exception.DownloadException; 4 | import io.github.gaeqs.javayoutubedownloader.util.HTMLUtils; 5 | 6 | import javax.script.Invocable; 7 | import javax.script.ScriptEngine; 8 | import javax.script.ScriptEngineManager; 9 | import javax.script.ScriptException; 10 | import java.net.URL; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.ConcurrentMap; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * This decrypt algorithm uses the video player code from the youtube's web to decrypt the signature code. 18 | */ 19 | public class HTML5SignatureDecrypt implements Decrypt { 20 | 21 | private static Pattern[] MAIN_FUNCTION_PATTERNS = new Pattern[]{ 22 | Pattern.compile("\\b[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\s*\\(\\s*([a-zA-Z0-9$]+)\\("), 23 | Pattern.compile("\\b[a-zA-Z0-9]+\\s*&&\\s*[a-zA-Z0-9]+\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\s*\\(\\s*([a-zA-Z0-9$]+)\\("), 24 | Pattern.compile("\\b([a-zA-Z0-9$]{2})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)"), 25 | Pattern.compile("([a-zA-Z0-9$]+)\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)"), 26 | Pattern.compile("([\"'])signature\\1\\s*,\\s*([a-zA-Z0-9$]+)\\("), 27 | Pattern.compile("\\.sig\\|\\|([a-zA-Z0-9$]+)\\("), 28 | Pattern.compile("yt\\.akamaized\\.net/\\)\\s*\\|\\|\\s*.*?\\s*[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*(?:encodeURIComponent\\s*\\()?\\s*()$"), 29 | Pattern.compile("\\b[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*([a-zA-Z0-9$]+)\\("), 30 | Pattern.compile("\\b[a-zA-Z0-9]+\\s*&&\\s*[a-zA-Z0-9]+\\.set\\([^,]+\\s*,\\s*([a-zA-Z0-9$]+)\\("), 31 | Pattern.compile("\\bc\\s*&&\\s*a\\.set\\([^,]+\\s*,\\s*\\([^)]*\\)\\s*\\(\\s*([a-zA-Z0-9$]+)\\("), 32 | Pattern.compile("\\bc\\s*&&\\s*[a-zA-Z0-9]+\\.set\\([^,]+\\s*,\\s*\\([^)]*\\)\\s*\\(\\s*([a-zA-Z0-9$]+)\\(") 33 | }; 34 | 35 | 36 | public static ConcurrentMap playerCache = new ConcurrentHashMap<>(); 37 | 38 | private DecryptScript getScript(String jsUrl) throws ScriptException { 39 | DecryptScript script = playerCache.get(jsUrl); 40 | if (script != null) return script; 41 | 42 | ScriptEngineManager manager = new ScriptEngineManager(); 43 | // use a js script engine 44 | ScriptEngine engine = manager.getEngineByName("JavaScript"); 45 | 46 | String playerScript = getHtml5PlayerScript(jsUrl); 47 | String decodeFuncName = getMainDecodeFunctionName(playerScript); 48 | String decodeScript = extractDecodeFunctions(playerScript, decodeFuncName); 49 | 50 | engine.eval(decodeScript); 51 | Invocable invocable = (Invocable) engine; 52 | script = new DecryptScript(decodeFuncName, invocable); 53 | playerCache.put(jsUrl, script); 54 | return script; 55 | } 56 | 57 | private String getHtml5PlayerScript(String jsUrl) { 58 | try { 59 | return HTMLUtils.readAll(new URL(jsUrl)); 60 | } catch (Exception e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | private String getMainDecodeFunctionName(String playerJS) { 66 | for (Pattern pattern : MAIN_FUNCTION_PATTERNS) { 67 | Matcher matcher = pattern.matcher(playerJS); 68 | if (matcher.find()) { 69 | return matcher.group(1); 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | public String extractDecodeFunctions(String playerJS, String functionName) { 76 | StringBuilder decodeScript = new StringBuilder(); 77 | //May change. 78 | Pattern decodeFunction = Pattern.compile(String.format("(%s=function\\([a-zA-Z0-9$]+\\)\\{.*?\\})[,;]", Pattern.quote(functionName)), 79 | Pattern.DOTALL); 80 | Matcher decodeFunctionMatch = decodeFunction.matcher(playerJS); 81 | if (decodeFunctionMatch.find()) { 82 | decodeScript.append(decodeFunctionMatch.group(1)).append(';'); 83 | } else { 84 | throw new DownloadException("Unable to extract the main decode function!"); 85 | } 86 | 87 | // determine the name of the helper function which is used by the 88 | // main decode function 89 | Pattern decodeFunctionHelperName = Pattern.compile("\\);([a-zA-Z0-9]+)\\."); 90 | Matcher decodeFunctionHelperNameMatch = decodeFunctionHelperName.matcher(decodeScript.toString()); 91 | if (decodeFunctionHelperNameMatch.find()) { 92 | final String decodeFuncHelperName = decodeFunctionHelperNameMatch.group(1); 93 | 94 | Pattern decodeFunctionHelper = Pattern.compile( 95 | String.format("(var %s=\\{[a-zA-Z0-9]*:function\\(.*?\\};)", Pattern.quote(decodeFuncHelperName)), 96 | Pattern.DOTALL); 97 | Matcher decodeFunctionHelperMatch = decodeFunctionHelper.matcher(playerJS); 98 | if (decodeFunctionHelperMatch.find()) { 99 | decodeScript.append(decodeFunctionHelperMatch.group(1)); 100 | } else { 101 | throw new DownloadException("Unable to extract the helper decode functions!"); 102 | } 103 | 104 | } else { 105 | throw new DownloadException("Unable to determine the name of the helper decode function!"); 106 | } 107 | return decodeScript.toString(); 108 | } 109 | 110 | @Override 111 | public String decrypt(String jsUrl, String signature) { 112 | 113 | 114 | String decodedSignature; 115 | try { 116 | DecryptScript script = getScript(jsUrl); 117 | decodedSignature = (String) script.getInvocable().invokeFunction(script.getFunctionName(), signature); 118 | } catch (Exception e) { 119 | throw new DownloadException("Unable to decrypt signature!", e); 120 | } 121 | return decodedSignature; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/exception/DownloadException.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.exception; 2 | 3 | public class DownloadException extends RuntimeException { 4 | 5 | public DownloadException(String message) { 6 | super(message); 7 | } 8 | 9 | public DownloadException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/exception/EmbeddedExtractionException.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.exception; 2 | 3 | public class EmbeddedExtractionException extends RuntimeException { 4 | 5 | public EmbeddedExtractionException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/exception/HTMLExtractionException.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.exception; 2 | 3 | public class HTMLExtractionException extends RuntimeException { 4 | 5 | public HTMLExtractionException(String message) { 6 | super(message); 7 | } 8 | 9 | public HTMLExtractionException(Exception ex) { 10 | super(ex); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/exception/InvalidYoutubeURL.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.exception; 2 | 3 | public class InvalidYoutubeURL extends RuntimeException { 4 | 5 | 6 | public InvalidYoutubeURL(String url) { 7 | super(url + " is an invalid Youtube URL."); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/exception/StreamEncodedException.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.exception; 2 | 3 | public class StreamEncodedException extends RuntimeException { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/stream/EncodedStream.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.stream; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.JavaYoutubeDownloader; 4 | import io.github.gaeqs.javayoutubedownloader.decrypt.Decrypt; 5 | import io.github.gaeqs.javayoutubedownloader.decrypt.HTML5SignatureDecrypt; 6 | import io.github.gaeqs.javayoutubedownloader.exception.StreamEncodedException; 7 | import io.github.gaeqs.javayoutubedownloader.tag.ITagMap; 8 | import io.github.gaeqs.javayoutubedownloader.util.Validate; 9 | 10 | import java.net.URL; 11 | import java.util.Optional; 12 | 13 | /** 14 | * Represents a {@link StreamOption} that is not decoded. You can created the decoded {@link StreamOption} 15 | * using the method {@link #decode(String, boolean)}. 16 | * The iTag and the url cannot be null. 17 | */ 18 | public class EncodedStream { 19 | 20 | private static final String DEFAULT_JS_SCRIPT = "https://youtube.com/yts/jsbin/player_ias-vflEO2H8R/en_US/base.js"; 21 | private static final String SIGNATURE_PARAMETER = "&sig="; 22 | 23 | private static final Decrypt DECRYPT = new HTML5SignatureDecrypt(); 24 | 25 | private final int iTag; 26 | private final String url; 27 | private final String signature; 28 | 29 | private StreamOption decodedStream; 30 | 31 | /** 32 | * Creates an EncodedStream using an iTag and an url. 33 | * 34 | * @param iTag the iTag. 35 | * @param url the url. 36 | */ 37 | public EncodedStream(int iTag, String url) { 38 | this(iTag, url, null); 39 | } 40 | 41 | /** 42 | * Creates an EncodedStream using an iTag, an url and a signature, or null. 43 | * 44 | * @param iTag the iTag. 45 | * @param url the url. 46 | * @param signature the signature code, or null. 47 | */ 48 | public EncodedStream(int iTag, String url, String signature) { 49 | Validate.notNull(url, "url cannot be null!"); 50 | this.iTag = iTag; 51 | this.url = url; 52 | this.signature = signature; 53 | this.decodedStream = null; 54 | } 55 | 56 | /** 57 | * Returns the iTag of the stream. You can receive more data from the iTag using {@link JavaYoutubeDownloader#getITagMap()} 58 | * and {@link ITagMap#get(Object)}. 59 | * 60 | * @return the iTag. 61 | */ 62 | public int getITag() { 63 | return iTag; 64 | } 65 | 66 | /** 67 | * Returns the base url of the stream. This url may vary from the decoded version, 68 | * as the decoded one may have the signature parameter. 69 | * 70 | * @return the url. 71 | */ 72 | public String getUrl() { 73 | return url; 74 | } 75 | 76 | /** 77 | * Returns the encrypted signature code, if present. The signature code is decrypted when the method 78 | * {@link #decode(String, boolean)} is executed. 79 | * 80 | * @return the encrypted signature code. 81 | */ 82 | public Optional getSignature() { 83 | return Optional.ofNullable(signature); 84 | } 85 | 86 | /** 87 | * Returns whether this encoded stream has a signature code. 88 | * 89 | * @return whether this encoded stream has a signature code. 90 | */ 91 | public boolean hasSignature() { 92 | return signature != null; 93 | } 94 | 95 | /** 96 | * Returns the decoded stream if the method {@link #decode(String, boolean)} was previously executed, or 97 | * throws a {@link StreamEncodedException} if no decoded stream is found. 98 | * 99 | * @return the decoded stream. 100 | * @throws StreamEncodedException if no decoded stream is found. Use the method {@link #decode(String, boolean)} 101 | * to generate one. 102 | */ 103 | public StreamOption getDecodedStream() { 104 | if (decodedStream == null) throw new StreamEncodedException(); 105 | return decodedStream; 106 | } 107 | 108 | /** 109 | * Generates a {@link StreamOption} decoding the data of this EncodedStream. A decode may fail, so 110 | * the method returns whether the process was successful. 111 | * 112 | * @param jsUrl the youtube JS file url, or null if you don't have one. This is used to decrypt the 113 | * signature using the online algorithm. 114 | * @param checkConnection whether the method will check if the decoded URL is accessible. If it's not the created 115 | * decoded stream will be deleted, and this method will return false. 116 | * @return whether the decode was successful. 117 | */ 118 | public boolean decode(String jsUrl, boolean checkConnection) { 119 | if (decodedStream != null) return true; 120 | boolean created; 121 | if (!hasSignature()) { 122 | created = decodeSimple(); 123 | } else { 124 | created = decodeComplex(jsUrl); 125 | } 126 | if (!created) return false; 127 | if (!checkConnection) return true; 128 | if (!decodedStream.checkConnection()) { 129 | System.out.println("Error checking connection! (Signature not working)\n" + decodedStream.getUrl()); 130 | decodedStream = null; 131 | return false; 132 | } 133 | return true; 134 | } 135 | 136 | private boolean decodeSimple() { 137 | try { 138 | decodedStream = new StreamOption(new URL(url), JavaYoutubeDownloader.getITagMap().get(iTag)); 139 | return true; 140 | } catch (IllegalArgumentException ex) { 141 | if (JavaYoutubeDownloader.getITagMap().get(iTag) == null) { 142 | if (iTag > 393 && iTag <= 399) return false; //Unknown streams. 143 | System.err.println("Couldn't find the StreamType for the iTag " + iTag); 144 | } else ex.printStackTrace(); 145 | return false; 146 | } catch (Exception ex) { 147 | ex.printStackTrace(); 148 | return false; 149 | } 150 | } 151 | 152 | private boolean decodeComplex(String jsUrl) { 153 | String decryptedSignature = DECRYPT.decrypt(jsUrl == null ? DEFAULT_JS_SCRIPT : jsUrl, signature); 154 | try { 155 | decodedStream = new StreamOption(new URL(url + SIGNATURE_PARAMETER + decryptedSignature), 156 | JavaYoutubeDownloader.getITagMap().get(iTag)); 157 | } catch (IllegalArgumentException ex) { 158 | if (JavaYoutubeDownloader.getITagMap().get(iTag) == null) { 159 | if (iTag > 393 && iTag <= 399) return false; //Unknown streams. 160 | System.err.println("Couldn't find the StreamType for the iTag " + iTag); 161 | } else ex.printStackTrace(); 162 | return false; 163 | } catch (Exception ex) { 164 | ex.printStackTrace(); 165 | return false; 166 | } 167 | return true; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/stream/StreamOption.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.stream; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.stream.download.StreamDownloader; 4 | import io.github.gaeqs.javayoutubedownloader.tag.StreamType; 5 | import io.github.gaeqs.javayoutubedownloader.util.HTMLUtils; 6 | import io.github.gaeqs.javayoutubedownloader.util.Validate; 7 | 8 | import javax.net.ssl.HttpsURLConnection; 9 | import java.net.URL; 10 | 11 | /** 12 | * A stream option stores the decoded url and the type of a stream. These instances can be used to 13 | * download the represented stream using a {@link StreamDownloader} 14 | * or another method you have. 15 | */ 16 | public class StreamOption { 17 | 18 | private final URL url; 19 | private final StreamType type; 20 | 21 | /** 22 | * Created a stream option by an {@link URL} and a {@link StreamType}. None of them can be null. 23 | * 24 | * @param url the decoded url. 25 | * @param type the stream type. 26 | */ 27 | public StreamOption(URL url, StreamType type) { 28 | Validate.notNull(url, "Url cannot be null!"); 29 | Validate.notNull(type, "Type cannot be null!"); 30 | this.url = url; 31 | this.type = type; 32 | } 33 | 34 | /** 35 | * Returns the decoded {@link URL} of the stream. 36 | * 37 | * @return the url. 38 | */ 39 | public URL getUrl() { 40 | return url; 41 | } 42 | 43 | /** 44 | * Returns the {@link StreamType} of the stream. This object can give us information about the stream, 45 | * such as the video and audio quality, the format, or the container. 46 | * * 47 | * 48 | * @return the {@link StreamType} 49 | */ 50 | public StreamType getType() { 51 | return type; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "{Url:" + url + ", Stream: " + type.toString() + "}"; 57 | } 58 | 59 | /** 60 | * Checks whether the stream is accessible. 61 | * 62 | * @return whether the stream is accessible. 63 | */ 64 | public boolean checkConnection() { 65 | HttpsURLConnection connection = null; 66 | try { 67 | connection = (HttpsURLConnection) url.openConnection(); 68 | connection.setRequestProperty("User-Agent", HTMLUtils.USER_AGENT); 69 | connection.setDoInput(true); 70 | connection.connect(); 71 | HTMLUtils.check(connection); 72 | connection.disconnect(); 73 | return true; 74 | } catch (Exception ex) { 75 | if (connection != null) { 76 | try { 77 | connection.disconnect(); 78 | } catch (Exception ignore) { 79 | } 80 | } 81 | return false; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/stream/YoutubeVideo.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.stream; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.decoder.Decoder; 4 | import io.github.gaeqs.javayoutubedownloader.util.Validate; 5 | 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Represents a Youtube video. An instance of this class contains the title, the author and all available streams 12 | * of the represented video. 13 | * {@link Decoder}s return an instance of this class with all information you need. 14 | * 15 | * @see Decoder 16 | */ 17 | public class YoutubeVideo { 18 | 19 | private final String title; 20 | private final String author; 21 | private final List streamOptions; 22 | 23 | /** 24 | * Creates a video using the title and the author. The stream list will be empty. 25 | * 26 | * @param title the title. 27 | * @param author the author. 28 | */ 29 | public YoutubeVideo(String title, String author) { 30 | this(title, author, null); 31 | } 32 | 33 | /** 34 | * Creates a video using the title, the author and the stream options. If the stream list is 35 | * {@code null} an empty {@link LinkedList} will be created. 36 | * 37 | * @param title the title. 38 | * @param author the author. 39 | * @param streamOptions the stream list. 40 | */ 41 | public YoutubeVideo(String title, String author, List streamOptions) { 42 | Validate.notNull(title, "Title cannot be null!"); 43 | this.title = title; 44 | this.author = author; 45 | this.streamOptions = streamOptions == null ? new LinkedList<>() : streamOptions; 46 | } 47 | 48 | /** 49 | * Returns the title of the video. 50 | * 51 | * @return the title of the video. 52 | */ 53 | public String getTitle() { 54 | return title; 55 | } 56 | 57 | /** 58 | * Returns the author of the video, or {@link Optional#empty()} if not present. 59 | * Embedded decoders can give the author name, while HTML decoders cannot. 60 | * 61 | * @return the author of the video. 62 | */ 63 | public Optional getAuthor() { 64 | return Optional.ofNullable(author); 65 | } 66 | 67 | /** 68 | * Returns a mutable {@link List} with all available {@link StreamOption} of this video. 69 | * 70 | * @return the mutable {@link List}. 71 | * @see StreamOption 72 | */ 73 | public List getStreamOptions() { 74 | return streamOptions; 75 | } 76 | 77 | /** 78 | * Adds all stream options of the given YoutubeVideo to this. 79 | * This method is used by multi-decoder extractions. 80 | * 81 | * @param video the given video. 82 | */ 83 | public void merge(YoutubeVideo video) { 84 | streamOptions.addAll(video.streamOptions); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/stream/download/DownloadStatus.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.stream.download; 2 | 3 | /** 4 | * Represents the status of a {@link StreamDownloader}. 5 | */ 6 | public enum DownloadStatus { 7 | 8 | READY, DOWNLOADING, DONE 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/stream/download/StreamDownloader.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.stream.download; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.exception.DownloadException; 4 | import io.github.gaeqs.javayoutubedownloader.stream.StreamOption; 5 | import io.github.gaeqs.javayoutubedownloader.util.HTMLUtils; 6 | import io.github.gaeqs.javayoutubedownloader.util.Validate; 7 | 8 | import java.io.BufferedInputStream; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.io.RandomAccessFile; 12 | import java.net.HttpURLConnection; 13 | 14 | /** 15 | * A StreamDownloader downloads the given {@link StreamOption} and stores it into the given {@link File}. 16 | * A {@link StreamDownloaderNotifier} can be given to know the status of the download. 17 | *

18 | * As it implements the {@link Runnable} interface, the StreamDownloader can be used easily in threads. 19 | */ 20 | public class StreamDownloader implements Runnable { 21 | 22 | private static final int BUFFER_SIZE = 1024 << 2; 23 | 24 | private final StreamOption option; 25 | private final File target; 26 | private StreamDownloaderNotifier notifier; 27 | 28 | private int length, count; 29 | private DownloadStatus status; 30 | 31 | public StreamDownloader(StreamOption option, File target, StreamDownloaderNotifier notifier) { 32 | Validate.notNull(option, "Option cannot be null!"); 33 | Validate.notNull(target, "Target cannot be null!"); 34 | this.option = option; 35 | this.target = target; 36 | this.notifier = notifier; 37 | this.length = 0; 38 | this.count = 0; 39 | this.status = DownloadStatus.READY; 40 | } 41 | 42 | public StreamOption getOption() { 43 | return option; 44 | } 45 | 46 | public File getTarget() { 47 | return target; 48 | } 49 | 50 | public StreamDownloaderNotifier getNotifier() { 51 | return notifier; 52 | } 53 | 54 | public void setNotifier(StreamDownloaderNotifier notifier) { 55 | this.notifier = notifier; 56 | } 57 | 58 | public int getLength() { 59 | return length; 60 | } 61 | 62 | public int getCount() { 63 | return count; 64 | } 65 | 66 | public DownloadStatus getStatus() { 67 | return status; 68 | } 69 | 70 | @Override 71 | public void run() { 72 | if (status == DownloadStatus.DOWNLOADING) throw new RuntimeException("This downloader is already running!"); 73 | status = DownloadStatus.DOWNLOADING; 74 | RandomAccessFile randomAccessFile = null; 75 | try { 76 | HttpURLConnection connection = (HttpURLConnection) option.getUrl().openConnection(); 77 | connection.setRequestProperty("User-Agent", HTMLUtils.USER_AGENT); 78 | connection.setDoInput(true); 79 | if (!target.createNewFile()) throw new DownloadException("File couldn't be created"); 80 | randomAccessFile = new RandomAccessFile(target, "rw"); 81 | byte[] bytes = new byte[BUFFER_SIZE]; 82 | BufferedInputStream bufferedInputStream = new BufferedInputStream(connection.getInputStream()); 83 | HTMLUtils.check(connection); 84 | 85 | length = connection.getContentLength(); 86 | count = 0; 87 | 88 | int read; 89 | if (notifier != null) notifier.onStart(this); 90 | while ((read = bufferedInputStream.read(bytes)) > 0) { 91 | randomAccessFile.write(bytes, 0, read); 92 | count += read; 93 | 94 | if (notifier != null) notifier.onDownload(this); 95 | 96 | if (Thread.interrupted()) 97 | throw new DownloadException("Thread interrupted"); 98 | } 99 | bufferedInputStream.close(); 100 | if (notifier != null) notifier.onFinish(this); 101 | } catch (Exception ex) { 102 | if (notifier != null) 103 | notifier.onError(this, ex); 104 | } finally { 105 | if (randomAccessFile != null) { 106 | try { 107 | randomAccessFile.close(); 108 | } catch (IOException e) { 109 | e.printStackTrace(); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/stream/download/StreamDownloaderNotifier.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.stream.download; 2 | 3 | /** 4 | * StreamDownloaderNotifiers are used to collect information from {@link StreamDownloader}s. 5 | * They're called when the download starts, when a download loop is completed, when the download 6 | * is completed and when an exception is thrown. 7 | */ 8 | public interface StreamDownloaderNotifier { 9 | 10 | void onStart(StreamDownloader downloader); 11 | 12 | void onDownload(StreamDownloader downloader); 13 | 14 | void onFinish(StreamDownloader downloader); 15 | 16 | void onError(StreamDownloader downloader, Exception ex); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/AudioQuality.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Represents the audio quality of an audio channel. 5 | */ 6 | public enum AudioQuality { 7 | 8 | k256, k192, k160, k128, k96, k70, k64, k50, k48, k36, k24 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/Container.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Represents the file format of a stream. 5 | */ 6 | public enum Container { 7 | 8 | FLV, GP3, MP4, M4A, WEBM 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/Encoding.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Represents the encoding of an video or audio channel. 5 | */ 6 | public enum Encoding { 7 | 8 | H263, H264, VP8, VP9, MP4, MP3, AAC, VORBIS, OPUS, DTSE, EC_3 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/FPS.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Returns the frames per second of a video stream. 5 | */ 6 | public enum FPS { 7 | 8 | f30, f60 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/FormatNote.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Represents the format note of the stream. Some streams don't have format note. 5 | */ 6 | public enum FormatNote { 7 | 8 | NONE, THREE_DIMENSIONAL, HLS, DASH, 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/ITagMap.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.JavaYoutubeDownloader; 4 | 5 | import java.util.HashMap; 6 | 7 | /** 8 | * This special {@link HashMap} contains all youtube's iTag possibilities and their properties. 9 | * You can acces to this map using {@link JavaYoutubeDownloader#getITagMap()} or {@link ITagMap#MAP}. 10 | */ 11 | public class ITagMap extends HashMap { 12 | 13 | public static final ITagMap MAP = new ITagMap(); 14 | 15 | private ITagMap() { 16 | put(5, new StreamType(Container.FLV, Encoding.H263, VideoQuality.p240, Encoding.MP3, AudioQuality.k64, FormatNote.NONE)); 17 | put(6, new StreamType(Container.FLV, Encoding.H263, VideoQuality.p270, Encoding.MP3, AudioQuality.k64, FormatNote.NONE)); 18 | put(13, new StreamType(Container.GP3, Encoding.MP4, VideoQuality.p144, Encoding.AAC, AudioQuality.k24, FormatNote.NONE)); 19 | put(17, new StreamType(Container.GP3, Encoding.MP4, VideoQuality.p144, Encoding.AAC, AudioQuality.k24, FormatNote.NONE)); 20 | put(18, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p360, Encoding.AAC, AudioQuality.k96, FormatNote.NONE)); 21 | put(22, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p720, Encoding.AAC, AudioQuality.k192, FormatNote.NONE)); 22 | put(34, new StreamType(Container.FLV, Encoding.H264, VideoQuality.p360, Encoding.AAC, AudioQuality.k128, FormatNote.NONE)); 23 | put(35, new StreamType(Container.FLV, Encoding.H264, VideoQuality.p480, Encoding.AAC, AudioQuality.k128, FormatNote.NONE)); 24 | put(36, new StreamType(Container.GP3, Encoding.MP4, VideoQuality.p240, Encoding.AAC, AudioQuality.k36, FormatNote.NONE)); 25 | put(37, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p1080, Encoding.AAC, AudioQuality.k192, FormatNote.NONE)); 26 | put(38, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p3072, Encoding.AAC, AudioQuality.k192, FormatNote.NONE)); 27 | put(43, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p360, Encoding.VORBIS, AudioQuality.k128, FormatNote.NONE)); 28 | put(44, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p480, Encoding.VORBIS, AudioQuality.k128, FormatNote.NONE)); 29 | put(45, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p720, Encoding.VORBIS, AudioQuality.k192, FormatNote.NONE)); 30 | put(46, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p1080, Encoding.VORBIS, AudioQuality.k192, FormatNote.NONE)); 31 | put(59, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p480, Encoding.AAC, AudioQuality.k128, FormatNote.NONE)); 32 | put(78, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p480, Encoding.AAC, AudioQuality.k128, FormatNote.NONE)); 33 | 34 | //3D videos 35 | put(82, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p360, Encoding.AAC, AudioQuality.k128, FormatNote.THREE_DIMENSIONAL)); 36 | put(83, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p480, Encoding.AAC, AudioQuality.k128, FormatNote.THREE_DIMENSIONAL)); 37 | put(84, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p720, Encoding.AAC, AudioQuality.k192, FormatNote.THREE_DIMENSIONAL)); 38 | put(85, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p1080, Encoding.AAC, AudioQuality.k192, FormatNote.THREE_DIMENSIONAL)); 39 | put(100, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p360, Encoding.VORBIS, AudioQuality.k128, FormatNote.THREE_DIMENSIONAL)); 40 | put(101, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p480, Encoding.VORBIS, AudioQuality.k192, FormatNote.THREE_DIMENSIONAL)); 41 | put(102, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p720, Encoding.VORBIS, AudioQuality.k192, FormatNote.THREE_DIMENSIONAL)); 42 | 43 | //Apple HTTP Live Streaming 44 | put(91, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p144, Encoding.AAC, AudioQuality.k48, FormatNote.HLS)); 45 | put(92, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p240, Encoding.AAC, AudioQuality.k48, FormatNote.HLS)); 46 | put(93, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p360, Encoding.AAC, AudioQuality.k128, FormatNote.HLS)); 47 | put(94, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p480, Encoding.AAC, AudioQuality.k128, FormatNote.HLS)); 48 | put(95, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p720, Encoding.AAC, AudioQuality.k256, FormatNote.HLS)); 49 | put(96, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p1080, Encoding.AAC, AudioQuality.k256, FormatNote.HLS)); 50 | put(132, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p240, Encoding.AAC, AudioQuality.k48, FormatNote.HLS)); 51 | put(151, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p72, Encoding.AAC, AudioQuality.k24, FormatNote.HLS)); 52 | 53 | //Dash mp4 video 54 | put(133, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p240, FormatNote.DASH)); 55 | put(134, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p360, FormatNote.DASH)); 56 | put(135, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p480, FormatNote.DASH)); 57 | put(136, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p720, FormatNote.DASH)); 58 | put(137, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p1080, FormatNote.DASH)); 59 | put(138, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p2160, FormatNote.DASH)); 60 | put(160, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p144, FormatNote.DASH)); 61 | put(212, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p480, FormatNote.DASH)); 62 | put(264, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p1440, FormatNote.DASH)); 63 | put(298, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p720, FormatNote.DASH, FPS.f60)); 64 | put(299, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p1080, FormatNote.DASH, FPS.f60)); 65 | put(266, new StreamType(Container.MP4, Encoding.H264, VideoQuality.p2160, FormatNote.DASH)); 66 | 67 | //Dash mp4 audio 68 | put(139, new StreamType(Container.M4A, Encoding.AAC, AudioQuality.k48, FormatNote.DASH)); 69 | put(140, new StreamType(Container.M4A, Encoding.AAC, AudioQuality.k128, FormatNote.DASH)); 70 | put(141, new StreamType(Container.M4A, Encoding.AAC, AudioQuality.k256, FormatNote.DASH)); 71 | put(256, new StreamType(Container.M4A, Encoding.AAC, AudioQuality.k24, FormatNote.DASH)); 72 | put(258, new StreamType(Container.M4A, Encoding.AAC, AudioQuality.k24, FormatNote.DASH)); 73 | put(325, new StreamType(Container.M4A, Encoding.DTSE, AudioQuality.k24, FormatNote.DASH)); 74 | put(328, new StreamType(Container.M4A, Encoding.EC_3, AudioQuality.k24, FormatNote.DASH)); 75 | 76 | //Dash webm 77 | put(167, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p360, FormatNote.DASH)); 78 | put(168, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p480, FormatNote.DASH)); 79 | put(169, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p720, FormatNote.DASH)); 80 | put(170, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p1080, FormatNote.DASH)); 81 | put(218, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p480, FormatNote.DASH)); 82 | put(219, new StreamType(Container.WEBM, Encoding.VP8, VideoQuality.p480, FormatNote.DASH)); 83 | put(278, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p144, FormatNote.DASH)); 84 | put(242, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p240, FormatNote.DASH)); 85 | put(243, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p360, FormatNote.DASH)); 86 | put(244, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p480, FormatNote.DASH)); 87 | put(245, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p480, FormatNote.DASH)); 88 | put(246, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p480, FormatNote.DASH)); 89 | put(247, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p720, FormatNote.DASH)); 90 | put(248, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p1080, FormatNote.DASH)); 91 | put(271, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p1440, FormatNote.DASH)); 92 | put(272, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p2160, FormatNote.DASH)); 93 | put(302, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p720, FormatNote.DASH, FPS.f60)); 94 | put(303, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p1080, FormatNote.DASH, FPS.f60)); 95 | put(308, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p1440, FormatNote.DASH, FPS.f60)); 96 | put(313, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p2160, FormatNote.DASH)); 97 | put(315, new StreamType(Container.WEBM, Encoding.VP9, VideoQuality.p2160, FormatNote.DASH, FPS.f60)); 98 | 99 | //Dash webm audio 100 | put(171, new StreamType(Container.WEBM, Encoding.VORBIS, AudioQuality.k128, FormatNote.DASH)); 101 | put(172, new StreamType(Container.WEBM, Encoding.VORBIS, AudioQuality.k256, FormatNote.DASH)); 102 | 103 | //Dash webm audio with opus 104 | put(249, new StreamType(Container.WEBM, Encoding.OPUS, AudioQuality.k50, FormatNote.DASH)); 105 | put(250, new StreamType(Container.WEBM, Encoding.OPUS, AudioQuality.k70, FormatNote.DASH)); 106 | put(251, new StreamType(Container.WEBM, Encoding.OPUS, AudioQuality.k160, FormatNote.DASH)); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/StreamType.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Represents a stream type. Each stream has its properties, and they're represented in this class. 5 | */ 6 | public class StreamType { 7 | 8 | private final Container container; 9 | private final Encoding videoEncoding, audioEncoding; 10 | private final VideoQuality videoQuality; 11 | private final AudioQuality audioQuality; 12 | private final FormatNote formatNote; 13 | private final FPS fps; 14 | 15 | public StreamType(Container container, Encoding videoEncoding, VideoQuality videoQuality, Encoding audioEncoding, AudioQuality audioQuality, FormatNote formatNote) { 16 | this.container = container; 17 | this.videoEncoding = videoEncoding; 18 | this.videoQuality = videoQuality; 19 | this.audioEncoding = audioEncoding; 20 | this.audioQuality = audioQuality; 21 | this.formatNote = formatNote; 22 | this.fps = FPS.f30; 23 | } 24 | 25 | public StreamType(Container container, Encoding videoEncoding, VideoQuality videoQuality, FormatNote formatNote) { 26 | this.container = container; 27 | this.videoEncoding = videoEncoding; 28 | this.videoQuality = videoQuality; 29 | this.audioEncoding = null; 30 | this.audioQuality = null; 31 | this.formatNote = formatNote; 32 | this.fps = FPS.f30; 33 | } 34 | 35 | 36 | public StreamType(Container container, Encoding audioEncoding, AudioQuality audioQuality, FormatNote formatNote) { 37 | this.container = container; 38 | this.audioEncoding = audioEncoding; 39 | this.audioQuality = audioQuality; 40 | this.videoEncoding = null; 41 | this.videoQuality = null; 42 | this.formatNote = formatNote; 43 | this.fps = FPS.f30; 44 | } 45 | 46 | public StreamType(Container container, Encoding videoEncoding, VideoQuality videoQuality, Encoding audioEncoding, AudioQuality audioQuality, FormatNote formatNote, FPS fps) { 47 | this.container = container; 48 | this.videoEncoding = videoEncoding; 49 | this.videoQuality = videoQuality; 50 | this.audioEncoding = audioEncoding; 51 | this.audioQuality = audioQuality; 52 | this.formatNote = formatNote; 53 | this.fps = fps; 54 | } 55 | 56 | public StreamType(Container container, Encoding videoEncoding, VideoQuality videoQuality, FormatNote formatNote, FPS fps) { 57 | this.container = container; 58 | this.videoEncoding = videoEncoding; 59 | this.videoQuality = videoQuality; 60 | this.audioEncoding = null; 61 | this.audioQuality = null; 62 | this.formatNote = formatNote; 63 | this.fps = fps; 64 | } 65 | 66 | 67 | public StreamType(Container container, Encoding audioEncoding, AudioQuality audioQuality, FormatNote formatNote, FPS fps) { 68 | this.container = container; 69 | this.audioEncoding = audioEncoding; 70 | this.audioQuality = audioQuality; 71 | this.videoEncoding = null; 72 | this.videoQuality = null; 73 | this.formatNote = formatNote; 74 | this.fps = fps; 75 | } 76 | 77 | /** 78 | * Returns the container of the stream. 79 | * 80 | * @return the container. 81 | * @see Container 82 | */ 83 | public Container getContainer() { 84 | return container; 85 | } 86 | 87 | /** 88 | * Returns the video encoding of the stream. 89 | * It may be null if the stream doesn't have a video channel. 90 | * 91 | * @return the video encoding. 92 | * @see Encoding 93 | */ 94 | public Encoding getVideoEncoding() { 95 | return videoEncoding; 96 | } 97 | 98 | /** 99 | * Returns the audio encoding of the stream. 100 | * It may be {@code null} if the stream doesn't have an audio channel. 101 | * 102 | * @return the audio encoding. 103 | * @see Encoding 104 | */ 105 | public Encoding getAudioEncoding() { 106 | return audioEncoding; 107 | } 108 | 109 | /** 110 | * Returns the video quality of the stream. 111 | * It may be {@code null} if the stream doesn't have a video channel. 112 | * 113 | * @return the video quality. 114 | * @see VideoQuality 115 | */ 116 | public VideoQuality getVideoQuality() { 117 | return videoQuality; 118 | } 119 | 120 | /** 121 | * Returns the audio quality of the stream. 122 | * It may be {@code null} if the stream doesn't have an audio channel. 123 | * 124 | * @return the audio quality. 125 | * @see AudioQuality 126 | */ 127 | public AudioQuality getAudioQuality() { 128 | return audioQuality; 129 | } 130 | 131 | /** 132 | * Returns the format note of the stream. It may be {@code null}. 133 | * 134 | * @return the format note. 135 | * @see FormatNote 136 | */ 137 | public FormatNote getFormatNote() { 138 | return formatNote; 139 | } 140 | 141 | /** 142 | * Returns the frames per second of the stream. 143 | * 144 | * @return the frames per second. 145 | * @see FPS 146 | */ 147 | public FPS getFps() { 148 | return fps; 149 | } 150 | 151 | /** 152 | * Returns whether the stream has a video channel. 153 | * 154 | * @return whether the stream has a video channel. 155 | */ 156 | public boolean hasVideo() { 157 | return videoQuality != null && videoEncoding != null; 158 | } 159 | 160 | /** 161 | * Returns whether the stream has an audio channel. 162 | * 163 | * @return whether the stream has an audio channel. 164 | */ 165 | public boolean hasAudio() { 166 | return audioQuality != null && audioEncoding != null; 167 | } 168 | 169 | @Override 170 | public String toString() { 171 | StringBuilder builder = new StringBuilder("[Video: " + hasVideo() + ", Audio: " + hasAudio() + ", Container: " + container); 172 | if (hasVideo()) 173 | builder.append(", VEncoding: ").append(videoEncoding).append(", VQuality: ").append(videoQuality); 174 | if (hasAudio()) 175 | builder.append(", AEncoding: ").append(audioEncoding).append(", AQuality: ").append(audioQuality); 176 | builder.append(", Format Note: ").append(formatNote).append(", FPS: ").append(fps).append("]"); 177 | return builder.toString(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/tag/VideoQuality.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.tag; 2 | 3 | /** 4 | * Represents the video quality of a video channel. 5 | */ 6 | public enum VideoQuality { 7 | 8 | p3072, p2304, p2160, p1440, p1080, p720, p520, p480, p360, p270, p240, p224, p144, p72 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/util/EncodedStreamUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import io.github.gaeqs.javayoutubedownloader.stream.EncodedStream; 5 | 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.URLDecoder; 8 | import java.util.Collection; 9 | 10 | public class EncodedStreamUtils { 11 | 12 | /** 13 | * Parses an URLEncoded string to {@link EncodedStream}s, and adds them to the given collection. 14 | * 15 | * @param encodedString the encoded stream. 16 | * @param collection the collection. 17 | * @param urlEncoding the url encoding. UTF-8 is used by default. 18 | * @throws UnsupportedEncodingException whether the encoding is not supported. 19 | */ 20 | public static void addEncodedStreams(String encodedString, Collection collection, String urlEncoding) throws UnsupportedEncodingException { 21 | String[] streams = encodedString.trim().split(","); 22 | String[] pairs; 23 | 24 | int idx; 25 | String key; 26 | 27 | String url = null; 28 | String encoding = null; 29 | int iTag = -1; 30 | for (String stream : streams) { 31 | pairs = stream.split("&"); 32 | for (String pair : pairs) { 33 | idx = pair.indexOf('='); 34 | if (idx == -1) continue; 35 | key = URLDecoder.decode(pair.substring(0, idx).toLowerCase(), urlEncoding); 36 | switch (key) { 37 | case "url": 38 | url = URLDecoder.decode(pair.substring(idx + 1), urlEncoding); 39 | break; 40 | case "s": 41 | encoding = URLDecoder.decode(pair.substring(idx + 1), urlEncoding); 42 | break; 43 | case "itag": 44 | key = URLDecoder.decode(pair.substring(idx + 1), urlEncoding); 45 | if (!NumericUtils.isInteger(key)) continue; 46 | iTag = Integer.valueOf(key); 47 | break; 48 | } 49 | } 50 | if (iTag == -1 || url == null) continue; 51 | collection.add(new EncodedStream(iTag, url, encoding)); 52 | } 53 | } 54 | 55 | public static void addEncodedStreams(JSONObject json, Collection collection, String urlEnconding) 56 | throws UnsupportedEncodingException { 57 | 58 | int iTag = json.getInteger("itag"); 59 | 60 | if (json.containsKey("signatureCipher")) { 61 | String cipher = json.getString("signatureCipher").replace("\\u0026", "&"); 62 | String[] pairs = cipher.split("&"); 63 | 64 | String encodedUrl = null; 65 | String signature = null; 66 | 67 | int equalsIndex; 68 | String key, value; 69 | for (String pair : pairs) { 70 | equalsIndex = pair.indexOf('='); 71 | key = pair.substring(0, equalsIndex); 72 | value = pair.substring(equalsIndex + 1); 73 | 74 | if (key.equals("url")) { 75 | encodedUrl = value; 76 | } else if (key.equals("s")) { 77 | signature = value; 78 | } 79 | } 80 | 81 | if (encodedUrl == null) { 82 | System.err.println("Encoded URL is null."); 83 | return; 84 | } 85 | encodedUrl = URLDecoder.decode(encodedUrl, urlEnconding); 86 | if (!encodedUrl.contains("signature") && !encodedUrl.contains("&sig=") && !encodedUrl.contains("&lsign=")) { 87 | if (signature != null) { 88 | signature = URLDecoder.decode(signature, urlEnconding); 89 | } 90 | collection.add(new EncodedStream(iTag, encodedUrl, signature)); 91 | } else { 92 | collection.add(new EncodedStream(iTag, encodedUrl)); 93 | } 94 | } else { 95 | if (!json.containsKey("url")) return; 96 | collection.add(new EncodedStream(iTag, json.getString("url"))); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/util/HTMLUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.net.HttpURLConnection; 8 | import java.net.URL; 9 | import java.net.URLConnection; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | public class HTMLUtils { 14 | 15 | public static String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0"; 16 | 17 | /** 18 | * Reads all the text from an {@link URL}. 19 | * 20 | * @param url the url. 21 | * @return the text. 22 | * @throws IOException whether an IO exception is thrown. 23 | */ 24 | public static String readAll(URL url) throws IOException { 25 | 26 | URLConnection connection = url.openConnection(); 27 | connection.setRequestProperty("User-Agent", USER_AGENT); 28 | connection.setDoInput(true); 29 | 30 | InputStream is = connection.getInputStream(); 31 | String enc = connection.getContentEncoding(); 32 | if (enc == null) { 33 | Pattern p = Pattern.compile("charset=(.*)"); 34 | Matcher m = p.matcher(connection.getHeaderField("Content-Type")); 35 | if (m.find()) enc = m.group(1); 36 | else enc = "UTF-8"; 37 | } 38 | 39 | BufferedReader reader = new BufferedReader(new InputStreamReader(is, enc)); 40 | 41 | String line; 42 | StringBuilder html = new StringBuilder(); 43 | while ((line = reader.readLine()) != null) { 44 | html.append(line).append("\n"); 45 | if (Thread.currentThread().isInterrupted()) 46 | throw new RuntimeException("HTML download has been interrupted."); 47 | } 48 | 49 | return html.toString(); 50 | } 51 | 52 | /** 53 | * Checks if a connection is valid. 54 | * 55 | * @param c the connection. 56 | * @throws IOException if the connection is not valid. 57 | */ 58 | public static void check(HttpURLConnection c) throws IOException { 59 | int code = c.getResponseCode(); 60 | String message = c.getResponseMessage(); 61 | 62 | switch (code) { 63 | case HttpURLConnection.HTTP_OK: 64 | case HttpURLConnection.HTTP_PARTIAL: 65 | return; 66 | case HttpURLConnection.HTTP_MOVED_TEMP: 67 | case HttpURLConnection.HTTP_MOVED_PERM: 68 | // rfc2616: the user agent MUST NOT automatically redirect the 69 | // request unless it can be confirmed by the user 70 | throw new RuntimeException("Download moved" + " (" + message + ")"); 71 | case HttpURLConnection.HTTP_PROXY_AUTH: 72 | throw new RuntimeException("Proxy auth" + " (" + message + ")"); 73 | case HttpURLConnection.HTTP_FORBIDDEN: 74 | throw new RuntimeException("Http forbidden: " + code + " (" + message + ")"); 75 | case 416: 76 | throw new RuntimeException("Requested range nt satisfiable" + " (" + message + ")"); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/util/IdExtractor.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.util; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.exception.InvalidYoutubeURL; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class IdExtractor { 9 | 10 | private static final Pattern NORMAL_PATTERN = Pattern.compile("youtube.com/watch?.*v=([^&]*)"); 11 | private static final Pattern SHORTENED_PATTERN = Pattern.compile("youtube.com/v/([^&]*)"); 12 | 13 | /** 14 | * Extracts the id of a youtube video by its url. 15 | * 16 | * @param url the url. 17 | * @return the id. 18 | * @throws InvalidYoutubeURL whether the url is invalid. 19 | */ 20 | public static String extractId(String url) { 21 | Matcher um = NORMAL_PATTERN.matcher(url); 22 | if (um.find()) 23 | return um.group(1); 24 | um = SHORTENED_PATTERN.matcher(url); 25 | if (um.find()) 26 | return um.group(1); 27 | throw new InvalidYoutubeURL(url); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/util/NumericUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.util; 2 | 3 | /** 4 | * An util class for numbers. 5 | */ 6 | public class NumericUtils { 7 | 8 | public static boolean isInteger(String s) { 9 | try { 10 | Integer.parseInt(s); 11 | return true; 12 | } catch (Throwable ex) { 13 | return false; 14 | } 15 | } 16 | 17 | 18 | public static boolean isDouble(String s) { 19 | try { 20 | Double.parseDouble(s); 21 | return true; 22 | } catch (Throwable ex) { 23 | return false; 24 | } 25 | } 26 | 27 | 28 | public static boolean isFloat(String s) { 29 | try { 30 | Float.parseFloat(s); 31 | return true; 32 | } catch (Throwable ex) { 33 | return false; 34 | } 35 | } 36 | 37 | 38 | public static boolean isShort(String s) { 39 | try { 40 | Short.parseShort(s); 41 | return true; 42 | } catch (Throwable ex) { 43 | return false; 44 | } 45 | } 46 | 47 | 48 | public static boolean isLong(String s) { 49 | try { 50 | Long.parseLong(s); 51 | return true; 52 | } catch (Throwable ex) { 53 | return false; 54 | } 55 | } 56 | 57 | 58 | public static boolean isByte(String s) { 59 | try { 60 | Byte.parseByte(s); 61 | return true; 62 | } catch (Throwable ex) { 63 | return false; 64 | } 65 | } 66 | 67 | 68 | public static String toRomanNumeral(int input) { 69 | if (input < 1 || input > 3999) return "X"; 70 | StringBuilder s = new StringBuilder(); 71 | while (input >= 1000) { 72 | s.append("M"); 73 | input -= 1000; 74 | } 75 | while (input >= 900) { 76 | s.append("CM"); 77 | input -= 900; 78 | } 79 | while (input >= 500) { 80 | s.append("D"); 81 | input -= 500; 82 | } 83 | while (input >= 400) { 84 | s.append("CD"); 85 | input -= 400; 86 | } 87 | while (input >= 100) { 88 | s.append("C"); 89 | input -= 100; 90 | } 91 | while (input >= 90) { 92 | s.append("XC"); 93 | input -= 90; 94 | } 95 | while (input >= 50) { 96 | s.append("L"); 97 | input -= 50; 98 | } 99 | while (input >= 40) { 100 | s.append("XL"); 101 | input -= 40; 102 | } 103 | while (input >= 10) { 104 | s.append("X"); 105 | input -= 10; 106 | } 107 | while (input >= 9) { 108 | s.append("IX"); 109 | input -= 9; 110 | } 111 | while (input >= 5) { 112 | s.append("V"); 113 | input -= 5; 114 | } 115 | while (input >= 4) { 116 | s.append("IV"); 117 | input -= 4; 118 | } 119 | while (input >= 1) { 120 | s.append("I"); 121 | input -= 1; 122 | } 123 | return s.toString(); 124 | } 125 | 126 | 127 | public static int floor(double num) { 128 | int floor = (int) num; 129 | return (double) floor == num ? floor : floor - (int) (Double.doubleToRawLongBits(num) >>> 63); 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/util/PlayerResponseUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.util; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import io.github.gaeqs.javayoutubedownloader.stream.EncodedStream; 6 | 7 | import java.net.URLDecoder; 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | public class PlayerResponseUtils { 13 | 14 | public static final String STREAMING_DATA_JSON_PARAMETER = "streamingData"; 15 | public static final String FORMATS_JSON_PARAMETER = "formats"; 16 | 17 | public static void addPlayerResponseStreams(String json, Collection streams, String urlEncoding) { 18 | //Player response data. 19 | JSONObject obj = JSONObject.parseObject(json); 20 | if (obj.containsKey(STREAMING_DATA_JSON_PARAMETER)) { 21 | obj = obj.getJSONObject(STREAMING_DATA_JSON_PARAMETER); 22 | if (obj.containsKey(FORMATS_JSON_PARAMETER)) { 23 | addJSONStreams(obj.getJSONArray(FORMATS_JSON_PARAMETER), streams, urlEncoding); 24 | } 25 | 26 | } 27 | } 28 | 29 | 30 | private static void addJSONStreams(JSONArray array, Collection streams, String urlEncoding) { 31 | array.forEach(target -> { 32 | if (!(target instanceof JSONObject)) return; 33 | JSONObject obj = (JSONObject) target; 34 | try { 35 | if (obj.containsKey("cipher")) { 36 | List list = new ArrayList<>(); 37 | EncodedStreamUtils.addEncodedStreams(URLDecoder.decode(obj.getString("signatureCipher"), urlEncoding), list, urlEncoding); 38 | list.forEach(stream -> System.out.println(stream.getUrl() + "\n - " + stream.getSignature().orElse(null))); 39 | streams.addAll(list); 40 | } 41 | if (obj.containsKey("url")) { 42 | int iTag = obj.getInteger("itag"); 43 | String url = URLDecoder.decode(obj.getString("url"), urlEncoding); 44 | streams.add(new EncodedStream(iTag, url)); 45 | } 46 | } catch (Exception e) { 47 | System.err.println(obj); 48 | System.err.println("Error while parsing url."); 49 | e.printStackTrace(); 50 | } 51 | }); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/github/gaeqs/javayoutubedownloader/util/Validate.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader.util; 2 | 3 | import java.util.Collection; 4 | import java.util.Iterator; 5 | import java.util.Map; 6 | 7 | public class Validate { 8 | public Validate() { 9 | } 10 | 11 | public static void isTrue(boolean expression, String message, Object value) { 12 | if (!expression) { 13 | throw new IllegalArgumentException(message + value); 14 | } 15 | } 16 | 17 | public static void isTrue(boolean expression, String message, long value) { 18 | if (!expression) { 19 | throw new IllegalArgumentException(message + value); 20 | } 21 | } 22 | 23 | public static void isTrue(boolean expression, String message, double value) { 24 | if (!expression) { 25 | throw new IllegalArgumentException(message + value); 26 | } 27 | } 28 | 29 | public static void isTrue(boolean expression, String message) { 30 | if (!expression) { 31 | throw new IllegalArgumentException(message); 32 | } 33 | } 34 | 35 | public static void isTrue(boolean expression) { 36 | if (!expression) { 37 | throw new IllegalArgumentException("The validated expression is false"); 38 | } 39 | } 40 | 41 | public static void notNull(Object object) { 42 | notNull(object, "The validated object is null"); 43 | } 44 | 45 | public static void notNull(Object object, String message) { 46 | if (object == null) { 47 | throw new IllegalArgumentException(message); 48 | } 49 | } 50 | 51 | public static void notEmpty(Object[] array, String message) { 52 | if (array == null || array.length == 0) { 53 | throw new IllegalArgumentException(message); 54 | } 55 | } 56 | 57 | public static void notEmpty(Object[] array) { 58 | notEmpty(array, "The validated array is empty"); 59 | } 60 | 61 | public static void notEmpty(Collection collection, String message) { 62 | if (collection == null || collection.size() == 0) { 63 | throw new IllegalArgumentException(message); 64 | } 65 | } 66 | 67 | public static void notEmpty(Collection collection) { 68 | notEmpty(collection, "The validated collection is empty"); 69 | } 70 | 71 | public static void notEmpty(Map map, String message) { 72 | if (map == null || map.size() == 0) { 73 | throw new IllegalArgumentException(message); 74 | } 75 | } 76 | 77 | public static void notEmpty(Map map) { 78 | notEmpty(map, "The validated map is empty"); 79 | } 80 | 81 | public static void notEmpty(String string, String message) { 82 | if (string == null || string.length() == 0) { 83 | throw new IllegalArgumentException(message); 84 | } 85 | } 86 | 87 | public static void notEmpty(String string) { 88 | notEmpty(string, "The validated string is empty"); 89 | } 90 | 91 | public static void noNullElements(Object[] array, String message) { 92 | notNull(array); 93 | Object[] var2 = array; 94 | int var3 = array.length; 95 | 96 | for (int var4 = 0; var4 < var3; ++var4) { 97 | Object anArray = var2[var4]; 98 | if (anArray == null) { 99 | throw new IllegalArgumentException(message); 100 | } 101 | } 102 | 103 | } 104 | 105 | public static void noNullElements(Object[] array) { 106 | notNull(array); 107 | 108 | for (int i = 0; i < array.length; ++i) { 109 | if (array[i] == null) { 110 | throw new IllegalArgumentException("The validated array contains null element at index: " + i); 111 | } 112 | } 113 | 114 | } 115 | 116 | public static void noNullElements(Collection collection, String message) { 117 | notNull(collection); 118 | Iterator it = collection.iterator(); 119 | 120 | while (it.hasNext()) { 121 | if (it.next() == null) { 122 | throw new IllegalArgumentException(message); 123 | } 124 | } 125 | 126 | } 127 | 128 | public static void noNullElements(Collection collection) { 129 | notNull(collection); 130 | int i = 0; 131 | 132 | for (Iterator it = collection.iterator(); it.hasNext(); ++i) { 133 | if (it.next() == null) { 134 | throw new IllegalArgumentException("The validated collection contains null element at index: " + i); 135 | } 136 | } 137 | 138 | } 139 | 140 | public static void allElementsOfType(Collection collection, Class clazz, String message) { 141 | notNull(collection); 142 | notNull(clazz); 143 | Iterator it = collection.iterator(); 144 | 145 | while (it.hasNext()) { 146 | if (!clazz.isInstance(it.next())) { 147 | throw new IllegalArgumentException(message); 148 | } 149 | } 150 | 151 | } 152 | 153 | public static void allElementsOfType(Collection collection, Class clazz) { 154 | notNull(collection); 155 | notNull(clazz); 156 | int i = 0; 157 | 158 | for (Iterator it = collection.iterator(); it.hasNext(); ++i) { 159 | if (!clazz.isInstance(it.next())) { 160 | throw new IllegalArgumentException("The validated collection contains an element not of type " + clazz.getName() + " at index: " + i); 161 | } 162 | } 163 | 164 | } 165 | } 166 | 167 | -------------------------------------------------------------------------------- /src/main/test/io/github/gaeqs/javayoutubedownloader/JavaYoutubeDownloaderTest.java: -------------------------------------------------------------------------------- 1 | package io.github.gaeqs.javayoutubedownloader; 2 | 3 | import io.github.gaeqs.javayoutubedownloader.decoder.MultipleDecoderMethod; 4 | import io.github.gaeqs.javayoutubedownloader.stream.YoutubeVideo; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.net.MalformedURLException; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | class JavaYoutubeDownloaderTest { 13 | 14 | private static final int SLEEP = 0; 15 | 16 | @Test 17 | public void htmlTest() throws MalformedURLException, InterruptedException { 18 | Thread.sleep(SLEEP); 19 | YoutubeVideo video = JavaYoutubeDownloader.decode("https://www.youtube.com/watch?v=PNK8TmaRSQY", 20 | MultipleDecoderMethod.AND, "html"); 21 | assertNotNull(video, "Video is null."); 22 | assertFalse(video.getStreamOptions().isEmpty(), "Video options list is empty."); 23 | System.out.println("URLs:"); 24 | video.getStreamOptions().forEach(target -> System.out.println(target.getType() + "\n" + target.getUrl())); 25 | } 26 | 27 | @Test 28 | public void embeddedTest() throws MalformedURLException, InterruptedException { 29 | Thread.sleep(SLEEP); 30 | YoutubeVideo video = JavaYoutubeDownloader.decode("https://www.youtube.com/watch?v=PNK8TmaRSQY", 31 | MultipleDecoderMethod.AND, "embedded"); 32 | assertNotNull(video, "Video is null."); 33 | assertFalse(video.getStreamOptions().isEmpty(), "Video options list is empty."); 34 | video.getStreamOptions().forEach(target -> System.out.println(target.getType() + "\n" + target.getUrl())); 35 | } 36 | 37 | @Test 38 | public void htmlTestOldVideo() throws MalformedURLException, InterruptedException { 39 | Thread.sleep(SLEEP); 40 | YoutubeVideo video = JavaYoutubeDownloader.decode("https://www.youtube.com/watch?v=l_jUBScR1RA", 41 | MultipleDecoderMethod.AND, "html"); 42 | assertNotNull(video, "Video is null."); 43 | assertFalse(video.getStreamOptions().isEmpty(), "Video options list is empty."); 44 | System.out.println("URLs:"); 45 | video.getStreamOptions().forEach(target -> System.out.println(target.getType() + "\n" + target.getUrl())); 46 | } 47 | 48 | @Test 49 | public void embeddedTestOldVideo() throws MalformedURLException, InterruptedException { 50 | Thread.sleep(SLEEP); 51 | YoutubeVideo video = JavaYoutubeDownloader.decode("https://www.youtube.com/watch?v=l_jUBScR1RA", 52 | MultipleDecoderMethod.AND, "embedded"); 53 | assertNotNull(video, "Video is null."); 54 | assertFalse(video.getStreamOptions().isEmpty(), "Video options list is empty."); 55 | video.getStreamOptions().forEach(target -> System.out.println(target.getType() + "\n" + target.getUrl())); 56 | } 57 | 58 | @Test 59 | public void htmlTestProtected() throws MalformedURLException, InterruptedException { 60 | Thread.sleep(SLEEP); 61 | YoutubeVideo video = JavaYoutubeDownloader.decode("https://www.youtube.com/watch?v=kJQP7kiw5Fk", 62 | MultipleDecoderMethod.AND, "html"); 63 | assertNotNull(video, "Video is null."); 64 | assertFalse(video.getStreamOptions().isEmpty(), "Video options list is empty."); 65 | video.getStreamOptions().forEach(target -> System.out.println(target.getType() + "\n" + target.getUrl())); 66 | } 67 | 68 | @Test 69 | public void embeddedTestProtected() throws MalformedURLException, InterruptedException { 70 | Thread.sleep(SLEEP); 71 | YoutubeVideo video = JavaYoutubeDownloader.decode("https://www.youtube.com/watch?v=Nx-DvH41Tjo", 72 | MultipleDecoderMethod.AND, "embedded"); 73 | assertNotNull(video, "Video is null."); 74 | assertFalse(video.getStreamOptions().isEmpty(), "Video options list is empty."); 75 | video.getStreamOptions().forEach(target -> System.out.println(target.getType() + "\n" + target.getUrl())); 76 | } 77 | 78 | } --------------------------------------------------------------------------------