From 3b0870665b1e87ac84590ebaea2ad0bc008a7043 Mon Sep 17 00:00:00 2001 From: Igor K Date: Sat, 11 Jan 2020 15:47:57 +0200 Subject: [PATCH] add/fix Itags; bug fixes and refactoring; added some tests --- ...ck.java => OnYoutubeDownloadListener.java} | 2 +- .../kiulian/downloader/YoutubeDownloader.java | 9 -- .../cipher/CachedCipherFactory.java | 21 ++- .../downloader/cipher/CipherFactory.java | 2 +- .../extractor/DefaultExtractor.java | 47 +++--- .../downloader/extractor/Extractor.java | 8 +- .../github/kiulian/downloader/model/Itag.java | 97 +++++++------ .../downloader/model/VideoDetails.java | 12 +- .../downloader/model/YoutubeVideo.java | 11 +- .../model/formats/AudioVideoFormat.java | 4 - .../downloader/model/formats/Format.java | 19 +-- .../model/quality/VideoQuality.java | 1 + .../downloader/parser/DefaultParser.java | 7 +- .../downloader/YoutubeDownloader_Tests.java | 136 ++++++++++++++++++ 14 files changed, 256 insertions(+), 120 deletions(-) rename src/main/java/com/github/kiulian/downloader/{YoutubeDownloadCallback.java => OnYoutubeDownloadListener.java} (94%) create mode 100644 src/test/java/com/github/kiulian/downloader/YoutubeDownloader_Tests.java diff --git a/src/main/java/com/github/kiulian/downloader/YoutubeDownloadCallback.java b/src/main/java/com/github/kiulian/downloader/OnYoutubeDownloadListener.java similarity index 94% rename from src/main/java/com/github/kiulian/downloader/YoutubeDownloadCallback.java rename to src/main/java/com/github/kiulian/downloader/OnYoutubeDownloadListener.java index a0c493e..f3eea44 100644 --- a/src/main/java/com/github/kiulian/downloader/YoutubeDownloadCallback.java +++ b/src/main/java/com/github/kiulian/downloader/OnYoutubeDownloadListener.java @@ -22,7 +22,7 @@ import java.io.File; -public interface YoutubeDownloadCallback { +public interface OnYoutubeDownloadListener { void onDownloading(int progress); diff --git a/src/main/java/com/github/kiulian/downloader/YoutubeDownloader.java b/src/main/java/com/github/kiulian/downloader/YoutubeDownloader.java index 80e5d99..09aac6c 100644 --- a/src/main/java/com/github/kiulian/downloader/YoutubeDownloader.java +++ b/src/main/java/com/github/kiulian/downloader/YoutubeDownloader.java @@ -39,15 +39,6 @@ public class YoutubeDownloader { public static final char[] ILLEGAL_FILENAME_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'}; - public interface DownloadCallback { - - void onDownloading(int progress); - - void onFinished(File file); - - void onError(Throwable throwable); - } - private Parser parser; public YoutubeDownloader() { diff --git a/src/main/java/com/github/kiulian/downloader/cipher/CachedCipherFactory.java b/src/main/java/com/github/kiulian/downloader/cipher/CachedCipherFactory.java index e582682..8dfac47 100644 --- a/src/main/java/com/github/kiulian/downloader/cipher/CachedCipherFactory.java +++ b/src/main/java/com/github/kiulian/downloader/cipher/CachedCipherFactory.java @@ -82,16 +82,11 @@ public void clearCache() { } @Override - public Cipher createCipher(String jsUrl) throws YoutubeException.CipherException { + public Cipher createCipher(String jsUrl) throws YoutubeException { Cipher cipher = ciphers.get(jsUrl); if (cipher == null) { - String js; - try { - js = extractor.loadUrl(jsUrl); - } catch (IOException e) { - throw new YoutubeException.CipherException("Could not load js file: " + jsUrl); - } + String js = extractor.loadUrl(jsUrl); List transformFunctions = getTransformFunctions(js); String var = transformFunctions.get(0).getVar(); @@ -105,7 +100,7 @@ public Cipher createCipher(String jsUrl) throws YoutubeException.CipherException return cipher; } - private List getTransformFunctions(String js) throws YoutubeException.CipherException { + private List getTransformFunctions(String js) throws YoutubeException { String name = getInitialFunctionName(js).replaceAll("[^A-Za-z0-9_]", ""); Pattern pattern = Pattern.compile(name + "=function\\(\\w\\)\\{[a-z=\\.\\(\\\"\\)]*;(.*);(?:.+)}"); @@ -129,7 +124,7 @@ private List getTransformFunctions(String js) throws YoutubeExceptio throw new YoutubeException.CipherException("Transformation functions not found"); } - private String getInitialFunctionName(String js) throws YoutubeException.CipherException { + private String getInitialFunctionName(String js) throws YoutubeException { for (Pattern pattern : knownInitialFunctionPatterns) { Matcher matcher = pattern.matcher(js); if (matcher.find()) { @@ -140,7 +135,7 @@ private String getInitialFunctionName(String js) throws YoutubeException.CipherE throw new YoutubeException.CipherException("Initial function name not found"); } - private Map getTransformFunctionsMap(String var, String js) throws YoutubeException.CipherException { + private Map getTransformFunctionsMap(String var, String js) throws YoutubeException { String[] transformObject = getTransformObject(var, js); Map mapper = new HashMap<>(); for (String obj : transformObject) { @@ -154,7 +149,7 @@ private Map getTransformFunctionsMap(String var, String return mapper; } - private String[] getTransformObject(String var, String js) throws YoutubeException.CipherException { + private String[] getTransformObject(String var, String js) throws YoutubeException { var = var.replaceAll("[^A-Za-z0-9_]", ""); Pattern pattern = Pattern.compile(String.format("var %s=\\{(.*?)};", var), Pattern.DOTALL); Matcher matcher = pattern.matcher(js); @@ -166,7 +161,7 @@ private String[] getTransformObject(String var, String js) throws YoutubeExcepti } - private CipherFunction mapFunction(String jsFunction) throws YoutubeException.CipherException { + private CipherFunction mapFunction(String jsFunction) throws YoutubeException { for (Map.Entry entry : functionsEquivalentMap.entrySet()) { Matcher matcher = entry.getKey().matcher(jsFunction); if (matcher.find()) { @@ -177,7 +172,7 @@ private CipherFunction mapFunction(String jsFunction) throws YoutubeException.Ci throw new YoutubeException.CipherException("Map function not found"); } - private String[] parseFunction(String jsFunction) throws YoutubeException.CipherException { + private String[] parseFunction(String jsFunction) throws YoutubeException { Matcher matcher = JS_FUNCTION_PATTERN.matcher(jsFunction); String[] nameAndArgument = new String[2]; diff --git a/src/main/java/com/github/kiulian/downloader/cipher/CipherFactory.java b/src/main/java/com/github/kiulian/downloader/cipher/CipherFactory.java index a8f51c5..8f8e491 100644 --- a/src/main/java/com/github/kiulian/downloader/cipher/CipherFactory.java +++ b/src/main/java/com/github/kiulian/downloader/cipher/CipherFactory.java @@ -24,5 +24,5 @@ public interface CipherFactory { - Cipher createCipher(String jsUrl) throws YoutubeException.CipherException; + Cipher createCipher(String jsUrl) throws YoutubeException; } diff --git a/src/main/java/com/github/kiulian/downloader/extractor/DefaultExtractor.java b/src/main/java/com/github/kiulian/downloader/extractor/DefaultExtractor.java index 5774d34..404588e 100644 --- a/src/main/java/com/github/kiulian/downloader/extractor/DefaultExtractor.java +++ b/src/main/java/com/github/kiulian/downloader/extractor/DefaultExtractor.java @@ -20,7 +20,6 @@ * # */ -import com.alibaba.fastjson.JSONObject; import com.github.kiulian.downloader.YoutubeException; import java.io.BufferedReader; @@ -38,20 +37,28 @@ public class DefaultExtractor implements Extractor { private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"; private static final String DEFAULT_ACCEPT_LANG = "en-US,en;"; + private static final int DEFAULT_RETRY_ON_FAILURE = 3; private Map requestProperties = new HashMap<>(); + private int retryOnFailure; public DefaultExtractor() { setRequestProperty("User-Agent", DEFAULT_USER_AGENT); setRequestProperty("Accept_language", DEFAULT_ACCEPT_LANG); + retryOnFailure = DEFAULT_RETRY_ON_FAILURE; } + public void setRequestProperty(String key, String value) { requestProperties.put(key, value); } + public void setRetryOnFailure(int retryOnFailure) { + this.retryOnFailure = retryOnFailure; + } + @Override - public String extractYtPlayerConfig(String html) throws YoutubeException.BadPageException { + public String extractYtPlayerConfig(String html) throws YoutubeException { Matcher matcher = YT_PLAYER_CONFIG.matcher(html); if (matcher.find()) { @@ -62,20 +69,26 @@ public String extractYtPlayerConfig(String html) throws YoutubeException.BadPage } @Override - public String loadUrl(String url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - requestProperties.forEach(connection::setRequestProperty); - - BufferedReader in = new BufferedReader(new InputStreamReader( - connection.getInputStream())); - - StringBuilder sb = new StringBuilder(); - String inputLine; - while ((inputLine = in.readLine()) != null) - sb.append(inputLine).append('\n'); - in.close(); - - return sb.toString(); + public String loadUrl(String url) throws YoutubeException { + int retryCount = retryOnFailure; + while (retryCount > 0) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + requestProperties.forEach(connection::setRequestProperty); + + BufferedReader in = new BufferedReader(new InputStreamReader( + connection.getInputStream())); + + StringBuilder sb = new StringBuilder(); + String inputLine; + while ((inputLine = in.readLine()) != null) + sb.append(inputLine).append('\n'); + in.close(); + return sb.toString(); + } catch (IOException e) { + retryCount--; + } + } + throw new YoutubeException.VideoUnavailableException("Could not load url: " + url); } - } diff --git a/src/main/java/com/github/kiulian/downloader/extractor/Extractor.java b/src/main/java/com/github/kiulian/downloader/extractor/Extractor.java index b1f78b6..796ef86 100644 --- a/src/main/java/com/github/kiulian/downloader/extractor/Extractor.java +++ b/src/main/java/com/github/kiulian/downloader/extractor/Extractor.java @@ -9,9 +9,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,13 +23,11 @@ import com.github.kiulian.downloader.YoutubeException; -import java.io.IOException; - public interface Extractor { String extractYtPlayerConfig(String html) throws YoutubeException; - String loadUrl(String url) throws IOException; + String loadUrl(String url) throws YoutubeException; } diff --git a/src/main/java/com/github/kiulian/downloader/model/Itag.java b/src/main/java/com/github/kiulian/downloader/model/Itag.java index 4bf9b7f..419131f 100644 --- a/src/main/java/com/github/kiulian/downloader/model/Itag.java +++ b/src/main/java/com/github/kiulian/downloader/model/Itag.java @@ -45,50 +45,59 @@ public void setId(int id) { i37(VideoQuality.hd1080, AudioQuality.medium), i38(VideoQuality.highres, AudioQuality.medium), - i43(VideoQuality.medium, AudioQuality.unknown), - i44(VideoQuality.large, AudioQuality.unknown), - i45(VideoQuality.hd720, AudioQuality.unknown), - i46(VideoQuality.hd1080, AudioQuality.unknown), - - i82(VideoQuality.medium, AudioQuality.unknown, true), - i83(VideoQuality.large, AudioQuality.unknown, true), - i84(VideoQuality.hd720, AudioQuality.unknown, true), - i85(VideoQuality.hd1080, AudioQuality.unknown, true), - - i92(VideoQuality.small, AudioQuality.unknown, true), - i93(VideoQuality.medium, AudioQuality.unknown, true), - i94(VideoQuality.large, AudioQuality.unknown, true), - i95(VideoQuality.hd720, AudioQuality.unknown, true), - i96(VideoQuality.hd1080, AudioQuality.unknown), - - i100(VideoQuality.medium, AudioQuality.unknown, true), - i101(VideoQuality.large, AudioQuality.unknown, true), - i102(VideoQuality.hd720, AudioQuality.unknown, true), - - i132(VideoQuality.small, AudioQuality.unknown), + i43(VideoQuality.medium, AudioQuality.medium), + i44(VideoQuality.large, AudioQuality.medium), + i45(VideoQuality.hd720, AudioQuality.medium), + i46(VideoQuality.hd1080, AudioQuality.medium), + + // 3D videos + i82(VideoQuality.medium, AudioQuality.medium, true), + i83(VideoQuality.large, AudioQuality.medium, true), + i84(VideoQuality.hd720, AudioQuality.medium, true), + i85(VideoQuality.hd1080, AudioQuality.medium, true), + i100(VideoQuality.medium, AudioQuality.medium, true), + i101(VideoQuality.large, AudioQuality.medium, true), + i102(VideoQuality.hd720, AudioQuality.medium, true), + + // Apple HTTP Live Streaming + i91(VideoQuality.tiny, AudioQuality.low), + i92(VideoQuality.small, AudioQuality.low), + i93(VideoQuality.medium, AudioQuality.medium), + i94(VideoQuality.large, AudioQuality.medium), + i95(VideoQuality.hd720, AudioQuality.high), + i96(VideoQuality.hd1080, AudioQuality.high), + i132(VideoQuality.small, AudioQuality.low), + i151(VideoQuality.tiny, AudioQuality.low), + + // DASH mp4 video i133(VideoQuality.small), i134(VideoQuality.medium), i135(VideoQuality.large), i136(VideoQuality.hd720), i137(VideoQuality.hd1080), i138(VideoQuality.hd2160), + i160(VideoQuality.tiny), + i212(VideoQuality.large), + i264(VideoQuality.hd1440), + i266(VideoQuality.hd2160), + i298(VideoQuality.hd720), + i299(VideoQuality.hd1080), + + // DASH mp4 audio i139(AudioQuality.low), i140(AudioQuality.medium), i141(AudioQuality.high), + i256(AudioQuality.unknown), + i325(AudioQuality.unknown), + i328(AudioQuality.unknown), - i151(VideoQuality.tiny, AudioQuality.unknown), - - i160(VideoQuality.tiny), + // DASH webm video i167(VideoQuality.medium), i168(VideoQuality.large), - i169(VideoQuality.hd1080), - - i171(AudioQuality.medium), - - + i169(VideoQuality.hd720), + i170(VideoQuality.hd1080), i218(VideoQuality.large), i219(VideoQuality.tiny), - i242(VideoQuality.small), i243(VideoQuality.medium), i244(VideoQuality.large), @@ -96,27 +105,25 @@ public void setId(int id) { i246(VideoQuality.large), i247(VideoQuality.hd720), i248(VideoQuality.hd1080), - i249(AudioQuality.low), - i250(AudioQuality.medium), - i251(AudioQuality.medium), - - i264(VideoQuality.hd1440), - i266(VideoQuality.hd2160), - i271(VideoQuality.hd1440), i272(VideoQuality.highres), i278(VideoQuality.tiny), - - i298(VideoQuality.hd720), - i299(VideoQuality.hd1080), - i302(VideoQuality.hd720), i303(VideoQuality.hd1080), i308(VideoQuality.hd1440), - i313(VideoQuality.hd2160), i315(VideoQuality.hd2160), + // DASH webm audio + i171(AudioQuality.medium), + i172(AudioQuality.high), + + // Dash webm audio with opus inside + i249(AudioQuality.low), + i250(AudioQuality.low), + i251(AudioQuality.medium), + + // Dash webm hdr video i330(VideoQuality.tiny), i331(VideoQuality.small), i332(VideoQuality.medium), @@ -126,14 +133,16 @@ public void setId(int id) { i336(VideoQuality.hd1440), i337(VideoQuality.hd2160), - + // av01 video only formats i394(VideoQuality.tiny), i395(VideoQuality.small), i396(VideoQuality.medium), i397(VideoQuality.large), i398(VideoQuality.hd720), i399(VideoQuality.hd1080), - + i400(VideoQuality.hd1440), + i401(VideoQuality.hd2160), + i402(VideoQuality.hd2880p) ; diff --git a/src/main/java/com/github/kiulian/downloader/model/VideoDetails.java b/src/main/java/com/github/kiulian/downloader/model/VideoDetails.java index 5286268..30737bb 100644 --- a/src/main/java/com/github/kiulian/downloader/model/VideoDetails.java +++ b/src/main/java/com/github/kiulian/downloader/model/VideoDetails.java @@ -37,7 +37,7 @@ public class VideoDetails { private String shortDescription; private List thumbnails; private String author; - private int viewCount; + private long viewCount; private int averageRating; private boolean isLiveContent; @@ -47,7 +47,7 @@ public VideoDetails() { public VideoDetails(JSONObject json) { videoId = json.getString("videoId"); title = json.getString("title"); - lengthSeconds = json.getInteger("lengthSeconds"); + lengthSeconds = json.getIntValue("lengthSeconds"); keywords = json.containsKey("keywords") ? json.getJSONArray("keywords").toJavaList(String.class) : Collections.emptyList(); shortDescription = json.getString("shortDescription"); JSONArray jsonThumbnails = json.getJSONObject("thumbnail").getJSONArray("thumbnails"); @@ -57,10 +57,10 @@ public VideoDetails(JSONObject json) { if (jsonObject.containsKey("url")) thumbnails.add(jsonObject.getString("url")); } - averageRating = json.getInteger("averageRating"); - viewCount = json.getInteger("viewCount"); + averageRating = json.getIntValue("averageRating"); + viewCount = json.getLongValue("viewCount"); author = json.getString("author"); - isLiveContent = json.getBoolean("isLiveContent"); + isLiveContent = json.getBooleanValue("isLiveContent"); } public String videoId() { @@ -91,7 +91,7 @@ public String author() { return author; } - public int viewCount() { + public long viewCount() { return viewCount; } diff --git a/src/main/java/com/github/kiulian/downloader/model/YoutubeVideo.java b/src/main/java/com/github/kiulian/downloader/model/YoutubeVideo.java index 9f6edb6..0b69a77 100644 --- a/src/main/java/com/github/kiulian/downloader/model/YoutubeVideo.java +++ b/src/main/java/com/github/kiulian/downloader/model/YoutubeVideo.java @@ -21,6 +21,7 @@ */ +import com.github.kiulian.downloader.OnYoutubeDownloadListener; import com.github.kiulian.downloader.YoutubeDownloader; import com.github.kiulian.downloader.YoutubeException; import com.github.kiulian.downloader.model.formats.AudioFormat; @@ -30,9 +31,7 @@ import com.github.kiulian.downloader.model.quality.VideoQuality; import java.io.*; -import java.net.HttpURLConnection; import java.net.URL; -import java.net.URLConnection; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -156,7 +155,7 @@ public File download(Format format, File outDir) throws IOException, YoutubeExce return outputFile; } - public void downloadAsync(Format format, File outDir, YoutubeDownloader.DownloadCallback callback) throws IOException, YoutubeException { + public void downloadAsync(Format format, File outDir, OnYoutubeDownloadListener listener) throws IOException, YoutubeException { if (videoDetails.isLive()) throw new YoutubeException.LiveVideoException("Can not download live stream"); @@ -192,13 +191,13 @@ public void downloadAsync(Format format, File outDir, YoutubeDownloader.Download int newProgress = (int) ((total / format.contentLength()) * 100); if (newProgress > progress) { progress = newProgress; - callback.onDownloading(progress); + listener.onDownloading(progress); } } - callback.onFinished(finalOutputFile); + listener.onFinished(finalOutputFile); } catch (IOException e) { - callback.onError(e); + listener.onError(e); } } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/github/kiulian/downloader/model/formats/AudioVideoFormat.java b/src/main/java/com/github/kiulian/downloader/model/formats/AudioVideoFormat.java index 20ddb4d..941e1be 100644 --- a/src/main/java/com/github/kiulian/downloader/model/formats/AudioVideoFormat.java +++ b/src/main/java/com/github/kiulian/downloader/model/formats/AudioVideoFormat.java @@ -54,8 +54,4 @@ public Integer height() { return height; } - public AudioQuality audioQuality() { - return itag.audioQuality(); - } - } diff --git a/src/main/java/com/github/kiulian/downloader/model/formats/Format.java b/src/main/java/com/github/kiulian/downloader/model/formats/Format.java index 68b1bea..1cb32b5 100644 --- a/src/main/java/com/github/kiulian/downloader/model/formats/Format.java +++ b/src/main/java/com/github/kiulian/downloader/model/formats/Format.java @@ -32,16 +32,17 @@ public abstract class Format { public static final String AUDIO_VIDEO = "audio/video"; - protected Itag itag; - private final String url; - private final String mimeType; - private final Extension extension; - private final Integer bitrate; - private final Long contentLength; - private final Long lastModified; - private final Long approxDurationMs; + protected final Itag itag; + protected final String url; + protected final String mimeType; + protected final Extension extension; + protected final Integer bitrate; + protected final Long contentLength; + protected final Long lastModified; + protected final Long approxDurationMs; protected Format(JSONObject json) { + Itag itag; try { itag = Itag.valueOf("i" + json.getInteger("itag")); } catch (ExceptionInInitializerError e) { @@ -49,6 +50,8 @@ protected Format(JSONObject json) { itag = Itag.unknown; itag.setId(json.getIntValue("itag")); } + this.itag = itag; + url = json.getString("url").replace("\\u0026", "&"); mimeType = json.getString("mimeType"); bitrate = json.getInteger("bitrate"); diff --git a/src/main/java/com/github/kiulian/downloader/model/quality/VideoQuality.java b/src/main/java/com/github/kiulian/downloader/model/quality/VideoQuality.java index 677d3f2..9d50109 100644 --- a/src/main/java/com/github/kiulian/downloader/model/quality/VideoQuality.java +++ b/src/main/java/com/github/kiulian/downloader/model/quality/VideoQuality.java @@ -23,6 +23,7 @@ public enum VideoQuality { unknown, highres, // 3072p + hd2880p, hd2160, hd1440, hd1080, diff --git a/src/main/java/com/github/kiulian/downloader/parser/DefaultParser.java b/src/main/java/com/github/kiulian/downloader/parser/DefaultParser.java index 854e1bf..3885877 100644 --- a/src/main/java/com/github/kiulian/downloader/parser/DefaultParser.java +++ b/src/main/java/com/github/kiulian/downloader/parser/DefaultParser.java @@ -52,12 +52,7 @@ public DefaultParser(Extractor extractor, CipherFactory cipherFactory) { @Override public JSONObject getPlayerConfig(String htmlUrl) throws YoutubeException { - String html; - try { - html = extractor.loadUrl(htmlUrl); - } catch (IOException e) { - throw new YoutubeException.NetworkException("Could not load page:" + htmlUrl); - } + String html = extractor.loadUrl(htmlUrl); String ytPlayerConfig = extractor.extractYtPlayerConfig(html); try { diff --git a/src/test/java/com/github/kiulian/downloader/YoutubeDownloader_Tests.java b/src/test/java/com/github/kiulian/downloader/YoutubeDownloader_Tests.java new file mode 100644 index 0000000..c2cfff1 --- /dev/null +++ b/src/test/java/com/github/kiulian/downloader/YoutubeDownloader_Tests.java @@ -0,0 +1,136 @@ +package com.github.kiulian.downloader; + +import com.github.kiulian.downloader.model.Extension; +import com.github.kiulian.downloader.model.VideoDetails; +import com.github.kiulian.downloader.model.YoutubeVideo; +import com.github.kiulian.downloader.model.formats.AudioVideoFormat; +import com.github.kiulian.downloader.model.formats.Format; +import com.github.kiulian.downloader.model.formats.VideoFormat; +import com.github.kiulian.downloader.model.quality.AudioQuality; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +// TODO: add tests for different itags + +@DisplayName("Youtube downloader tests") +class YoutubeDownloader_Tests { + + @Test + @DisplayName("getVideo should be successful for default videos without signature") + void getVideo_WithoutSignature_Success() { + YoutubeDownloader downloader = new YoutubeDownloader(); + String videoId = "jNQXAC9IVRw"; // me in the zoo + String title = "Me at the zoo"; + + assertDoesNotThrow(() -> { + YoutubeVideo video = downloader.getVideo(videoId); + + VideoDetails details = video.details(); + assertEquals(videoId, details.videoId(), "videoId should be " + videoId); + assertEquals(title, details.title(), "title should be " + title); + assertFalse(details.thumbnails().isEmpty(), "thumbnails should be not empty"); + assertNotEquals(0, details.lengthSeconds(), "length should not be 0"); + assertNotEquals(0, details.viewCount(), "viewCount should not be 0"); + + List formats = video.formats(); + assertFalse(formats.isEmpty(), "formats should not be empty"); + + int itag = 43; + Optional formatByItag = video.findFormatByItag(itag); + assertTrue(formatByItag.isPresent(), "findFormatByItag should return format"); + Format format = formatByItag.get(); + assertTrue(format instanceof AudioVideoFormat, "format with itag " + itag + " should be instance of AudioVideoFormat"); + assertEquals(itag, format.itag().id(), "itag should be " + itag); + + Integer width = ((AudioVideoFormat) format).width(); + assertNotNull(width, "width should not be null"); + assertEquals(640, width.intValue(), "format with itag " + itag + " should have width " + 640); + + Integer height = ((AudioVideoFormat) format).height(); + assertNotNull(height, "height should not be null"); + assertEquals(360, height.intValue(), "format with itag " + itag + " should have width " + 360); + + assertEquals(AudioQuality.medium, ((AudioVideoFormat) format).audioQuality(), "audioQuality should be medium"); + + assertTrue(format.mimeType().contains("video/webm"), "mimetype should be video/webm"); + assertEquals(Extension.WEBM, format.extension(), "extension should be webm"); + assertEquals("360p", ((AudioVideoFormat) format).qualityLabel(), "qualityLable should be 360p"); + + assertNotNull(format.url(), "url should not be null"); + + assertTrue(isReachable(format.url())); + }); + } + + + @Test + @DisplayName("getVideo should be successful for default videos without signature") + void getVideo_WithSignature_Success() { + YoutubeDownloader downloader = new YoutubeDownloader(); + String videoId = "kJQP7kiw5Fk"; // despacito + String title = "Luis Fonsi - Despacito ft. Daddy Yankee"; + + assertDoesNotThrow(() -> { + YoutubeVideo video = downloader.getVideo(videoId); + + VideoDetails details = video.details(); + assertEquals(videoId, details.videoId(), "videoId should be " + videoId); + assertEquals(title, details.title(), "title should be " + title); + assertFalse(details.thumbnails().isEmpty(), "thumbnails should be not empty"); + assertNotEquals(0, details.lengthSeconds(), "length should not be 0"); + assertNotEquals(0, details.viewCount(), "viewCount should not be 0"); + + List formats = video.formats(); + assertFalse(formats.isEmpty(), "formats should not be empty"); + + int itag = 137; + Optional formatByItag = video.findFormatByItag(itag); + assertTrue(formatByItag.isPresent(), "findFormatByItag should return format"); + Format format = formatByItag.get(); + assertTrue(format instanceof VideoFormat, "format with itag " + itag + " should be instance of AudioVideoFormat"); + assertEquals(itag, format.itag().id(), "itag should be " + itag); + + assertNotEquals(0, ((VideoFormat) format).fps(), "fps should not be 0"); + + Integer width = ((VideoFormat) format).width(); + assertNotNull(width, "width should not be null"); + assertEquals(1920, width.intValue(), "format with itag " + itag + " should have width " + 1920); + + Integer height = ((VideoFormat) format).height(); + assertNotNull(height, "height should not be null"); + assertEquals(1080, height.intValue(), "format with itag " + itag + " should have width " + 1080); + + assertTrue(format.mimeType().contains("video/mp4"), "mimetype should be video/mp4"); + assertEquals(Extension.MP4, format.extension(), "extension should be mp4"); + assertEquals("1080p", ((VideoFormat) format).qualityLabel(), "qualityLable should be 1080p"); + + assertNotNull(format.url(), "url should not be null"); + + assertTrue(isReachable(format.url())); + }); + + } + + + private static boolean isReachable(String url) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout(1000); + connection.setReadTimeout(1000); + connection.setRequestMethod("HEAD"); + int responseCode = connection.getResponseCode(); + return (200 <= responseCode && responseCode <= 399); + } catch (IOException exception) { + return false; + } + } + +}