Merge pull request #1372 from Isira-Seneviratne/Refactor-date-parsing
Some checks failed
CI / build-and-test (push) Has been cancelled

Refactor date parsing
This commit is contained in:
Isira Seneviratne
2025-10-10 08:44:14 +05:30
committed by GitHub
29 changed files with 246 additions and 329 deletions

View File

@@ -1,12 +1,16 @@
package org.schabi.newpipe.extractor.localization;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.time.format.DateTimeParseException;
/**
* A wrapper class that provides a field to describe if the date/time is precise or just an
@@ -14,52 +18,61 @@ import java.util.GregorianCalendar;
*/
public class DateWrapper implements Serializable {
@Nonnull
private final OffsetDateTime offsetDateTime;
private final Instant instant;
private final boolean isApproximation;
/**
* @deprecated Use {@link #DateWrapper(OffsetDateTime)} instead.
*/
@Deprecated
public DateWrapper(@Nonnull final Calendar calendar) {
//noinspection deprecation
this(calendar, false);
}
/**
* @deprecated Use {@link #DateWrapper(OffsetDateTime, boolean)} instead.
*/
@Deprecated
public DateWrapper(@Nonnull final Calendar calendar, final boolean isApproximation) {
this(OffsetDateTime.ofInstant(calendar.toInstant(), ZoneOffset.UTC), isApproximation);
}
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime) {
this(offsetDateTime, false);
}
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime,
final boolean isApproximation) {
this.offsetDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
this(offsetDateTime.toInstant(), isApproximation);
}
public DateWrapper(@Nonnull final Instant instant) {
this(instant, false);
}
public DateWrapper(@Nonnull final Instant instant, final boolean isApproximation) {
this.instant = instant;
this.isApproximation = isApproximation;
}
/**
* @return the wrapped date/time as a {@link Calendar}.
* @deprecated use {@link #offsetDateTime()} instead.
*/
@Deprecated
@Nonnull
public Calendar date() {
return GregorianCalendar.from(offsetDateTime.toZonedDateTime());
public DateWrapper(@Nonnull final LocalDateTime dateTime, final boolean isApproximation) {
this(dateTime.atZone(ZoneId.systemDefault()).toInstant(), isApproximation);
}
/**
* @return the wrapped date/time.
* @return the wrapped {@link Instant}
*/
@Nonnull
public Instant getInstant() {
return instant;
}
/**
* @return the wrapped {@link Instant} as an {@link OffsetDateTime} set to UTC.
*/
@Nonnull
public OffsetDateTime offsetDateTime() {
return offsetDateTime;
return instant.atOffset(ZoneOffset.UTC);
}
/**
* @return the wrapped {@link Instant} as a {@link LocalDateTime} in the current time zone.
*/
@Nonnull
public LocalDateTime getLocalDateTime() {
return getLocalDateTime(ZoneId.systemDefault());
}
/**
* @return the wrapped {@link Instant} as a {@link LocalDateTime} in the given time zone.
*/
@Nonnull
public LocalDateTime getLocalDateTime(@Nonnull final ZoneId zoneId) {
return LocalDateTime.ofInstant(instant, zoneId);
}
/**
@@ -73,8 +86,42 @@ public class DateWrapper implements Serializable {
@Override
public String toString() {
return "DateWrapper{"
+ "offsetDateTime=" + offsetDateTime
+ "instant=" + instant
+ ", isApproximation=" + isApproximation
+ '}';
}
/**
* Parses a date string that matches the ISO-8601 {@link OffsetDateTime} pattern, e.g.
* "2011-12-03T10:15:30+01:00".
*
* @param date The date string
* @return a non-approximate {@link DateWrapper}, or null if the string is null
* @throws ParsingException if the string does not match the expected format
*/
@Nullable
public static DateWrapper fromOffsetDateTime(final String date) throws ParsingException {
try {
return date != null ? new DateWrapper(OffsetDateTime.parse(date)) : null;
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse date: \"" + date + "\"", e);
}
}
/**
* Parses a date string that matches the ISO-8601 {@link Instant} pattern, e.g.
* "2011-12-03T10:15:30Z".
*
* @param date The date string
* @return a non-approximate {@link DateWrapper}, or null if the string is null
* @throws ParsingException if the string does not match the expected format
*/
@Nullable
public static DateWrapper fromInstant(final String date) throws ParsingException {
try {
return date != null ? new DateWrapper(Instant.parse(date)) : null;
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse date: \"" + date + "\"", e);
}
}
}

