1717import org .schabi .newpipe .extractor .utils .JsonUtils ;
1818import org .schabi .newpipe .extractor .utils .Utils ;
1919
20+ import java .time .LocalDateTime ;
21+ import java .time .OffsetDateTime ;
22+ import java .time .ZoneOffset ;
23+ import java .time .format .DateTimeFormatter ;
2024import java .util .List ;
2125import java .util .Optional ;
2226import java .util .stream .Collectors ;
3034 * The following features are currently not implemented because they have never been observed:
3135 * <ul>
3236 * <li>Shorts</li>
33- * <li>Premieres</li>
3437 * <li>Paid content (Premium, members first or only)</li>
3538 * </ul>
3639 */
3740public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtractor {
3841
3942 private static final String NO_VIEWS_LOWERCASE = "no views" ;
43+ // This approach is language dependant (en-GB)
44+ // Leading end space is voluntary included
45+ private static final String PREMIERES_TEXT = "Premieres " ;
46+ private static final DateTimeFormatter PREMIERES_DATE_FORMATTER =
47+ DateTimeFormatter .ofPattern ("dd/MM/yyyy, HH:mm" );
4048
4149 private final JsonObject lockupViewModel ;
4250 private final TimeAgoParser timeAgoParser ;
4351
4452 private StreamType cachedStreamType ;
4553 private String cachedName ;
46- private Optional <String > cachedTextualUploadDate ;
54+ private Optional <String > cachedDateText ;
4755
4856 private ChannelImageViewModel cachedChannelImageViewModel ;
4957 private JsonArray cachedMetadataRows ;
@@ -137,7 +145,9 @@ public String getName() throws ParsingException {
137145 @ Override
138146 public long getDuration () throws ParsingException {
139147 // Duration cannot be extracted for live streams, but only for normal videos
140- if (isLive ()) {
148+ // Exact duration cannot be extracted for premieres, an approximation is only available in
149+ // accessibility context label
150+ if (isLive () || isPremiere ()) {
141151 return -1 ;
142152 }
143153
@@ -237,20 +247,37 @@ public boolean isUploaderVerified() throws ParsingException {
237247 @ Nullable
238248 @ Override
239249 public String getTextualUploadDate () throws ParsingException {
240- if (cachedTextualUploadDate != null ) {
241- return cachedTextualUploadDate .orElse (null );
242- }
243-
244250 // Live streams have no upload date
245251 if (isLive ()) {
246- cachedTextualUploadDate = Optional .empty ();
247252 return null ;
248253 }
249254
250- // This might be null e.g. for live streams
251- this .cachedTextualUploadDate = metadataPart (1 , 1 )
252- .map (this ::getTextContentFromMetadataPart );
253- return cachedTextualUploadDate .orElse (null );
255+ // Date string might be null e.g. for live streams
256+ final Optional <String > dateText = getDateText ();
257+
258+ if (isPremiere ()) {
259+ final LocalDateTime premiereDate = getDateFromPremiere (dateText );
260+ if (premiereDate == null ) {
261+ return null ;
262+ }
263+ return DateTimeFormatter .ofPattern ("yyyy-MM-dd HH:mm" ).format (premiereDate );
264+ }
265+
266+ return dateText .orElse (null );
267+ }
268+
269+ private LocalDateTime getDateFromPremiere (final Optional <String > dateText ) {
270+ // This approach is language dependent
271+ // Remove the premieres text from the upload date metadata part
272+ final String trimmedTextUploadDate =
273+ dateText .map (str -> str .replace (PREMIERES_TEXT , "" ))
274+ .orElse (null );
275+ if (trimmedTextUploadDate == null ) {
276+ return null ;
277+ }
278+
279+ // As we request a UTC offset of 0 minutes, we get the UTC date
280+ return LocalDateTime .parse (trimmedTextUploadDate , PREMIERES_DATE_FORMATTER );
254281 }
255282
256283 @ Nullable
@@ -265,11 +292,26 @@ public DateWrapper getUploadDate() throws ParsingException {
265292 if (textualUploadDate == null ) {
266293 return null ;
267294 }
295+
296+ if (isPremiere ()) {
297+ final LocalDateTime premiereDate = getDateFromPremiere (getDateText ());
298+ if (premiereDate == null ) {
299+ throw new ParsingException ("Could not get upload date from premiere" );
300+ }
301+
302+ return new DateWrapper (OffsetDateTime .of (premiereDate , ZoneOffset .UTC ));
303+ }
304+
268305 return timeAgoParser .parse (textualUploadDate );
269306 }
270307
271308 @ Override
272309 public long getViewCount () throws ParsingException {
310+ if (isPremiere ()) {
311+ // The number of people returned for premieres is the one currently waiting
312+ return -1 ;
313+ }
314+
273315 final Optional <String > optTextContent = metadataPart (1 , 0 )
274316 .map (this ::getTextContentFromMetadataPart );
275317 // We could do this inline if the ParsingException would be a RuntimeException -.-
@@ -357,6 +399,20 @@ private boolean isLive() throws ParsingException {
357399 return getStreamType () != StreamType .VIDEO_STREAM ;
358400 }
359401
402+ private Optional <String > getDateText () throws ParsingException {
403+ if (cachedDateText == null ) {
404+ cachedDateText = metadataPart (1 , 1 )
405+ .map (this ::getTextContentFromMetadataPart );
406+ }
407+ return cachedDateText ;
408+ }
409+
410+ private boolean isPremiere () throws ParsingException {
411+ return getDateText ().map (dateText -> dateText .contains (PREMIERES_TEXT ))
412+ // If we can't get date text, assume it is not a premiere, it should be a livestream
413+ .orElse (false );
414+ }
415+
360416 abstract static class ChannelImageViewModel {
361417 protected JsonObject viewModel ;
362418
0 commit comments