Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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 @@ -76,6 +76,7 @@ public enum InfoType {
STREAM,
PLAYLIST,
CHANNEL,
COMMENT
COMMENT,
BULLET_COMMENT
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
import org.schabi.newpipe.extractor.bulletComments.BulletCommentsExtractor;
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
Expand Down Expand Up @@ -76,7 +77,7 @@
}

public enum MediaCapability {
AUDIO, VIDEO, LIVE, COMMENTS
AUDIO, VIDEO, LIVE, COMMENTS, BULLET_COMMENTS
}
}

Expand Down Expand Up @@ -163,6 +164,9 @@
*/
public abstract SearchQueryHandlerFactory getSearchQHFactory();
public abstract ListLinkHandlerFactory getCommentsLHFactory();
public ListLinkHandlerFactory getBulletCommentsLHFactory() {
return null;
}

/*//////////////////////////////////////////////////////////////////////////
// Extractors
Expand Down Expand Up @@ -241,6 +245,10 @@

public abstract CommentsExtractor getCommentsExtractor(ListLinkHandler linkHandler)
throws ExtractionException;
public BulletCommentsExtractor getBulletCommentsExtractor(
final ListLinkHandler linkHandler) throws ExtractionException {

Check warning on line 249 in extractor/src/main/java/org/schabi/newpipe/extractor/StreamingService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused method parameter "linkHandler".

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-Fhv7cCKCwTge_7W&open=AZ29-Fhv7cCKCwTge_7W&pullRequest=1481
return null;
}

/*//////////////////////////////////////////////////////////////////////////
// Extractors without link handler
Expand Down Expand Up @@ -303,6 +311,15 @@
return getStreamExtractor(getStreamLHFactory().fromUrl(url));
}

public BulletCommentsExtractor getBulletCommentsExtractor(final String url)
throws ExtractionException {
final ListLinkHandlerFactory listLinkHandlerFactory = getBulletCommentsLHFactory();
if (listLinkHandlerFactory == null) {
return null;
}
return getBulletCommentsExtractor(listLinkHandlerFactory.fromUrl(url));
}

