Skip to content

Commit b113062

Browse files
authored
Merge pull request #73 from karyogamy/live
HLS Livestream Extraction
2 parents 86db415 + cc3f3b8 commit b113062

6 files changed

Lines changed: 187 additions & 44 deletions

File tree

src/main/java/org/schabi/newpipe/extractor/search/SearchResult.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,10 @@ public SearchResult(int serviceId, String suggestion, List<InfoItem> results, Li
4444
this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
4545
}
4646

47-
public static SearchResult getSearchResult(SearchEngine engine, String query, int page, String languageCode, SearchEngine.Filter filter)
47+
public static SearchResult getSearchResult(@Nonnull final SearchEngine engine, final String query, final int page,
48+
final String languageCode, final SearchEngine.Filter filter)
4849
throws IOException, ExtractionException {
49-
50-
SearchResult result = engine
51-
.search(query, page, languageCode, filter)
52-
.getSearchResult();
53-
if (result.resultList.isEmpty()) {
54-
if (result.suggestion.isEmpty()) {
55-
if (result.errors.isEmpty()) {
56-
throw new ExtractionException("Empty result despite no error");
57-
}
58-
} else {
59-
// This is used as a fallback. Do not relay on it !!!
60-
throw new SearchEngine.NothingFoundException(result.suggestion);
61-
}
62-
}
63-
return result;
50+
return engine.search(query, page, languageCode, filter).getSearchResult();
6451
}
6552

6653
public String getSuggestion() {

src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractor.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,16 @@ public String getUploaderAvatarUrl() {
116116
return SoundcloudParsingHelper.getAvatarUrl(track);
117117
}
118118

119+
@Nonnull
119120
@Override
120121
public String getDashMpdUrl() {
121-
return null;
122+
return "";
123+
}
124+
125+
@Nonnull
126+
@Override
127+
public String getHlsUrl() throws ParsingException {
128+
return "";
122129
}
123130

124131
@Override

src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,6 @@ public class GemaException extends ContentNotAvailableException {
6666
}
6767
}
6868

