2023-01-28 12:15:13 +00:00
|
|
|
/*
|
|
|
|
* Created by Christian Schabesberger on 02.02.16.
|
|
|
|
*
|
|
|
|
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
|
|
|
* YoutubeStreamLinkHandlerFactory.java is part of NewPipe Extractor.
|
|
|
|
*
|
|
|
|
* NewPipe Extractor is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* NewPipe Extractor is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2018-07-13 16:02:40 +00:00
|
|
|
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isHooktubeURL;
|
2022-08-09 03:44:34 +00:00
|
|
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isInvidiousURL;
|
2022-03-18 14:09:06 +00:00
|
|
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isY2ubeURL;
|
|
|
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL;
|
|
|
|
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL;
|
|
|
|
|
2017-03-01 17:47:52 +00:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
|
|
|
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
2019-01-13 11:52:07 +00:00
|
|
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
|
|
|
import org.schabi.newpipe.extractor.utils.Utils;
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2019-01-13 11:52:07 +00:00
|
|
|
import java.net.MalformedURLException;
|
2017-03-01 17:47:52 +00:00
|
|
|
import java.net.URI;
|
|
|
|
import java.net.URISyntaxException;
|
2019-01-13 11:52:07 +00:00
|
|
|
import java.net.URL;
|
2021-02-10 16:06:51 +00:00
|
|
|
import java.util.List;
|
2020-09-03 00:29:18 +00:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2023-01-28 12:15:13 +00:00
|
|
|
import javax.annotation.Nonnull;
|
2022-03-18 14:09:06 +00:00
|
|
|
import javax.annotation.Nullable;
|
|
|
|
|
|
|
|
public final class YoutubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
private static final Pattern YOUTUBE_VIDEO_ID_REGEX_PATTERN
|
|
|
|
= Pattern.compile("^([a-zA-Z0-9_-]{11})");
|
|
|
|
private static final YoutubeStreamLinkHandlerFactory INSTANCE
|
|
|
|
= new YoutubeStreamLinkHandlerFactory();
|
|
|
|
private static final List<String> SUBPATHS
|
2023-01-28 12:15:13 +00:00
|
|
|
= List.of("embed/", "live/", "shorts/", "watch/", "v/", "w/");
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2018-07-13 16:02:40 +00:00
|
|
|
private YoutubeStreamLinkHandlerFactory() {
|
2017-06-29 18:12:55 +00:00
|
|
|
}
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2018-07-13 16:02:40 +00:00
|
|
|
public static YoutubeStreamLinkHandlerFactory getInstance() {
|
2022-03-18 14:09:06 +00:00
|
|
|
return INSTANCE;
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2020-09-03 00:29:18 +00:00
|
|
|
@Nullable
|
|
|
|
private static String extractId(@Nullable final String id) {
|
|
|
|
if (id != null) {
|
|
|
|
final Matcher m = YOUTUBE_VIDEO_ID_REGEX_PATTERN.matcher(id);
|
|
|
|
return m.find() ? m.group(1) : null;
|
|
|
|
}
|
|
|
|
return null;
|
2020-05-06 13:41:01 +00:00
|
|
|
}
|
|
|
|
|
2023-01-28 12:15:13 +00:00
|
|
|
@Nonnull
|
2020-09-03 00:29:18 +00:00
|
|
|
private static String assertIsId(@Nullable final String id) throws ParsingException {
|
|
|
|
final String extractedId = extractId(id);
|
|
|
|
if (extractedId != null) {
|
|
|
|
return extractedId;
|
2020-05-06 13:41:01 +00:00
|
|
|
} else {
|
2023-01-28 12:15:13 +00:00
|
|
|
throw new ParsingException("The given string is not a YouTube video ID");
|
2019-01-13 11:52:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-28 12:15:13 +00:00
|
|
|
@Nonnull
|
2017-03-01 17:47:52 +00:00
|
|
|
@Override
|
2022-03-18 14:09:06 +00:00
|
|
|
public String getUrl(final String id) {
|
2017-08-07 16:12:51 +00:00
|
|
|
return "https://www.youtube.com/watch?v=" + id;
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2022-03-18 23:02:57 +00:00
|
|
|
@SuppressWarnings("AvoidNestedBlocks")
|
2023-01-28 12:15:13 +00:00
|
|
|
@Nonnull
|
2017-03-01 17:47:52 +00:00
|
|
|
@Override
|
2022-03-18 14:09:06 +00:00
|
|
|
public String getId(final String theUrlString)
|
|
|
|
throws ParsingException, IllegalArgumentException {
|
|
|
|
String urlString = theUrlString;
|
2019-01-13 11:52:07 +00:00
|
|
|
try {
|
2022-03-18 14:09:06 +00:00
|
|
|
final URI uri = new URI(urlString);
|
|
|
|
final String scheme = uri.getScheme();
|
2017-03-01 17:47:52 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
if (scheme != null
|
|
|
|
&& (scheme.equals("vnd.youtube") || scheme.equals("vnd.youtube.launch"))) {
|
|
|
|
final String schemeSpecificPart = uri.getSchemeSpecificPart();
|
2019-01-20 00:31:30 +00:00
|
|
|
if (schemeSpecificPart.startsWith("//")) {
|
2020-09-03 00:29:18 +00:00
|
|
|
final String extractedId = extractId(schemeSpecificPart.substring(2));
|
|
|
|
if (extractedId != null) {
|
|
|
|
return extractedId;
|
2020-05-06 13:41:01 +00:00
|
|
|
}
|
|
|
|
|
2019-01-20 00:31:30 +00:00
|
|
|
urlString = "https:" + schemeSpecificPart;
|
2019-01-13 11:52:07 +00:00
|
|
|
} else {
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(schemeSpecificPart);
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|
2018-07-25 15:21:42 +00:00
|
|
|
}
|
2022-03-18 14:09:06 +00:00
|
|
|
} catch (final URISyntaxException ignored) {
|
2018-07-25 15:21:42 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final URL url;
|
2019-01-13 11:52:07 +00:00
|
|
|
try {
|
2019-01-20 00:31:30 +00:00
|
|
|
url = Utils.stringToURL(urlString);
|
2022-03-18 14:09:06 +00:00
|
|
|
} catch (final MalformedURLException e) {
|
2019-01-13 11:52:07 +00:00
|
|
|
throw new IllegalArgumentException("The given URL is not valid");
|
|
|
|
}
|
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final String host = url.getHost();
|
2019-01-13 11:52:07 +00:00
|
|
|
String path = url.getPath();
|
|
|
|
// remove leading "/" of URL-path if URL-path is given
|
|
|
|
if (!path.isEmpty()) {
|
|
|
|
path = path.substring(1);
|
|
|
|
}
|
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
if (!Utils.isHTTP(url) || !(isYoutubeURL(url) || isYoutubeServiceURL(url)
|
2022-08-09 03:44:34 +00:00
|
|
|
|| isHooktubeURL(url) || isInvidiousURL(url) || isY2ubeURL(url))) {
|
2019-01-13 11:52:07 +00:00
|
|
|
if (host.equalsIgnoreCase("googleads.g.doubleclick.net")) {
|
2023-01-28 12:15:13 +00:00
|
|
|
throw new FoundAdException("Error: found ad: " + urlString);
|
2018-02-12 16:29:36 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2023-01-28 12:15:13 +00:00
|
|
|
throw new ParsingException("The URL is not a YouTube URL");
|
2019-01-13 11:52:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (YoutubePlaylistLinkHandlerFactory.getInstance().acceptUrl(urlString)) {
|
2023-01-28 12:15:13 +00:00
|
|
|
throw new ParsingException("Error: no suitable URL: " + urlString);
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
// Using uppercase instead of lowercase, because toLowercase replaces some unicode
|
|
|
|
// characters with their lowercase ASCII equivalent. Using toLowercase could result in
|
|
|
|
// faultily matching unicode urls.
|
2019-01-13 11:52:07 +00:00
|
|
|
switch (host.toUpperCase()) {
|
|
|
|
case "WWW.YOUTUBE-NOCOOKIE.COM": {
|
|
|
|
if (path.startsWith("embed/")) {
|
2022-03-18 14:09:06 +00:00
|
|
|
return assertIsId(path.substring(6));
|
2019-01-13 11:52:07 +00:00
|
|
|
}
|
2019-01-20 13:39:06 +00:00
|
|
|
break;
|
2018-07-25 15:21:42 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
|
|
|
case "YOUTUBE.COM":
|
|
|
|
case "WWW.YOUTUBE.COM":
|
2019-09-12 02:43:49 +00:00
|
|
|
case "M.YOUTUBE.COM":
|
|
|
|
case "MUSIC.YOUTUBE.COM": {
|
2019-01-13 11:52:07 +00:00
|
|
|
if (path.equals("attribution_link")) {
|
2022-03-18 14:09:06 +00:00
|
|
|
final String uQueryValue = Utils.getQueryValue(url, "u");
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final URL decodedURL;
|
2019-01-13 11:52:07 +00:00
|
|
|
try {
|
2023-01-28 12:15:13 +00:00
|
|
|
decodedURL = Utils.stringToURL("https://www.youtube.com" + uQueryValue);
|
2022-03-18 14:09:06 +00:00
|
|
|
} catch (final MalformedURLException e) {
|
2023-01-28 12:15:13 +00:00
|
|
|
throw new ParsingException("Error: no suitable URL: " + urlString);
|
2019-01-13 11:52:07 +00:00
|
|
|
}
|
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final String viewQueryValue = Utils.getQueryValue(decodedURL, "v");
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(viewQueryValue);
|
2019-01-13 11:52:07 +00:00
|
|
|
}
|
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final String maybeId = getIdFromSubpathsInPath(path);
|
|
|
|
if (maybeId != null) {
|
|
|
|
return maybeId;
|
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final String viewQueryValue = Utils.getQueryValue(url, "v");
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(viewQueryValue);
|
2018-07-25 15:21:42 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2021-10-22 19:48:18 +00:00
|
|
|
case "Y2U.BE":
|
2019-01-13 11:52:07 +00:00
|
|
|
case "YOUTU.BE": {
|
2022-03-18 14:09:06 +00:00
|
|
|
final String viewQueryValue = Utils.getQueryValue(url, "v");
|
2019-01-13 11:52:07 +00:00
|
|
|
if (viewQueryValue != null) {
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(viewQueryValue);
|
2019-01-13 11:52:07 +00:00
|
|
|
}
|
|
|
|
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(path);
|
2018-07-25 15:21:42 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2021-02-10 16:06:51 +00:00
|
|
|
case "HOOKTUBE.COM":
|
2019-08-08 00:19:02 +00:00
|
|
|
case "INVIDIO.US":
|
2021-01-22 19:58:14 +00:00
|
|
|
case "DEV.INVIDIO.US":
|
|
|
|
case "WWW.INVIDIO.US":
|
|
|
|
case "REDIRECT.INVIDIOUS.IO":
|
2019-08-08 00:19:02 +00:00
|
|
|
case "INVIDIOUS.SNOPYTA.ORG":
|
2020-07-02 19:31:05 +00:00
|
|
|
case "YEWTU.BE":
|
2020-11-11 14:54:16 +00:00
|
|
|
case "TUBE.CONNECT.CAFE":
|
2021-08-12 08:06:41 +00:00
|
|
|
case "TUBUS.EDUVID.ORG":
|
2020-11-11 14:54:16 +00:00
|
|
|
case "INVIDIOUS.KAVIN.ROCKS":
|
2021-06-23 12:12:03 +00:00
|
|
|
case "INVIDIOUS-US.KAVIN.ROCKS":
|
|
|
|
case "PIPED.KAVIN.ROCKS":
|
2020-11-11 14:54:16 +00:00
|
|
|
case "INVIDIOUS.SITE":
|
|
|
|
case "VID.MINT.LGBT":
|
|
|
|
case "INVIDIOU.SITE":
|
2021-01-22 18:20:22 +00:00
|
|
|
case "INVIDIOUS.FDN.FR":
|
|
|
|
case "INVIDIOUS.048596.XYZ":
|
|
|
|
case "INVIDIOUS.ZEE.LI":
|
|
|
|
case "VID.PUFFYAN.US":
|
2021-06-23 12:12:03 +00:00
|
|
|
case "YTPRIVATE.COM":
|
|
|
|
case "INVIDIOUS.NAMAZSO.EU":
|
|
|
|
case "INVIDIOUS.SILKKY.CLOUD":
|
|
|
|
case "INVIDIOUS.EXONIP.DE":
|
|
|
|
case "INV.RIVERSIDE.ROCKS":
|
|
|
|
case "INVIDIOUS.BLAMEFRAN.NET":
|
|
|
|
case "INVIDIOUS.MOOMOO.ME":
|
|
|
|
case "YTB.TROM.TF":
|
|
|
|
case "YT.CYBERHOST.UK":
|
|
|
|
case "Y.COM.CM": { // code-block for hooktube.com and Invidious instances
|
2019-01-24 10:13:01 +00:00
|
|
|
if (path.equals("watch")) {
|
2022-03-18 14:09:06 +00:00
|
|
|
final String viewQueryValue = Utils.getQueryValue(url, "v");
|
2019-01-24 10:13:01 +00:00
|
|
|
if (viewQueryValue != null) {
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(viewQueryValue);
|
2019-01-24 10:13:01 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-18 14:09:06 +00:00
|
|
|
final String maybeId = getIdFromSubpathsInPath(path);
|
|
|
|
if (maybeId != null) {
|
|
|
|
return maybeId;
|
|
|
|
}
|
2019-01-27 00:28:51 +00:00
|
|
|
|
2022-03-18 14:09:06 +00:00
|
|
|
final String viewQueryValue = Utils.getQueryValue(url, "v");
|
2020-02-12 17:59:46 +00:00
|
|
|
if (viewQueryValue != null) {
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(viewQueryValue);
|
2020-02-12 17:59:46 +00:00
|
|
|
}
|
|
|
|
|
2020-05-06 13:41:01 +00:00
|
|
|
return assertIsId(path);
|
2018-07-25 15:21:42 +00:00
|
|
|
}
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|
2019-01-13 11:52:07 +00:00
|
|
|
|
2023-01-28 12:15:13 +00:00
|
|
|
throw new ParsingException("Error: no suitable URL: " + urlString);
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2017-07-11 03:08:03 +00:00
|
|
|
@Override
|
2018-07-01 14:21:40 +00:00
|
|
|
public boolean onAcceptUrl(final String url) throws FoundAdException {
|
2018-07-25 15:21:42 +00:00
|
|
|
try {
|
|
|
|
getId(url);
|
|
|
|
return true;
|
2022-03-18 14:09:06 +00:00
|
|
|
} catch (final FoundAdException fe) {
|
2018-07-25 15:21:42 +00:00
|
|
|
throw fe;
|
2022-03-18 14:09:06 +00:00
|
|
|
} catch (final ParsingException e) {
|
2017-03-01 17:47:52 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2021-02-10 16:06:51 +00:00
|
|
|
|
2023-01-28 12:15:13 +00:00
|
|
|
@Nullable
|
|
|
|
private String getIdFromSubpathsInPath(@Nonnull final String path) throws ParsingException {
|
2021-02-16 18:07:24 +00:00
|
|
|
for (final String subpath : SUBPATHS) {
|
2021-02-13 21:22:50 +00:00
|
|
|
if (path.startsWith(subpath)) {
|
2022-03-18 14:09:06 +00:00
|
|
|
final String id = path.substring(subpath.length());
|
2021-02-10 16:06:51 +00:00
|
|
|
return assertIsId(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2017-03-01 17:47:52 +00:00
|
|
|
}
|