Skip to content
Draft
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 @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand why there are two ways to determine whether this extractor handles a live stream/chat. While isLiveStream is used and set individually for internal purposes, isLiveChat() relies on liveChatContinuation. I think isLiveChat can be removed.

Please explain why two different ways are needed. As far as I understand YouTube how handles this, a video can either have comments or a live chat, but not both at the same time. Is this correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLiveChat() cannot be removed because it represents the extractor's configuration mode set externally via setLiveChatContinuation() whereas isLiveStream represents the video's metadata. A live-stream video may or may not have live chat configured on this extractor instance.

However, you are correct that the ternary inside fetchLiveChat() is dead code because isLiveStream is overwriten to true at the top of the method. I will either remove the ternary or pass the replay/live flag as a parameter instead.


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 on lines +403 to +417
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLiveChat is always true here

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