Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MediaCCC] Allow obtaining channel tab link handler #1148

Merged
merged 3 commits into from
Mar 28, 2024
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
Expand Up @@ -19,6 +19,7 @@
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCChannelTabExtractor;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCConferenceExtractor;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCConferenceKiosk;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamExtractor;
Expand Down Expand Up @@ -57,7 +58,9 @@ public ListLinkHandlerFactory getChannelLHFactory() {

@Override
public ListLinkHandlerFactory getChannelTabLHFactory() {
return null;
// there is just one channel tab in MediaCCC, the one containing conferences, so there is
// no need for a specific channel tab link handler, but we can just use the channel one
return MediaCCCConferenceLinkHandlerFactory.getInstance();
}

@Override
Expand Down Expand Up @@ -86,17 +89,13 @@ public ChannelExtractor getChannelExtractor(final ListLinkHandler linkHandler) {
@Override
public ChannelTabExtractor getChannelTabExtractor(final ListLinkHandler linkHandler) {
if (linkHandler instanceof ReadyChannelTabListLinkHandler) {
// conference data has already been fetched, let the ReadyChannelTabListLinkHandler
// create a MediaCCCChannelTabExtractor with that data
return ((ReadyChannelTabListLinkHandler) linkHandler).getChannelTabExtractor(this);
} else {
// conference data has not been fetched yet, so pass null instead
return new MediaCCCChannelTabExtractor(this, linkHandler, null);
}

/*
Channel tab extractors are only supported in conferences and should only come from a
ReadyChannelTabListLinkHandler instance with a ChannelTabExtractorBuilder instance of the
conferences extractor

If that's not the case, return null in this case, so no channel tabs support
*/
return null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.schabi.newpipe.extractor.services.media_ccc.extractors;

import com.grack.nanojson.JsonObject;

import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems.MediaCCCStreamInfoItemExtractor;

import java.io.IOException;
import java.util.Objects;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* MediaCCC does not really have channel tabs, but rather a list of videos for each conference,
* so this class just acts as a videos channel tab extractor.
*/
public class MediaCCCChannelTabExtractor extends ChannelTabExtractor {
@Nullable
private JsonObject conferenceData;

/**
* @param conferenceData will be not-null if conference data has already been fetched by
* {@link MediaCCCConferenceExtractor}. Otherwise, if this parameter is
* {@code null}, conference data will be fetched anew.
*/
public MediaCCCChannelTabExtractor(final StreamingService service,
final ListLinkHandler linkHandler,
@Nullable final JsonObject conferenceData) {
super(service, linkHandler);
this.conferenceData = conferenceData;
}

@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws ExtractionException, IOException {
if (conferenceData == null) {
// only fetch conference data if we don't have it already
conferenceData = MediaCCCConferenceExtractor.fetchConferenceData(downloader, getId());
}
}

@Nonnull
@Override
public ListExtractor.InfoItemsPage<InfoItem> getInitialPage() {
final MultiInfoItemsCollector collector =
new MultiInfoItemsCollector(getServiceId());
Objects.requireNonNull(conferenceData) // will surely be != null after onFetchPage
.getArray("events")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.forEach(event -> collector.commit(new MediaCCCStreamInfoItemExtractor(event)));
return new ListExtractor.InfoItemsPage<>(collector, null);
}

@Override
public ListExtractor.InfoItemsPage<InfoItem> getPage(final Page page) {
return ListExtractor.InfoItemsPage.emptyPage();
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
package org.schabi.newpipe.extractor.services.media_ccc.extractors;

import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getImageListFromLogoImageUrl;

import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;

import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems.MediaCCCStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory;

import java.io.IOException;
Expand All @@ -27,8 +23,6 @@

import javax.annotation.Nonnull;

import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getImageListFromLogoImageUrl;

public class MediaCCCConferenceExtractor extends ChannelExtractor {
private JsonObject conferenceData;

Expand All @@ -37,6 +31,19 @@ public MediaCCCConferenceExtractor(final StreamingService service,
super(service, linkHandler);
}

static JsonObject fetchConferenceData(@Nonnull final Downloader downloader,
@Nonnull final String conferenceId)
throws IOException, ExtractionException {
final String conferenceUrl
= MediaCCCConferenceLinkHandlerFactory.CONFERENCE_API_ENDPOINT + conferenceId;
try {
return JsonParser.object().from(downloader.get(conferenceUrl).responseBody());
} catch (final JsonParserException jpe) {
throw new ExtractionException("Could not parse json returned by URL: " + conferenceUrl);
}
}


@Nonnull
@Override
public List<Image> getAvatars() {
Expand Down Expand Up @@ -88,76 +95,22 @@ public boolean isVerified() {
@Nonnull
@Override
public List<ListLinkHandler> getTabs() throws ParsingException {
return List.of(new ReadyChannelTabListLinkHandler(getUrl(), getId(),
ChannelTabs.VIDEOS, new VideosTabExtractorBuilder(conferenceData)));
// avoid keeping a reference to MediaCCCConferenceExtractor inside the lambda
final JsonObject theConferenceData = conferenceData;
return List.of(new ReadyChannelTabListLinkHandler(getUrl(), getId(), ChannelTabs.VIDEOS,
(service, linkHandler) ->
new MediaCCCChannelTabExtractor(service, linkHandler, theConferenceData)));
}

@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
final String conferenceUrl
= MediaCCCConferenceLinkHandlerFactory.CONFERENCE_API_ENDPOINT + getId();
try {
conferenceData = JsonParser.object().from(downloader.get(conferenceUrl).responseBody());
} catch (final JsonParserException jpe) {
throw new ExtractionException("Could not parse json returned by URL: " + conferenceUrl);
}
conferenceData = fetchConferenceData(downloader, getId());
}

@Nonnull
@Override
public String getName() throws ParsingException {
return conferenceData.getString("title");
}

private static final class VideosTabExtractorBuilder
implements ReadyChannelTabListLinkHandler.ChannelTabExtractorBuilder {

private final JsonObject conferenceData;

VideosTabExtractorBuilder(final JsonObject conferenceData) {
this.conferenceData = conferenceData;
}

@Nonnull
@Override
public ChannelTabExtractor build(@Nonnull final StreamingService service,
@Nonnull final ListLinkHandler linkHandler) {
return new VideosChannelTabExtractor(service, linkHandler, conferenceData);
}
}

private static final class VideosChannelTabExtractor extends ChannelTabExtractor {
private final JsonObject conferenceData;

VideosChannelTabExtractor(final StreamingService service,
final ListLinkHandler linkHandler,
final JsonObject conferenceData) {
super(service, linkHandler);
this.conferenceData = conferenceData;
}

@Override
public void onFetchPage(@Nonnull final Downloader downloader) {
// Nothing to do here, as data was already fetched
}

@Nonnull
@Override
public ListExtractor.InfoItemsPage<InfoItem> getInitialPage() {
final MultiInfoItemsCollector collector =
new MultiInfoItemsCollector(getServiceId());
conferenceData.getArray("events")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.forEach(event -> collector.commit(new MediaCCCStreamInfoItemExtractor(event)));
return new InfoItemsPage<>(collector, null);
}

@Override
public InfoItemsPage<InfoItem> getPage(final Page page) {
return InfoItemsPage.emptyPage();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package org.schabi.newpipe.extractor.services.media_ccc.linkHandler;

import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Parser;

import java.util.List;

/**
* Since MediaCCC does not really have channel tabs (i.e. it only has one single "tab" with videos),
* this link handler acts both as the channel link handler and the channel tab link handler. That's
* why {@link #getAvailableContentFilter()} has been overridden.
*/
public final class MediaCCCConferenceLinkHandlerFactory extends ListLinkHandlerFactory {

private static final MediaCCCConferenceLinkHandlerFactory INSTANCE
Expand Down Expand Up @@ -46,4 +52,15 @@ public boolean onAcceptUrl(final String url) {
return false;
}
}

/**
* @see MediaCCCConferenceLinkHandlerFactory
* @return MediaCCC's only channel "tab", i.e. {@link ChannelTabs#VIDEOS}
*/
@Override
public String[] getAvailableContentFilter() {
return new String[]{
ChannelTabs.VIDEOS,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.schabi.newpipe.extractor.services.media_ccc;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.schabi.newpipe.extractor.ServiceList.MediaCCC;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;

/**
* Test that it is possible to create and use a channel tab extractor ({@link
* org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCChannelTabExtractor}) without
* passing through the conference extractor
*/
public class MediaCCCChannelTabExtractorTest {
public static class CCCamp2023 {
private static ChannelTabExtractor extractor;

@BeforeAll
public static void setUpClass() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
extractor = MediaCCC.getChannelTabExtractorFromId("camp2023", ChannelTabs.VIDEOS);
extractor.fetchPage();
}

@Test
void testName() {
assertEquals(ChannelTabs.VIDEOS, extractor.getName());
}

@Test
void testGetUrl() throws Exception {
assertEquals("https://media.ccc.de/c/camp2023", extractor.getUrl());
}

@Test
void testGetOriginalUrl() throws Exception {
assertEquals("https://media.ccc.de/c/camp2023", extractor.getOriginalUrl());
}

@Test
void testGetInitalPage() throws Exception {
assertEquals(177, extractor.getInitialPage().getItems().size());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
import static org.schabi.newpipe.extractor.ServiceList.MediaCCC;

/**
* Test {@link MediaCCCConferenceExtractor}
* Test {@link MediaCCCConferenceExtractor} and {@link
* org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCChannelTabExtractor}
*/
public class MediaCCCConferenceExtractorTest {
public static class FrOSCon2017 {
Expand Down