Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a95a3db
feat: port YouTube heatmap extraction logic from youtube-heatmap-clipper
Ecomont Apr 19, 2026
1309c92
Improve code readability in heatmap marker extraction
Ecomont Apr 19, 2026
943a41f
Add bullet comments core framework
Ecomont Apr 22, 2026
00b933f
Add YouTube bullet comments extractor
Ecomont Apr 22, 2026
b012ade
Add live chat fallback when comments are disabled
Ecomont Apr 22, 2026
cb82024
Add YoutubeLiveChatInfoItemExtractor
Ecomont Apr 22, 2026
08f6570
Add isLiveChat flag to CommentsExtractor
Ecomont Apr 22, 2026
b782992
Update YoutubeCommentsExtractor for live chat pages
Ecomont Apr 22, 2026
8a7cc39
Fix isCommentsDisabled to allow live chat
Ecomont Apr 23, 2026
05a8d36
Mark live chat pages with live_chat identifier
Ecomont Apr 23, 2026
620b756
Fix fetchLiveChat using replay endpoint on fresh extractors
Ecomont Apr 23, 2026
40c54a7
Fix emoji extraction showing null for emoji-only messages
Ecomont Apr 23, 2026
81b39fa
Improve custom emoji fallback handling
Ecomont Apr 23, 2026
fc533e4
Add debug logging to live chat polling
Ecomont Apr 23, 2026
70c2734
Fix checkstyle violations in live chat and bullet comments code
Ecomont Apr 24, 2026
9443929
Remove debug logging from live chat flow
Ecomont Apr 24, 2026
3169692
Remove unused bullet comments code
Ecomont Apr 24, 2026
05f6a5e
Separate live chat from comments extractor
Ecomont Apr 24, 2026
2430540
Add tests for live chat functionality in YouTube extractors
Ecomont Apr 28, 2026
84fa667
Remove unused isLiveStream flag from YoutubeCommentsExtractor
Ecomont May 5, 2026
19892e7
Refactor emoji handling in live chat message extraction
Ecomont May 5, 2026
7ccf36f
Add ANDROID_VR client as fallback for SABR-only responses
pythonivelt May 27, 2026
9175dce
Enhance livestream extraction by always fetching iOS client and impro…
Ecomont May 27, 2026
a42d735
Merge branch 'fix/livestream-hls-dash-manifest' into feature/live-cha…
Ecomont Jun 3, 2026
8becf3f
Merge branch 'feature/youtube-heatmap-extraction' into feature/live-c…
Ecomont Jun 7, 2026
ced7ad1
Merge branch 'pr-1498' into feature/live-chat-comments
Ecomont Jun 7, 2026
588f69d
Move liveChatContinuation field from StreamExtractor to YoutubeStream…
Ecomont Jun 20, 2026
67578b1
Fix imports in YoutubeCommentsExtractorTest.LiveChatMode
Ecomont Jun 20, 2026
e1952dd
Merge branch 'upstream/dev' into feature/live-chat-comments
Ecomont Jun 20, 2026
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 @@ -22,6 +22,23 @@ public boolean isCommentsDisabled() throws ExtractionException {
return false;
}

/**
* @apiNote Warning: This method is experimental and may get removed in a future release.
* @return <code>true</code> if the comments source is a live chat
* otherwise <code>false</code> (default)
*/
public boolean isLiveChat() throws ExtractionException {
return false;
}

/**
* @apiNote Warning: This method is experimental and may get removed in a future release.
* Configures this extractor to fetch live chat messages using the given continuation.
*/
public void setLiveChatContinuation(final String continuation) {
// no-op by default
}

/**
* @return the total number of comments
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public static CommentsInfo getInfo(final CommentsExtractor commentsExtractor)
final InfoItemsPage<CommentsInfoItem> initialCommentsPage =
ExtractorHelper.getItemsPageOrLogError(commentsInfo, commentsExtractor);
commentsInfo.setCommentsDisabled(commentsExtractor.isCommentsDisabled());
commentsInfo.setLiveChat(commentsExtractor.isLiveChat());
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
try {
commentsInfo.setCommentsCount(commentsExtractor.getCommentsCount());
Expand Down Expand Up @@ -81,6 +82,7 @@ public static InfoItemsPage<CommentsInfoItem> getMoreItems(

private transient CommentsExtractor commentsExtractor;
private boolean commentsDisabled = false;
private boolean liveChat = false;
private int commentsCount;

public CommentsExtractor getCommentsExtractor() {
Expand All @@ -106,6 +108,22 @@ public void setCommentsDisabled(final boolean commentsDisabled) {
this.commentsDisabled = commentsDisabled;
}

/**
* @apiNote Warning: This method is experimental and may get removed in a future release.
* @return {@code true} if the comments are from a live chat otherwise {@code false} (default)
*/
public boolean isLiveChat() {
return liveChat;
}

/**
* @apiNote Warning: This method is experimental and may get removed in a future release.
* @param liveChat {@code true} if the comments are from a live chat otherwise {@code false}
*/
public void setLiveChat(final boolean liveChat) {
this.liveChat = liveChat;
}

/**
* Returns the total number of comments.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
*/

