Compare commits

...

3 Commits

Author SHA1 Message Date
Stypox 4e9e7cb29c
Improve assertTabsContain() to also check size 2024-04-20 11:48:36 +02:00
Stypox 9d0dd36034
[YouTube] Create constants for client names/versions 2024-04-20 11:43:54 +02:00
Stypox d4e6d22e64
[YouTube] Improve meta info code for review 2024-04-20 11:43:08 +02:00
4 changed files with 66 additions and 37 deletions

View File

@ -170,41 +170,34 @@ public final class YoutubeMetaInfoHelper {
// usually an encouragement like "We are with you"
final String title = getTextFromObjectOrThrow(r.getObject("title"), "title");
// usually a phone number
final String action;
final String action; // this variable is expected to start with "\n"
if (r.has("actionText")) {
action = getTextFromObjectOrThrow(r.getObject("actionText"), "action");
action = "\n" + getTextFromObjectOrThrow(r.getObject("actionText"), "action");
} else if (r.has("contacts")) {
final JsonArray contacts = r.getArray("contacts");
final StringBuilder stringBuilder = new StringBuilder();
int i = 0;
final int contactsSize = contacts.size();
if (contactsSize != 0) {
// Loop over contacts item from the first contact to the last one, if there is
// not only one, in order to not add an unneeded line return
for (; i < contactsSize - 1; i++) {
stringBuilder.append(getTextFromObjectOrThrow(contacts.getObject(i)
.getObject("actionText"), "contacts.actionText"));
stringBuilder.append("\n");
}
// Add the latest contact without an extra line return
// Loop over contacts item from the first contact to the last one
for (int i = 0; i < contacts.size(); i++) {
stringBuilder.append("\n");
stringBuilder.append(getTextFromObjectOrThrow(contacts.getObject(i)
.getObject("actionText"), "contacts.actionText"));
}
action = stringBuilder.toString();
} else {
action = null;
action = "";
}
// usually details about the phone number
final String details = getTextFromObjectOrThrow(r.getObject("detailsText"), "details");
// usually the name of an association
final String urlText = getTextFromObjectOrThrow(r.getObject("navigationText"),
"urlText");
metaInfo.setTitle(title);
metaInfo.setContent(new Description(isNullOrEmpty(action)
? details : details + "\n" + action, Description.PLAIN_TEXT));
metaInfo.setContent(new Description(details + action, Description.PLAIN_TEXT));
metaInfo.addUrlText(urlText);
// usually the webpage of the association

View File

@ -144,6 +144,11 @@ public final class YoutubeParsingHelper {
*/
public static final String RACY_CHECK_OK = "racyCheckOk";
/**
* The hardcoded client ID used for InnerTube requests with the {@code WEB} client.
*/
private static final String WEB_CLIENT_ID = "1";
/**
* The client version for InnerTube requests with the {@code WEB} client, used as the last
* fallback if the extraction of the real one failed.
@ -177,6 +182,11 @@ public final class YoutubeParsingHelper {
*/
private static final String TVHTML5_SIMPLY_EMBED_CLIENT_VERSION = "2.0";
/**
* The hardcoded client ID used for InnerTube requests with the YouTube Music desktop client.
*/
private static final String YOUTUBE_MUSIC_CLIENT_ID = "67";
/**
* The hardcoded client version used for InnerTube requests with the YouTube Music desktop
* client.
@ -212,6 +222,30 @@ public final class YoutubeParsingHelper {
*/
private static final String IOS_DEVICE_MODEL = "iPhone15,4";
/**
* Spoofing an iPhone 15 running iOS 17.4.1 with the hardcoded version of the iOS app. To be
* used for the {@code "osVersion"} field in JSON POST requests.
* <p>
* The value of this field seems to use the following structure:
* "iOS major version.minor version.patch version.build version", where
* "patch version" is equal to 0 if it isn't set
* The build version corresponding to the iOS version used can be found on
* <a href="https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15">
* https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15</a>
* </p>
*
* @see #IOS_USER_AGENT_VERSION
*/
private static final String IOS_OS_VERSION = "17.4.1.21E237";
/**
* Spoofing an iPhone 15 running iOS 17.4.1 with the hardcoded version of the iOS app. To be
* used in the user agent for requests.
*
* @see #IOS_OS_VERSION
*/
private static final String IOS_USER_AGENT_VERSION = "17_4_1";
private static Random numberGenerator = new Random();
private static final String FEED_BASE_CHANNEL_ID =
@ -529,7 +563,7 @@ public final class YoutubeParsingHelper {
.end().done().getBytes(StandardCharsets.UTF_8);
// @formatter:on
final var headers = getClientHeaders("1", HARDCODED_CLIENT_VERSION);
final var headers = getClientHeaders(WEB_CLIENT_ID, HARDCODED_CLIENT_VERSION);
// This endpoint is fetched by the YouTube website to get the items of its main menu and is
// pretty lightweight (around 30kB)
@ -723,7 +757,8 @@ public final class YoutubeParsingHelper {
// @formatter:on
final var headers = new HashMap<>(getOriginReferrerHeaders(YOUTUBE_MUSIC_URL));
headers.putAll(getClientHeaders("67", HARDCODED_YOUTUBE_MUSIC_CLIENT_VERSION));
headers.putAll(getClientHeaders(YOUTUBE_MUSIC_CLIENT_ID,
HARDCODED_YOUTUBE_MUSIC_CLIENT_VERSION));
final Response response = getDownloader().postWithContentTypeJson(url, headers, json);
// Ensure to have a valid response
@ -1217,14 +1252,7 @@ public final class YoutubeParsingHelper {
.value("deviceModel", IOS_DEVICE_MODEL)
.value("platform", "MOBILE")
.value("osName", "iOS")
/*
The value of this field seems to use the following structure:
"iOS major version.minor version.patch version.build version", where
"patch version" is equal to 0 if it isn't set
The build version corresponding to the iOS version used can be found on
https://theapplewiki.com/wiki/Firmware/iPhone/17.x#iPhone_15
*/
.value("osVersion", "17.4.1.21E237")
.value("osVersion", IOS_OS_VERSION)
.value("hl", localization.getLocalizationCode())
.value("gl", contentCountry.getCountryCode())
.value("utcOffsetMinutes", 0)
@ -1345,7 +1373,8 @@ public final class YoutubeParsingHelper {
public static String getIosUserAgent(@Nullable final Localization localization) {
// Spoofing an iPhone 15 running iOS 17.4.1 with the hardcoded version of the iOS app
return "com.google.ios.youtube/" + IOS_YOUTUBE_CLIENT_VERSION
+ "(" + IOS_DEVICE_MODEL + "; U; CPU iOS 17_4_1 like Mac OS X; "
+ "(" + IOS_DEVICE_MODEL + "; U; CPU iOS "
+ IOS_USER_AGENT_VERSION + " like Mac OS X; "
+ (localization != null ? localization : Localization.DEFAULT).getCountryCode()
+ ")";
}
@ -1356,7 +1385,8 @@ public final class YoutubeParsingHelper {
@Nonnull
public static Map<String, List<String>> getYoutubeMusicHeaders() {
final var headers = new HashMap<>(getOriginReferrerHeaders(YOUTUBE_MUSIC_URL));
headers.putAll(getClientHeaders("67", youtubeMusicClientVersion));
headers.putAll(getClientHeaders(YOUTUBE_MUSIC_CLIENT_ID,
youtubeMusicClientVersion));
return headers;
}
@ -1378,7 +1408,7 @@ public final class YoutubeParsingHelper {
public static Map<String, List<String>> getClientInfoHeaders()
throws ExtractionException, IOException {
final var headers = new HashMap<>(getOriginReferrerHeaders("https://www.youtube.com"));
headers.putAll(getClientHeaders("1", getClientVersion()));
headers.putAll(getClientHeaders(WEB_CLIENT_ID, getClientVersion()));
return headers;
}

View File

@ -171,8 +171,17 @@ public class ExtractorAsserts {
public static void assertTabsContain(@Nonnull final List<ListLinkHandler> tabs,
@Nonnull final String... expectedTabs) {
final Set<String> tabSet = tabs.stream()
.map(linkHandler -> linkHandler.getContentFilters().get(0))
.map(linkHandler -> {
assertEquals(1, linkHandler.getContentFilters().size(),
"Unexpected content filters for channel tab: "
+ linkHandler.getContentFilters());
return linkHandler.getContentFilters().get(0);
})
.collect(Collectors.toUnmodifiableSet());
assertEquals(expectedTabs.length, tabSet.size(),
"Different amount of tabs returned:\nExpected: "
+ Arrays.toString(expectedTabs) + "\nActual: " + tabSet);
Arrays.stream(expectedTabs)
.forEach(expectedTab -> assertTrue(tabSet.contains(expectedTab),
"Missing " + expectedTab + " tab (got " + tabSet + ")"));

View File

@ -541,7 +541,8 @@ public class YoutubeChannelExtractorTest {
@Test
@Override
public void testTabs() throws Exception {
assertTabsContain(extractor.getTabs(), ChannelTabs.VIDEOS, ChannelTabs.PLAYLISTS);
assertTabsContain(extractor.getTabs(),
ChannelTabs.VIDEOS, ChannelTabs.PLAYLISTS, ChannelTabs.SHORTS);
assertTrue(extractor.getTabs().stream()
.filter(it -> ChannelTabs.VIDEOS.equals(it.getContentFilters().get(0)))
.allMatch(ReadyChannelTabListLinkHandler.class::isInstance));
@ -920,11 +921,7 @@ public class YoutubeChannelExtractorTest {
// Gaming topic channels tabs are not yet supported
// However, a Shorts tab like on other channel types is returned, so it is supported
// Check that it is returned
final List<ListLinkHandler> channelTabs = extractor.getTabs();
assertEquals(1, channelTabs.size());
final List<String> contentFilters = channelTabs.get(0).getContentFilters();
assertEquals(1, contentFilters.size());
assertEquals(ChannelTabs.SHORTS, contentFilters.get(0));
assertTabsContain(extractor.getTabs(), ChannelTabs.SHORTS);
}
@Test