public CommentsExtractor getCommentsExtractor(final String url) throws ExtractionException {
final ListLinkHandlerFactory listLinkHandlerFactory = getCommentsLHFactory();
if (listLinkHandlerFactory == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.schabi.newpipe.extractor.bulletComments;

Check warning on line 1 in extractor/src/main/java/org/schabi/newpipe/extractor/bulletComments/BulletCommentsExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this package name to match the regular expression '^[a-z_]+(\.[a-z_][a-z0-9_]*)*$'.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-FhB7cCKCwTge_7Q&open=AZ29-FhB7cCKCwTge_7Q&pullRequest=1481

import javax.annotation.Nonnull;

import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;

import java.io.IOException;
import java.util.List;

public abstract class BulletCommentsExtractor extends ListExtractor<BulletCommentsInfoItem> {
public BulletCommentsExtractor(final StreamingService service,

Check warning on line 16 in extractor/src/main/java/org/schabi/newpipe/extractor/bulletComments/BulletCommentsExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-FhB7cCKCwTge_7R&open=AZ29-FhB7cCKCwTge_7R&pullRequest=1481
final ListLinkHandler uiHandler) {
super(service, uiHandler);
}

@Nonnull
@Override
public String getName() throws ParsingException {
return "BulletComments";
}

@Override
public InfoItemsPage<BulletCommentsInfoItem> getPage(final Page page)
throws IOException, ExtractionException {
return null;
}

public List<BulletCommentsInfoItem> getLiveMessages() throws ParsingException {
return null;

Check warning on line 34 in extractor/src/main/java/org/schabi/newpipe/extractor/bulletComments/BulletCommentsExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty collection instead of null.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-FhB7cCKCwTge_7S&open=AZ29-FhB7cCKCwTge_7S&pullRequest=1481
}

public boolean isLive() {
return false;
}

public boolean isDisabled() {
return false;
}

public void disconnect() {

}

public void reconnect() {

}

public void setCurrentPlayPosition(final long currentPlayPosition) {
}

public void clearMappingState() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.schabi.newpipe.extractor.bulletComments;

Check warning on line 1 in extractor/src/main/java/org/schabi/newpipe/extractor/bulletComments/BulletCommentsInfo.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this package name to match the regular expression '^[a-z_]+(\.[a-z_][a-z0-9_]*)*$'.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-FhI7cCKCwTge_7T&open=AZ29-FhI7cCKCwTge_7T&pullRequest=1481

import java.io.IOException;

import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.utils.ExtractorLogger;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;

public final class BulletCommentsInfo extends ListInfo<BulletCommentsInfoItem> {
private BulletCommentsInfo(
final int serviceId,
final ListLinkHandler listUrlIdHandler,
final String name) {
super(serviceId, listUrlIdHandler, name);
}

public static BulletCommentsInfo getInfo(final String url)
throws IOException, ExtractionException {
return getInfo(NewPipe.getServiceByUrl(url), url);
}

public static BulletCommentsInfo getInfo(final StreamingService service, final String url)
throws ExtractionException, IOException {
return getInfo(service.getBulletCommentsExtractor(url));
}

public static BulletCommentsInfo getInfo(final BulletCommentsExtractor commentsExtractor)
throws IOException, ExtractionException {
// for services which do not have a comments extractor
if (commentsExtractor == null) {
ExtractorLogger.d("BulletCommentsInfo", "getInfo() extractor is null");
return null;
}

ExtractorLogger.d("BulletCommentsInfo", "getInfo() fetching page for "
+ commentsExtractor.getUrl());
commentsExtractor.fetchPage();

final String name = commentsExtractor.getName();
final int serviceId = commentsExtractor.getServiceId();
final ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();

final BulletCommentsInfo commentsInfo = new BulletCommentsInfo(
serviceId, listUrlIdHandler, name);
commentsInfo.setBulletCommentsExtractor(commentsExtractor);
final InfoItemsPage<BulletCommentsInfoItem> initialCommentsPage = ExtractorHelper
.getItemsPageOrLogError(commentsInfo, commentsExtractor);
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
commentsInfo.setNextPage(initialCommentsPage.getNextPage());

return commentsInfo;
}

public static InfoItemsPage<BulletCommentsInfoItem> getMoreItems(
final BulletCommentsInfo commentsInfo,
final Page page) throws ExtractionException, IOException {
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo.getUrl(),
page);
}

public static InfoItemsPage<BulletCommentsInfoItem> getMoreItems(
final StreamingService service,
final BulletCommentsInfo commentsInfo,
final Page page) throws IOException, ExtractionException {
return getMoreItems(service, commentsInfo.getUrl(), page);
}

public static InfoItemsPage<BulletCommentsInfoItem> getMoreItems(
final StreamingService service,
final String url,
final Page page) throws IOException, ExtractionException {
return service.getBulletCommentsExtractor(url).getPage(page);
}

private transient BulletCommentsExtractor commentsExtractor;

public BulletCommentsExtractor getBulletCommentsExtractor() {
return commentsExtractor;
}

public void setBulletCommentsExtractor(
final BulletCommentsExtractor bulletCommentsExtractor) {
this.commentsExtractor = bulletCommentsExtractor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.schabi.newpipe.extractor.bulletComments;

Check warning on line 1 in extractor/src/main/java/org/schabi/newpipe/extractor/bulletComments/BulletCommentsInfoItem.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this package name to match the regular expression '^[a-z_]+(\.[a-z_][a-z0-9_]*)*$'.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-Fg57cCKCwTge_7P&open=AZ29-Fg57cCKCwTge_7P&pullRequest=1481

import java.time.Duration;

import org.schabi.newpipe.extractor.InfoItem;

import javax.annotation.Nonnull;

public class BulletCommentsInfoItem extends InfoItem implements Comparable<BulletCommentsInfoItem> {
@Override
public int compareTo(@Nonnull final BulletCommentsInfoItem bulletCommentsInfoItem) {
return this.duration.compareTo(bulletCommentsInfoItem.duration);
}

public enum Position {
REGULAR,
BOTTOM,
TOP,
SUPERCHAT
}

private String commentText;
private int argbColor;
private Position position;
private double relativeFontSize;
/* It really should be named as timePosition or some other thing*/
private Duration duration;
private int lastingTime;
private boolean isLive;

public BulletCommentsInfoItem(final int serviceId, final String url, final String name) {
super(InfoType.COMMENT, serviceId, url, name);
}

public String getCommentText() {
return commentText;
}

public void setCommentText(final String commentText) {
this.commentText = commentText;
}

public int getArgbColor() {
return argbColor;
}

public void setArgbColor(final int argbColor) {
this.argbColor = argbColor;
}

public Position getPosition() {
return position;
}

public void setPosition(final Position position) {
this.position = position;
}

public double getRelativeFontSize() {
return relativeFontSize;
}

public void setRelativeFontSize(final double relativeFontSize) {
this.relativeFontSize = relativeFontSize;
}

public Duration getDuration() {
return duration;
}

public void setDuration(final Duration duration) {
this.duration = duration;
}

public int getLastingTime() {
return -1;
}

public void setLastingTime(final int lastingTime) {
this.lastingTime = lastingTime;
}

public boolean isLive() {
return isLive;
}

public void setLive(final boolean live) {
isLive = live;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.schabi.newpipe.extractor.bulletComments;

Check warning on line 1 in extractor/src/main/java/org/schabi/newpipe/extractor/bulletComments/BulletCommentsInfoItemExtractor.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this package name to match the regular expression '^[a-z_]+(\.[a-z_][a-z0-9_]*)*$'.

See more on https://sonarcloud.io/project/issues?id=TeamNewPipe_NewPipeExtractor&issues=AZ29-FhT7cCKCwTge_7U&open=AZ29-FhT7cCKCwTge_7U&pullRequest=1481

import org.schabi.newpipe.extractor.InfoItemExtractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;

import java.time.Duration;
import java.util.Collections;
import java.util.List;

public interface BulletCommentsInfoItemExtractor extends InfoItemExtractor {
@Override
default String getName() throws ParsingException {
return null;
}

@Override
default List<org.schabi.newpipe.extractor.Image> getThumbnails() throws ParsingException {
return Collections.emptyList();
}

@Override
default String getUrl() throws ParsingException {
return null;
}

default String getCommentText() throws ParsingException {
return "";
}

/**
* Returns ARGB32 int. White: 0xFFFFFFFF.
* @return ARGB32 int. White: 0xFFFFFFFF.
*/
default int getArgbColor() throws ParsingException {
return 0xFFFFFFFF;
}

default BulletCommentsInfoItem.Position getPosition() throws ParsingException {
return BulletCommentsInfoItem.Position.REGULAR;
}

default double getRelativeFontSize() throws ParsingException {
return 0.7;
}

default int getLastingTime() {
return -1;
}

default boolean isLive() throws ParsingException {
return false;
}

// Must be implemented. If that is a live stream you should at least
// calculate the time from the start of the stream.
Duration getDuration() throws ParsingException;
}
Loading