|
| 1 | +package org.schabi.newpipe.extractor.services.youtube.extractors.kiosk; |
| 2 | + |
| 3 | +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER; |
| 4 | +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL; |
| 5 | +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientVersion; |
| 6 | +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; |
| 7 | +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareJsonBuilder; |
| 8 | +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; |
| 9 | + |
| 10 | +import com.grack.nanojson.JsonArray; |
| 11 | +import com.grack.nanojson.JsonObject; |
| 12 | +import com.grack.nanojson.JsonWriter; |
| 13 | + |
| 14 | +import org.schabi.newpipe.extractor.Page; |
| 15 | +import org.schabi.newpipe.extractor.StreamingService; |
| 16 | +import org.schabi.newpipe.extractor.downloader.Downloader; |
| 17 | +import org.schabi.newpipe.extractor.exceptions.ExtractionException; |
| 18 | +import org.schabi.newpipe.extractor.exceptions.ParsingException; |
| 19 | +import org.schabi.newpipe.extractor.kiosk.KioskExtractor; |
| 20 | +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; |
| 21 | +import org.schabi.newpipe.extractor.localization.TimeAgoParser; |
| 22 | +import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo; |
| 23 | +import org.schabi.newpipe.extractor.services.youtube.YoutubeChannelHelper; |
| 24 | +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemExtractor; |
| 25 | +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemLockupExtractor; |
| 26 | +import org.schabi.newpipe.extractor.stream.StreamInfoItem; |
| 27 | +import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; |
| 28 | + |
| 29 | +import java.io.IOException; |
| 30 | +import java.nio.charset.StandardCharsets; |
| 31 | + |
| 32 | +import javax.annotation.Nonnull; |
| 33 | +import javax.annotation.Nullable; |
| 34 | + |
| 35 | +abstract class YoutubeDesktopBaseKioskExtractor extends KioskExtractor<StreamInfoItem> { |
| 36 | + |
| 37 | + protected final String browseId; |
| 38 | + protected final String params; |
| 39 | + |
| 40 | + protected YoutubeChannelHelper.ChannelResponseData responseData; |
| 41 | + |
| 42 | + protected YoutubeDesktopBaseKioskExtractor(final StreamingService streamingService, |
| 43 | + final ListLinkHandler linkHandler, |
| 44 | + final String kioskId, |
| 45 | + final String browseId, |
| 46 | + final String params) { |
| 47 | + super(streamingService, linkHandler, kioskId); |
| 48 | + this.browseId = browseId; |
| 49 | + this.params = params; |
| 50 | + } |
| 51 | + |
| 52 | + @Override |
| 53 | + public void onFetchPage(@Nonnull final Downloader downloader) |
| 54 | + throws IOException, ExtractionException { |
| 55 | + responseData = YoutubeChannelHelper.getChannelResponse( |
| 56 | + browseId, |
| 57 | + params, |
| 58 | + getExtractorLocalization(), |
| 59 | + getExtractorContentCountry()); |
| 60 | + } |
| 61 | + |
| 62 | + @Nonnull |
| 63 | + @Override |
| 64 | + public String getName() throws ParsingException { |
| 65 | + return YoutubeChannelHelper.getChannelName( |
| 66 | + YoutubeChannelHelper.getChannelHeader(responseData.jsonResponse), |
| 67 | + null, |
| 68 | + responseData.jsonResponse); |
| 69 | + } |
| 70 | + |
| 71 | + @Nonnull |
| 72 | + @Override |
| 73 | + public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException { |
| 74 | + final JsonObject tabRendererContent = responseData.jsonResponse.getObject("contents") |
| 75 | + .getObject("twoColumnBrowseResultsRenderer") |
| 76 | + .getArray("tabs") |
| 77 | + .getObject(0) |
| 78 | + .getObject("tabRenderer") |
| 79 | + .getObject("content"); |
| 80 | + |
| 81 | + final JsonArray tabContents; |
| 82 | + if (tabRendererContent.has("sectionListRenderer")) { |
| 83 | + tabContents = tabRendererContent.getObject("sectionListRenderer") |
| 84 | + .getArray("contents") |
| 85 | + .getObject(0) |
| 86 | + .getObject("itemSectionRenderer") |
| 87 | + .getArray("contents") |
| 88 | + .getObject(0) |
| 89 | + .getObject("shelfRenderer") |
| 90 | + .getObject("content") |
| 91 | + .getObject("gridRenderer") |
| 92 | + .getArray("items"); |
| 93 | + } else if (tabRendererContent.has("richGridRenderer")) { |
| 94 | + tabContents = tabRendererContent.getObject("richGridRenderer") |
| 95 | + .getArray("contents"); |
| 96 | + } else { |
| 97 | + tabContents = new JsonArray(); |
| 98 | + } |
| 99 | + |
| 100 | + return collectStreamItems(tabContents, |
| 101 | + responseData.jsonResponse.getObject("responseContext") |
| 102 | + .getString("visitorData")); |
| 103 | + } |
| 104 | + |
| 105 | + @Override |
| 106 | + public InfoItemsPage<StreamInfoItem> getPage(final Page page) |
| 107 | + throws IOException, ExtractionException { |
| 108 | + if (page == null || page.getBody() == null) { |
| 109 | + throw new IllegalArgumentException("Page is null or doesn't contain a body"); |
| 110 | + } |
| 111 | + |
| 112 | + final JsonObject continuationResponse = getJsonPostResponse("browse", page.getBody(), |
| 113 | + getExtractorLocalization()); |
| 114 | + |
| 115 | + final JsonArray continuationItems = |
| 116 | + continuationResponse.getArray("onResponseReceivedActions") |
| 117 | + .stream() |
| 118 | + .filter(JsonObject.class::isInstance) |
| 119 | + .map(JsonObject.class::cast) |
| 120 | + .filter(jsonObject -> jsonObject.has("appendContinuationItemsAction")) |
| 121 | + .map(jsonObject -> jsonObject.getObject("appendContinuationItemsAction")) |
| 122 | + .findFirst() |
| 123 | + .orElse(new JsonObject()) |
| 124 | + .getArray("continuationItems"); |
| 125 | + |
| 126 | + // The page ID is the visitor data |
| 127 | + return collectStreamItems(continuationItems, page.getId()); |
| 128 | + } |
| 129 | + |
| 130 | + private InfoItemsPage<StreamInfoItem> collectStreamItems( |
| 131 | + @Nonnull final JsonArray items, |
| 132 | + @Nullable final String visitorData) throws IOException, ExtractionException { |
| 133 | + final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); |
| 134 | + |
| 135 | + final Page nextPage; |
| 136 | + if (items.isEmpty()) { |
| 137 | + nextPage = null; |
| 138 | + } else { |
| 139 | + final TimeAgoParser timeAgoParser = getTimeAgoParser(); |
| 140 | + items.streamAsJsonObjects() |
| 141 | + .forEachOrdered(content -> { |
| 142 | + if (content.has("richItemRenderer")) { |
| 143 | + final JsonObject richItem = content.getObject("richItemRenderer") |
| 144 | + .getObject("content"); |
| 145 | + |
| 146 | + if (richItem.has("videoRenderer")) { |
| 147 | + collector.commit(new YoutubeStreamInfoItemExtractor( |
| 148 | + richItem.getObject("videoRenderer"), timeAgoParser)); |
| 149 | + } |
| 150 | + } else if (content.has("gridVideoRenderer")) { |
| 151 | + collector.commit(new YoutubeStreamInfoItemExtractor( |
| 152 | + content.getObject("gridVideoRenderer"), timeAgoParser)); |
| 153 | + } else if (content.has("lockupViewModel")) { |
| 154 | + // lockupViewModels are not used yet, but may be in the future |
| 155 | + final JsonObject lockupViewModel = content.getObject("lockupViewModel"); |
| 156 | + if ("LOCKUP_CONTENT_TYPE_VIDEO".equals( |
| 157 | + lockupViewModel.getString("contentType"))) { |
| 158 | + collector.commit(new YoutubeStreamInfoItemLockupExtractor( |
| 159 | + lockupViewModel, timeAgoParser)); |
| 160 | + } |
| 161 | + } |
| 162 | + }); |
| 163 | + |
| 164 | + final JsonObject lastContent = items.getObject(items.size() - 1); |
| 165 | + if (lastContent.has("continuationItemRenderer")) { |
| 166 | + nextPage = getNextPageFrom( |
| 167 | + lastContent.getObject("continuationItemRenderer"), visitorData); |
| 168 | + } else { |
| 169 | + nextPage = null; |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + return new InfoItemsPage<>(collector, nextPage); |
| 174 | + } |
| 175 | + |
| 176 | + |
| 177 | + @Nullable |
| 178 | + private Page getNextPageFrom(@Nullable final JsonObject continuation, |
| 179 | + @Nullable final String visitorData) |
| 180 | + throws IOException, ExtractionException { |
| 181 | + if (isNullOrEmpty(continuation)) { |
| 182 | + return null; |
| 183 | + } |
| 184 | + |
| 185 | + final JsonObject continuationEndpoint = continuation.getObject("continuationEndpoint"); |
| 186 | + final String continuationToken = continuationEndpoint.getObject("continuationCommand") |
| 187 | + .getString("token"); |
| 188 | + |
| 189 | + // Visitor data is required to get videos in continuations, so we need to apply it to the |
| 190 | + // next page and save it as an ID so it can be applied to future continuations |
| 191 | + final InnertubeClientRequestInfo webClientRequestInfo = |
| 192 | + InnertubeClientRequestInfo.ofWebClient(); |
| 193 | + webClientRequestInfo.clientInfo.clientVersion = getClientVersion(); |
| 194 | + webClientRequestInfo.clientInfo.visitorData = visitorData; |
| 195 | + |
| 196 | + final byte[] body = JsonWriter.string(prepareJsonBuilder(getExtractorLocalization(), |
| 197 | + getExtractorContentCountry(), |
| 198 | + webClientRequestInfo, |
| 199 | + null) |
| 200 | + .value("continuation", continuationToken) |
| 201 | + .done()) |
| 202 | + .getBytes(StandardCharsets.UTF_8); |
| 203 | + |
| 204 | + // The URL is not needed and used, it is only provided to make Page.isValid return true |
| 205 | + return new Page(YOUTUBEI_V1_URL + "browse?" + DISABLE_PRETTY_PRINT_PARAMETER, visitorData, |
| 206 | + null, null, body); |
| 207 | + } |
| 208 | +} |
0 commit comments