69-
public class LiveStreamException extends ContentNotAvailableException {
70-
LiveStreamException(String message) {
71-
super(message);
72-
}
73-
}
74-
7569
public class SubtitlesException extends ContentNotAvailableException {
7670
SubtitlesException(String message, Throwable cause) {
7771
super(message, cause);
@@ -338,6 +332,7 @@ public String getUploaderAvatarUrl() throws ParsingException {
338332
}
339333
}
340334

335+
@Nonnull
341336
@Override
342337
public String getDashMpdUrl() throws ParsingException {
343338
assertPageFetched();
@@ -365,6 +360,24 @@ public String getDashMpdUrl() throws ParsingException {
365360
}
366361
}
367362

363+
@Nonnull
364+
@Override
365+
public String getHlsUrl() throws ParsingException {
366+
assertPageFetched();
367+
try {
368+
String hlsvp;
369+
if (playerArgs != null && playerArgs.isString("hlsvp")) {
370+
hlsvp = playerArgs.getString("hlsvp", "");
371+
} else {
372+
return "";
373+
}
374+
375+
return hlsvp;
376+
} catch (Exception e) {
377+
throw new ParsingException("Could not get hls manifest url", e);
378+
}
379+
}
380+
368381
@Override
369382
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
370383
assertPageFetched();
@@ -428,7 +441,7 @@ public List<VideoStream> getVideoOnlyStreams() throws IOException, ExtractionExc
428441
@Override
429442
@Nonnull
430443
public List<Subtitles> getSubtitlesDefault() throws IOException, ExtractionException {
431-
return getSubtitles(SubtitlesFormat.VTT);
444+
return getSubtitles(SubtitlesFormat.TTML);
432445
}
433446

434447
@Override
@@ -444,7 +457,15 @@ public List<Subtitles> getSubtitles(final SubtitlesFormat format) throws IOExcep
444457

445458
@Override
446459
public StreamType getStreamType() throws ParsingException {
447-
//todo: if implementing livestream support this value should be generated dynamically
460+
assertPageFetched();
461+
try {
462+
if (playerArgs != null && (playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live") ||
463+
playerArgs.get(URL_ENCODED_FMT_STREAM_MAP).toString().isEmpty())) {
464+
return StreamType.LIVE_STREAM;
465+
}
466+
} catch (Exception e) {
467+
throw new ParsingException("Could not get hls manifest url", e);
468+
}
448469
return StreamType.VIDEO_STREAM;
449470
}
450471

@@ -517,13 +538,16 @@ public String getErrorMessage() {
517538
private static final String CONTENT = "content";
518539
private static final String DECRYPTION_FUNC_NAME = "decrypt";
519540

541+
private static final String VERIFIED_URL_PARAMS = "&has_verified=1&bpctr=9999999999";
542+
520543
private volatile String decryptionCode = "";
521544

522545
private String pageHtml = null;
523546

524-
private String getPageHtml(Downloader downloader) throws IOException, ExtractionException{
547+
private String getPageHtml(Downloader downloader) throws IOException, ExtractionException {
548+
final String verifiedUrl = getCleanUrl() + VERIFIED_URL_PARAMS;
525549
if (pageHtml == null) {
526-
pageHtml = downloader.download(getCleanUrl());
550+
pageHtml = downloader.download(verifiedUrl);
527551
}
528552
return pageHtml;
529553
}
@@ -534,7 +558,6 @@ public void onFetchPage(@Nonnull Downloader downloader) throws IOException, Extr
534558
doc = Jsoup.parse(pageContent, getCleanUrl());
535559

536560
final String playerUrl;
537-
// TODO: use embedded videos to fetch DASH manifest for all videos
538561
// Check if the video is age restricted
539562
if (pageContent.contains("<meta property=\"og:restrictions:age")) {
540563
final EmbeddedInfo info = getEmbeddedInfo();
@@ -582,21 +605,11 @@ private JsonObject getPlayerArgs(JsonObject playerConfig) throws ParsingExceptio
582605
JsonObject playerArgs;
583606

584607
//attempt to load the youtube js player JSON arguments
585-
boolean isLiveStream = false; //used to determine if this is a livestream or not
586608
try {
587609
playerArgs = playerConfig.getObject("args");
588-
589-
// check if we have a live stream. We need to filter it, since its not yet supported.
590-
if ((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
591-
|| (playerArgs.get(URL_ENCODED_FMT_STREAM_MAP).toString().isEmpty())) {
592-
isLiveStream = true;
593-
}
594610
} catch (Exception e) {
595611
throw new ParsingException("Could not parse yt player config", e);
596612
}
597-
if (isLiveStream) {
598-
throw new LiveStreamException("This is a Live stream. Can't use those right now.");
599-
}
600613

601614
return playerArgs;
602615
}
@@ -806,11 +819,6 @@ private static String getVideoInfoUrl(final String id, final String sts) {
806819
"&sts=" + sts + "&ps=default&gl=US&hl=en";
807820
}
808821

809-
@Nonnull
810-
private static String getSubtitleFormatUrl(final String baseUrl, final SubtitlesFormat format) {
811-
return baseUrl.replaceAll("&fmt=[^&]*", "") + "&fmt=" + format.getExtension();
812-
}
813-
814822
private Map<String, ItagItem> getItags(String encodedUrlMapKey, ItagItem.ItagType itagTypeWanted) throws ParsingException {
815823
Map<String, ItagItem> urlAndItags = new LinkedHashMap<>();
816824

src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ protected long getTimestampSeconds(String regexPattern) throws ParsingException
126126
* @return the url as a string or an empty string
127127
* @throws ParsingException if an error occurs while reading
128128
*/
129-
public abstract String getDashMpdUrl() throws ParsingException;
129+
@Nonnull public abstract String getDashMpdUrl() throws ParsingException;
130+
@Nonnull public abstract String getHlsUrl() throws ParsingException;
130131
public abstract List<AudioStream> getAudioStreams() throws IOException, ExtractionException;
131132
public abstract List<VideoStream> getVideoStreams() throws IOException, ExtractionException;
132133
public abstract List<VideoStream> getVideoOnlyStreams() throws IOException, ExtractionException;

src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ public String getDashMpdUrl() {
126126
return dashMpdUrl;
127127
}
128128

129+
public String getHlsUrl() {
130+
return hlsUrl;
131+
}
132+
129133
public StreamInfoItem getNextVideo() {
130134
return next_video;
131135
}
@@ -206,6 +210,10 @@ public void setDashMpdUrl(String dashMpdUrl) {
206210
this.dashMpdUrl = dashMpdUrl;
207211
}
208212

213+
public void setHlsUrl(String hlsUrl) {
214+
this.hlsUrl = hlsUrl;
215+
}
216+
209217
public void setNextVideo(StreamInfoItem next_video) {
210218
this.next_video = next_video;
211219
}
@@ -298,6 +306,12 @@ private static StreamInfo extractStreams(StreamInfo streamInfo, StreamExtractor
298306
streamInfo.addError(new ExtractionException("Couldn't get Dash manifest", e));
299307
}
300308

309+
try {
310+
streamInfo.setHlsUrl(extractor.getHlsUrl());
311+
} catch (Exception e) {
312+
streamInfo.addError(new ExtractionException("Couldn't get HLS manifest", e));
313+
}
314+
301315
/* Load and extract audio */
302316
try {
303317
streamInfo.setAudioStreams(extractor.getAudioStreams());
@@ -447,6 +461,7 @@ private static StreamInfo extractOptionalData(StreamInfo streamInfo, StreamExtra
447461
// crawling such a file is not service dependent. Therefore getting audio only streams by yust
448462
// providing the dash mpd file will be possible in the future.
449463
public String dashMpdUrl;
464+
public String hlsUrl;
450465

451466
public StreamInfoItem next_video;
452467
public List<InfoItem> related_streams;
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package org.schabi.newpipe.extractor.services.youtube;
2+
3+
import org.junit.BeforeClass;
4+
import org.junit.Ignore;
5+
import org.junit.Test;
6+
import org.schabi.newpipe.Downloader;
7+
import org.schabi.newpipe.extractor.NewPipe;
8+
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
9+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
10+
import org.schabi.newpipe.extractor.stream.StreamExtractor;
11+
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
12+
import org.schabi.newpipe.extractor.stream.VideoStream;
13+
14+
import java.io.IOException;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
import static org.junit.Assert.*;
19+
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl;
20+
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
21+
22+
/**
23+
* Test for {@link YoutubeStreamUrlIdHandler}
24+
*/
25+
public class YoutubeStreamExtractorControversialTest {
26+
private static YoutubeStreamExtractor extractor;
27+
28+
@BeforeClass
29+
public static void setUp() throws Exception {
30+
NewPipe.init(Downloader.getInstance());
31+
extractor = (YoutubeStreamExtractor) YouTube
32+
.getStreamExtractor("https://www.youtube.com/watch?v=T4XJQO3qol8");
33+
extractor.fetchPage();
34+
}
35+
36+
@Test
37+
public void testGetInvalidTimeStamp() throws ParsingException {
38+
assertTrue(extractor.getTimeStamp() + "", extractor.getTimeStamp() <= 0);
39+
}
40+
41+
@Test
42+
public void testGetValidTimeStamp() throws IOException, ExtractionException {
43+
StreamExtractor extractor = YouTube.getStreamExtractor("https://youtu.be/FmG385_uUys?t=174");
44+
assertEquals(extractor.getTimeStamp() + "", "174");
45+
}
46+
47+
@Test
48+
@Ignore
49+
public void testGetAgeLimit() throws ParsingException {
50+
assertEquals(18, extractor.getAgeLimit());
51+
}
52+
53+
@Test
54+
public void testGetName() throws ParsingException {
55+
assertNotNull("name is null", extractor.getName());
56+
assertFalse("name is empty", extractor.getName().isEmpty());
57+
}
58+
59+
@Test
60+
public void testGetDescription() throws ParsingException {
61+
assertNotNull(extractor.getDescription());
62+
assertFalse(extractor.getDescription().isEmpty());
63+
}
64+
65+
@Test
66+
public void testGetUploaderName() throws ParsingException {
67+
assertNotNull(extractor.getUploaderName());
68+
assertFalse(extractor.getUploaderName().isEmpty());
69+
}
70+
71+
@Ignore // Currently there is no way get the length from restricted videos
72+
@Test
73+
public void testGetLength() throws ParsingException {
74+
assertTrue(extractor.getLength() > 0);
75+
}
76+
77+
@Test
78+
public void testGetViews() throws ParsingException {
79+
assertTrue(extractor.getViewCount() > 0);
80+
}
81+
82+
@Test
83+
public void testGetUploadDate() throws ParsingException {
84+
assertTrue(extractor.getUploadDate().length() > 0);
85+
}
86+
87+
@Test
88+
public void testGetThumbnailUrl() throws ParsingException {
89+
assertIsSecureUrl(extractor.getThumbnailUrl());
90+
}
91+
92+
@Test
93+
public void testGetUploaderAvatarUrl() throws ParsingException {
94+
assertIsSecureUrl(extractor.getUploaderAvatarUrl());
95+
}
96+
97+
// FIXME: 25.11.17 Are there no streams or are they not listed?
98+
@Ignore
99+
@Test
100+
public void testGetAudioStreams() throws IOException, ExtractionException {
101+
// audio streams are not always necessary
102+
assertFalse(extractor.getAudioStreams().isEmpty());
103+
}
104+
105+
@Test
106+
public void testGetVideoStreams() throws IOException, ExtractionException {
107+
List<VideoStream> streams = new ArrayList<>();
108+
streams.addAll(extractor.getVideoStreams());
109+
streams.addAll(extractor.getVideoOnlyStreams());
110+
assertTrue(streams.size() > 0);
111+
}
112+
113+
114+
@Test
115+
public void testGetSubtitlesListDefault() throws IOException, ExtractionException {
116+
// Video (/view?v=YQHsXMglC9A) set in the setUp() method has no captions => null
117+
assertTrue(!extractor.getSubtitlesDefault().isEmpty());
118+
}
119+
120+
@Test
121+
public void testGetSubtitlesList() throws IOException, ExtractionException {
122+
// Video (/view?v=YQHsXMglC9A) set in the setUp() method has no captions => null
123+
assertTrue(!extractor.getSubtitles(SubtitlesFormat.TTML).isEmpty());
124+
}
125+
}

0 commit comments

Comments
 (0)