From 88e1f0eaa4cb2ce64264a046d5713c77a88e7655 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 25 Jun 2026 11:56:02 +0200 Subject: [PATCH] fix(youtube): read the sort token from the dropdown chip on channels using it --- .../YoutubeChannelTabExtractor.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java index 6acebdff..7cef8ca9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java @@ -205,6 +205,42 @@ private String getSortContinuationToken(@Nonnull final JsonObject currentTabData throw new ParsingException("YouTube channel tab sort filter is not available"); } + // Some channels expose the sort options as a single "Sort by" dropdown chip + // (displayType = CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN). The actual Latest/Popular/Oldest + // tokens then live inside that chip's sheet listItems, not in the chipBar directly. + // Reading chips[sortIndex] in that case picked filter chips (e.g. "Members only", "Public") + // and returned their continuation tokens, so the sort never applied. + if (!chips.isEmpty()) { + final JsonObject firstChip = chips.getObject(0).getObject("chipViewModel"); + if ("CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN".equals(firstChip.getString("displayType"))) { + final JsonArray listItems = firstChip + .getObject("tapCommand") + .getObject("innertubeCommand") + .getObject("showSheetCommand") + .getObject("panelLoadingStrategy") + .getObject("inlineContent") + .getObject("sheetViewModel") + .getObject("content") + .getObject("listViewModel") + .getArray("listItems"); + if (listItems.size() <= sortIndex) { + throw new ParsingException("YouTube channel tab sort filter is not available"); + } + final String token = extractContinuationTokenFromListItem( + listItems.getObject(sortIndex).getObject("listItemViewModel")); + if (isNullOrEmpty(token)) { + throw new ParsingException( + "Could not get YouTube channel tab sort continuation"); + } + return token; + } + } + + // Flat format: chips[sortIndex] is itself the sort chip. + if (chips.size() <= sortIndex) { + throw new ParsingException("YouTube channel tab sort filter is not available"); + } + final String token = chips.getObject(sortIndex) .getObject("chipViewModel") .getObject("tapCommand") @@ -219,6 +255,39 @@ private String getSortContinuationToken(@Nonnull final JsonObject currentTabData return token; } + /** + * Walks a sort-dropdown listItem to find its continuationCommand token. + * The token lives inside {@code commandExecutorCommand.commands[]} (typically alongside an + * entityUpdateCommand for the chip state). We scan the commands array instead of guessing + * an index because other commands can sit before the continuationCommand. + */ + @Nullable + private static String extractContinuationTokenFromListItem( + @Nonnull final JsonObject listItemViewModel) { + final JsonObject innertube = listItemViewModel + .getObject("rendererContext") + .getObject("commandContext") + .getObject("onTap") + .getObject("innertubeCommand"); + final JsonObject executor = innertube.getObject("commandExecutorCommand"); + if (executor == null) { + return null; + } + final JsonArray commands = executor.getArray("commands"); + for (final Object o : commands) { + final JsonObject command = (JsonObject) o; + if (command.has("continuationCommand")) { + final String token = command + .getObject("continuationCommand") + .getString("token"); + if (!isNullOrEmpty(token)) { + return token; + } + } + } + return null; + } + private int getSelectedSortFilterIndex() throws ParsingException { final List sortFilter = getLinkHandler().getSortFilter(); if (sortFilter == null || sortFilter.isEmpty()) {