fix: use url parser instead of regex for extracting track type

This commit is contained in:
ThetaDev 2023-03-21 16:03:26 +01:00
parent 6e5b6b76a2
commit f2c167f2dd
3 changed files with 200 additions and 150 deletions

View File

@ -43,6 +43,7 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Parser;
@ -436,7 +437,7 @@ public final class YoutubeParsingHelper {
/** /**
* @param playlistId the playlist id to parse * @param playlistId the playlist id to parse
* @return the {@link PlaylistInfo.PlaylistType} extracted from the playlistId (mix playlist * @return the {@link PlaylistInfo.PlaylistType} extracted from the playlistId (mix playlist
* types included) * types included)
* @throws ParsingException if the playlistId is null or empty, if the playlistId is not a mix, * @throws ParsingException if the playlistId is null or empty, if the playlistId is not a mix,
* if it is a mix but it's not based on a specific stream (this is the * if it is a mix but it's not based on a specific stream (this is the
* case for channel or genre mixes) * case for channel or genre mixes)
@ -469,7 +470,7 @@ public final class YoutubeParsingHelper {
// 11 characters then it can't be a video id, hence we are dealing with a different // 11 characters then it can't be a video id, hence we are dealing with a different
// type of mix (e.g. genre mixes handled above, of the form RDGMEM{garbage}) // type of mix (e.g. genre mixes handled above, of the form RDGMEM{garbage})
throw new ParsingException("Video id could not be determined from mix id: " throw new ParsingException("Video id could not be determined from mix id: "
+ playlistId); + playlistId);
} }
return playlistId.substring(2); return playlistId.substring(2);
@ -482,7 +483,7 @@ public final class YoutubeParsingHelper {
/** /**
* @param playlistId the playlist id to parse * @param playlistId the playlist id to parse
* @return the {@link PlaylistInfo.PlaylistType} extracted from the playlistId (mix playlist * @return the {@link PlaylistInfo.PlaylistType} extracted from the playlistId (mix playlist
* types included) * types included)
* @throws ParsingException if the playlistId is null or empty * @throws ParsingException if the playlistId is null or empty
*/ */
@Nonnull @Nonnull
@ -510,7 +511,7 @@ public final class YoutubeParsingHelper {
/** /**
* @param playlistUrl the playlist url to parse * @param playlistUrl the playlist url to parse
* @return the {@link PlaylistInfo.PlaylistType} extracted from the playlistUrl's list param * @return the {@link PlaylistInfo.PlaylistType} extracted from the playlistUrl's list param
* (mix playlist types included) * (mix playlist types included)
* @throws ParsingException if the playlistUrl is malformed, if has no list param or if the list * @throws ParsingException if the playlistUrl is malformed, if has no list param or if the list
* param is empty * param is empty
*/ */
@ -540,20 +541,20 @@ public final class YoutubeParsingHelper {
} }
// @formatter:off // @formatter:off
final byte[] body = JsonWriter.string() final byte[] body = JsonWriter.string()
.object() .object()
.object("context") .object("context")
.object("client") .object("client")
.value("hl", "en-GB") .value("hl", "en-GB")
.value("gl", "GB") .value("gl", "GB")
.value("clientName", "WEB") .value("clientName", "WEB")
.value("clientVersion", HARDCODED_CLIENT_VERSION) .value("clientVersion", HARDCODED_CLIENT_VERSION)
.end() .end()
.object("user") .object("user")
.value("lockedSafetyMode", false) .value("lockedSafetyMode", false)
.end() .end()
.value("fetchLiveState", true) .value("fetchLiveState", true)
.end() .end()
.end().done().getBytes(StandardCharsets.UTF_8); .end().done().getBytes(StandardCharsets.UTF_8);
// @formatter:on // @formatter:on
final var headers = getClientHeaders("1", HARDCODED_CLIENT_VERSION); final var headers = getClientHeaders("1", HARDCODED_CLIENT_VERSION);
@ -637,14 +638,14 @@ public final class YoutubeParsingHelper {
throw new ParsingException( throw new ParsingException(
// CHECKSTYLE:OFF // CHECKSTYLE:OFF
"Could not extract YouTube WEB InnerTube API key from HTML search results page"); "Could not extract YouTube WEB InnerTube API key from HTML search results page");
// CHECKSTYLE:ON // CHECKSTYLE:ON
} }
if (clientVersion == null) { if (clientVersion == null) {
throw new ParsingException( throw new ParsingException(
// CHECKSTYLE:OFF // CHECKSTYLE:OFF
"Could not extract YouTube WEB InnerTube client version from HTML search results page"); "Could not extract YouTube WEB InnerTube client version from HTML search results page");
// CHECKSTYLE:ON // CHECKSTYLE:ON
} }
keyAndVersionExtracted = true; keyAndVersionExtracted = true;
@ -771,30 +772,30 @@ public final class YoutubeParsingHelper {
// @formatter:off // @formatter:off
final byte[] json = JsonWriter.string() final byte[] json = JsonWriter.string()
.object() .object()
.object("context") .object("context")
.object("client") .object("client")
.value("clientName", "WEB_REMIX") .value("clientName", "WEB_REMIX")
.value("clientVersion", HARDCODED_YOUTUBE_MUSIC_KEY[2]) .value("clientVersion", HARDCODED_YOUTUBE_MUSIC_KEY[2])
.value("hl", "en-GB") .value("hl", "en-GB")
.value("gl", "GB") .value("gl", "GB")
.array("experimentIds").end() .array("experimentIds").end()
.value("experimentsToken", "") .value("experimentsToken", "")
.object("locationInfo").end() .object("locationInfo").end()
.object("musicAppInfo").end() .object("musicAppInfo").end()
.end() .end()
.object("capabilities").end() .object("capabilities").end()
.object("request") .object("request")
.array("internalExperimentFlags").end() .array("internalExperimentFlags").end()
.object("sessionIndex").end() .object("sessionIndex").end()
.end() .end()
.object("activePlayers").end() .object("activePlayers").end()
.object("user") .object("user")
.value("enableSafetyMode", false) .value("enableSafetyMode", false)
.end() .end()
.end() .end()
.value("input", "") .value("input", "")
.end().done().getBytes(StandardCharsets.UTF_8); .end().done().getBytes(StandardCharsets.UTF_8);
// @formatter:on // @formatter:on
final var headers = new HashMap<>(getOriginReferrerHeaders(YOUTUBE_MUSIC_URL)); final var headers = new HashMap<>(getOriginReferrerHeaders(YOUTUBE_MUSIC_URL));
@ -838,7 +839,7 @@ public final class YoutubeParsingHelper {
musicClientName = Parser.matchGroup1(INNERTUBE_CLIENT_NAME_REGEX, html); musicClientName = Parser.matchGroup1(INNERTUBE_CLIENT_NAME_REGEX, html);
} }
youtubeMusicKey = new String[] {musicKey, musicClientName, musicClientVersion}; youtubeMusicKey = new String[]{musicKey, musicClientName, musicClientVersion};
return youtubeMusicKey; return youtubeMusicKey;
} }
@ -901,7 +902,7 @@ public final class YoutubeParsingHelper {
if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds")) { if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds")) {
url.append("&t=") url.append("&t=")
.append(navigationEndpoint.getObject("watchEndpoint") .append(navigationEndpoint.getObject("watchEndpoint")
.getInt("startTimeSeconds")); .getInt("startTimeSeconds"));
} }
return url.toString(); return url.toString();
} }
@ -1233,24 +1234,24 @@ public final class YoutubeParsingHelper {
// @formatter:off // @formatter:off
return JsonObject.builder() return JsonObject.builder()
.object("context") .object("context")
.object("client") .object("client")
.value("hl", localization.getLocalizationCode()) .value("hl", localization.getLocalizationCode())
.value("gl", contentCountry.getCountryCode()) .value("gl", contentCountry.getCountryCode())
.value("clientName", "WEB") .value("clientName", "WEB")
.value("clientVersion", getClientVersion()) .value("clientVersion", getClientVersion())
.value("originalUrl", "https://www.youtube.com") .value("originalUrl", "https://www.youtube.com")
.value("platform", "DESKTOP") .value("platform", "DESKTOP")
.end() .end()
.object("request") .object("request")
.array("internalExperimentFlags") .array("internalExperimentFlags")
.end() .end()
.value("useSsl", true) .value("useSsl", true)
.end() .end()
.object("user") .object("user")
// TO DO: provide a way to enable restricted mode with: // TO DO: provide a way to enable restricted mode with:
// .value("enableSafetyMode", boolean) // .value("enableSafetyMode", boolean)
.value("lockedSafetyMode", false) .value("lockedSafetyMode", false)
.end() .end()
.end(); .end();
// @formatter:on // @formatter:on
} }
@ -1262,32 +1263,32 @@ public final class YoutubeParsingHelper {
// @formatter:off // @formatter:off
return JsonObject.builder() return JsonObject.builder()
.object("context") .object("context")
.object("client") .object("client")
.value("clientName", "ANDROID") .value("clientName", "ANDROID")
.value("clientVersion", ANDROID_YOUTUBE_CLIENT_VERSION) .value("clientVersion", ANDROID_YOUTUBE_CLIENT_VERSION)
.value("platform", "MOBILE") .value("platform", "MOBILE")
.value("osName", "Android") .value("osName", "Android")
.value("osVersion", "12") .value("osVersion", "12")
/* /*
A valid Android SDK version is required to be sure to get a valid player A valid Android SDK version is required to be sure to get a valid player
response response
If this parameter is not provided, the player response may be replaced by If this parameter is not provided, the player response may be replaced by
the one of a 5-minute video saying the message "The following content is the one of a 5-minute video saying the message "The following content is
not available on this app. Watch this content on the latest version on not available on this app. Watch this content on the latest version on
YouTube" YouTube"
See https://github.com/TeamNewPipe/NewPipe/issues/8713 See https://github.com/TeamNewPipe/NewPipe/issues/8713
The Android SDK version corresponding to the Android version used in The Android SDK version corresponding to the Android version used in
requests is sent requests is sent
*/ */
.value("androidSdkVersion", 31) .value("androidSdkVersion", 31)
.value("hl", localization.getLocalizationCode()) .value("hl", localization.getLocalizationCode())
.value("gl", contentCountry.getCountryCode()) .value("gl", contentCountry.getCountryCode())
.end() .end()
.object("user") .object("user")
// TO DO: provide a way to enable restricted mode with: // TO DO: provide a way to enable restricted mode with:
// .value("enableSafetyMode", boolean) // .value("enableSafetyMode", boolean)
.value("lockedSafetyMode", false) .value("lockedSafetyMode", false)
.end() .end()
.end(); .end();
// @formatter:on // @formatter:on
} }
@ -1299,27 +1300,27 @@ public final class YoutubeParsingHelper {
// @formatter:off // @formatter:off
return JsonObject.builder() return JsonObject.builder()
.object("context") .object("context")
.object("client") .object("client")
.value("clientName", "IOS") .value("clientName", "IOS")
.value("clientVersion", IOS_YOUTUBE_CLIENT_VERSION) .value("clientVersion", IOS_YOUTUBE_CLIENT_VERSION)
.value("deviceMake", "Apple") .value("deviceMake", "Apple")
// Device model is required to get 60fps streams // Device model is required to get 60fps streams
.value("deviceModel", IOS_DEVICE_MODEL) .value("deviceModel", IOS_DEVICE_MODEL)
.value("platform", "MOBILE") .value("platform", "MOBILE")
.value("osName", "iOS") .value("osName", "iOS")
// The value of this field seems to use the following structure: // The value of this field seems to use the following structure:
// "iOS version.0.build version" // "iOS version.0.build version"
// The build version corresponding to the iOS version used can be found on // The build version corresponding to the iOS version used can be found on
// https://www.theiphonewiki.com/wiki/Firmware/iPhone/15.x#iPhone_13 // https://www.theiphonewiki.com/wiki/Firmware/iPhone/15.x#iPhone_13
.value("osVersion", "15.6.0.19G71") .value("osVersion", "15.6.0.19G71")
.value("hl", localization.getLocalizationCode()) .value("hl", localization.getLocalizationCode())
.value("gl", contentCountry.getCountryCode()) .value("gl", contentCountry.getCountryCode())
.end() .end()
.object("user") .object("user")
// TO DO: provide a way to enable restricted mode with: // TO DO: provide a way to enable restricted mode with:
// .value("enableSafetyMode", boolean) // .value("enableSafetyMode", boolean)
.value("lockedSafetyMode", false) .value("lockedSafetyMode", false)
.end() .end()
.end(); .end();
// @formatter:on // @formatter:on
} }
@ -1332,22 +1333,22 @@ public final class YoutubeParsingHelper {
// @formatter:off // @formatter:off
return JsonObject.builder() return JsonObject.builder()
.object("context") .object("context")
.object("client") .object("client")
.value("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER") .value("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER")
.value("clientVersion", TVHTML5_SIMPLY_EMBED_CLIENT_VERSION) .value("clientVersion", TVHTML5_SIMPLY_EMBED_CLIENT_VERSION)
.value("clientScreen", "EMBED") .value("clientScreen", "EMBED")
.value("platform", "TV") .value("platform", "TV")
.value("hl", localization.getLocalizationCode()) .value("hl", localization.getLocalizationCode())
.value("gl", contentCountry.getCountryCode()) .value("gl", contentCountry.getCountryCode())
.end() .end()
.object("thirdParty") .object("thirdParty")
.value("embedUrl", "https://www.youtube.com/watch?v=" + videoId) .value("embedUrl", "https://www.youtube.com/watch?v=" + videoId)
.end() .end()
.object("user") .object("user")
// TO DO: provide a way to enable restricted mode with: // TO DO: provide a way to enable restricted mode with:
// .value("enableSafetyMode", boolean) // .value("enableSafetyMode", boolean)
.value("lockedSafetyMode", false) .value("lockedSafetyMode", false)
.end() .end()
.end(); .end();
// @formatter:on // @formatter:on
} }
@ -1364,19 +1365,19 @@ public final class YoutubeParsingHelper {
return JsonWriter.string((isTvHtml5DesktopJsonBuilder return JsonWriter.string((isTvHtml5DesktopJsonBuilder
? prepareTvHtml5EmbedJsonBuilder(localization, contentCountry, videoId) ? prepareTvHtml5EmbedJsonBuilder(localization, contentCountry, videoId)
: prepareDesktopJsonBuilder(localization, contentCountry)) : prepareDesktopJsonBuilder(localization, contentCountry))
.object("playbackContext") .object("playbackContext")
.object("contentPlaybackContext") .object("contentPlaybackContext")
// Signature timestamp from the JavaScript base player is needed to get // Signature timestamp from the JavaScript base player is needed to get
// working obfuscated URLs // working obfuscated URLs
.value("signatureTimestamp", sts) .value("signatureTimestamp", sts)
.value("referer", "https://www.youtube.com/watch?v=" + videoId) .value("referer", "https://www.youtube.com/watch?v=" + videoId)
.end() .end()
.end() .end()
.value(CPN, contentPlaybackNonce) .value(CPN, contentPlaybackNonce)
.value(VIDEO_ID, videoId) .value(VIDEO_ID, videoId)
.value(CONTENT_CHECK_OK, true) .value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true) .value(RACY_CHECK_OK, true)
.done()) .done())
.getBytes(StandardCharsets.UTF_8); .getBytes(StandardCharsets.UTF_8);
// @formatter:on // @formatter:on
} }
@ -1472,7 +1473,7 @@ public final class YoutubeParsingHelper {
* Returns an unmodifiable {@link Map} containing the {@code X-YouTube-Client-Name} and * Returns an unmodifiable {@link Map} containing the {@code X-YouTube-Client-Name} and
* {@code X-YouTube-Client-Version} headers. * {@code X-YouTube-Client-Version} headers.
* *
* @param name The X-YouTube-Client-Name value. * @param name The X-YouTube-Client-Name value.
* @param version X-YouTube-Client-Version value. * @param version X-YouTube-Client-Version value.
*/ */
private static Map<String, List<String>> getClientHeaders(@Nonnull final String name, private static Map<String, List<String>> getClientHeaders(@Nonnull final String name,
@ -1483,6 +1484,7 @@ public final class YoutubeParsingHelper {
/** /**
* Create a map with the required cookie header. * Create a map with the required cookie header.
*
* @return A singleton map containing the header. * @return A singleton map containing the header.
*/ */
public static Map<String, List<String>> getCookieHeader() { public static Map<String, List<String>> getCookieHeader() {
@ -1801,4 +1803,52 @@ public final class YoutubeParsingHelper {
public static boolean isConsentAccepted() { public static boolean isConsentAccepted() {
return consentAccepted; return consentAccepted;
} }
private static final Pattern AUDIO_STREAM_TYPE_REGEX =
Pattern.compile("&xtags=[\\w%]*acont(?:=|%3D)([a-z]+)(?:=|%3D|:|%3A|&|$)");
/**
* Extract the audio track type from a YouTube stream URL.
* <p>
* The track type is parsed from the {@code xtags} URL parameter
* (Example: {@code acont=original:lang=en}).
* </p>
* @param streamUrl YouTube stream URL
* @return {@link AudioTrackType} or {@code null} if no track type was found
*/
@Nullable
public static AudioTrackType extractAudioTrackType(final String streamUrl) {
final String xtags;
try {
xtags = Utils.getQueryValue(new URL(streamUrl), "xtags");
} catch (final MalformedURLException e) {
return null;
}
if (xtags == null) {
return null;
}
String atype = null;
for (final String param : xtags.split(":")) {
final String[] kv = param.split("=", 2);
if (kv.length > 1 && kv[0].equals("acont")) {
atype = kv[1];
break;
}
}
if (atype == null) {
return null;
}
switch (atype) {
case "original":
return AudioTrackType.ORIGINAL;
case "dubbed":
return AudioTrackType.DUBBED;
case "descriptive":
return AudioTrackType.DESCRIPTIVE;
default:
return null;
}
}
} }

View File

@ -72,7 +72,6 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.services.youtube.YoutubeThrottlingDecrypter; import org.schabi.newpipe.extractor.services.youtube.YoutubeThrottlingDecrypter;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory; import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.Frameset; import org.schabi.newpipe.extractor.stream.Frameset;
@ -100,7 +99,6 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -812,8 +810,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
"\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(" "\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\("
}; };
private static final String STS_REGEX = "signatureTimestamp[=:](\\d+)"; private static final String STS_REGEX = "signatureTimestamp[=:](\\d+)";
private static final Pattern AUDIO_STREAM_TYPE_REGEX =
Pattern.compile("&xtags=[\\w%]*acont(?:=|%3D)([a-z]+)(?:=|%3D|:|%3A|&|$)");
@Override @Override
public void onFetchPage(@Nonnull final Downloader downloader) public void onFetchPage(@Nonnull final Downloader downloader)
@ -1488,20 +1484,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
itagItem.setAudioLocale(LocaleCompat.forLanguageTag( itagItem.setAudioLocale(LocaleCompat.forLanguageTag(
audioTrackId.substring(0, audioTrackIdLastLocaleCharacter))); audioTrackId.substring(0, audioTrackIdLastLocaleCharacter)));
} }
itagItem.setAudioTrackType(YoutubeParsingHelper.extractAudioTrackType(streamUrl));
try {
final String atype = Parser.matchGroup1(AUDIO_STREAM_TYPE_REGEX, streamUrl);
switch (atype) {
case "original":
itagItem.setAudioTrackType(AudioTrackType.ORIGINAL);
break;
case "dubbed":
itagItem.setAudioTrackType(AudioTrackType.DUBBED);
break;
case "descriptive":
itagItem.setAudioTrackType(AudioTrackType.DESCRIPTIVE);
}
} catch (final Parser.RegexException ignored) { }
} }
itagItem.setAudioTrackName(formatData.getObject("audioTrack") itagItem.setAudioTrackName(formatData.getObject("audioTrack")

View File

@ -1,16 +1,20 @@
package org.schabi.newpipe.extractor.services.youtube; package org.schabi.newpipe.extractor.services.youtube;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderFactory; import org.schabi.newpipe.downloader.DownloaderFactory;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import static org.junit.jupiter.api.Assertions.*;
public class YoutubeParsingHelperTest { public class YoutubeParsingHelperTest {
@ -48,4 +52,17 @@ public class YoutubeParsingHelperTest {
assertEquals("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html", assertEquals("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html",
YoutubeParsingHelper.extractCachedUrlIfNeeded("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html")); YoutubeParsingHelper.extractCachedUrlIfNeeded("https://www.infektionsschutz.de/coronavirus-sars-cov-2.html"));
} }
@Test
public void extractAudioTrackType() {
final String originalUrl = "https://rr2---sn-4g5lzned.googlevideo.com/videoplayback?expire=1679429648&ei=sLsZZKrICIuR1gLSnYbgAg&ip=2001%3A638%3A102%3A26%3A1a7c%3A106b%3A6e4a%3Adc09&id=o-ALWn2ZwDxUXEZKzlsT_X9iuDjRMSi__SgRXVrVjKZEhc&itag=251&source=youtube&requiressl=yes&mh=nU&mm=31%2C29&mn=sn-4g5lzned%2Csn-4g5edndz&ms=au%2Crdu&mv=m&mvi=2&pl=40&initcwndbps=1740000&spc=H3gIhgXQzBxvKu2MOEmFaaEenC4DKdVUwudTeu3dtKwmq-Xv5g&vprv=1&xtags=acont%3Doriginal%3Alang%3Den&mime=audio%2Fwebm&ns=-lg0OQZL1LZRQO-dzE0W4E4L&gir=yes&clen=3513412&dur=303.681&lmt=1679342942566207&mt=1679407764&fvip=1&keepalive=yes&fexp=24007246&c=WEB&txp=5532434&n=gDLP5pImH9Vr7v&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgPFQ1yX8aoc35sz2eV2-wzNIhTQeOHGCsOmIonmo776kCIFo5k6HZ5kAQ6DycRCAG8jJgk9jNyncILGPrGZMZUuuo&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhANODPaBuc32MWI9gF3Bn1iz3byEn7EwUiXpNLuCcQqW9AiBB88Qrrz2fJCzYKg14_nnGxGQH1Uoi7i31OSrHK6_dGw%3D%3D";
final String dubbedUrl = "https://rr2---sn-4g5lzned.googlevideo.com/videoplayback?expire=1679429648&ei=sLsZZKrICIuR1gLSnYbgAg&ip=2001%3A638%3A102%3A26%3A1a7c%3A106b%3A6e4a%3Adc09&id=o-ALWn2ZwDxUXEZKzlsT_X9iuDjRMSi__SgRXVrVjKZEhc&itag=251&source=youtube&requiressl=yes&mh=nU&mm=31%2C29&mn=sn-4g5lzned%2Csn-4g5edndz&ms=au%2Crdu&mv=m&mvi=2&pl=40&initcwndbps=1740000&spc=H3gIhgXQzBxvKu2MOEmFaaEenC4DKdVUwudTeu3dtKwmq-Xv5g&vprv=1&xtags=acont%3Ddubbed%3Alang%3Den&mime=audio%2Fwebm&ns=-lg0OQZL1LZRQO-dzE0W4E4L&gir=yes&clen=3884070&dur=303.721&lmt=1679342946044954&mt=1679407764&fvip=1&keepalive=yes&fexp=24007246&c=WEB&txp=5532434&n=gDLP5pImH9Vr7v&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAKEMLB8yLZJf2jXAu4P1Q8AVEciYsmjjr2syYAWZfJg6AiAfu-XI11zYpCLqljw_MCegh26pJHYyfatgfFGWfpL-6Q%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhANODPaBuc32MWI9gF3Bn1iz3byEn7EwUiXpNLuCcQqW9AiBB88Qrrz2fJCzYKg14_nnGxGQH1Uoi7i31OSrHK6_dGw%3D%3D";
final String descriptiveUrl = "https://rr2---sn-4g5lzned.googlevideo.com/videoplayback?expire=1679429648&ei=sLsZZKrICIuR1gLSnYbgAg&ip=2001%3A638%3A102%3A26%3A1a7c%3A106b%3A6e4a%3Adc09&id=o-ALWn2ZwDxUXEZKzlsT_X9iuDjRMSi__SgRXVrVjKZEhc&itag=251&source=youtube&requiressl=yes&mh=nU&mm=31%2C29&mn=sn-4g5lzned%2Csn-4g5edndz&ms=au%2Crdu&mv=m&mvi=2&pl=40&initcwndbps=1740000&spc=H3gIhgXQzBxvKu2MOEmFaaEenC4DKdVUwudTeu3dtKwmq-Xv5g&vprv=1&xtags=acont%3Ddescriptive%3Alang%3Den&mime=audio%2Fwebm&ns=-lg0OQZL1LZRQO-dzE0W4E4L&gir=yes&clen=4061711&dur=303.721&lmt=1679342946800120&mt=1679407764&fvip=1&keepalive=yes&fexp=24007246&c=WEB&txp=5532434&n=gDLP5pImH9Vr7v&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAKFUzoNscV1hbNcPwcnQO3vOy47q69szj7BdLhFYS52pAiEA2oPhLZIZsrUQrx62iH4dHvTBlCloC3NieJw6edo7LL8%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhANODPaBuc32MWI9gF3Bn1iz3byEn7EwUiXpNLuCcQqW9AiBB88Qrrz2fJCzYKg14_nnGxGQH1Uoi7i31OSrHK6_dGw%3D%3D";
final String noTrackUrl = "https://rr2---sn-4g5ednz7.googlevideo.com/videoplayback?expire=1679430240&ei=AL4ZZKiXJefYx_APj_6ECA&ip=2001%3A638%3A102%3A26%3A1a7c%3A106b%3A6e4a%3Adc09&id=o-ALKVh9uHVEvurL3bZOZCEMzFod9ZmJJd6GszA6UEIuKy&itag=251&source=youtube&requiressl=yes&mh=8L&mm=31%2C26&mn=sn-4g5ednz7%2Csn-i5heen7z&ms=au%2Conr&mv=m&mvi=2&pl=40&initcwndbps=1793750&spc=H3gIhh2s06nxQJg3zEgY9pw84syUasRiagYDsQ5UHHfcu5bfTA&vprv=1&mime=audio%2Fwebm&ns=VumObYcnTZNicexX7Ek2WakL&gir=yes&clen=3711099&dur=299.201&lmt=1679334484198077&mt=1679408487&fvip=2&keepalive=yes&fexp=24007246&c=WEB&txp=3318224&n=10c-m6ZvG6C7rC&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAODS0aHRBgdrHm5qwquqGC6zq3rU81W59y4BtV0Y9KStAiAPT8ykXXj_7GzAyZbLPgYKs-B1HWT-4bY0CppmZ2rReg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAL8fS6T-V9BNqrx55mdMvve5be2gcjIY8pYfxlUMPY6pAiAgiCMbqR4eSS_HvLu9KBe6cCFZeMcSTc7vzWtL9y0xvw%3D%3D";
assertEquals(AudioTrackType.ORIGINAL, YoutubeParsingHelper.extractAudioTrackType(originalUrl));
assertEquals(AudioTrackType.DUBBED, YoutubeParsingHelper.extractAudioTrackType(dubbedUrl));
assertEquals(AudioTrackType.DESCRIPTIVE, YoutubeParsingHelper.extractAudioTrackType(descriptiveUrl));
assertNull(YoutubeParsingHelper.extractAudioTrackType(noTrackUrl));
}
} }