Skip to content

Commit 4d683e7

Browse files
committed
Support YouTube's learning playlists
1 parent 0b4977b commit 4d683e7

2 files changed

Lines changed: 196 additions & 14 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubePlaylistExtractor.java

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@
88
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
99
import org.schabi.newpipe.extractor.exceptions.ParsingException;
1010
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
11+
import org.schabi.newpipe.extractor.localization.DateWrapper;
1112
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
1213
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
1314
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
15+
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
1416
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
17+
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
1518
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
19+
import org.schabi.newpipe.extractor.stream.StreamType;
1620
import org.schabi.newpipe.extractor.utils.Utils;
1721

1822
import java.io.IOException;
1923

2024
import javax.annotation.Nonnull;
25+
import javax.annotation.Nullable;
2126

2227
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
2328
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonResponse;
@@ -152,34 +157,47 @@ public long getStreamCount() throws ParsingException {
152157

153158
@Nonnull
154159
@Override
155-
public String getSubChannelName() throws ParsingException {
160+
public String getSubChannelName() {
156161
return "";
157162
}
158163

159164
@Nonnull
160165
@Override
161-
public String getSubChannelUrl() throws ParsingException {
166+
public String getSubChannelUrl() {
162167
return "";
163168
}
164169

165170
@Nonnull
166171
@Override
167-
public String getSubChannelAvatarUrl() throws ParsingException {
172+
public String getSubChannelAvatarUrl() {
168173
return "";
169174
}
170175

171176
@Nonnull
172177
@Override
173178
public InfoItemsPage<StreamInfoItem> getInitialPage() {
174-
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
179+
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
175180

176-
JsonArray videos = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
181+
final JsonArray contents = initialData.getObject("contents").getObject("twoColumnBrowseResultsRenderer")
177182
.getArray("tabs").getObject(0).getObject("tabRenderer").getObject("content")
178183
.getObject("sectionListRenderer").getArray("contents").getObject(0)
179-
.getObject("itemSectionRenderer").getArray("contents").getObject(0)
180-
.getObject("playlistVideoListRenderer").getArray("contents");
184+
.getObject("itemSectionRenderer").getArray("contents");
185+
186+
if (contents.getObject(0).has("playlistSegmentRenderer")) {
187+
for (final Object segment : contents) {
188+
if (((JsonObject) segment).getObject("playlistSegmentRenderer").has("trailer")) {
189+
collectTrailerFrom(collector, ((JsonObject) segment));
190+
} else if (((JsonObject) segment).getObject("playlistSegmentRenderer").has("videoList")) {
191+
collectStreamsFrom(collector, ((JsonObject) segment).getObject("playlistSegmentRenderer")
192+
.getObject("videoList").getObject("playlistVideoListRenderer").getArray("contents"));
193+
}
194+
}
195+
} else if (contents.getObject(0).has("playlistVideoListRenderer")) {
196+
final JsonArray videos = contents.getObject(0)
197+
.getObject("playlistVideoListRenderer").getArray("contents");
198+
collectStreamsFrom(collector, videos);
199+
}
181200

182-
collectStreamsFrom(collector, videos);
183201
return new InfoItemsPage<>(collector, getNextPageUrl());
184202
}
185203

@@ -189,18 +207,18 @@ public InfoItemsPage<StreamInfoItem> getPage(final String pageUrl) throws IOExce
189207
throw new ExtractionException(new IllegalArgumentException("Page url is empty or null"));
190208
}
191209

192-
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
210+
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
193211
final JsonArray ajaxJson = getJsonResponse(pageUrl, getExtractorLocalization());
194212

195-
JsonObject sectionListContinuation = ajaxJson.getObject(1).getObject("response")
213+
final JsonObject sectionListContinuation = ajaxJson.getObject(1).getObject("response")
196214
.getObject("continuationContents").getObject("playlistVideoListContinuation");
197215

198216
collectStreamsFrom(collector, sectionListContinuation.getArray("contents"));
199217

200218
return new InfoItemsPage<>(collector, getNextPageUrlFrom(sectionListContinuation.getArray("continuations")));
201219
}
202220

203-
private String getNextPageUrlFrom(JsonArray continuations) {
221+
private String getNextPageUrlFrom(final JsonArray continuations) {
204222
if (isNullOrEmpty(continuations)) {
205223
return "";
206224
}
@@ -212,9 +230,7 @@ private String getNextPageUrlFrom(JsonArray continuations) {
212230
+ "&itct=" + clickTrackingParams;
213231
}
214232

215-
private void collectStreamsFrom(StreamInfoItemsCollector collector, JsonArray videos) {
216-
collector.reset();
217-
233+
private void collectStreamsFrom(final StreamInfoItemsCollector collector, final JsonArray videos) {
218234
final TimeAgoParser timeAgoParser = getTimeAgoParser();
219235

220236
for (Object video : videos) {
@@ -228,4 +244,72 @@ public long getViewCount() {
228244
}
229245
}
230246
}
247+
248+
private void collectTrailerFrom(final StreamInfoItemsCollector collector,
249+
final JsonObject segment) {
250+
collector.commit(new StreamInfoItemExtractor() {
251+
@Override
252+
public String getName() throws ParsingException {
253+
return getTextFromObject(segment.getObject("playlistSegmentRenderer")
254+
.getObject("title"));
255+
}
256+
257+
@Override
258+
public String getUrl() throws ParsingException {
259+
return YoutubeStreamLinkHandlerFactory.getInstance()
260+
.fromId(segment.getObject("playlistSegmentRenderer").getObject("trailer")
261+
.getObject("playlistVideoPlayerRenderer").getString("videoId"))
262+
.getUrl();
263+
}
264+
265+
@Override
266+
public String getThumbnailUrl() {
267+
return null;
268+
}
269+
270+
@Override
271+
public StreamType getStreamType() {
272+
return StreamType.VIDEO_STREAM;
273+
}
274+
275+
@Override
276+
public boolean isAd() {
277+
return false;
278+
}
279+
280+
@Override
281+
public long getDuration() throws ParsingException {
282+
return YoutubeParsingHelper.parseDurationString(
283+
getTextFromObject(segment.getObject("playlistSegmentRenderer")
284+
.getObject("segmentAnnotation")).split("•")[0]);
285+
}
286+
287+
@Override
288+
public long getViewCount() {
289+
return -1;
290+
}
291+
292+
@Override
293+
public String getUploaderName() throws ParsingException {
294+
return YoutubePlaylistExtractor.this.getUploaderName();
295+
}
296+
297+
@Override
298+
public String getUploaderUrl() throws ParsingException {
299+
return YoutubePlaylistExtractor.this.getUploaderUrl();
300+
}
301+
302+
@Nullable
303+
@Override
304+
public String getTextualUploadDate() {
305+
return null;
306+
}
307+
308+
@Nullable
309+
@Override
310+
public DateWrapper getUploadDate() {
311+
return null;
312+
}
313+
});
314+
}
231315
}

extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubePlaylistExtractorTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,102 @@ public void testStreamCount() throws Exception {
254254
assertTrue("Error in the streams count", extractor.getStreamCount() > 100);
255255
}
256256
}
257+
258+
public static class LearningPlaylist implements BasePlaylistExtractorTest {
259+
private static YoutubePlaylistExtractor extractor;
260+
261+
@BeforeClass
262+
public static void setUp() throws Exception {
263+
NewPipe.init(DownloaderTestImpl.getInstance());
264+
extractor = (YoutubePlaylistExtractor) YouTube
265+
.getPlaylistExtractor("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8");
266+
extractor.fetchPage();
267+
}
268+
269+
/*//////////////////////////////////////////////////////////////////////////
270+
// Extractor
271+
//////////////////////////////////////////////////////////////////////////*/
272+
273+
@Test
274+
public void testServiceId() {
275+
assertEquals(YouTube.getServiceId(), extractor.getServiceId());
276+
}
277+
278+
@Test
279+
public void testName() throws Exception {
280+
String name = extractor.getName();
281+
assertTrue(name, name.startsWith("Anatomy & Physiology"));
282+
}
283+
284+
@Test
285+
public void testId() throws Exception {
286+
assertEquals("PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getId());
287+
}
288+
289+
@Test
290+
public void testUrl() throws ParsingException {
291+
assertEquals("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getUrl());
292+
}
293+
294+
@Test
295+
public void testOriginalUrl() throws ParsingException {
296+
assertEquals("https://www.youtube.com/playlist?list=PL8dPuuaLjXtOAKed_MxxWBNaPno5h3Zs8", extractor.getOriginalUrl());
297+
}
298+
299+
/*//////////////////////////////////////////////////////////////////////////
300+
// ListExtractor
301+
//////////////////////////////////////////////////////////////////////////*/
302+
303+
@Test
304+
public void testRelatedItems() throws Exception {
305+
defaultTestRelatedItems(extractor);
306+
}
307+
308+
@Ignore
309+
@Test
310+
public void testMoreRelatedItems() throws Exception {
311+
defaultTestMoreItems(extractor);
312+
}
313+
314+
/*//////////////////////////////////////////////////////////////////////////
315+
// PlaylistExtractor
316+
//////////////////////////////////////////////////////////////////////////*/
317+
318+
@Test
319+
public void testThumbnailUrl() throws Exception {
320+
final String thumbnailUrl = extractor.getThumbnailUrl();
321+
assertIsSecureUrl(thumbnailUrl);
322+
assertTrue(thumbnailUrl, thumbnailUrl.contains("yt"));
323+
}
324+
325+
@Ignore
326+
@Test
327+
public void testBannerUrl() throws Exception {
328+
final String bannerUrl = extractor.getBannerUrl();
329+
assertIsSecureUrl(bannerUrl);
330+
assertTrue(bannerUrl, bannerUrl.contains("yt"));
331+
}
332+
333+
@Test
334+
public void testUploaderUrl() throws Exception {
335+
assertEquals("https://www.youtube.com/channel/UCX6b17PVsYBQ0ip5gyeme-Q", extractor.getUploaderUrl());
336+
}
337+
338+
@Test
339+
public void testUploaderName() throws Exception {
340+
final String uploaderName = extractor.getUploaderName();
341+
assertTrue(uploaderName, uploaderName.contains("CrashCourse"));
342+
}
343+
344+
@Test
345+
public void testUploaderAvatarUrl() throws Exception {
346+
final String uploaderAvatarUrl = extractor.getUploaderAvatarUrl();
347+
assertTrue(uploaderAvatarUrl, uploaderAvatarUrl.contains("yt"));
348+
}
349+
350+
@Test
351+
public void testStreamCount() throws Exception {
352+
assertTrue("Error in the streams count", extractor.getStreamCount() > 40);
353+
}
354+
}
257355
}

0 commit comments

Comments
 (0)