public class YoutubeService extends StreamingService {

public YoutubeService(final int id) {
super(id, "YouTube", EnumSet.of(AUDIO, VIDEO, LIVE, COMMENTS));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;

public class YoutubeCommentsExtractor extends CommentsExtractor {
private static final String TAG = YoutubeCommentsExtractor.class.getSimpleName();

private static final String COMMENT_VIEW_MODEL_KEY = "commentViewModel";
private static final String COMMENT_RENDERER_KEY = "commentRenderer";
Expand All @@ -43,6 +44,16 @@
*/
private JsonObject ajaxJson;

/**
* Live chat continuation token, used when regular comments are disabled.
*/
private String liveChatContinuation;

/**
* Whether this video is / was a live stream.
*/
private boolean isLiveStream;
Comment thread
Ecomont marked this conversation as resolved.
Outdated

public YoutubeCommentsExtractor(
final StreamingService service,
final ListLinkHandler uiHandler) {
Expand All @@ -54,6 +65,10 @@
public InfoItemsPage<CommentsInfoItem> getInitialPage()
throws IOException, ExtractionException {

if (liveChatContinuation != null) {
return fetchLiveChat(liveChatContinuation);
}

if (commentsDisabled) {
return getInfoItemsPageForDisabledComments();
}
Expand Down Expand Up @@ -194,6 +209,11 @@
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
throws IOException, ExtractionException {

if ("live_chat".equals(page.getUrl()) || liveChatContinuation != null) {
isLiveStream = true;
return fetchLiveChat(page.getId());
}

if (commentsDisabled) {
return getInfoItemsPageForDisabledComments();
}
Expand All @@ -206,7 +226,7 @@
// @formatter:off
final byte[] body = JsonWriter.string(
prepareDesktopJsonBuilder(localization, getExtractorContentCountry())
.value("continuation", page.getId())

Check failure on line 229 in extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "continuation" 6 times.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-Ffp7cCKCwTge_69&open=AZ29-Ffp7cCKCwTge_69&pullRequest=1481
.done())
.getBytes(StandardCharsets.UTF_8);
// @formatter:on
Expand Down Expand Up @@ -351,8 +371,8 @@
.getBytes(StandardCharsets.UTF_8);
// @formatter:on

final String initialToken =
findInitialCommentsToken(getJsonPostResponse("next", body, localization));
final JsonObject nextResponse = getJsonPostResponse("next", body, localization);
final String initialToken = findInitialCommentsToken(nextResponse);

if (initialToken == null) {
return;
Expand All @@ -369,10 +389,110 @@
ajaxJson = getJsonPostResponse("next", ajaxBody, localization);
}

/**
* Configures this extractor to fetch live chat messages.
*/
@Override
public void setLiveChatContinuation(final String continuation) {
this.liveChatContinuation = continuation;
}

/**
* Fetches live chat messages and converts them to CommentsInfoItem.
*/
private InfoItemsPage<CommentsInfoItem> fetchLiveChat(final String chatContinuation)
throws IOException, ExtractionException {
isLiveStream = true;
final Localization localization = getExtractorLocalization();
final byte[] json = JsonWriter.string(
prepareDesktopJsonBuilder(localization, getExtractorContentCountry())
.value("continuation", chatContinuation)
.object("currentPlayerState")
.value("playerOffsetMs", "0")
.end()
.done())
.getBytes(StandardCharsets.UTF_8);

final String endpoint = "live_chat/"
+ (isLiveStream ? "get_live_chat" : "get_live_chat_replay");
Comment thread
Ecomont marked this conversation as resolved.
Outdated
final JsonObject result = getJsonPostResponse(endpoint, json, localization);

return extractLiveChatComments(result);
}

/**
* Extracts live chat actions into CommentsInfoItem objects.
*/
private InfoItemsPage<CommentsInfoItem> extractLiveChatComments(

Check failure on line 426 in extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-Ffp7cCKCwTge_6-&open=AZ29-Ffp7cCKCwTge_6-&pullRequest=1481
final JsonObject result) throws ExtractionException {
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(
getServiceId());

try {
final JsonObject chatContinuation = result
.getObject("continuationContents")
.getObject("liveChatContinuation");
final JsonArray actions = chatContinuation.getArray("actions");

for (int i = 0; i < actions.size(); i++) {
final JsonObject action = actions.getObject(i);
final JsonObject item;
if (action.has("addChatItemAction")) {

Check failure on line 440 in extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCommentsExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "addChatItemAction" 3 times.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-Ffp7cCKCwTge_68&open=AZ29-Ffp7cCKCwTge_68&pullRequest=1481
item = action.getObject("addChatItemAction")
.getObject("item");
} else if (action.has("replayChatItemAction")) {
item = action.getObject("replayChatItemAction")
.getArray("actions").getObject(0)
.getObject("addChatItemAction")
.getObject("item");
} else {
continue;
}

if (item.has("liveChatTextMessageRenderer")) {
collector.commit(new YoutubeLiveChatInfoItemExtractor(
item.getObject("liveChatTextMessageRenderer")));
}
}

// Extract next continuation
final JsonArray continuations = chatContinuation
.getArray("continuations");
final Page nextPage;
if (!continuations.isEmpty()) {
final JsonObject contObj = continuations.getObject(
continuations.size() - 1);
String nextCont = null;
if (contObj.has("timedContinuationData")) {
nextCont = contObj.getObject("timedContinuationData")
.getString("continuation");
} else if (contObj.has("invalidationContinuationData")) {
nextCont = contObj.getObject("invalidationContinuationData")
.getString("continuation");
} else if (contObj.has("liveChatReplayContinuationData")) {
nextCont = contObj.getObject("liveChatReplayContinuationData")
.getString("continuation");
}
nextPage = nextCont != null ? new Page("live_chat", nextCont) : null;
} else {
nextPage = null;
}

return new InfoItemsPage<>(collector, nextPage);
} catch (final Exception e) {
return getInfoItemsPageForDisabledComments();
}
}


@Override
public boolean isCommentsDisabled() {
return commentsDisabled;
return commentsDisabled && !isLiveChat();
}

@Override
public boolean isLiveChat() {
return liveChatContinuation != null;
}

@Override
Expand Down
Loading