11package org .schabi .newpipe .extractor .services .youtube .extractors ;
22
3- import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getJsonPostResponse ;
4- import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .prepareDesktopJsonBuilder ;
5- import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
6-
7- import java .io .IOException ;
8- import java .nio .charset .StandardCharsets ;
9- import java .util .Collections ;
10- import java .util .List ;
11- import java .util .Optional ;
12-
13- import javax .annotation .Nonnull ;
14- import javax .annotation .Nullable ;
15-
3+ import com .grack .nanojson .JsonArray ;
4+ import com .grack .nanojson .JsonObject ;
5+ import com .grack .nanojson .JsonWriter ;
166import org .schabi .newpipe .extractor .Page ;
177import org .schabi .newpipe .extractor .StreamingService ;
188import org .schabi .newpipe .extractor .comments .CommentsExtractor ;
2414import org .schabi .newpipe .extractor .linkhandler .ListLinkHandler ;
2515import org .schabi .newpipe .extractor .localization .Localization ;
2616import org .schabi .newpipe .extractor .utils .JsonUtils ;
17+ import org .schabi .newpipe .extractor .utils .Utils ;
2718
28- import com .grack .nanojson .JsonArray ;
29- import com .grack .nanojson .JsonObject ;
30- import com .grack .nanojson .JsonWriter ;
19+ import javax .annotation .Nonnull ;
20+ import javax .annotation .Nullable ;
21+ import java .io .IOException ;
22+ import java .nio .charset .StandardCharsets ;
23+ import java .util .Collections ;
24+ import java .util .List ;
25+
26+ import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getJsonPostResponse ;
27+ import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .getTextFromObject ;
28+ import static org .schabi .newpipe .extractor .services .youtube .YoutubeParsingHelper .prepareDesktopJsonBuilder ;
29+ import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
3130
3231public class YoutubeCommentsExtractor extends CommentsExtractor {
3332
34- private JsonObject nextResponse ;
33+ /**
34+ * Whether comments are disabled on video.
35+ */
36+ private boolean commentsDisabled ;
3537
3638 /**
37- * Caching mechanism and holder of the commentsDisabled value.
38- * <br/>
39- * Initial value = empty -> unknown if comments are disabled or not<br/>
40- * Some method calls {@link #findInitialCommentsToken()}
41- * -> value is set<br/>
42- * If the method or another one that is depending on disabled comments
43- * is now called again, the method execution can avoid unnecessary calls
39+ * The second ajax <b>/next</b> response.
4440 */
45- @ SuppressWarnings ("OptionalUsedAsFieldOrParameterType" )
46- private Optional <Boolean > optCommentsDisabled = Optional .empty ();
41+ private JsonObject ajaxJson ;
4742
4843 public YoutubeCommentsExtractor (
4944 final StreamingService service ,
@@ -56,32 +51,25 @@ public YoutubeCommentsExtractor(
5651 public InfoItemsPage <CommentsInfoItem > getInitialPage ()
5752 throws IOException , ExtractionException {
5853
59- // Check if findInitialCommentsToken was already called and optCommentsDisabled initialized
60- if (optCommentsDisabled .orElse (false )) {
61- return getInfoItemsPageForDisabledComments ();
62- }
63-
64- // Get the token
65- final String commentsToken = findInitialCommentsToken ();
66- // Check if the comments have been disabled
67- if (optCommentsDisabled .get ()) {
54+ if (commentsDisabled ) {
6855 return getInfoItemsPageForDisabledComments ();
6956 }
7057
71- return getPage ( getNextPage ( commentsToken ) );
58+ return extractComments ( ajaxJson );
7259 }
7360
7461 /**
7562 * Finds the initial comments token and initializes commentsDisabled.
7663 * <br/>
77- * Also sets {@link #optCommentsDisabled }.
64+ * Also sets {@link #commentsDisabled }.
7865 *
7966 * @return the continuation token or null if none was found
8067 */
8168 @ Nullable
82- private String findInitialCommentsToken () throws ExtractionException {
69+ private String findInitialCommentsToken (final JsonObject nextResponse )
70+ throws ExtractionException {
8371 final String token = JsonUtils .getArray (nextResponse ,
84- "contents.twoColumnWatchNextResults.results.results.contents" )
72+ "contents.twoColumnWatchNextResults.results.results.contents" )
8573 .stream ()
8674 // Only use JsonObjects
8775 .filter (JsonObject .class ::isInstance )
@@ -112,7 +100,7 @@ private String findInitialCommentsToken() throws ExtractionException {
112100 .orElse (null );
113101
114102 // The comments are disabled if we couldn't get a token
115- optCommentsDisabled = Optional . of ( token == null ) ;
103+ commentsDisabled = token == null ;
116104
117105 return token ;
118106 }
@@ -123,9 +111,9 @@ private InfoItemsPage<CommentsInfoItem> getInfoItemsPageForDisabledComments() {
123111 }
124112
125113 @ Nullable
126- private Page getNextPage (@ Nonnull final JsonObject ajaxJson ) throws ExtractionException {
114+ private Page getNextPage (@ Nonnull final JsonObject jsonObject ) throws ExtractionException {
127115 final JsonArray onResponseReceivedEndpoints =
128- ajaxJson .getArray ("onResponseReceivedEndpoints" );
116+ jsonObject .getArray ("onResponseReceivedEndpoints" );
129117
130118 // Prevent ArrayIndexOutOfBoundsException
131119 if (onResponseReceivedEndpoints .isEmpty ()) {
@@ -173,30 +161,39 @@ private Page getNextPage(final String continuation) throws ParsingException {
173161 @ Override
174162 public InfoItemsPage <CommentsInfoItem > getPage (final Page page )
175163 throws IOException , ExtractionException {
176- if (optCommentsDisabled .orElse (false )) {
164+
165+ if (commentsDisabled ) {
177166 return getInfoItemsPageForDisabledComments ();
178167 }
168+
179169 if (page == null || isNullOrEmpty (page .getId ())) {
180170 throw new IllegalArgumentException ("Page doesn't have the continuation." );
181171 }
182172
183173 final Localization localization = getExtractorLocalization ();
174+ // @formatter:off
184175 final byte [] body = JsonWriter .string (
185176 prepareDesktopJsonBuilder (localization , getExtractorContentCountry ())
186177 .value ("continuation" , page .getId ())
187178 .done ())
188179 .getBytes (StandardCharsets .UTF_8 );
180+ // @formatter:on
189181
190- final JsonObject ajaxJson = getJsonPostResponse ("next" , body , localization );
182+ final var jsonObject = getJsonPostResponse ("next" , body , localization );
191183
184+ return extractComments (jsonObject );
185+ }
186+
187+ private InfoItemsPage <CommentsInfoItem > extractComments (final JsonObject jsonObject )
188+ throws ExtractionException {
192189 final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector (
193190 getServiceId ());
194- collectCommentsFrom (collector , ajaxJson );
195- return new InfoItemsPage <>(collector , getNextPage (ajaxJson ));
191+ collectCommentsFrom (collector );
192+ return new InfoItemsPage <>(collector , getNextPage (jsonObject ));
196193 }
197194
198- private void collectCommentsFrom (final CommentsInfoItemsCollector collector ,
199- @ Nonnull final JsonObject ajaxJson ) throws ParsingException {
195+ private void collectCommentsFrom (final CommentsInfoItemsCollector collector )
196+ throws ParsingException {
200197
201198 final JsonArray onResponseReceivedEndpoints =
202199 ajaxJson .getArray ("onResponseReceivedEndpoints" );
@@ -254,24 +251,59 @@ private void collectCommentsFrom(final CommentsInfoItemsCollector collector,
254251 public void onFetchPage (@ Nonnull final Downloader downloader )
255252 throws IOException , ExtractionException {
256253 final Localization localization = getExtractorLocalization ();
254+ // @formatter:off
257255 final byte [] body = JsonWriter .string (
258256 prepareDesktopJsonBuilder (localization , getExtractorContentCountry ())
259257 .value ("videoId" , getId ())
260258 .done ())
261259 .getBytes (StandardCharsets .UTF_8 );
260+ // @formatter:on
261+
262+ final String initialToken =
263+ findInitialCommentsToken (getJsonPostResponse ("next" , body , localization ));
264+
265+ if (initialToken == null ) {
266+ return ;
267+ }
268+
269+ // @formatter:off
270+ final byte [] ajaxBody = JsonWriter .string (
271+ prepareDesktopJsonBuilder (localization , getExtractorContentCountry ())
272+ .value ("continuation" , initialToken )
273+ .done ())
274+ .getBytes (StandardCharsets .UTF_8 );
275+ // @formatter:on
262276
263- nextResponse = getJsonPostResponse ("next" , body , localization );
277+ ajaxJson = getJsonPostResponse ("next" , ajaxBody , localization );
264278 }
265279
266280
267281 @ Override
268- public boolean isCommentsDisabled () throws ExtractionException {
269- // Check if commentsDisabled has to be initialized
270- if (!optCommentsDisabled .isPresent ()) {
271- // Initialize commentsDisabled
272- this .findInitialCommentsToken ();
282+ public boolean isCommentsDisabled () {
283+ return commentsDisabled ;
284+ }
285+
286+ @ Override
287+ public int getCommentsCount () throws ExtractionException {
288+ assertPageFetched ();
289+
290+ if (commentsDisabled ) {
291+ return -1 ;
273292 }
274293
275- return optCommentsDisabled .get ();
294+ final JsonObject countText = ajaxJson
295+ .getArray ("onResponseReceivedEndpoints" ).getObject (0 )
296+ .getObject ("reloadContinuationItemsCommand" )
297+ .getArray ("continuationItems" ).getObject (0 )
298+ .getObject ("commentsHeaderRenderer" )
299+ .getObject ("countText" );
300+
301+ try {
302+ return Integer .parseInt (
303+ Utils .removeNonDigitCharacters (getTextFromObject (countText ))
304+ );
305+ } catch (final Exception e ) {
306+ throw new ExtractionException ("Unable to get comments count" , e );
307+ }
276308 }
277309}
0 commit comments