@@ -43,6 +43,16 @@ public class YoutubeCommentsExtractor extends CommentsExtractor {
4343 */
4444 private JsonObject ajaxJson ;
4545
46+ /**
47+ * Live chat continuation token, used when regular comments are disabled.
48+ */
49+ private String liveChatContinuation ;
50+
51+ /**
52+ * Whether this video is / was a live stream.
53+ */
54+ private boolean isLiveStream ;
55+
4656 public YoutubeCommentsExtractor (
4757 final StreamingService service ,
4858 final ListLinkHandler uiHandler ) {
@@ -54,6 +64,10 @@ public YoutubeCommentsExtractor(
5464 public InfoItemsPage <CommentsInfoItem > getInitialPage ()
5565 throws IOException , ExtractionException {
5666
67+ if (commentsDisabled && liveChatContinuation != null ) {
68+ return fetchLiveChat (liveChatContinuation );
69+ }
70+
5771 if (commentsDisabled ) {
5872 return getInfoItemsPageForDisabledComments ();
5973 }
@@ -351,10 +365,12 @@ public void onFetchPage(@Nonnull final Downloader downloader)
351365 .getBytes (StandardCharsets .UTF_8 );
352366 // @formatter:on
353367
354- final String initialToken =
355- findInitialCommentsToken (getJsonPostResponse ( "next" , body , localization ) );
368+ final JsonObject nextResponse = getJsonPostResponse ( "next" , body , localization );
369+ final String initialToken = findInitialCommentsToken (nextResponse );
356370
357371 if (initialToken == null ) {
372+ // Try to extract live chat continuation for live streams
373+ findLiveChatContinuation (nextResponse );
358374 return ;
359375 }
360376
@@ -369,10 +385,116 @@ public void onFetchPage(@Nonnull final Downloader downloader)
369385 ajaxJson = getJsonPostResponse ("next" , ajaxBody , localization );
370386 }
371387
388+ /**
389+ * Tries to extract a live chat continuation token from the next response.
390+ * This is used when regular comments are disabled on a live stream.
391+ */
392+ private void findLiveChatContinuation (final JsonObject nextResponse ) {
393+ try {
394+ final JsonObject liveChatRenderer = nextResponse
395+ .getObject ("contents" )
396+ .getObject ("twoColumnWatchNextResults" )
397+ .getObject ("conversationBar" )
398+ .getObject ("liveChatRenderer" );
399+ liveChatContinuation = liveChatRenderer
400+ .getArray ("continuations" )
401+ .getObject (0 )
402+ .getObject ("reloadContinuationData" )
403+ .getString ("continuation" );
404+ } catch (final Exception e ) {
405+ liveChatContinuation = null ;
406+ }
407+ }
408+
409+ /**
410+ * Fetches live chat messages and converts them to CommentsInfoItem.
411+ */
412+ private InfoItemsPage <CommentsInfoItem > fetchLiveChat (final String chatContinuation )
413+ throws IOException , ExtractionException {
414+ final Localization localization = getExtractorLocalization ();
415+ final byte [] json = JsonWriter .string (
416+ prepareDesktopJsonBuilder (localization , getExtractorContentCountry ())
417+ .value ("continuation" , chatContinuation )
418+ .object ("currentPlayerState" )
419+ .value ("playerOffsetMs" , "0" )
420+ .end ()
421+ .done ())
422+ .getBytes (StandardCharsets .UTF_8 );
423+
424+ final String endpoint = "live_chat/" + (isLiveStream ? "get_live_chat" : "get_live_chat_replay" );
425+ final JsonObject result = getJsonPostResponse (endpoint , json , localization );
426+
427+ return extractLiveChatComments (result );
428+ }
429+
430+ /**
431+ * Extracts live chat actions into CommentsInfoItem objects.
432+ */
433+ private InfoItemsPage <CommentsInfoItem > extractLiveChatComments (
434+ final JsonObject result ) throws ExtractionException {
435+ final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector (
436+ getServiceId ());
437+
438+ try {
439+ final JsonObject chatContinuation = result
440+ .getObject ("continuationContents" )
441+ .getObject ("liveChatContinuation" );
442+ final JsonArray actions = chatContinuation .getArray ("actions" );
443+
444+ for (int i = 0 ; i < actions .size (); i ++) {
445+ final JsonObject action = actions .getObject (i );
446+ final JsonObject item ;
447+ if (action .has ("addChatItemAction" )) {
448+ item = action .getObject ("addChatItemAction" )
449+ .getObject ("item" );
450+ } else if (action .has ("replayChatItemAction" )) {
451+ item = action .getObject ("replayChatItemAction" )
452+ .getArray ("actions" ).getObject (0 )
453+ .getObject ("addChatItemAction" )
454+ .getObject ("item" );
455+ } else {
456+ continue ;
457+ }
458+
459+ if (item .has ("liveChatTextMessageRenderer" )) {
460+ collector .commit (new YoutubeLiveChatInfoItemExtractor (
461+ item .getObject ("liveChatTextMessageRenderer" )));
462+ }
463+ }
464+
465+ // Extract next continuation
466+ final JsonArray continuations = chatContinuation
467+ .getArray ("continuations" );
468+ final Page nextPage ;
469+ if (!continuations .isEmpty ()) {
470+ final JsonObject contObj = continuations .getObject (
471+ continuations .size () - 1 );
472+ String nextCont = null ;
473+ if (contObj .has ("timedContinuationData" )) {
474+ nextCont = contObj .getObject ("timedContinuationData" )
475+ .getString ("continuation" );
476+ } else if (contObj .has ("invalidationContinuationData" )) {
477+ nextCont = contObj .getObject ("invalidationContinuationData" )
478+ .getString ("continuation" );
479+ } else if (contObj .has ("liveChatReplayContinuationData" )) {
480+ nextCont = contObj .getObject ("liveChatReplayContinuationData" )
481+ .getString ("continuation" );
482+ }
483+ nextPage = nextCont != null ? new Page (getUrl (), nextCont ) : null ;
484+ } else {
485+ nextPage = null ;
486+ }
487+
488+ return new InfoItemsPage <>(collector , nextPage );
489+ } catch (final Exception e ) {
490+ return getInfoItemsPageForDisabledComments ();
491+ }
492+ }
493+
372494
373495 @ Override
374496 public boolean isCommentsDisabled () {
375- return commentsDisabled ;
497+ return commentsDisabled && liveChatContinuation == null ;
376498 }
377499
378500 @ Override
0 commit comments