├── .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 | }
--------------------------------------------------------------------------------