View File

@@ -4,8 +4,7 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
import org.schabi.newpipe.extractor.utils.Parser;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.regex.Pattern;
@@ -16,20 +15,7 @@ import java.util.regex.Pattern;
*/
public class TimeAgoParser {
private final PatternsHolder patternsHolder;
private final OffsetDateTime now;
/**
* Creates a helper to parse upload dates in the format '2 days ago'.
* <p>
* Instantiate a new {@link TimeAgoParser} every time you extract a new batch of items.
* </p>
*
* @param patternsHolder An object that holds the "time ago" patterns, special cases, and the
* language word separator.
*/
public TimeAgoParser(final PatternsHolder patternsHolder) {
this(patternsHolder, OffsetDateTime.now(ZoneOffset.UTC));
}
private final LocalDateTime now;
/**
* Creates a helper to parse upload dates in the format '2 days ago'.
@@ -41,7 +27,7 @@ public class TimeAgoParser {
* language word separator.
* @param now The current time
*/
public TimeAgoParser(final PatternsHolder patternsHolder, final OffsetDateTime now) {
public TimeAgoParser(final PatternsHolder patternsHolder, final LocalDateTime now) {
this.patternsHolder = patternsHolder;
this.now = now;
}
@@ -118,34 +104,13 @@ public class TimeAgoParser {
}
private DateWrapper getResultFor(final int timeAgoAmount, final ChronoUnit chronoUnit) {
OffsetDateTime offsetDateTime = now;
boolean isApproximation = false;
switch (chronoUnit) {
case SECONDS:
case MINUTES:
case HOURS:
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
break;
case DAYS:
case WEEKS:
case MONTHS:
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
isApproximation = true;
break;
case YEARS:
final var localDateTime = chronoUnit == ChronoUnit.YEARS
// minusDays is needed to prevent `PrettyTime` from showing '12 months ago'.
offsetDateTime = offsetDateTime.minusYears(timeAgoAmount).minusDays(1);
isApproximation = true;
break;
}
if (isApproximation) {
offsetDateTime = offsetDateTime.truncatedTo(ChronoUnit.HOURS);
}
return new DateWrapper(offsetDateTime, isApproximation);
? now.minusYears(timeAgoAmount).minusDays(1)
: now.minus(timeAgoAmount, chronoUnit);
final boolean isApproximate = chronoUnit.isDateBased();
final var resolvedDateTime =
isApproximate ? localDateTime.truncatedTo(ChronoUnit.DAYS) : localDateTime;
return new DateWrapper(resolvedDateTime, isApproximate);
}
}

View File

@@ -3,7 +3,7 @@ package org.schabi.newpipe.extractor.localization;
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
import org.schabi.newpipe.extractor.timeago.PatternsManager;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -20,25 +20,13 @@ public final class TimeAgoPatternsManager {
@Nullable
public static TimeAgoParser getTimeAgoParserFor(@Nonnull final Localization localization) {
final PatternsHolder holder = getPatternsFor(localization);
if (holder == null) {
return null;
}
return new TimeAgoParser(holder);
return getTimeAgoParserFor(localization, LocalDateTime.now());
}
@Nullable
public static TimeAgoParser getTimeAgoParserFor(
@Nonnull final Localization localization,
@Nonnull final OffsetDateTime now) {
public static TimeAgoParser getTimeAgoParserFor(@Nonnull final Localization localization,
@Nonnull final LocalDateTime now) {
final PatternsHolder holder = getPatternsFor(localization);
if (holder == null) {
return null;
}
return new TimeAgoParser(holder, now);
return holder == null ? null : new TimeAgoParser(holder, now);
}
}

View File

@@ -204,7 +204,7 @@ public final class BandcampExtractorHelper {
try {
final ZonedDateTime zonedDateTime = ZonedDateTime.parse(textDate,
DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH));
return new DateWrapper(zonedDateTime.toOffsetDateTime(), false);
return new DateWrapper(zonedDateTime.toInstant());
} catch (final DateTimeException e) {
throw new ParsingException("Could not parse date '" + textDate + "'", e);
}

View File

@@ -8,15 +8,12 @@ import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.Image.ResolutionLevel;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.Localization;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -33,15 +30,6 @@ public final class MediaCCCParsingHelper {
private MediaCCCParsingHelper() { }
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
throws ParsingException {
try {
return OffsetDateTime.parse(textualUploadDate);
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"", e);
}
}
/**
* Check whether an id is a live stream id
* @param id the {@code id} to check

View File

@@ -52,12 +52,10 @@ public class MediaCCCRecentKiosk extends KioskExtractor<StreamInfoItem> {
// Streams in the recent kiosk are not ordered by the release date.
// Sort them to have the latest stream at the beginning of the list.
final Comparator<StreamInfoItem> comparator = Comparator
.comparing(StreamInfoItem::getUploadDate, Comparator
.nullsLast(Comparator.comparing(DateWrapper::offsetDateTime)))
final var comparator = Comparator.comparing(StreamInfoItem::getUploadDate,
Comparator.nullsLast(Comparator.comparing(DateWrapper::getInstant)))
.reversed();
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId(),
comparator);
final var collector = new StreamInfoItemsCollector(getServiceId(), comparator);
events.stream()
.filter(JsonObject.class::isInstance)

View File

@@ -92,6 +92,6 @@ public class MediaCCCRecentKioskExtractor implements StreamInfoItemExtractor {
public DateWrapper getUploadDate() throws ParsingException {
final ZonedDateTime zonedDateTime = ZonedDateTime.parse(event.getString("date"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSzzzz"));
return new DateWrapper(zonedDateTime.toOffsetDateTime(), false);
return new DateWrapper(zonedDateTime.toInstant());
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.extractor.services.media_ccc.extractors;
import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getImageListFromLogoImageUrl;
import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getThumbnailsFromStreamItem;
import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.parseDateFrom;
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN;
@@ -37,6 +36,7 @@ import java.util.List;
import java.util.Locale;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MediaCCCStreamExtractor extends StreamExtractor {
private JsonObject data;
@@ -46,16 +46,16 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
super(service, linkHandler);
}
@Nonnull
@Nullable
@Override
public String getTextualUploadDate() {
return data.getString("release_date");
}
@Nonnull
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
return new DateWrapper(parseDateFrom(getTextualUploadDate()));
return DateWrapper.fromOffsetDateTime(getTextualUploadDate());
}
@Nonnull

View File

@@ -4,7 +4,6 @@ import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
@@ -65,11 +64,8 @@ public class MediaCCCStreamInfoItemExtractor implements StreamInfoItemExtractor
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
final String date = getTextualUploadDate();
if (date == null) {
return null; // event is in the future...
}
return new DateWrapper(MediaCCCParsingHelper.parseDateFrom(date));
// if null, event is in the future...
return DateWrapper.fromOffsetDateTime(getTextualUploadDate());
}
@Override

View File

@@ -18,10 +18,6 @@ import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Parser;
import javax.annotation.Nonnull;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -48,15 +44,6 @@ public final class PeertubeParsingHelper {
}
}
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
throws ParsingException {
try {
return OffsetDateTime.ofInstant(Instant.parse(textualUploadDate), ZoneOffset.UTC);
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"", e);
}
}
public static Page getNextPage(final String prevPageUrl, final long total) {
final String prevStart;
try {

View File

@@ -23,7 +23,6 @@ import java.util.Objects;
import static org.schabi.newpipe.extractor.services.peertube.extractors.PeertubeCommentsExtractor.CHILDREN;
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.getAvatarsFromOwnerAccountOrVideoChannelObject;
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.parseDateFrom;
public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
@Nonnull
@@ -73,8 +72,7 @@ public class PeertubeCommentsInfoItemExtractor implements CommentsInfoItemExtrac
@Override
public DateWrapper getUploadDate() throws ParsingException {
final String textualUploadDate = getTextualUploadDate();
return new DateWrapper(parseDateFrom(textualUploadDate));
return DateWrapper.fromInstant(getTextualUploadDate());
}
@Nonnull

View File

@@ -79,13 +79,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
@Override
public DateWrapper getUploadDate() throws ParsingException {
final String textualUploadDate = getTextualUploadDate();
if (textualUploadDate == null) {
return null;
}
return new DateWrapper(PeertubeParsingHelper.parseDateFrom(textualUploadDate));
return DateWrapper.fromInstant(getTextualUploadDate());
}
@Nonnull

View File

@@ -14,7 +14,6 @@ import java.util.List;
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.getAvatarsFromOwnerAccountOrVideoChannelObject;
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.getThumbnailsFromPlaylistOrVideoItem;
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.parseDateFrom;
public class PeertubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@@ -85,13 +84,7 @@ public class PeertubeStreamInfoItemExtractor implements StreamInfoItemExtractor
@Override
public DateWrapper getUploadDate() throws ParsingException {
final String textualUploadDate = getTextualUploadDate();
if (textualUploadDate == null) {
return null;
}
return new DateWrapper(parseDateFrom(textualUploadDate));
return DateWrapper.fromInstant(getTextualUploadDate());
}
@Override

View File

@@ -23,6 +23,7 @@ import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelInfoItemExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudPlaylistInfoItemExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudLikesInfoItemExtractor;
@@ -133,17 +134,17 @@ public final class SoundcloudParsingHelper {
throw new ExtractionException("Couldn't extract client id");
}
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
throws ParsingException {
@Nullable
public static DateWrapper parseDate(final String uploadDate) throws ParsingException {
try {
return OffsetDateTime.parse(textualUploadDate);
} catch (final DateTimeParseException e1) {
return DateWrapper.fromInstant(uploadDate);
} catch (final DateTimeParseException e) {
try {
return OffsetDateTime.parse(textualUploadDate, DateTimeFormatter
.ofPattern("yyyy/MM/dd HH:mm:ss +0000"));
} catch (final DateTimeParseException e2) {
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\""
+ ", " + e1.getMessage(), e2);
return new DateWrapper(OffsetDateTime.parse(uploadDate,
DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss +0000")));
} catch (final DateTimeParseException e1) {
e1.addSuppressed(e);
throw new ParsingException("Could not parse date: \"" + uploadDate + "\"", e1);
}
}
}

View File

@@ -13,7 +13,7 @@ import java.util.List;
import java.util.Objects;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromArtworkOrAvatarUrl;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.parseDateFrom;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.parseDate;
public class SoundcloudCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
private final JsonObject json;
@@ -69,7 +69,7 @@ public class SoundcloudCommentsInfoItemExtractor implements CommentsInfoItemExtr
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
return new DateWrapper(parseDateFrom(getTextualUploadDate()));
return parseDate(getTextualUploadDate());
}
@Override

View File

@@ -5,7 +5,7 @@ import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsing
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromArtworkOrAvatarUrl;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromTrackObject;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAvatarUrl;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.parseDateFrom;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.parseDate;
import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@@ -88,18 +88,16 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
return track.getString("title");
}
@Nonnull
@Nullable
@Override
public String getTextualUploadDate() {
return track.getString("created_at")
.replace("T", " ")
.replace("Z", "");
return track.getString("created_at");
}
@Nonnull
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
return new DateWrapper(parseDateFrom(track.getString("created_at")));
return parseDate(getTextualUploadDate());
}
@Nonnull

View File

@@ -13,7 +13,7 @@ import java.util.List;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromArtworkOrAvatarUrl;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromTrackObject;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.parseDateFrom;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.parseDate;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtractor {
@@ -68,7 +68,7 @@ public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtracto
@Override
public DateWrapper getUploadDate() throws ParsingException {
return new DateWrapper(parseDateFrom(getTextualUploadDate()));
return parseDate(getTextualUploadDate());
}
@Override

View File

@@ -67,10 +67,6 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -294,20 +290,6 @@ public final class YoutubeParsingHelper {
}
}
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
throws ParsingException {
try {
return OffsetDateTime.parse(textualUploadDate);
} catch (final DateTimeParseException e) {
try {
return LocalDate.parse(textualUploadDate).atStartOfDay().atOffset(ZoneOffset.UTC);
} catch (final DateTimeParseException e1) {
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"",
e1);
}
}
}
/**
* Checks if the given playlist id is a YouTube Mix (auto-generated playlist)
* Ids from a YouTube Mix start with "RD"

View File

@@ -10,8 +10,6 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.util.List;
public class YoutubeFeedInfoItemExtractor implements StreamInfoItemExtractor {
@@ -69,12 +67,7 @@ public class YoutubeFeedInfoItemExtractor implements StreamInfoItemExtractor {
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
try {
return new DateWrapper(OffsetDateTime.parse(getTextualUploadDate()));
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse date (\"" + getTextualUploadDate() + "\")",
e);
}
return DateWrapper.fromOffsetDateTime(getTextualUploadDate());
}
@Override

View File

@@ -87,20 +87,23 @@ import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class YoutubeStreamExtractor extends StreamExtractor {
private static final String PREMIERED = "Premiered ";
private static final String PREMIERED_ON = "Premiered on ";
@Nullable
private static PoTokenProvider poTokenProvider;
@@ -168,79 +171,74 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nullable
@Override
public String getTextualUploadDate() throws ParsingException {
if (!playerMicroFormatRenderer.getString("uploadDate", "").isEmpty()) {
return playerMicroFormatRenderer.getString("uploadDate");
} else if (!playerMicroFormatRenderer.getString("publishDate", "").isEmpty()) {
return playerMicroFormatRenderer.getString("publishDate");
public String getTextualUploadDate() {
String timestamp = playerMicroFormatRenderer.getString("uploadDate", "");
if (timestamp.isEmpty()) {
timestamp = playerMicroFormatRenderer.getString("publishDate", "");
}
if (!timestamp.isEmpty()) {
return timestamp;
}
final JsonObject liveDetails = playerMicroFormatRenderer.getObject(
"liveBroadcastDetails");
if (!liveDetails.getString("endTimestamp", "").isEmpty()) {
// an ended live stream
return liveDetails.getString("endTimestamp");
} else if (!liveDetails.getString("startTimestamp", "").isEmpty()) {
final var liveDetails = playerMicroFormatRenderer.getObject("liveBroadcastDetails");
timestamp = liveDetails.getString("endTimestamp", ""); // an ended live stream
if (timestamp.isEmpty()) {
// a running live stream
return liveDetails.getString("startTimestamp");
timestamp = liveDetails.getString("startTimestamp", "");
}
if (!timestamp.isEmpty()) {
return timestamp;
} else if (getStreamType() == StreamType.LIVE_STREAM) {
// this should never be reached, but a live stream without upload date is valid
return null;
}
final String videoPrimaryInfoRendererDateText =
getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText"));
if (videoPrimaryInfoRendererDateText != null) {
if (videoPrimaryInfoRendererDateText.startsWith("Premiered")) {
final String time = videoPrimaryInfoRendererDateText.substring(13);
try { // Premiered 20 hours ago
final TimeAgoParser timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(
new Localization("en"));
final OffsetDateTime parsedTime = timeAgoParser.parse(time).offsetDateTime();
return DateTimeFormatter.ISO_LOCAL_DATE.format(parsedTime);
} catch (final Exception ignored) {
}
try { // Premiered Feb 21, 2020
final LocalDate localDate = LocalDate.parse(time,
DateTimeFormatter.ofPattern("MMM dd, yyyy", Locale.ENGLISH));
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
} catch (final Exception ignored) {
}
try { // Premiered on 21 Feb 2020
final LocalDate localDate = LocalDate.parse(time,
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH));
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
} catch (final Exception ignored) {
}
}
try {
// TODO: this parses English formatted dates only, we need a better approach to
// parse the textual date
final LocalDate localDate = LocalDate.parse(videoPrimaryInfoRendererDateText,
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH));
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
} catch (final Exception e) {
throw new ParsingException("Could not get upload date", e);
}
final var textObject = getVideoPrimaryInfoRenderer().getObject("dateText");
final String rendererDateText = getTextFromObject(textObject);
if (rendererDateText == null) {
return null;
} else if (rendererDateText.startsWith(PREMIERED_ON)) { // Premiered on 21 Feb 2020
return rendererDateText.substring(PREMIERED_ON.length());
} else if (rendererDateText.startsWith(PREMIERED)) {
// Premiered 20 hours ago / Premiered Feb 21, 2020
return rendererDateText.substring(PREMIERED.length());
} else {
return rendererDateText;
}
throw new ParsingException("Could not get upload date");
}
@Override
public DateWrapper getUploadDate() throws ParsingException {
final String textualUploadDate = getTextualUploadDate();
if (isNullOrEmpty(textualUploadDate)) {
return null;
final String dateText = getTextualUploadDate();
try {
return DateWrapper.fromOffsetDateTime(dateText);
} catch (final ParsingException e) {
// Try other patterns first
}
return new DateWrapper(YoutubeParsingHelper.parseDateFrom(textualUploadDate), true);
try { // Premiered 20 hours ago
final var localization = new Localization("en");
return TimeAgoPatternsManager.getTimeAgoParserFor(localization).parse(dateText);
} catch (final ParsingException e) {
// Try other patterns first
}
return parseOptionalDate(dateText, "MMM dd, yyyy")
.or(() -> parseOptionalDate(dateText, "dd MMM yyyy"))
.map(date -> new DateWrapper(date.atStartOfDay(), true))
.orElseThrow(() ->
new ParsingException("Could not parse upload date \"" + dateText + "\""));
}
private Optional<LocalDate> parseOptionalDate(final String date, final String pattern) {
try {
// TODO: this parses English formatted dates only, we need a better approach to parse
// the textual date
final var formatter = DateTimeFormatter.ofPattern(pattern, Locale.ENGLISH);
return Optional.of(LocalDate.parse(date, formatter));
} catch (final DateTimeParseException e) {
return Optional.empty();
}
}
@Nonnull

View File

@@ -44,8 +44,8 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.regex.Pattern;
@@ -247,12 +247,14 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@Nullable
@Override
public String getTextualUploadDate() throws ParsingException {
if (getStreamType().equals(StreamType.LIVE_STREAM)) {
if (getStreamType() == StreamType.LIVE_STREAM) {
return null;
}
if (isPremiere()) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(getDateFromPremiere());
final var localDateTime = LocalDateTime.ofInstant(getInstantFromPremiere(),
ZoneId.systemDefault());
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(localDateTime);
}
String publishedTimeText = getTextFromObject(videoInfo.getObject("publishedTimeText"));
@@ -273,12 +275,12 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
if (getStreamType().equals(StreamType.LIVE_STREAM)) {
if (getStreamType() == StreamType.LIVE_STREAM) {
return null;
}
if (isPremiere()) {
return new DateWrapper(getDateFromPremiere());
return new DateWrapper(getInstantFromPremiere());
}
final String textualUploadDate = getTextualUploadDate();
@@ -402,15 +404,15 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
return isPremiere;
}
private OffsetDateTime getDateFromPremiere() throws ParsingException {
private Instant getInstantFromPremiere() throws ParsingException {
final JsonObject upcomingEventData = videoInfo.getObject("upcomingEventData");
final String startTime = upcomingEventData.getString("startTime");
try {
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(Long.parseLong(startTime)),
ZoneOffset.UTC);
return Instant.ofEpochSecond(Long.parseLong(startTime));
} catch (final Exception e) {
throw new ParsingException("Could not parse date from premiere: \"" + startTime + "\"");
final String message = "Could not parse date from premiere: \"" + startTime + "\"";
throw new ParsingException(message, e);
}
}

View File

@@ -18,7 +18,6 @@ import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
@@ -292,8 +291,8 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra
try {
// As we request a UTC offset of 0 minutes, we get the UTC date
return new DateWrapper(OffsetDateTime.of(LocalDateTime.parse(
premiereDate, PREMIERES_DATE_FORMATTER), ZoneOffset.UTC));
final var dateTime = LocalDateTime.parse(premiereDate, PREMIERES_DATE_FORMATTER);
return new DateWrapper(dateTime.atZone(ZoneOffset.UTC).toInstant(), false);
} catch (final DateTimeParseException e) {
throw new ParsingException("Could not parse premiere upload date", e);
}

View File

@@ -27,7 +27,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
@@ -203,20 +203,15 @@ abstract class YoutubeChartsBaseKioskExtractor extends KioskExtractor<StreamInfo
@Nonnull
@Override
public DateWrapper getUploadDate() {
final JsonObject releaseDate = videoObject.getObject("releaseDate");
return new DateWrapper(OffsetDateTime.of(
releaseDate.getInt("year"),
releaseDate.getInt("month"),
releaseDate.getInt("day"),
0,
0,
0,
0,
// We request that times should be returned with 0 offset to UTC timezone in
// the JSON body, but YouTube charts does it only in its HTTP headers
ZoneOffset.UTC),
// We don't have more info than the release day
true);
final var releaseDate = videoObject.getObject("releaseDate");
final var localDate = LocalDate.of(releaseDate.getInt("year"),
releaseDate.getInt("month"), releaseDate.getInt("day"));
// We request that times should be returned with 0 offset to UTC timezone in
// the JSON body, but YouTube charts does it only in its HTTP headers
final var instant = localDate.atStartOfDay(ZoneOffset.UTC).toInstant();
// We don't have more info than the release day, hence isApproximate=true
return new DateWrapper(instant, true);
}
@Override

View File

@@ -39,6 +39,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -71,7 +72,7 @@ public abstract class StreamExtractor extends Extractor {
}
/**
* A more general {@code Calendar} instance set to the date provided by the service.<br>
* A more general {@link Instant} instance set to the date provided by the service.<br>
* Implementations usually will just parse the date returned from the {@link
* #getTextualUploadDate()}.
*

View File

@@ -11,10 +11,8 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.Month;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -41,13 +39,9 @@ class TimeAgoParserTest {
@ParameterizedTest
@MethodSource
void parseTimeAgo(final ParseTimeAgoTestData testData) {
final OffsetDateTime now = OffsetDateTime.of(
LocalDateTime.of(2020, 1, 1, 1, 1, 1),
ZoneOffset.UTC);
final TimeAgoParser parser = Objects.requireNonNull(
TimeAgoPatternsManager.getTimeAgoParserFor(Localization.DEFAULT, now));
final OffsetDateTime expected = testData.getExpectedApplyToNow().apply(now);
final var now = LocalDateTime.of(2020, Month.JANUARY, 1, 1, 1, 1);
final var parser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.DEFAULT, now);
final var expected = testData.getExpectedApplyToNow().apply(now);
assertAll(
Stream.of(
@@ -55,7 +49,7 @@ class TimeAgoParserTest {
testData.getTextualDateShort())
.map(textualDate -> () -> assertEquals(
expected,
parser.parse(textualDate).offsetDateTime(),
parser.parse(textualDate).getLocalDateTime(),
"Expected " + expected + " for " + textualDate
))
);
@@ -63,12 +57,12 @@ class TimeAgoParserTest {
static class ParseTimeAgoTestData {
public static final String AGO_SUFFIX = " ago";
private final Function<OffsetDateTime, OffsetDateTime> expectedApplyToNow;
private final Function<LocalDateTime, LocalDateTime> expectedApplyToNow;
private final String textualDateLong;
private final String textualDateShort;
ParseTimeAgoTestData(
final Function<OffsetDateTime, OffsetDateTime> expectedApplyToNow,
final Function<LocalDateTime, LocalDateTime> expectedApplyToNow,
final String textualDateLong,
final String textualDateShort
) {
@@ -89,17 +83,17 @@ class TimeAgoParserTest {
}
public static ParseTimeAgoTestData greaterThanDay(
final Function<OffsetDateTime, OffsetDateTime> expectedApplyToNow,
final Function<LocalDateTime, LocalDateTime> expectedApplyToNow,
final String textualDateLong,
final String textualDateShort
) {
return new ParseTimeAgoTestData(
d -> expectedApplyToNow.apply(d).truncatedTo(ChronoUnit.HOURS),
expectedApplyToNow.andThen(d -> d.truncatedTo(ChronoUnit.DAYS)),
textualDateLong + AGO_SUFFIX,
textualDateShort + AGO_SUFFIX);
}
public Function<OffsetDateTime, OffsetDateTime> getExpectedApplyToNow() {
public Function<LocalDateTime, LocalDateTime> getExpectedApplyToNow() {
return expectedApplyToNow;
}

View File

@@ -5,7 +5,6 @@ import org.schabi.newpipe.extractor.ExtractorAsserts;
import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.ContentAvailability;
import org.schabi.newpipe.extractor.stream.Description;
@@ -17,6 +16,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import javax.annotation.Nullable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.net.MalformedURLException;
import java.net.URL;
@@ -195,18 +195,18 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
@Test
@Override
public void testUploadDate() throws Exception {
final DateWrapper dateWrapper = extractor().getUploadDate();
final var dateWrapper = extractor().getUploadDate();
final var expectedDate = expectedUploadDate();
if (expectedUploadDate() == null) {
if (expectedDate == null) {
assertNull(dateWrapper);
} else {
assertNotNull(dateWrapper);
final LocalDateTime expectedDateTime = LocalDateTime.parse(expectedUploadDate(),
final var expectedDateTime = LocalDateTime.parse(expectedDate,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
final LocalDateTime actualDateTime = dateWrapper.offsetDateTime().toLocalDateTime();
assertEquals(expectedDateTime, actualDateTime);
assertEquals(expectedDateTime, dateWrapper.getLocalDateTime(ZoneOffset.UTC));
}
}

View File

@@ -19,10 +19,11 @@ import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.io.IOException;
import java.util.Calendar;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
public class BandcampRadioStreamExtractorTest extends DefaultStreamExtractorTest {
@@ -82,14 +83,10 @@ public class BandcampRadioStreamExtractorTest extends DefaultStreamExtractorTest
@Override
@Test
public void testUploadDate() throws ParsingException {
final Calendar expectedCalendar = Calendar.getInstance();
// 16 May 2017 00:00:00 GMT
expectedCalendar.setTimeZone(TimeZone.getTimeZone("GMT"));
expectedCalendar.setTimeInMillis(0);
expectedCalendar.set(2017, Calendar.MAY, 16);
assertEquals(expectedCalendar.getTimeInMillis(), extractor().getUploadDate().offsetDateTime().toInstant().toEpochMilli());
final var expectedDate = LocalDate.of(2017, Month.MAY, 16);
final var actualDate = extractor().getUploadDate().getLocalDateTime(ZoneOffset.UTC)
.toLocalDate();
assertEquals(expectedDate, actualDate);
}
@Test

View File

@@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.fail;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.extractor.ExtractorAsserts;
import org.schabi.newpipe.extractor.MediaFormat;
@@ -27,7 +28,8 @@ import javax.annotation.Nullable;
public class SoundcloudStreamExtractorTest {
private static final String SOUNDCLOUD = "https://soundcloud.com/";
public static class SoundcloudGeoRestrictedTrack extends DefaultStreamExtractorTest {
@Nested
class SoundcloudGeoRestrictedTrack extends DefaultStreamExtractorTest {
private static final String ID = "one-touch";
private static final String UPLOADER = SOUNDCLOUD + "jessglynne";
private static final int TIMESTAMP = 0;
@@ -63,7 +65,7 @@ public class SoundcloudStreamExtractorTest {
@Override public long expectedTimestamp() { return TIMESTAMP; }
@Override public long expectedViewCountAtLeast() { return 43000; }
@Nullable @Override public String expectedUploadDate() { return "2019-05-16 16:28:45.000"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2019-05-16 16:28:45"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2019-05-16T16:28:45Z"; }
@Override public long expectedLikeCountAtLeast() { return 600; }
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasAudioStreams() { return false; }
@@ -82,7 +84,8 @@ public class SoundcloudStreamExtractorTest {
}
}
public static class SoundcloudGoPlusTrack extends DefaultStreamExtractorTest {
@Nested
class SoundcloudGoPlusTrack extends DefaultStreamExtractorTest {
private static final String ID = "places";
private static final String UPLOADER = SOUNDCLOUD + "martinsolveig";
private static final int TIMESTAMP = 0;
@@ -127,7 +130,7 @@ public class SoundcloudStreamExtractorTest {
@Override public long expectedTimestamp() { return TIMESTAMP; }
@Override public long expectedViewCountAtLeast() { return 386000; }
@Nullable @Override public String expectedUploadDate() { return "2016-11-11 01:16:37.000"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2016-11-11 01:16:37"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2016-11-11T01:16:37Z"; }
@Override public long expectedLikeCountAtLeast() { return 7350; }
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasAudioStreams() { return false; }
@@ -139,7 +142,8 @@ public class SoundcloudStreamExtractorTest {
@Override public String expectedCategory() { return "Dance"; }
}
static class CreativeCommonsOpenMindsEp21 extends DefaultStreamExtractorTest {
@Nested
class CreativeCommonsOpenMindsEp21 extends DefaultStreamExtractorTest {
private static final String ID = "open-minds-ep-21-dr-beth-harris-and-dr-steven-zucker-of-smarthistory";
private static final String UPLOADER = SOUNDCLOUD + "wearecc";
private static final int TIMESTAMP = 69;
@@ -167,7 +171,7 @@ public class SoundcloudStreamExtractorTest {
@Override public long expectedTimestamp() { return TIMESTAMP; }
@Override public long expectedViewCountAtLeast() { return 15000; }
@Nullable @Override public String expectedUploadDate() { return "2022-10-03 18:49:49.000"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2022-10-03 18:49:49"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2022-10-03T18:49:49Z"; }
@Override public long expectedLikeCountAtLeast() { return 10; }
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasRelatedItems() { return false; }

View File

@@ -73,9 +73,8 @@ public class YoutubeChannelLocalizationTest implements InitYoutubeTest {
+ "\n:::: " + item.getStreamType() + ", views = " + item.getViewCount();
final DateWrapper uploadDate = item.getUploadDate();
if (uploadDate != null) {
final String dateAsText = dateTimeFormatter.format(uploadDate.offsetDateTime());
debugMessage += "\n:::: " + item.getTextualUploadDate() +
"\n:::: " + dateAsText;
final String dateStr = dateTimeFormatter.format(uploadDate.getLocalDateTime());
debugMessage += "\n:::: " + item.getTextualUploadDate() + "\n:::: " + dateStr;
}
if (DEBUG) System.out.println(debugMessage + "\n");
}