Extract stream duration as a Java 8 Duration

This commit is contained in:
Isira Seneviratne 2024-07-12 08:24:03 +05:30
parent 592f1596e6
commit 0261e637d3
21 changed files with 94 additions and 80 deletions

View File

@ -11,7 +11,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.Duration;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL; import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
@ -26,13 +26,14 @@ public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
show = radioShow; show = radioShow;
} }
@Nonnull
@Override @Override
public long getDuration() { public Duration getDurationObject() {
/* Duration is only present in the more detailed information that has to be queried /* Duration is only present in the more detailed information that has to be queried
separately. Therefore, over 300 queries would be needed every time the kiosk is opened if we separately. Therefore, over 300 queries would be needed every time the kiosk is opened if we
were to display the real value. */ were to display the real value. */
//return query(show.getInt("id")).getLong("audio_duration"); //return Duration.ofSeconds(query(show.getInt("id")).getLong("audio_duration"));
return 0; return Duration.ZERO;
} }
@Nullable @Nullable

View File

@ -43,9 +43,4 @@ public class BandcampDiscographStreamInfoItemExtractor extends BandcampStreamInf
public List<Image> getThumbnails() throws ParsingException { public List<Image> getThumbnails() throws ParsingException {
return getImagesFromImageId(discograph.getLong("art_id"), true); return getImagesFromImageId(discograph.getLong("art_id"), true);
} }
@Override
public long getDuration() {
return -1;
}
} }

View File

@ -11,6 +11,7 @@ import org.schabi.newpipe.extractor.stream.StreamExtractor;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -46,9 +47,10 @@ public class BandcampPlaylistStreamInfoItemExtractor extends BandcampStreamInfoI
return getUploaderUrl() + track.getString("title_link"); return getUploaderUrl() + track.getString("title_link");
} }
@Nonnull
@Override @Override
public long getDuration() { public Duration getDurationObject() {
return track.getLong("duration"); return Duration.ofSeconds(track.getLong("duration"));
} }
@Override @Override

View File

@ -47,9 +47,4 @@ public class BandcampSearchStreamInfoItemExtractor extends BandcampStreamInfoIte
public List<Image> getThumbnails() throws ParsingException { public List<Image> getThumbnails() throws ParsingException {
return getImagesFromSearchResult(searchResult); return getImagesFromSearchResult(searchResult);
} }
@Override
public long getDuration() {
return -1;
}
} }

View File

@ -60,11 +60,6 @@ public class MediaCCCLiveStreamKioskExtractor implements StreamInfoItemExtractor
return false; return false;
} }
@Override
public long getDuration() throws ParsingException {
return 0;
}
@Override @Override
public long getViewCount() throws ParsingException { public long getViewCount() throws ParsingException {
return -1; return -1;

View File

@ -64,7 +64,7 @@ public class MediaCCCRecentKiosk extends KioskExtractor<StreamInfoItem> {
.map(JsonObject.class::cast) .map(JsonObject.class::cast)
.map(MediaCCCRecentKioskExtractor::new) .map(MediaCCCRecentKioskExtractor::new)
// #813 / voc/voctoweb#609 -> returns faulty data -> filter it out // #813 / voc/voctoweb#609 -> returns faulty data -> filter it out
.filter(extractor -> extractor.getDuration() > 0) .filter(extractor -> !extractor.getDurationObject().isZero())
.forEach(collector::commit); .forEach(collector::commit);
return new InfoItemsPage<>(collector, null); return new InfoItemsPage<>(collector, null);

View File

@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConfe
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import java.time.Duration;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
@ -53,10 +54,11 @@ public class MediaCCCRecentKioskExtractor implements StreamInfoItemExtractor {
} }
@Override @Override
public long getDuration() { @Nonnull
public Duration getDurationObject() {
// duration and length have the same value, see // duration and length have the same value, see
// https://github.com/voc/voctoweb/blob/master/app/views/public/shared/_event.json.jbuilder // https://github.com/voc/voctoweb/blob/master/app/views/public/shared/_event.json.jbuilder
return event.getInt("duration"); return Duration.ofSeconds(event.getLong("duration"));
} }
@Override @Override

View File

@ -10,6 +10,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.Duration;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getThumbnailsFromStreamItem; import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getThumbnailsFromStreamItem;
@ -32,8 +33,9 @@ public class MediaCCCStreamInfoItemExtractor implements StreamInfoItemExtractor
} }
@Override @Override
public long getDuration() { @Nonnull
return event.getInt("length"); public Duration getDurationObject() {
return Duration.ofSeconds(event.getLong("length"));
} }
@Override @Override

