Skip to content

Commit e643024

Browse files
committed
[YouTube] Add base class to parse trending videos' charts responses
1 parent a4aeedf commit e643024

1 file changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk;
2+
3+
import com.grack.nanojson.JsonArray;
4+
import com.grack.nanojson.JsonObject;
5+
import com.grack.nanojson.JsonWriter;
6+
import org.schabi.newpipe.extractor.Image;
7+
import org.schabi.newpipe.extractor.Page;
8+
import org.schabi.newpipe.extractor.StreamingService;
9+
import org.schabi.newpipe.extractor.downloader.Downloader;
10+
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
11+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
12+
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
13+
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
14+
import org.schabi.newpipe.extractor.localization.ContentCountry;
15+
import org.schabi.newpipe.extractor.localization.DateWrapper;
16+
import org.schabi.newpipe.extractor.localization.Localization;
17+
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo;
18+
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
19+
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
20+
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
21+
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
22+
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
23+
import org.schabi.newpipe.extractor.stream.StreamType;
24+
import org.schabi.newpipe.extractor.utils.JsonUtils;
25+
26+
import javax.annotation.Nonnull;
27+
import javax.annotation.Nullable;
28+
import java.io.IOException;
29+
import java.nio.charset.StandardCharsets;
30+
import java.time.OffsetDateTime;
31+
import java.time.ZoneOffset;
32+
import java.util.HashMap;
33+
import java.util.List;
34+
import java.util.Set;
35+
36+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER;
37+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientHeaders;
38+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getOriginReferrerHeaders;
39+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getThumbnailsFromInfoItem;
40+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getValidJsonResponseBody;
41+
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareJsonBuilder;
42+
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
43+
44+
/**
45+
* Base class parsing responses from YouTube Charts for all trending video charts.
46+
*
47+
* <p>
48+
* Note: YouTube Charts isn't officially supported in all YouTube supported countries (there are
49+
* fewer countries in the {@code LAUNCHED_CHART_COUNTRIES} array of YouTube Charts' HTML responses
50+
* than in the YouTube country selector).
51+
* </p>
52+
*
53+
* <p>
54+
* For some trends, some videos are still returned in unsupported countries, even if there are
55+
* fewer than in a supported country, for others an HTTP 400 error is returned saying
56+
* {@code Request contains an invalid argument.}.
57+
* </p>
58+
*/
59+
abstract class YoutubeChartsBaseKioskExtractor extends KioskExtractor<StreamInfoItem> {
60+
61+
// Extracted from YouTube Charts' HTML, in the array named LAUNCHED_CHART_COUNTRIES
62+
protected static final Set<String> YT_CHARTS_SUPPORTED_COUNTRY_CODES = Set.of(
63+
"AE", "AR", "AT", "AU", "BE", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CZ", "DE",
64+
"DK", "DO", "EC", "EE", "EG", "ES", "FI", "FR", "GB", "GT", "HN", "HU", "ID", "IE",
65+
"IL", "IN", "IS", "IT", "JP", "KE", "KR", "LU", "MX", "NG", "NI", "NL", "NO", "NZ",
66+
"PA", "PE", "PL", "PT", "PY", "RO", "RS", "RU", "SA", "SE", "SV", "TR", "TZ", "UA",
67+
"UG", "US", "UY", "ZA", "ZW");
68+
69+
protected static final String YT_CHARTS_ENDPOINT =
70+
"https://charts.youtube.com/youtubei/v1/browse?alt=json&"
71+
+ DISABLE_PRETTY_PRINT_PARAMETER;
72+
73+
protected final String chartType;
74+
protected JsonObject browseResponse;
75+
76+
protected YoutubeChartsBaseKioskExtractor(final StreamingService streamingService,
77+
final ListLinkHandler linkHandler,
78+
final String kioskId,
79+
final String chartType) {
80+
super(streamingService, linkHandler, kioskId);
81+
this.chartType = chartType;
82+
}
83+
84+
@Override
85+
public void onFetchPage(@Nonnull final Downloader downloader)
86+
throws IOException, ExtractionException {
87+
final Localization localization = getExtractorLocalization();
88+
final ContentCountry contentCountry = getExtractorContentCountry();
89+
90+
final InnertubeClientRequestInfo innertubeClientRequestInfo =
91+
InnertubeClientRequestInfo.ofWebMusicAnalyticsChartsClient();
92+
93+
final byte[] body = JsonWriter.string(prepareJsonBuilder(getExtractorLocalization(),
94+
contentCountry, innertubeClientRequestInfo, null)
95+
.value("browseId", "FEmusic_analytics_charts_home")
96+
.value("query", "perspective=CHART_DETAILS&chart_params_country_code="
97+
+ contentCountry.getCountryCode() + "&chart_params_chart_type="
98+
+ chartType)
99+
.done())
100+
.getBytes(StandardCharsets.UTF_8);
101+
102+
final var headers = new HashMap<>(getOriginReferrerHeaders("https://charts.youtube.com"));
103+
headers.putAll(getClientHeaders(innertubeClientRequestInfo.clientInfo.clientId,
104+
innertubeClientRequestInfo.clientInfo.clientVersion));
105+
106+
browseResponse = JsonUtils.toJsonObject(getValidJsonResponseBody(
107+
getDownloader().postWithContentTypeJson(
108+
YT_CHARTS_ENDPOINT, headers, body, localization)));
109+
}
110+
111+
@Nonnull
112+
@Override
113+
public abstract String getName() throws ParsingException;
114+
115+
@Nonnull
116+
@Override
117+
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
118+
final JsonArray videos = browseResponse.getObject("contents")
119+
.getObject("sectionListRenderer")
120+
.getArray("contents")
121+
.getObject(0)
122+
.getObject("musicAnalyticsSectionRenderer")
123+
.getObject("content")
124+
.getArray("videos")
125+
.getObject(0)
126+
.getArray("videoViews");
127+
128+
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
129+
130+
videos.streamAsJsonObjects()
131+
.forEachOrdered(video -> collector.commit(
132+
new YoutubeChartsVideoInfoItemExtractor(video)));
133+
134+
return new InfoItemsPage<>(collector, null);
135+
}
136+
137+
@Override
138+
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
139+
// There is no continuation in charts
140+
return InfoItemsPage.emptyPage();
141+
}
142+
143+
static final class YoutubeChartsVideoInfoItemExtractor
144+
implements StreamInfoItemExtractor {
145+
146+
@Nonnull
147+
private final JsonObject videoObject;
148+
149+
YoutubeChartsVideoInfoItemExtractor(@Nonnull final JsonObject videoObject) {
150+
this.videoObject = videoObject;
151+
}
152+
153+
@Override
154+
public StreamType getStreamType() {
155+
// There are only video streams in YouTube Charts, at least for now
156+
return StreamType.VIDEO_STREAM;
157+
}
158+
159+
@Override
160+
public boolean isAd() {
161+
return false;
162+
}
163+
164+
@Override
165+
public long getDuration() {
166+
return videoObject.getInt("videoDuration", -1);
167+
}
168+
169+
@Override
170+
public long getViewCount() {
171+
// View counts aren't returned, at least for now
172+
return -1;
173+
}
174+
175+
@Override
176+
public String getUploaderName() {
177+
return videoObject.getString("channelName");
178+
}
179+
180+
@Override
181+
public String getUploaderUrl() throws ParsingException {
182+
final String channelId = videoObject.getString("externalChannelId");
183+
184+
if (isNullOrEmpty(channelId)) {
185+
throw new ParsingException("Could not get channel ID");
186+
}
187+
188+
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + channelId);
189+
}
190+
191+
@Override
192+
public boolean isUploaderVerified() {
193+
// We don't have any info on this, at least for now
194+
return false;
195+
}
196+
197+
@Nullable
198+
@Override
199+
public String getTextualUploadDate() {
200+
return null;
201+
}
202+
203+
@Nonnull
204+
@Override
205+
public DateWrapper getUploadDate() {
206+
final JsonObject releaseDate = videoObject.getObject("releaseDate");
207+
return new DateWrapper(OffsetDateTime.of(
208+
releaseDate.getInt("year"),
209+
releaseDate.getInt("month"),
210+
releaseDate.getInt("day"),
211+
0,
212+
0,
213+
0,
214+
0,
215+
// We request that times should be returned with 0 offset to UTC timezone in
216+
// the JSON body, but YouTube charts does it only in its HTTP headers
217+
ZoneOffset.UTC),
218+
// We don't have more info than the release day
219+
true);
220+
}
221+
222+
@Override
223+
public String getName() {
224+
return videoObject.getString("title");
225+
}
226+
227+
@Override
228+
public String getUrl() throws ParsingException {
229+
return YoutubeStreamLinkHandlerFactory.getInstance().getUrl(
230+
videoObject.getString("id"));
231+
}
232+
233+
@Nonnull
234+
@Override
235+
public List<Image> getThumbnails() throws ParsingException {
236+
return getThumbnailsFromInfoItem(videoObject);
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)