mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-22 16:49:29 +01:00
Merge branch 'rrooij-age_restriction'
This commit is contained in:
commit
ff89dd00b6
@ -0,0 +1,84 @@
|
||||
package org.schabi.newpipe.extractor.youtube;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.VideoInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class YoutubeStreamExtractorRestrictedTest extends AndroidTestCase {
|
||||
private YoutubeStreamExtractor extractor;
|
||||
|
||||
public void setUp() throws IOException, ExtractionException {
|
||||
extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=i6JTvzrpBy0",
|
||||
new Downloader());
|
||||
}
|
||||
|
||||
public void testGetInvalidTimeStamp() throws ParsingException {
|
||||
assertTrue(Integer.toString(extractor.getTimeStamp()),
|
||||
extractor.getTimeStamp() <= 0);
|
||||
}
|
||||
|
||||
public void testGetValidTimeStamp() throws ExtractionException, IOException {
|
||||
YoutubeStreamExtractor extractor =
|
||||
new YoutubeStreamExtractor("https://youtu.be/FmG385_uUys?t=174", new Downloader());
|
||||
assertTrue(Integer.toString(extractor.getTimeStamp()),
|
||||
extractor.getTimeStamp() == 174);
|
||||
}
|
||||
|
||||
public void testGetAgeLimit() throws ParsingException {
|
||||
assertTrue(extractor.getAgeLimit() == 18);
|
||||
}
|
||||
|
||||
public void testGetTitle() throws ParsingException {
|
||||
assertTrue(!extractor.getTitle().isEmpty());
|
||||
}
|
||||
|
||||
public void testGetDescription() throws ParsingException {
|
||||
assertTrue(extractor.getDescription() != null);
|
||||
}
|
||||
|
||||
public void testGetUploader() throws ParsingException {
|
||||
assertTrue(!extractor.getUploader().isEmpty());
|
||||
}
|
||||
|
||||
public void testGetLength() throws ParsingException {
|
||||
assertTrue(extractor.getLength() > 0);
|
||||
}
|
||||
|
||||
public void testGetViews() throws ParsingException {
|
||||
assertTrue(extractor.getLength() > 0);
|
||||
}
|
||||
|
||||
public void testGetUploadDate() throws ParsingException {
|
||||
assertTrue(extractor.getUploadDate().length() > 0);
|
||||
}
|
||||
|
||||
public void testGetThumbnailUrl() throws ParsingException {
|
||||
assertTrue(extractor.getThumbnailUrl(),
|
||||
extractor.getThumbnailUrl().contains("https://"));
|
||||
}
|
||||
|
||||
public void testGetUploaderThumbnailUrl() throws ParsingException {
|
||||
assertTrue(extractor.getUploaderThumbnailUrl(),
|
||||
extractor.getUploaderThumbnailUrl().contains("https://"));
|
||||
}
|
||||
|
||||
public void testGetAudioStreams() throws ParsingException {
|
||||
assertTrue(!extractor.getAudioStreams().isEmpty());
|
||||
}
|
||||
|
||||
public void testGetVideoStreams() throws ParsingException {
|
||||
for(VideoInfo.VideoStream s : extractor.getVideoStreams()) {
|
||||
assertTrue(s.url,
|
||||
s.url.contains("https://"));
|
||||
assertTrue(s.resolution.length() > 0);
|
||||
assertTrue(Integer.toString(s.format),
|
||||
0 <= s.format && s.format <= 4);
|
||||
}
|
||||
}
|
||||
}
|
@ -181,9 +181,13 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
//todo: fix expired thread error:
|
||||
// If the thread calling this runnable is expired, the following function will crash.
|
||||
updateInfo(videoInfo);
|
||||
boolean show_age_restricted_content = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getBoolean(activity.getString(R.string.show_age_restricted_content), false);
|
||||
if(videoInfo.age_limit == 0 || show_age_restricted_content) {
|
||||
updateInfo(videoInfo);
|
||||
} else {
|
||||
onNotSpecifiedContentErrorWithMessage(R.string.video_is_age_restricted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,7 +422,7 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
imageLoader.displayImage(info.uploader_thumbnail_url,
|
||||
uploaderThumb, displayImageOptions, new ThumbnailLoadingListener());
|
||||
}
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty() && info.next_video != null) {
|
||||
imageLoader.displayImage(info.next_video.thumbnail_url,
|
||||
nextVideoThumb, displayImageOptions, new ThumbnailLoadingListener());
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.HashMap;
|
||||
@ -53,7 +55,11 @@ public class Parser {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for(String arg : input.split("&")) {
|
||||
String[] split_arg = arg.split("=");
|
||||
map.put(split_arg[0], URLDecoder.decode(split_arg[1], "UTF-8"));
|
||||
if(split_arg.length > 1) {
|
||||
map.put(split_arg[0], URLDecoder.decode(split_arg[1], "UTF-8"));
|
||||
} else {
|
||||
map.put(split_arg[0], "");
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
@ -52,10 +52,12 @@ public class VideoInfo extends AbstractVideoInfo {
|
||||
videoInfo.webpage_url = extractor.getPageUrl();
|
||||
videoInfo.id = uiconv.getVideoId(extractor.getPageUrl());
|
||||
videoInfo.title = extractor.getTitle();
|
||||
videoInfo.age_limit = extractor.getAgeLimit();
|
||||
|
||||
if((videoInfo.webpage_url == null || videoInfo.webpage_url.isEmpty())
|
||||
|| (videoInfo.id == null || videoInfo.id.isEmpty())
|
||||
|| (videoInfo.title == null /* videoInfo.title can be empty of course */));
|
||||
|| (videoInfo.title == null /* videoInfo.title can be empty of course */)
|
||||
|| (videoInfo.age_limit == -1));
|
||||
|
||||
return videoInfo;
|
||||
}
|
||||
@ -192,6 +194,11 @@ public class VideoInfo extends AbstractVideoInfo {
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
|
||||
} catch (Exception e) {
|
||||
videoInfo.addException(e);
|
||||
}
|
||||
|
||||
return videoInfo;
|
||||
}
|
||||
@ -209,7 +216,7 @@ public class VideoInfo extends AbstractVideoInfo {
|
||||
public String dashMpdUrl = "";
|
||||
public int duration = -1;
|
||||
|
||||
public int age_limit = 0;
|
||||
public int age_limit = -1;
|
||||
public int like_count = -1;
|
||||
public int dislike_count = -1;
|
||||
public String average_rating = "";
|
||||
|
@ -24,6 +24,8 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 06.08.15.
|
||||
@ -168,7 +170,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
private static final String TAG = YoutubeStreamExtractor.class.toString();
|
||||
private final Document doc;
|
||||
private JSONObject playerArgs;
|
||||
//private Map<String, String> videoInfoPage;
|
||||
private boolean isAgeRestricted;
|
||||
private Map<String, String> videoInfoPage;
|
||||
|
||||
// static values
|
||||
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
||||
@ -187,79 +190,123 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
this.pageUrl = pageUrl;
|
||||
String pageContent = downloader.download(urlidhandler.cleanUrl(pageUrl));
|
||||
doc = Jsoup.parse(pageContent, pageUrl);
|
||||
String ytPlayerConfigRaw;
|
||||
JSONObject ytPlayerConfig;
|
||||
String playerUrl;
|
||||
|
||||
//attempt to load the youtube js player JSON arguments
|
||||
boolean isLiveStream = false; //used to determine if this is a livestream or not
|
||||
// Check if the video is age restricted
|
||||
if (pageContent.contains("<meta property=\"og:restrictions:age")) {
|
||||
String videoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
|
||||
urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
|
||||
String videoInfoPageString = downloader.download(videoInfoUrl);
|
||||
videoInfoPage = Parser.compatParseMap(videoInfoPageString);
|
||||
playerUrl = getPlayerUrlFromRestrictedVideo(pageUrl);
|
||||
isAgeRestricted = true;
|
||||
} else {
|
||||
ytPlayerConfig = getPlayerConfig(pageContent);
|
||||
playerArgs = getPlayerArgs(ytPlayerConfig);
|
||||
playerUrl = getPlayerUrl(ytPlayerConfig);
|
||||
isAgeRestricted = false;
|
||||
}
|
||||
|
||||
if(decryptionCode.isEmpty()) {
|
||||
decryptionCode = loadDecryptionCode(playerUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getPlayerConfig(String pageContent) throws ParsingException {
|
||||
try {
|
||||
ytPlayerConfigRaw =
|
||||
String ytPlayerConfigRaw =
|
||||
Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent);
|
||||
ytPlayerConfig = new JSONObject(ytPlayerConfigRaw);
|
||||
playerArgs = ytPlayerConfig.getJSONObject("args");
|
||||
|
||||
// check if we have a live stream. We need to filter it, since its not yet supported.
|
||||
if((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
|
||||
|| (playerArgs.get("url_encoded_fmt_stream_map").toString().isEmpty())) {
|
||||
isLiveStream = true;
|
||||
}
|
||||
return new JSONObject(ytPlayerConfigRaw);
|
||||
} catch (Parser.RegexException e) {
|
||||
String errorReason = findErrorReason(doc);
|
||||
switch(errorReason) {
|
||||
case "GEMA":
|
||||
throw new GemaException(errorReason);
|
||||
case "":
|
||||
throw new ParsingException("player config empty", e);
|
||||
throw new ContentNotAvailableException("Content not available: player config empty", e);
|
||||
default:
|
||||
throw new ContentNotAvailableException("Content not available", e);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not parse yt player config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getPlayerArgs(JSONObject playerConfig) throws ParsingException {
|
||||
JSONObject playerArgs;
|
||||
|
||||
//attempt to load the youtube js player JSON arguments
|
||||
boolean isLiveStream = false; //used to determine if this is a livestream or not
|
||||
try {
|
||||
playerArgs = playerConfig.getJSONObject("args");
|
||||
|
||||
// check if we have a live stream. We need to filter it, since its not yet supported.
|
||||
if((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
|
||||
|| (playerArgs.get("url_encoded_fmt_stream_map").toString().isEmpty())) {
|
||||
isLiveStream = true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not parse yt player config", e);
|
||||
}
|
||||
if (isLiveStream) {
|
||||
throw new LiveStreamException();
|
||||
}
|
||||
|
||||
return playerArgs;
|
||||
}
|
||||
|
||||
/* not yet nececeary
|
||||
|
||||
|
||||
// get videoInfo page
|
||||
private String getPlayerUrl(JSONObject playerConfig) throws ParsingException {
|
||||
try {
|
||||
//Parser.unescapeEntities(url_data_str, true).split("&")
|
||||
String getVideoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
|
||||
urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
|
||||
videoInfoPage = Parser.compatParseMap(downloader.download(getVideoInfoUrl));
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException("Could not load video info page.", e);
|
||||
}
|
||||
*/
|
||||
// The Youtube service needs to be initialized by downloading the
|
||||
// js-Youtube-player. This is done in order to get the algorithm
|
||||
// for decrypting cryptic signatures inside certain stream urls.
|
||||
String playerUrl = "";
|
||||
|
||||
//----------------------------------
|
||||
// load and parse description code, if it isn't already initialised
|
||||
//----------------------------------
|
||||
if (decryptionCode.isEmpty()) {
|
||||
try {
|
||||
// The Youtube service needs to be initialized by downloading the
|
||||
// js-Youtube-player. This is done in order to get the algorithm
|
||||
// for decrypting cryptic signatures inside certain stream urls.
|
||||
JSONObject ytAssets = ytPlayerConfig.getJSONObject("assets");
|
||||
String playerUrl = ytAssets.getString("js");
|
||||
JSONObject ytAssets = playerConfig.getJSONObject("assets");
|
||||
playerUrl = ytAssets.getString("js");
|
||||
|
||||
if (playerUrl.startsWith("//")) {
|
||||
playerUrl = "https:" + playerUrl;
|
||||
}
|
||||
decryptionCode = loadDecryptionCode(playerUrl);
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException(
|
||||
"Could not load decryption code for the Youtube service.", e);
|
||||
if (playerUrl.startsWith("//")) {
|
||||
playerUrl = "https:" + playerUrl;
|
||||
}
|
||||
return playerUrl;
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException(
|
||||
"Could not load decryption code for the Youtube service.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlayerUrlFromRestrictedVideo(String pageUrl) throws ParsingException {
|
||||
try {
|
||||
String playerUrl = "";
|
||||
String videoId = urlidhandler.getVideoId(pageUrl);
|
||||
String embedUrl = "https://www.youtube.com/embed/" + videoId;
|
||||
String embedPageContent = downloader.download(embedUrl);
|
||||
//todo: find out if this can be reapaced by Parser.matchGroup1()
|
||||
Pattern assetsPattern = Pattern.compile("\"assets\":.+?\"js\":\\s*(\"[^\"]+\")");
|
||||
Matcher patternMatcher = assetsPattern.matcher(embedPageContent);
|
||||
while (patternMatcher.find()) {
|
||||
playerUrl = patternMatcher.group(1);
|
||||
}
|
||||
playerUrl = playerUrl.replace("\\", "").replace("\"", "");
|
||||
|
||||
if (playerUrl.startsWith("//")) {
|
||||
playerUrl = "https:" + playerUrl;
|
||||
}
|
||||
return playerUrl;
|
||||
} catch (IOException e) {
|
||||
throw new ParsingException(
|
||||
"Could load decryption code form restricted video for the Youtube service.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() throws ParsingException {
|
||||
try {//json player args method
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return videoInfoPage.get("title");
|
||||
}
|
||||
//json player args method
|
||||
return playerArgs.getString("title");
|
||||
} catch(JSONException je) {//html <meta> method
|
||||
je.printStackTrace();
|
||||
@ -283,7 +330,11 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
|
||||
@Override
|
||||
public String getUploader() throws ParsingException {
|
||||
try {//json player args method
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return videoInfoPage.get("author");
|
||||
}
|
||||
//json player args method
|
||||
return playerArgs.getString("author");
|
||||
} catch(JSONException je) {
|
||||
je.printStackTrace();
|
||||
@ -299,6 +350,9 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
@Override
|
||||
public int getLength() throws ParsingException {
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
return Integer.valueOf(videoInfoPage.get("length_seconds"));
|
||||
}
|
||||
return playerArgs.getInt("length_seconds");
|
||||
} catch (JSONException e) {//todo: find fallback method
|
||||
throw new ParsingException("failed to load video duration from JSON args", e);
|
||||
@ -339,6 +393,9 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
} catch (JSONException je) {
|
||||
throw new ParsingException(
|
||||
"failed to extract thumbnail URL from JSON args; trying to extract it from HTML", je);
|
||||
} catch (NullPointerException ne) {
|
||||
// Get from the video info page instead
|
||||
return videoInfoPage.get("thumbnail_url");
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,7 +436,13 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
public List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException {
|
||||
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
|
||||
try{
|
||||
String encoded_url_map = playerArgs.getString("adaptive_fmts");
|
||||
String encoded_url_map;
|
||||
// playerArgs could be null if the video is age restricted
|
||||
if (playerArgs == null) {
|
||||
encoded_url_map = videoInfoPage.get("adaptive_fmts");
|
||||
} else {
|
||||
encoded_url_map = playerArgs.getString("adaptive_fmts");
|
||||
}
|
||||
for(String url_data_str : encoded_url_map.split(",")) {
|
||||
// This loop iterates through multiple streams, therefor tags
|
||||
// is related to one and the same stream at a time.
|
||||
@ -416,7 +479,13 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
|
||||
|
||||
try{
|
||||
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
|
||||
String encoded_url_map;
|
||||
// playerArgs could be null if the video is age restricted
|
||||
if (playerArgs == null) {
|
||||
encoded_url_map = videoInfoPage.get("url_encoded_fmt_stream_map");
|
||||
} else {
|
||||
encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
|
||||
}
|
||||
for(String url_data_str : encoded_url_map.split(",")) {
|
||||
try {
|
||||
// This loop iterates through multiple streams, therefor tags
|
||||
@ -499,7 +568,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
int minutes = (minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString));
|
||||
int hours = (hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString));
|
||||
|
||||
int ret = seconds + (60 * minutes) + (3600 * hours);//don't trust BODMAS!
|
||||
//don't trust BODMAS!
|
||||
int ret = seconds + (60 * minutes) + (3600 * hours);
|
||||
//Log.d(TAG, "derived timestamp value:"+ret);
|
||||
return ret;
|
||||
//the ordering varies internationally
|
||||
@ -513,15 +583,24 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
||||
|
||||
@Override
|
||||
public int getAgeLimit() throws ParsingException {
|
||||
// Not yet implemented.
|
||||
// Also you need to be logged in to see age restricted videos on youtube,
|
||||
// therefore NP is not able to receive such videos.
|
||||
return 0;
|
||||
if (!isAgeRestricted) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(doc.head()
|
||||
.getElementsByAttributeValue("property", "og:restrictions:age")
|
||||
.attr("content").replace("+", ""));
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException("Could not get age restriction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAverageRating() throws ParsingException {
|
||||
try {
|
||||
if (playerArgs == null) {
|
||||
videoInfoPage.get("avg_rating");
|
||||
}
|
||||
return playerArgs.getString("avg_rating");
|
||||
} catch (JSONException e) {
|
||||
throw new ParsingException("Could not get Average rating", e);
|
||||
|
@ -3,6 +3,7 @@
|
||||
<!-- Categories -->
|
||||
<string name="settings_category_video_audio">settings_category_video_audio</string>
|
||||
<string name="settings_category_appearance">settings_category_appearance</string>
|
||||
<string name="settings_content_options">settings_content_options</string>
|
||||
<string name="settings_category_other">settings_category_other</string>
|
||||
<!-- Key values -->
|
||||
<string name="download_path_key">download_path</string>
|
||||
@ -207,5 +208,6 @@
|
||||
<item>日本語</item>
|
||||
<item>한국어</item>
|
||||
</string-array>
|
||||
<string name="show_age_restricted_content">show_age_restricted_content</string>
|
||||
<string name="use_tor_key">use_tor</string>
|
||||
</resources>
|
@ -74,6 +74,9 @@
|
||||
<string name="background_player_playing_toast">Playing in background</string>
|
||||
<string name="c3s_url" translatable="false">https://www.c3s.cc/</string>
|
||||
<string name="play_btn_text">Play</string>
|
||||
<string name="content">Content</string>
|
||||
<string name="show_age_restricted_content_title">Show age restricted content</string>
|
||||
<string name="video_is_age_restricted">Video is Age restricted. Enable age restricted videos in the settings first.</string>
|
||||
|
||||
<!-- error strings -->
|
||||
<string name="general_error">Error</string>
|
||||
|
@ -63,8 +63,8 @@
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/settings_category_other"
|
||||
android:title="@string/settings_category_other_title"
|
||||
android:key="@string/settings_content_options"
|
||||
android:title="@string/content"
|
||||
android:textAllCaps="true">
|
||||
|
||||
<ListPreference
|
||||
@ -75,6 +75,18 @@
|
||||
android:entryValues="@array/language_codes"
|
||||
android:defaultValue="@string/default_language_value" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="@string/show_age_restricted_content"
|
||||
android:title="@string/show_age_restricted_content_title"
|
||||
android:defaultValue="false"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/settings_category_other"
|
||||
android:title="@string/settings_category_other_title"
|
||||
android:textAllCaps="true">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="@string/download_path_key"
|
||||
android:title="@string/download_path_title"
|
||||
|
Loading…
Reference in New Issue
Block a user