View File

@ -10,6 +10,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.JsonUtils;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.getAvatarsFromOwnerAccountOrVideoChannelObject; import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.getAvatarsFromOwnerAccountOrVideoChannelObject;
@ -100,8 +101,9 @@ public class PeertubeStreamInfoItemExtractor implements StreamInfoItemExtractor
} }
@Override @Override
public long getDuration() { @Nonnull
return item.getLong("duration"); public Duration getDurationObject() {
return Duration.ofSeconds(item.getLong("duration"));
} }
protected void setBaseUrl(final String baseUrl) { protected void setBaseUrl(final String baseUrl) {

View File

@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromArtworkOrAvatarUrl; import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.getAllImagesFromArtworkOrAvatarUrl;
@ -35,8 +36,9 @@ public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtracto
} }
@Override @Override
public long getDuration() { @Nonnull
return itemObject.getLong("duration") / 1000L; public Duration getDurationObject() {
return Duration.ofMillis(itemObject.getLong("duration"));
} }
@Override @Override

View File

@ -32,8 +32,8 @@ import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException; import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonWriter; import com.grack.nanojson.JsonWriter;
import org.jsoup.nodes.Entities; import org.jsoup.nodes.Entities;
import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.Image.ResolutionLevel; import org.schabi.newpipe.extractor.Image.ResolutionLevel;
import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.downloader.Response;
@ -56,10 +56,12 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -310,21 +312,22 @@ public final class YoutubeParsingHelper {
* @return the duration in seconds * @return the duration in seconds
* @throws ParsingException when more than 3 separators are found * @throws ParsingException when more than 3 separators are found
*/ */
public static int parseDurationString(@Nonnull final String input) public static Duration parseDurationString(@Nonnull final String input)
throws ParsingException, NumberFormatException { throws ParsingException {
// If time separator : is not detected, try . instead // If time separator : is not detected, try . instead
final String[] splitInput = input.contains(":") final String[] splitInput = input.contains(":")
? input.split(":") ? input.split(":")
: input.split("\\."); : input.split("\\.");
final int[] units = {24, 60, 60, 1}; final var units = List.of(ChronoUnit.DAYS, ChronoUnit.HOURS, ChronoUnit.MINUTES,
final int offset = units.length - splitInput.length; ChronoUnit.SECONDS);
final int offset = units.size() - splitInput.length;
if (offset < 0) { if (offset < 0) {
throw new ParsingException("Error duration string with unknown format: " + input); throw new ParsingException("Error duration string with unknown format: " + input);
} }
int duration = 0; Duration duration = Duration.ZERO;
for (int i = 0; i < splitInput.length; i++) { for (int i = 0; i < splitInput.length; i++) {
duration = units[i + offset] * (duration + convertDurationToInt(splitInput[i])); duration = duration.plus(convertDurationToInt(splitInput[i]), units.get(i + offset));
} }
return duration; return duration;
} }
@ -341,11 +344,7 @@ public final class YoutubeParsingHelper {
* @return The converted integer or 0 if the conversion failed. * @return The converted integer or 0 if the conversion failed.
*/ */
private static int convertDurationToInt(final String input) { private static int convertDurationToInt(final String input) {
if (input == null || input.isEmpty()) { final String clearedInput = input != null ? Utils.removeNonDigitCharacters(input) : "";
return 0;
}
final String clearedInput = Utils.removeNonDigitCharacters(input);
try { try {
return Integer.parseInt(clearedInput); return Integer.parseInt(clearedInput);
} catch (final NumberFormatException ex) { } catch (final NumberFormatException ex) {

View File

@ -33,12 +33,6 @@ public class YoutubeFeedInfoItemExtractor implements StreamInfoItemExtractor {
return false; return false;
} }
@Override
public long getDuration() {
// Not available when fetching through the feed endpoint.
return -1;
}
@Override @Override
public long getViewCount() { public long getViewCount() {
return Long.parseLong(entryElement.getElementsByTag("media:statistics").first() return Long.parseLong(entryElement.getElementsByTag("media:statistics").first()

View File

@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
@ -67,7 +68,8 @@ public class YoutubeMusicSongOrVideoInfoItemExtractor implements StreamInfoItemE
} }
@Override @Override
public long getDuration() throws ParsingException { @Nonnull
public Duration getDurationObject() throws ParsingException {
final String duration = descriptionElements.getObject(descriptionElements.size() - 1) final String duration = descriptionElements.getObject(descriptionElements.size() - 1)
.getString("text"); .getString("text");
if (!isNullOrEmpty(duration)) { if (!isNullOrEmpty(duration)) {

View File

@ -5,7 +5,6 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.localization.DateWrapper;
@ -90,11 +89,6 @@ public class YoutubeReelInfoItemExtractor implements StreamInfoItemExtractor {
return false; return false;
} }
@Override
public long getDuration() throws ParsingException {
return -1;
}
@Override @Override
public String getUploaderName() throws ParsingException { public String getUploaderName() throws ParsingException {
return null; return null;

View File

@ -41,7 +41,7 @@ import org.schabi.newpipe.extractor.utils.Utils;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
@ -136,10 +136,11 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
throw new ParsingException("Could not get name"); throw new ParsingException("Could not get name");
} }
@Nonnull
@Override @Override
public long getDuration() throws ParsingException { public Duration getDurationObject() throws ParsingException {
if (getStreamType() == StreamType.LIVE_STREAM) { if (getStreamType() == StreamType.LIVE_STREAM) {
return -1; return Duration.ZERO;
} }
String duration = getTextFromObject(videoInfo.getObject("lengthText")); String duration = getTextFromObject(videoInfo.getObject("lengthText"));
@ -169,7 +170,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
if (isPremiere()) { if (isPremiere()) {
// Premieres can be livestreams, so the duration is not available in this // Premieres can be livestreams, so the duration is not available in this
// case // case
return -1; return Duration.ZERO;
} }
throw new ParsingException("Could not get duration"); throw new ParsingException("Could not get duration");

View File

@ -26,6 +26,7 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.Duration;
import java.util.List; import java.util.List;
/** /**
@ -40,7 +41,8 @@ public class StreamInfoItem extends InfoItem {
@Nullable @Nullable
private DateWrapper uploadDate; private DateWrapper uploadDate;
private long viewCount = -1; private long viewCount = -1;
private long duration = -1; @Nonnull
private Duration duration = Duration.ZERO;
private String uploaderUrl = null; private String uploaderUrl = null;
@Nonnull @Nonnull
@ -76,12 +78,21 @@ public class StreamInfoItem extends InfoItem {
this.viewCount = viewCount; this.viewCount = viewCount;
} }
public long getDuration() { @Nonnull
public Duration getDurationObject() {
return duration; return duration;
} }
public void setDurationObject(@Nonnull final Duration durationObject) {
this.duration = durationObject;
}
public long getDuration() {
return duration.toSeconds();
}
public void setDuration(final long duration) { public void setDuration(final long duration) {
this.duration = duration; this.duration = Duration.ofSeconds(duration);
} }
public String getUploaderUrl() { public String getUploaderUrl() {

View File

@ -27,6 +27,7 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.Duration;
import java.util.List; import java.util.List;
public interface StreamInfoItemExtractor extends InfoItemExtractor { public interface StreamInfoItemExtractor extends InfoItemExtractor {
@ -48,12 +49,25 @@ public interface StreamInfoItemExtractor extends InfoItemExtractor {
boolean isAd() throws ParsingException; boolean isAd() throws ParsingException;
/** /**
* Get the stream duration in seconds * Get the stream duration as a {@link Duration}.
* *
* @return the stream duration in seconds or -1 if no duration is available * @return the stream duration in seconds or {@link Duration#ZERO} if no duration is available
* @throws ParsingException if there is an error in the extraction * @throws ParsingException if there is an error in the extraction
*/ */
long getDuration() throws ParsingException; @Nonnull
default Duration getDurationObject() throws ParsingException {
return Duration.ZERO;
}
/**
* Get the stream duration in seconds.
*
* @return the stream duration in seconds or 0 if no duration is available
* @throws ParsingException if there is an error in the extraction
*/
default long getDuration() throws ParsingException {
return getDurationObject().toSeconds();
}
/** /**
* Parses the number of views * Parses the number of views

View File

@ -44,12 +44,12 @@ public class StreamInfoItemsCollector
throw new FoundAdException("Found ad"); throw new FoundAdException("Found ad");
} }
final StreamInfoItem resultItem = new StreamInfoItem( final var resultItem = new StreamInfoItem(getServiceId(), extractor.getUrl(),
getServiceId(), extractor.getUrl(), extractor.getName(), extractor.getStreamType()); extractor.getName(), extractor.getStreamType());
// optional information // optional information
try { try {
resultItem.setDuration(extractor.getDuration()); resultItem.setDurationObject(extractor.getDurationObject());
} catch (final Exception e) { } catch (final Exception e) {
addError(e); addError(e);
} }

View File

@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -99,12 +100,9 @@ public class ExtractorAsserts {
assertGreater(expected, actual, actual + " is not > " + expected); assertGreater(expected, actual, actual + " is not > " + expected);
} }
public static void assertGreater( public static <T extends Comparable<T>> void assertGreater(final T expected, final T actual,
final long expected, final String message) {
final long actual, assertTrue(actual.compareTo(expected) > 0, message);
final String message
) {
assertTrue(actual > expected, message);
} }
public static void assertGreaterOrEqual(final long expected, final long actual) { public static void assertGreaterOrEqual(final long expected, final long actual) {

View File

@ -14,6 +14,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.kiosk.KioskExtractor; import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -41,9 +42,9 @@ public class MediaCCCRecentListExtractorTest {
isNullOrEmpty(item.getName()), isNullOrEmpty(item.getName()),
"Name=[" + item.getName() + "] of " + item + " is empty or null" "Name=[" + item.getName() + "] of " + item + " is empty or null"
), ),
() -> assertGreater(0, () -> assertGreater(Duration.ZERO,
item.getDuration(), item.getDurationObject(),
"Duration[=" + item.getDuration() + "] of " + item + " is <= 0" "Duration[=" + item.getDurationObject() + "] of " + item + " is <= 0"
) )
); );
} }

View File

@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream.AudioTrackType; import org.schabi.newpipe.extractor.stream.AudioTrackType;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
@ -38,9 +39,12 @@ public class YoutubeParsingHelperTest {
@Test @Test
void testParseDurationString() throws ParsingException { void testParseDurationString() throws ParsingException {
assertEquals(1162567, YoutubeParsingHelper.parseDurationString("12:34:56:07")); assertEquals(Duration.ofDays(12).plusHours(34).plusMinutes(56).plusSeconds(7),
assertEquals(4445767, YoutubeParsingHelper.parseDurationString("1,234:56:07")); YoutubeParsingHelper.parseDurationString("12:34:56:07"));
assertEquals(754, YoutubeParsingHelper.parseDurationString("12:34 ")); assertEquals(Duration.ofHours(1234).plusMinutes(56).plusSeconds(7),
YoutubeParsingHelper.parseDurationString("1,234:56:07"));
assertEquals(Duration.ofMinutes(12).plusSeconds(34),
YoutubeParsingHelper.parseDurationString("12:34 "));
} }
@Test @Test