Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package org.schabi.newpipe.extractor.services.youtube;

import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;

import com.grack.nanojson.JsonObject;

import org.jsoup.nodes.Entities;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -242,23 +246,35 @@ private static void addAllCommandRuns(
return;
}

boolean isYoutubeUrl;
try {
final URL parsedUrl = new URL(url);
isYoutubeUrl = isYoutubeURL(parsedUrl) || isYoutubeServiceURL(parsedUrl);
} catch (final MalformedURLException ignored) {
// this should never happen, but just in case, assume this is a generic URL
isYoutubeUrl = false;
}

final String open = "<a href=\"" + Entities.escape(url) + "\">";
final Function<String, String> transformContent = getTransformContentFun(run);
final Function<String, String> transformContent = getTransformContentFun(
run, isYoutubeUrl);

openers.add(new Run(open, LINK_CLOSE, startIndex, transformContent));
closers.add(new Run(open, LINK_CLOSE, startIndex + length, transformContent));
});
}

private static Function<String, String> getTransformContentFun(final JsonObject run) {
private static Function<String, String> getTransformContentFun(final JsonObject run,
final boolean isYoutube) {
final String accessibilityLabel = run.getObject("onTapOptions")
.getObject("accessibilityInfo")
.getString("accessibilityLabel", "")
// accessibility labels are e.g. "Instagram Channel Link: instagram_profile_name"
.replaceFirst(" Channel Link", "");

final Function<String, String> transformContent;
if (accessibilityLabel.isEmpty() || accessibilityLabel.startsWith("YouTube: ")) {
if (isYoutube
|| accessibilityLabel.isEmpty() || accessibilityLabel.startsWith("YouTube: ")) {
// if there is no accessibility label, or the link points to YouTube, cleanup the link
// text, see LINK_CONTENT_CLEANER_REGEX's documentation for more details
transformContent = (content) -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.schabi.newpipe.extractor.services.youtube;

import static org.schabi.newpipe.extractor.utils.Parser.matchGroup1MultiplePatterns;
import static org.schabi.newpipe.extractor.utils.Parser.matchMultiplePatterns;

import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.JavaScript;
import org.schabi.newpipe.extractor.utils.Pair;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.jsextractor.JavaScriptExtractor;

import javax.annotation.Nonnull;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
Expand All @@ -24,6 +27,7 @@ final class YoutubeSignatureUtils {

private static final Pattern[] FUNCTION_REGEXES = {
// CHECKSTYLE:OFF
Pattern.compile("\\b(?:[a-zA-Z0-9_$]+)&&\\((?:[a-zA-Z0-9_$]+)=([a-zA-Z0-9_$]{2,})\\((\\d+,)decodeURIComponent\\((?:[a-zA-Z0-9_$]+)\\)\\)"),
Pattern.compile("\\b(?:[a-zA-Z0-9_$]+)&&\\((?:[a-zA-Z0-9_$]+)=([a-zA-Z0-9_$]{2,})\\(decodeURIComponent\\((?:[a-zA-Z0-9_$]+)\\)\\)"),
Pattern.compile("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)"),
Pattern.compile("\\bc&&\\(c=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(c\\)\\)"),
Expand All @@ -38,8 +42,10 @@ final class YoutubeSignatureUtils {
private static final String DEOBF_FUNC_REGEX_END = "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})";

// CHECKSTYLE:OFF
private static final String SIG_DEOBF_GLOBAL_ARRAY_REGEX = "(var [A-z]=['\"].*['\"].split\\(\";\"\\))";
private static final String SIG_DEOBF_HELPER_OBJ_NAME_REGEX = ";([A-Za-z0-9_\\$]{2,})\\[..";
private static final Pattern SIG_DEOBF_GLOBAL_ARRAY_REGEX =
Pattern.compile("(var [A-z]=['\"].*['\"].split\\(\"[;{]\"\\))");
private static final Pattern SIG_DEOBF_HELPER_OBJ_NAME_REGEX =
Pattern.compile("[;,]([A-Za-z0-9_$]{2,})\\[..");
private static final String SIG_DEOBF_HELPER_OBJ_REGEX_START = "(var ";
private static final String SIG_DEOBF_HELPER_OBJ_REGEX_END = "=\\{(?>.|\\n)+?\\}\\};)";
// CHECKSTYLE:ON
Expand Down Expand Up @@ -76,8 +82,10 @@ static String getSignatureTimestamp(@Nonnull final String javaScriptPlayerCode)
static String getDeobfuscationCode(@Nonnull final String javaScriptPlayerCode)
throws ParsingException {
try {
final String deobfuscationFunctionName = getDeobfuscationFunctionName(
javaScriptPlayerCode);
final Pair<String, String> deobfuscationFunctionNameAndParams =
getDeobfuscationFunctionNameAndParams(javaScriptPlayerCode);
final String deobfuscationFunctionName = deobfuscationFunctionNameAndParams.getFirst();
final String functionAdditionalParams = deobfuscationFunctionNameAndParams.getSecond();

String deobfuscationFunction;
try {
Expand All @@ -102,7 +110,7 @@ static String getDeobfuscationCode(@Nonnull final String javaScriptPlayerCode)
final String callerFunction = "function " + DEOBFUSCATION_FUNCTION_NAME
+ "(a){return "
+ deobfuscationFunctionName
+ "(a);}";
+ "(" + functionAdditionalParams + "a);}";

return globalVar + ";" + helperObject + deobfuscationFunction + ";" + callerFunction;
} catch (final Exception e) {
Expand All @@ -111,10 +119,18 @@ static String getDeobfuscationCode(@Nonnull final String javaScriptPlayerCode)
}

@Nonnull
private static String getDeobfuscationFunctionName(@Nonnull final String javaScriptPlayerCode)
throws ParsingException {
private static Pair<String, String> getDeobfuscationFunctionNameAndParams(
@Nonnull final String javaScriptPlayerCode) throws ParsingException {
try {
return matchGroup1MultiplePatterns(FUNCTION_REGEXES, javaScriptPlayerCode);
final Matcher m = matchMultiplePatterns(FUNCTION_REGEXES, javaScriptPlayerCode);
final String functionName = m.group(1);
final String functionAdditionalParams;
if (m.groupCount() > 1) {
functionAdditionalParams = m.group(2);
} else {
functionAdditionalParams = "";
}
return new Pair<>(functionName, functionAdditionalParams);
} catch (final Parser.RegexException e) {
throw new ParsingException(
"Could not find deobfuscation function with any of the known patterns", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.schabi.newpipe.downloader;

import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.utils.Utils;

import java.util.Locale;

Expand All @@ -23,7 +24,7 @@ private static DownloaderType getDownloaderType() {

private static DownloaderType determineDownloaderType() {
String propValue = System.getProperty("downloader");
if (propValue == null) {
if (Utils.isNullOrEmpty(propValue)) {
return DEFAULT_DOWNLOADER;
}
propValue = propValue.toUpperCase();
Expand All @@ -33,8 +34,8 @@ private static DownloaderType determineDownloaderType() {
}
try {
return DownloaderType.valueOf(propValue);
} catch (final Exception e) {
return DEFAULT_DOWNLOADER;
} catch (final IllegalArgumentException e) {
throw new IllegalArgumentException("Unknown downloader name: " + propValue, e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
*/
public class YoutubeChannelExtractorTest {

/**
* See <a href="https://youtube.fandom.com/wiki/Termination#Ban_Messages">here</a>
* for a list of account termination messages.
*/
public static class NotAvailable implements InitYoutubeTest {
@Test
void deletedFetch() throws Exception {
Expand Down Expand Up @@ -67,7 +71,7 @@ void accountTerminatedTOSFetch() throws Exception {
void accountTerminatedCommunityFetch() throws Exception {
// "This account has been terminated for violating YouTube's Community Guidelines."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://www.youtube.com/channel/UC0AuOxCr9TZ0TtEgL1zpIgA");
YouTube.getChannelExtractor("https://www.youtube.com/channel/UC-nQp2ewj2Yeg5w7VyoVBwQ");

final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
Expand All @@ -86,18 +90,6 @@ void accountTerminatedHateFetch() throws Exception {
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}

@Test
void accountTerminatedBullyFetch() throws Exception {
// "This account has been terminated due to multiple or severe violations
// of YouTube's policy prohibiting content designed to harass, bully or threaten."
final ChannelExtractor extractor =
YouTube.getChannelExtractor("https://youtube.com/channel/UCB1o7_gbFp2PLsamWxFenBg");

final AccountTerminatedException ex =
assertThrows(AccountTerminatedException.class, extractor::fetchPage);
assertEquals(AccountTerminatedException.Reason.VIOLATION, ex.getReason());
}

@Test
void accountTerminatedSpamFetch() throws Exception {
// "This account has been terminated due to multiple or severe violations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ void testGetCommentsAllData() throws IOException, ExtractionException {
}

public static class HeartedByCreator extends Base {
private final static String URL = "https://www.youtube.com/watch?v=tR11b7uh17Y";
private final static String URL = "https://www.youtube.com/watch?v=RwTdoQNVMTY";

@Override
protected String extractorUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestRelatedItems;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Disabled;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.BaseListExtractorTest;
import org.schabi.newpipe.extractor.services.DefaultSimpleExtractorTest;
Expand Down Expand Up @@ -295,6 +296,7 @@ public void testMoreRelatedItems() throws Exception {
}

// Deprecated (i.e. removed from the interface of YouTube) since July 21, 2025
@Disabled("Trending section was removed from YouTube")
public static class Trending extends DefaultSimpleExtractorTest<YoutubeTrendingExtractor>
implements BaseListExtractorTest, InitYoutubeTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ void getPlaylistType() throws ParsingException {
}

public static class InvalidPageEmpty extends Base {
private static final String VIDEO_ID = "QMVCAPd5cwBcg";
private static final String VIDEO_ID = "WRz2MxhAdJo";

@Override
protected String extractorUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
* along with NewPipe Extractor. If not, see <http://www.gnu.org/licenses/>.
*/

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeLiveLinkHandlerFactory;

/**
* Test for {@link KioskInfo}
Expand All @@ -41,9 +42,11 @@ class YoutubeTrendingKioskInfoTest implements InitYoutubeTest {
public void setUp() throws Exception {
InitYoutubeTest.super.setUp();

final LinkHandlerFactory linkHandlerFactory = YouTube.getKioskList().getListLinkHandlerFactoryByType("Trending");
final LinkHandlerFactory linkHandlerFactory = YouTube.getKioskList()
.getListLinkHandlerFactoryByType(YoutubeLiveLinkHandlerFactory.KIOSK_ID);

kioskInfo = KioskInfo.getInfo(YouTube, linkHandlerFactory.fromId("Trending").getUrl());
kioskInfo = KioskInfo.getInfo(YouTube,
linkHandlerFactory.fromId(YoutubeLiveLinkHandlerFactory.KIOSK_ID).getUrl());
}

@Test
Expand All @@ -53,8 +56,7 @@ void getStreams() {

@Test
void getId() {
assertTrue(kioskInfo.getId().equals("Trending")
|| kioskInfo.getId().equals("Trends"));
assertEquals(YoutubeLiveLinkHandlerFactory.KIOSK_ID, kioskInfo.getId());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ protected SearchExtractor createExtractor() throws Exception {
}

public static class MusicAlbums extends DefaultSearchExtractorTest implements InitYoutubeTest {
// searching for "scenography" on 28/07/2025 returns some autogenerated albums,
// searching for "axwell" on 27/01/2026 returns some autogenerated albums,
// and we want to test the extraction of those, too
private static final String QUERY = "scenography";
private static final String QUERY = "axwell";

@Override
protected SearchExtractor createExtractor() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void nonExistentFetch() throws Exception {

final StreamExtractor extractor =
YouTube.getStreamExtractor(BASE_URL + "don-t-exist");
assertThrows(ContentNotAvailableException.class, extractor::fetchPage);
assertThrows(ParsingException.class, extractor::fetchPage);
}

@Test
Expand Down Expand Up @@ -280,7 +280,6 @@ protected StreamExtractor createExtractor() throws Exception {
@Nullable @Override public String expectedTextualUploadDate() { return "2021-03-17T12:56:59-07:00"; }
@Override public long expectedLikeCountAtLeast() { return 2300; }
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasSubtitles() { return false; }
@Override public int expectedStreamSegmentsCount() { return 13; }
@Override public String expectedLicence() { return YOUTUBE_LICENCE; }
@Override public String expectedCategory() { return "News & Politics"; }
Expand Down
Loading