Merge branch 'advancedErrorHandling'
This commit is contained in:
commit
3aecd15916
|
@ -42,4 +42,5 @@ dependencies {
|
|||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'org.apache.directory.studio:org.apache.commons.lang:2.6'
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package org.schabi.newpipe.extractor.youtube;
|
|||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.schabi.newpipe.extractor.SearchResult;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeSearchEngine;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
|
@ -30,7 +32,7 @@ import java.util.ArrayList;
|
|||
*/
|
||||
|
||||
public class YoutubeSearchEngineTest extends AndroidTestCase {
|
||||
private SearchEngine.Result result;
|
||||
private SearchResult result;
|
||||
private ArrayList<String> suggestionReply;
|
||||
|
||||
@Override
|
||||
|
@ -39,12 +41,13 @@ public class YoutubeSearchEngineTest extends AndroidTestCase {
|
|||
SearchEngine engine = new YoutubeSearchEngine();
|
||||
|
||||
result = engine.search("bla",
|
||||
0, "de", new Downloader());
|
||||
0, "de", new Downloader()).getSearchResult();
|
||||
suggestionReply = engine.suggestionList("hello","de",new Downloader());
|
||||
}
|
||||
|
||||
public void testIfNoErrorOccur() {
|
||||
assertEquals(result.errorMessage, "");
|
||||
assertTrue(result.errors.isEmpty() ? "" : ExceptionUtils.getStackTrace(result.errors.get(0))
|
||||
,result.errors.isEmpty());
|
||||
}
|
||||
|
||||
public void testIfListIsNotEmpty() {
|
||||
|
@ -52,44 +55,44 @@ public class YoutubeSearchEngineTest extends AndroidTestCase {
|
|||
}
|
||||
|
||||
public void testItemsHaveTitle() {
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
for(StreamPreviewInfo i : result.resultList) {
|
||||
assertEquals(i.title.isEmpty(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveUploader() {
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
for(StreamPreviewInfo i : result.resultList) {
|
||||
assertEquals(i.uploader.isEmpty(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveRightDuration() {
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
for(StreamPreviewInfo i : result.resultList) {
|
||||
assertTrue(i.duration, i.duration.contains(":"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveRightThumbnail() {
|
||||
for (VideoPreviewInfo i : result.resultList) {
|
||||
for (StreamPreviewInfo i : result.resultList) {
|
||||
assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testItemsHaveRightVideoUrl() {
|
||||
for (VideoPreviewInfo i : result.resultList) {
|
||||
for (StreamPreviewInfo i : result.resultList) {
|
||||
assertTrue(i.webpage_url, i.webpage_url.contains("https://"));
|
||||
}
|
||||
}
|
||||
|
||||
public void testViewCount() {
|
||||
/*
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
for(StreamPreviewInfo i : result.resultList) {
|
||||
assertTrue(Long.toString(i.view_count), i.view_count != -1);
|
||||
}
|
||||
*/
|
||||
// that specific link used for this test, there are no videos with less
|
||||
// than 10.000 views, so we can test against that.
|
||||
for(VideoPreviewInfo i : result.resultList) {
|
||||
for(StreamPreviewInfo i : result.resultList) {
|
||||
assertTrue(Long.toString(i.view_count), i.view_count >= 10000);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import org.schabi.newpipe.Downloader;
|
|||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.VideoInfo;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -94,7 +94,7 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase {
|
|||
}
|
||||
|
||||
public void testGetVideoStreams() throws ParsingException {
|
||||
for(VideoInfo.VideoStream s : extractor.getVideoStreams()) {
|
||||
for(StreamInfo.VideoStream s : extractor.getVideoStreams()) {
|
||||
assertTrue(s.url,
|
||||
s.url.contains("https://"));
|
||||
assertTrue(s.resolution.length() > 0);
|
||||
|
|
|
@ -5,7 +5,7 @@ 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.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -73,7 +73,7 @@ public class YoutubeStreamExtractorRestrictedTest extends AndroidTestCase {
|
|||
}
|
||||
|
||||
public void testGetVideoStreams() throws ParsingException {
|
||||
for(VideoInfo.VideoStream s : extractor.getVideoStreams()) {
|
||||
for(StreamInfo.VideoStream s : extractor.getVideoStreams()) {
|
||||
assertTrue(s.url,
|
||||
s.url.contains("https://"));
|
||||
assertTrue(s.resolution.length() > 0);
|
||||
|
|
|
@ -2,22 +2,24 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".VideoItemListActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="orientation|screenSize">
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -26,10 +28,10 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".VideoItemDetailActivity"
|
||||
android:label="@string/title_videoitem_detail"
|
||||
android:theme="@style/AppTheme"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:screenOrientation="portrait">
|
||||
android:label="@string/title_videoitem_detail"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".VideoItemListActivity" />
|
||||
|
@ -76,20 +78,21 @@
|
|||
<data android:scheme="vnd.youtube.launch" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".PlayVideoActivity"
|
||||
<activity
|
||||
android:name=".PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
android:parentActivityName=".VideoItemDetailActivity"
|
||||
tools:ignore="UnusedAttribute">
|
||||
</activity>
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
tools:ignore="UnusedAttribute"></activity>
|
||||
|
||||
<service
|
||||
android:name=".BackgroundPlayer"
|
||||
android:label="@string/background_player_name"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:label="@string/background_player_name" />
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/settings_activity_title" >
|
||||
</activity>
|
||||
android:label="@string/settings_activity_title"></activity>
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
|
@ -97,11 +100,15 @@
|
|||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".ErrorActivity"></activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -12,7 +12,7 @@ import android.view.MenuItem;
|
|||
import android.widget.ArrayAdapter;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.VideoInfo;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -75,7 +75,7 @@ class ActionBarHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public void setupStreamList(final List<VideoInfo.VideoStream> videoStreams) {
|
||||
public void setupStreamList(final List<StreamInfo.VideoStream> videoStreams) {
|
||||
if (activity != null) {
|
||||
selectedVideoStream = 0;
|
||||
|
||||
|
@ -83,7 +83,7 @@ class ActionBarHandler {
|
|||
// this array will be shown in the dropdown menu for selecting the stream/resolution.
|
||||
String[] itemArray = new String[videoStreams.size()];
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoInfo.VideoStream item = videoStreams.get(i);
|
||||
StreamInfo.VideoStream item = videoStreams.get(i);
|
||||
itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution;
|
||||
}
|
||||
int defaultResolution = getDefaultResolution(videoStreams);
|
||||
|
@ -108,13 +108,13 @@ class ActionBarHandler {
|
|||
}
|
||||
|
||||
|
||||
private int getDefaultResolution(final List<VideoInfo.VideoStream> videoStreams) {
|
||||
private int getDefaultResolution(final List<StreamInfo.VideoStream> videoStreams) {
|
||||
String defaultResolution = defaultPreferences
|
||||
.getString(activity.getString(R.string.default_resolution_key),
|
||||
activity.getString(R.string.default_resolution_value));
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
VideoInfo.VideoStream item = videoStreams.get(i);
|
||||
StreamInfo.VideoStream item = videoStreams.get(i);
|
||||
if (defaultResolution.equals(item.resolution)) {
|
||||
return i;
|
||||
}
|
||||
|
|
|
@ -22,10 +22,12 @@ package org.schabi.newpipe;
|
|||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Singleton:
|
||||
* Used to send data between certain Activity/Services within the same process.
|
||||
* This can be considered as hack inside the Android universe. **/
|
||||
* This can be considered as an ugly hack inside the Android universe. **/
|
||||
public class ActivityCommunicator {
|
||||
|
||||
private static ActivityCommunicator activityCommunicator = null;
|
||||
|
@ -39,4 +41,9 @@ public class ActivityCommunicator {
|
|||
|
||||
// Thumbnail send from VideoItemDetailFragment to BackgroundPlayer
|
||||
public volatile Bitmap backgroundPlayerThumbnail;
|
||||
|
||||
// Sent from any activity to ErrorActivity.
|
||||
public volatile List<Exception> errorList;
|
||||
public volatile Class returnActivity;
|
||||
public volatile ErrorActivity.ErrorInfo errorInfo;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ErrorActivity.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ErrorActivity extends AppCompatActivity {
|
||||
public static class ErrorInfo {
|
||||
public int userAction;
|
||||
public String request;
|
||||
public String serviceName;
|
||||
public int message;
|
||||
|
||||
public static ErrorInfo make(int userAction, String serviceName, String request, int message) {
|
||||
ErrorInfo info = new ErrorInfo();
|
||||
info.userAction = userAction;
|
||||
info.serviceName = serviceName;
|
||||
info.request = request;
|
||||
info.message = message;
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
public static final int SEARCHED = 0;
|
||||
public static final int REQUESTED_STREAM = 1;
|
||||
public static final int GET_SUGGESTIONS = 2;
|
||||
public static final String SEARCHED_STRING = "searched";
|
||||
public static final String REQUESTED_STREAM_STRING = "requested stream";
|
||||
public static final String GET_SUGGESTIONS_STRING = "get suggestions";
|
||||
|
||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME;
|
||||
|
||||
private List<Exception> errorList;
|
||||
private ErrorInfo errorInfo;
|
||||
private Class returnActivity;
|
||||
private String currentTimeStamp;
|
||||
private String globIpRange;
|
||||
Thread globIpRangeThread = null;
|
||||
|
||||
// views
|
||||
private TextView errorView;
|
||||
private EditText userCommentBox;
|
||||
private Button reportButton;
|
||||
private TextView infoView;
|
||||
private TextView errorMessageView;
|
||||
|
||||
public static void reportError(final Context context, final List<Exception> el,
|
||||
final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) {
|
||||
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(R.string.error_snackbar_action, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
ac.errorList = el;
|
||||
ac.returnActivity = returnAcitivty;
|
||||
ac.errorInfo = errorInfo;
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}).show();
|
||||
} else {
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
ac.errorList = el;
|
||||
ac.returnActivity = returnAcitivty;
|
||||
ac.errorInfo = errorInfo;
|
||||
Intent intent = new Intent(context, ErrorActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void reportError(final Context context, final Exception e,
|
||||
final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) {
|
||||
List<Exception> el = new Vector<>();
|
||||
el.add(e);
|
||||
reportError(context, el, returnAcitivty, rootView, errorInfo);
|
||||
}
|
||||
|
||||
// async call
|
||||
public static void reportError(Handler handler, final Context context, final Exception e,
|
||||
final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) {
|
||||
List<Exception> el = new Vector<>();
|
||||
el.add(e);
|
||||
reportError(handler, context, el, returnAcitivty, rootView, errorInfo);
|
||||
}
|
||||
|
||||
// async call
|
||||
public static void reportError(Handler handler, final Context context, final List<Exception> el,
|
||||
final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
reportError(context, el, returnAcitivty, rootView, errorInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_error);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
errorList = ac.errorList;
|
||||
returnActivity = ac.returnActivity;
|
||||
errorInfo = ac.errorInfo;
|
||||
|
||||
reportButton = (Button) findViewById(R.id.errorReportButton);
|
||||
userCommentBox = (EditText) findViewById(R.id.errorCommentBox);
|
||||
errorView = (TextView) findViewById(R.id.errorView);
|
||||
infoView = (TextView) findViewById(R.id.errorInfosView);
|
||||
errorMessageView = (TextView) findViewById(R.id.errorMessageView);
|
||||
|
||||
errorView.setText(formErrorText(errorList));
|
||||
|
||||
//importand add gurumeditaion
|
||||
addGuruMeditaion();
|
||||
currentTimeStamp = getCurrentTimeStamp();
|
||||
buildInfo(errorInfo);
|
||||
|
||||
reportButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_SENDTO);
|
||||
intent.setData(Uri.parse("mailto:" + ERROR_EMAIL_ADDRESS))
|
||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT)
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
|
||||
startActivity(Intent.createChooser(intent, "Send Email"));
|
||||
}
|
||||
});
|
||||
reportButton.setEnabled(false);
|
||||
|
||||
globIpRangeThread = new Thread(new IpRagneRequester());
|
||||
globIpRangeThread.start();
|
||||
|
||||
if(errorInfo.message != 0) {
|
||||
errorMessageView.setText(errorInfo.message);
|
||||
} else {
|
||||
errorMessageView.setVisibility(View.GONE);
|
||||
findViewById(R.id.messageWhatHappenedView).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.error_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
goToReturnActivity();
|
||||
break;
|
||||
case R.id.menu_item_share_error: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String formErrorText(List<Exception> el) {
|
||||
String text = "";
|
||||
for (Exception e : el) {
|
||||
text += "-------------------------------------\n"
|
||||
+ ExceptionUtils.getStackTrace(e);
|
||||
}
|
||||
text += "-------------------------------------";
|
||||
return text;
|
||||
}
|
||||
|
||||
private void goToReturnActivity() {
|
||||
if (returnActivity == null) {
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
Intent intent;
|
||||
if (returnActivity != null &&
|
||||
returnActivity.isAssignableFrom(Activity.class)) {
|
||||
intent = new Intent(this, returnActivity);
|
||||
} else {
|
||||
intent = new Intent(this, VideoItemListActivity.class);
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildInfo(ErrorInfo info) {
|
||||
TextView infoLabelView = (TextView) findViewById(R.id.errorInfoLabelsView);
|
||||
TextView infoView = (TextView) findViewById(R.id.errorInfosView);
|
||||
String text = "";
|
||||
|
||||
infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n"));
|
||||
|
||||
text += getUserActionString(info.userAction)
|
||||
+ "\n" + info.request
|
||||
+ "\n" + getContentLangString()
|
||||
+ "\n" + info.serviceName
|
||||
+ "\n" + currentTimeStamp
|
||||
+ "\n" + BuildConfig.VERSION_NAME
|
||||
+ "\n" + getOsString();
|
||||
|
||||
infoView.setText(text);
|
||||
}
|
||||
|
||||
private String buildJson() {
|
||||
JSONObject errorObject = new JSONObject();
|
||||
|
||||
try {
|
||||
errorObject.put("user_action", getUserActionString(errorInfo.userAction))
|
||||
.put("request", errorInfo.request)
|
||||
.put("content_language", getContentLangString())
|
||||
.put("service", errorInfo.serviceName)
|
||||
.put("version", BuildConfig.VERSION_NAME)
|
||||
.put("os", getOsString())
|
||||
.put("time", currentTimeStamp)
|
||||
.put("ip_range", globIpRange);
|
||||
|
||||
JSONArray exceptionArray = new JSONArray();
|
||||
for (Exception e : errorList) {
|
||||
exceptionArray.put(ExceptionUtils.getStackTrace(e));
|
||||
}
|
||||
|
||||
errorObject.put("exceptions", exceptionArray);
|
||||
errorObject.put("user_comment", userCommentBox.getText().toString());
|
||||
|
||||
return errorObject.toString(3);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error while erroring: Could not build json");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getUserActionString(int userAction) {
|
||||
switch (userAction) {
|
||||
case REQUESTED_STREAM:
|
||||
return REQUESTED_STREAM_STRING;
|
||||
case SEARCHED:
|
||||
return SEARCHED_STRING;
|
||||
case GET_SUGGESTIONS:
|
||||
return GET_SUGGESTIONS_STRING;
|
||||
default:
|
||||
return "Your description is in another castle.";
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentLangString() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString(this.getString(R.string.search_language_key), "none");
|
||||
}
|
||||
|
||||
private String getOsString() {
|
||||
String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android";
|
||||
return System.getProperty("os.name")
|
||||
+ " " + (osBase.isEmpty() ? "Android" : osBase)
|
||||
+ " " + Build.VERSION.RELEASE
|
||||
+ " - " + Integer.toString(Build.VERSION.SDK_INT);
|
||||
}
|
||||
|
||||
private void addGuruMeditaion() {
|
||||
//just an easter egg
|
||||
TextView sorryView = (TextView) findViewById(R.id.errorSorryView);
|
||||
String text = sorryView.getText().toString();
|
||||
text += "\n" + getString(R.string.guru_meditation);
|
||||
sorryView.setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
//super.onBackPressed();
|
||||
goToReturnActivity();
|
||||
}
|
||||
|
||||
public String getCurrentTimeStamp() {
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
df.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
private class IpRagneRequester implements Runnable {
|
||||
Handler h = new Handler();
|
||||
public void run() {
|
||||
String ipRange = "none";
|
||||
try {
|
||||
Downloader dl = new Downloader();
|
||||
String ip = dl.download("https://ifcfg.me/ip");
|
||||
|
||||
ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip)
|
||||
+ "0.0";
|
||||
} catch(Exception e) {
|
||||
Log.d(TAG, "Error while error: could not get iprange");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
h.post(new IpRageReturnRunnable(ipRange));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class IpRageReturnRunnable implements Runnable {
|
||||
String ipRange;
|
||||
public IpRageReturnRunnable(String ipRange) {
|
||||
this.ipRange = ipRange;
|
||||
}
|
||||
public void run() {
|
||||
globIpRange = ipRange;
|
||||
if(infoView != null) {
|
||||
String text = infoView.getText().toString();
|
||||
text += "\n" + globIpRange;
|
||||
infoView.setText(text);
|
||||
reportButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,8 @@ import android.view.ViewGroup;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
|
@ -40,8 +41,10 @@ class VideoInfoItemViewCreator {
|
|||
this.inflater = inflater;
|
||||
}
|
||||
|
||||
public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info, Context context) {
|
||||
public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, StreamPreviewInfo info) {
|
||||
ViewHolder holder;
|
||||
|
||||
// generate holder
|
||||
if(convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.video_item, parent, false);
|
||||
holder = new ViewHolder();
|
||||
|
@ -56,20 +59,41 @@ class VideoInfoItemViewCreator {
|
|||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
// fill with information
|
||||
|
||||
/*
|
||||
if(info.thumbnail == null) {
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail);
|
||||
} else {
|
||||
holder.itemThumbnailView.setImageBitmap(info.thumbnail);
|
||||
}
|
||||
*/
|
||||
holder.itemVideoTitleView.setText(info.title);
|
||||
if(info.uploader != null && !info.uploader.isEmpty()) {
|
||||
holder.itemUploaderView.setText(info.uploader);
|
||||
} else {
|
||||
holder.itemDurationView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if(info.duration != null && !info.duration.isEmpty()) {
|
||||
holder.itemDurationView.setText(info.duration);
|
||||
} else {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
if(info.view_count >= 0) {
|
||||
holder.itemViewCountView.setText(shortViewCount(info.view_count));
|
||||
} else {
|
||||
holder.itemViewCountView.setVisibility(View.GONE);
|
||||
}
|
||||
if(!info.upload_date.isEmpty()) {
|
||||
holder.itemUploadDateView.setText(info.upload_date+" • ");
|
||||
}
|
||||
|
||||
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
|
||||
imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions);
|
||||
} else {
|
||||
holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail);
|
||||
}
|
||||
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
|
|
@ -50,9 +50,9 @@ import org.schabi.newpipe.extractor.MediaFormat;
|
|||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.VideoInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
|
||||
|
||||
|
@ -127,12 +127,34 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
VideoInfo videoInfo = null;
|
||||
StreamInfo streamInfo = null;
|
||||
try {
|
||||
streamExtractor = service.getExtractorInstance(videoUrl, new Downloader());
|
||||
videoInfo = VideoInfo.getVideoInfo(streamExtractor, new Downloader());
|
||||
streamInfo = StreamInfo.getVideoInfo(streamExtractor, new Downloader());
|
||||
|
||||
h.post(new VideoResultReturnedRunnable(videoInfo));
|
||||
h.post(new VideoResultReturnedRunnable(streamInfo));
|
||||
|
||||
// look for errors during extraction
|
||||
// this if statement only covers extra information.
|
||||
// if these are not available or caused an error, they are just not available
|
||||
// but don't render the stream information unusalbe.
|
||||
if(streamInfo != null &&
|
||||
!streamInfo.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for (Exception e : streamInfo.errors) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
Activity a = getActivity();
|
||||
View rootView = a != null ? a.findViewById(R.id.videoitem_detail) : null;
|
||||
ErrorActivity.reportError(h, getActivity(),
|
||||
streamInfo.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, 0 /* no message for the user */));
|
||||
}
|
||||
|
||||
// These errors render the stream information unusable.
|
||||
} catch (IOException e) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
e.printStackTrace();
|
||||
|
@ -165,40 +187,69 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(StreamInfo.StreamExctractException e) {
|
||||
if(!streamInfo.errors.isEmpty()) {
|
||||
// !!! if this case ever kicks in someone gets kicked out !!!
|
||||
ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
} else {
|
||||
ErrorActivity.reportError(h, getActivity(), streamInfo.errors, VideoItemListActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream));
|
||||
}
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch (ParsingException e) {
|
||||
postNewErrorToast(h, e.getMessage());
|
||||
ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.parsing_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
} catch(Exception e) {
|
||||
postNewErrorToast(h, R.string.general_error);
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if(videoInfo != null &&
|
||||
!videoInfo.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:");
|
||||
for(Exception e : videoInfo.errors) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM,
|
||||
service.getServiceInfo().name, videoUrl, R.string.general_error));
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class VideoResultReturnedRunnable implements Runnable {
|
||||
private final VideoInfo videoInfo;
|
||||
public VideoResultReturnedRunnable(VideoInfo videoInfo) {
|
||||
this.videoInfo = videoInfo;
|
||||
private final StreamInfo streamInfo;
|
||||
public VideoResultReturnedRunnable(StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
boolean show_age_restricted_content = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
Activity a = getActivity();
|
||||
if(a != null) {
|
||||
boolean show_age_restricted_content = PreferenceManager.getDefaultSharedPreferences(a)
|
||||
.getBoolean(activity.getString(R.string.show_age_restricted_content), false);
|
||||
if(videoInfo.age_limit == 0 || show_age_restricted_content) {
|
||||
updateInfo(videoInfo);
|
||||
if (streamInfo.age_limit == 0 || show_age_restricted_content) {
|
||||
updateInfo(streamInfo);
|
||||
} else {
|
||||
onNotSpecifiedContentErrorWithMessage(R.string.video_is_age_restricted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ThumbnailLoadingListener implements ImageLoadingListener {
|
||||
@Override
|
||||
|
@ -220,7 +271,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
public void onLoadingCancelled(String imageUri, View view) {}
|
||||
}
|
||||
|
||||
private void updateInfo(final VideoInfo info) {
|
||||
private void updateInfo(final StreamInfo info) {
|
||||
try {
|
||||
Context c = getContext();
|
||||
VideoInfoItemViewCreator videoItemViewCreator =
|
||||
|
@ -249,7 +300,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
View nextVideoView = null;
|
||||
if(info.next_video != null) {
|
||||
nextVideoView = videoItemViewCreator
|
||||
.getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video, getContext());
|
||||
.getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video);
|
||||
} else {
|
||||
activity.findViewById(R.id.detailNextVidButtonAndContentLayout).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.detailNextVideoTitle).setVisibility(View.GONE);
|
||||
|
@ -337,8 +388,8 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
// parse streams
|
||||
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
|
||||
for (VideoInfo.VideoStream i : info.video_streams) {
|
||||
Vector<StreamInfo.VideoStream> streamsToUse = new Vector<>();
|
||||
for (StreamInfo.VideoStream i : info.video_streams) {
|
||||
if (useStream(i, streamsToUse)) {
|
||||
streamsToUse.add(i);
|
||||
}
|
||||
|
@ -394,7 +445,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void initThumbnailViews(VideoInfo info, View nextVideoFrame) {
|
||||
private void initThumbnailViews(StreamInfo info, View nextVideoFrame) {
|
||||
ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView);
|
||||
ImageView uploaderThumb
|
||||
= (ImageView) activity.findViewById(R.id.detailUploaderThumbnailView);
|
||||
|
@ -437,7 +488,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void setupActionBarHandler(final VideoInfo info) {
|
||||
private void setupActionBarHandler(final StreamInfo info) {
|
||||
actionBarHandler.setupStreamList(info.video_streams);
|
||||
|
||||
actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() {
|
||||
|
@ -504,7 +555,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
// website which was crawled. Then the ui has to understand this and act right.
|
||||
|
||||
if (info.audio_streams != null) {
|
||||
VideoInfo.AudioStream audioStream =
|
||||
StreamInfo.AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
|
||||
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
|
||||
|
@ -513,7 +564,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
|
||||
if (info.video_streams != null) {
|
||||
VideoInfo.VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
|
||||
StreamInfo.VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
|
||||
String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format);
|
||||
args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix);
|
||||
args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url);
|
||||
|
@ -540,7 +591,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
Intent intent;
|
||||
VideoInfo.AudioStream audioStream =
|
||||
StreamInfo.AudioStream audioStream =
|
||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||
//internal music player: explicit intent
|
||||
|
@ -599,7 +650,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
private int getPreferredAudioStreamId(final VideoInfo info) {
|
||||
private int getPreferredAudioStreamId(final StreamInfo info) {
|
||||
String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getString(activity.getString(R.string.default_audio_format_key), "webm");
|
||||
|
||||
|
@ -626,12 +677,12 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private void initSimilarVideos(final VideoInfo info, VideoInfoItemViewCreator videoItemViewCreator) {
|
||||
private void initSimilarVideos(final StreamInfo info, VideoInfoItemViewCreator videoItemViewCreator) {
|
||||
LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similarVideosView);
|
||||
ArrayList<VideoPreviewInfo> similar = new ArrayList<>(info.related_videos);
|
||||
for (final VideoPreviewInfo item : similar) {
|
||||
ArrayList<StreamPreviewInfo> similar = new ArrayList<>(info.related_videos);
|
||||
for (final StreamPreviewInfo item : similar) {
|
||||
View similarView = videoItemViewCreator
|
||||
.getViewFromVideoInfoItem(null, similarLayout, item, getContext());
|
||||
.getViewFromVideoInfoItem(null, similarLayout, item);
|
||||
|
||||
similarView.setClickable(true);
|
||||
similarView.setFocusable(true);
|
||||
|
@ -703,8 +754,8 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
.show();
|
||||
}
|
||||
|
||||
private boolean useStream(VideoInfo.VideoStream stream, Vector<VideoInfo.VideoStream> streams) {
|
||||
for(VideoInfo.VideoStream i : streams) {
|
||||
private boolean useStream(StreamInfo.VideoStream stream, Vector<StreamInfo.VideoStream> streams) {
|
||||
for(StreamInfo.VideoStream i : streams) {
|
||||
if(i.resolution.equals(stream.resolution)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -794,9 +845,9 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
public void playVideo(final VideoInfo info) {
|
||||
public void playVideo(final StreamInfo info) {
|
||||
// ----------- THE MAGIC MOMENT ---------------
|
||||
VideoInfo.VideoStream selectedVideoStream =
|
||||
StreamInfo.VideoStream selectedVideoStream =
|
||||
info.video_streams.get(actionBarHandler.getSelectedVideoStream());
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.schabi.newpipe.extractor.ExtractionException;
|
|||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -173,11 +172,17 @@ public class VideoItemListActivity extends AppCompatActivity
|
|||
ArrayList<String>suggestions = engine.suggestionList(query,searchLanguage,new Downloader());
|
||||
h.post(new SuggestionResultRunnable(suggestions));
|
||||
} catch (ExtractionException e) {
|
||||
postNewErrorToast(h, R.string.parsing_error);
|
||||
ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ "Youtube", query, R.string.parsing_error));
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list),
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ "Youtube", query, R.string.general_error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
@ -17,8 +18,8 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.SearchResult;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
|
||||
|
@ -68,9 +69,9 @@ public class VideoItemListFragment extends ListFragment {
|
|||
private boolean loadingNextPage = true;
|
||||
|
||||
private class ResultRunnable implements Runnable {
|
||||
private final SearchEngine.Result result;
|
||||
private final SearchResult result;
|
||||
private final int requestId;
|
||||
public ResultRunnable(SearchEngine.Result result, int requestId) {
|
||||
public ResultRunnable(SearchResult result, int requestId) {
|
||||
this.result = result;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
@ -101,32 +102,60 @@ public class VideoItemListFragment extends ListFragment {
|
|||
}
|
||||
@Override
|
||||
public void run() {
|
||||
SearchResult result = null;
|
||||
try {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
String searchLanguageKey = getContext().getString(R.string.search_language_key);
|
||||
String searchLanguage = sp.getString(searchLanguageKey,
|
||||
getString(R.string.default_language_value));
|
||||
SearchEngine.Result result = engine.search(query, page, searchLanguage,
|
||||
new Downloader());
|
||||
result = SearchResult
|
||||
.getSearchResult(engine, query, page, searchLanguage, new Downloader());
|
||||
|
||||
//Log.i(TAG, "language code passed:\""+searchLanguage+"\"");
|
||||
if(runs) {
|
||||
h.post(new ResultRunnable(result, requestId));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
postNewErrorToast(h, R.string.network_error);
|
||||
|
||||
// look for errors during extraction
|
||||
// soft errors:
|
||||
if(result != null &&
|
||||
!result.errors.isEmpty()) {
|
||||
Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:");
|
||||
for(Exception e : result.errors) {
|
||||
e.printStackTrace();
|
||||
} catch(ExtractionException ce) {
|
||||
postNewErrorToast(h, R.string.parsing_error);
|
||||
ce.printStackTrace();
|
||||
Log.e(TAG, "------");
|
||||
}
|
||||
|
||||
Activity a = getActivity();
|
||||
View rootView = a.findViewById(R.id.videoitem_list);
|
||||
ErrorActivity.reportError(h, getActivity(), result.errors, null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ "Youtube", query, R.string.light_parsing_error));
|
||||
|
||||
}
|
||||
// hard errors:
|
||||
} catch(IOException e) {
|
||||
postNewNothingFoundToast(h, R.string.network_error);
|
||||
e.printStackTrace();
|
||||
} catch(SearchEngine.NothingFoundException e) {
|
||||
postNewErrorToast(h, e.getMessage());
|
||||
} catch(ExtractionException e) {
|
||||
ErrorActivity.reportError(h, getActivity(), e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ "Youtube", query, R.string.parsing_error));
|
||||
//postNewErrorToast(h, R.string.parsing_error);
|
||||
e.printStackTrace();
|
||||
|
||||
} catch(Exception e) {
|
||||
postNewErrorToast(h, R.string.general_error);
|
||||
ErrorActivity.reportError(h, getActivity(), e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
|
||||
/* todo: this shoudl not be assigned static */ "Youtube", query, R.string.general_error));
|
||||
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void present(List<VideoPreviewInfo> videoList) {
|
||||
public void present(List<StreamPreviewInfo> videoList) {
|
||||
mode = PRESENT_VIDEOS_MODE;
|
||||
setListShown(true);
|
||||
getListView().smoothScrollToPosition(0);
|
||||
|
@ -166,22 +195,19 @@ public class VideoItemListFragment extends ListFragment {
|
|||
this.streamingService = streamingService;
|
||||
}
|
||||
|
||||
private void updateListOnResult(SearchEngine.Result result, int requestId) {
|
||||
private void updateListOnResult(SearchResult result, int requestId) {
|
||||
if(requestId == currentRequestId) {
|
||||
setListShown(true);
|
||||
if (result.resultList.isEmpty()) {
|
||||
Toast.makeText(getActivity(), result.errorMessage, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
if (!result.suggestion.isEmpty()) {
|
||||
Toast.makeText(getActivity(), getString(R.string.did_you_mean) + result.suggestion + " ?",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
updateList(result.resultList);
|
||||
if(!result.suggestion.isEmpty()) {
|
||||
Toast.makeText(getActivity(),
|
||||
String.format(getString(R.string.did_you_mean), result.suggestion),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateList(List<VideoPreviewInfo> list) {
|
||||
private void updateList(List<StreamPreviewInfo> list) {
|
||||
try {
|
||||
videoListAdapter.addVideoList(list);
|
||||
terminateThreads();
|
||||
|
@ -318,13 +344,24 @@ public class VideoItemListFragment extends ListFragment {
|
|||
mActivatedPosition = position;
|
||||
}
|
||||
|
||||
private void postNewErrorToast(Handler h, final int stringResource) {
|
||||
private void postNewErrorToast(Handler h, final String message) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setListShown(true);
|
||||
Toast.makeText(getActivity(), message,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void postNewNothingFoundToast(Handler h, final int stringResource) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setListShown(true);
|
||||
Toast.makeText(getActivity(), getString(stringResource),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import android.view.ViewGroup;
|
|||
import android.widget.BaseAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
@ -36,7 +36,7 @@ import java.util.Vector;
|
|||
class VideoListAdapter extends BaseAdapter {
|
||||
private final Context context;
|
||||
private final VideoInfoItemViewCreator viewCreator;
|
||||
private Vector<VideoPreviewInfo> videoList = new Vector<>();
|
||||
private Vector<StreamPreviewInfo> videoList = new Vector<>();
|
||||
private final ListView listView;
|
||||
|
||||
public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) {
|
||||
|
@ -47,7 +47,7 @@ class VideoListAdapter extends BaseAdapter {
|
|||
this.context = context;
|
||||
}
|
||||
|
||||
public void addVideoList(List<VideoPreviewInfo> videos) {
|
||||
public void addVideoList(List<StreamPreviewInfo> videos) {
|
||||
videoList.addAll(videos);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ class VideoListAdapter extends BaseAdapter {
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public Vector<VideoPreviewInfo> getVideoList() {
|
||||
public Vector<StreamPreviewInfo> getVideoList() {
|
||||
return videoList;
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class VideoListAdapter extends BaseAdapter {
|
|||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position), context);
|
||||
convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position));
|
||||
|
||||
if(listView.isItemChecked(position)) {
|
||||
convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color));
|
||||
|
|
|
@ -20,7 +20,7 @@ import android.graphics.Bitmap;
|
|||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**Common properties between VideoInfo and VideoPreviewInfo.*/
|
||||
/**Common properties between StreamInfo and StreamPreviewInfo.*/
|
||||
public abstract class AbstractVideoInfo {
|
||||
public String id = "";
|
||||
public String title = "";
|
||||
|
|
|
@ -37,7 +37,7 @@ public class DashMpdParser {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<VideoInfo.AudioStream> getAudioStreams(String dashManifestUrl,
|
||||
public static List<StreamInfo.AudioStream> getAudioStreams(String dashManifestUrl,
|
||||
Downloader downloader)
|
||||
throws DashMpdParsingException {
|
||||
String dashDoc;
|
||||
|
@ -46,7 +46,7 @@ public class DashMpdParser {
|
|||
} catch(IOException ioe) {
|
||||
throw new DashMpdParsingException("Could not get dash mpd: " + dashManifestUrl, ioe);
|
||||
}
|
||||
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
|
||||
Vector<StreamInfo.AudioStream> audioStreams = new Vector<>();
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setInput(new StringReader(dashDoc));
|
||||
|
@ -83,7 +83,7 @@ public class DashMpdParser {
|
|||
} else if(currentMimeType.equals(MediaFormat.M4A.mimeType)) {
|
||||
format = MediaFormat.M4A.id;
|
||||
}
|
||||
audioStreams.add(new VideoInfo.AudioStream(parser.getText(),
|
||||
audioStreams.add(new StreamInfo.AudioStream(parser.getText(),
|
||||
format, currentBandwidth, currentSamplingRate));
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -21,8 +21,6 @@ package org.schabi.newpipe.extractor;
|
|||
*/
|
||||
|
||||
public class ExtractionException extends Exception {
|
||||
public ExtractionException() {}
|
||||
|
||||
public ExtractionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
|
|
@ -22,13 +22,9 @@ package org.schabi.newpipe.extractor;
|
|||
|
||||
|
||||
public class ParsingException extends ExtractionException {
|
||||
public ParsingException() {}
|
||||
public ParsingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public ParsingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
public ParsingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
|
|
@ -27,16 +27,16 @@ import java.util.Vector;
|
|||
|
||||
@SuppressWarnings("ALL")
|
||||
public interface SearchEngine {
|
||||
class Result {
|
||||
public String errorMessage = "";
|
||||
public String suggestion = "";
|
||||
public final List<VideoPreviewInfo> resultList = new Vector<>();
|
||||
public class NothingFoundException extends ExtractionException {
|
||||
public NothingFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> suggestionList(String query,String contentCountry, Downloader dl)
|
||||
throws ExtractionException, IOException;
|
||||
|
||||
//Result search(String query, int page);
|
||||
Result search(String query, int page, String contentCountry, Downloader dl)
|
||||
StreamPreviewInfoCollector search(String query, int page, String contentCountry, Downloader dl)
|
||||
throws ExtractionException, IOException;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 29.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* SearchResult.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SearchResult {
|
||||
public static SearchResult getSearchResult(SearchEngine engine, String query,
|
||||
int page, String languageCode, Downloader dl)
|
||||
throws ExtractionException, IOException {
|
||||
|
||||
SearchResult result = engine.search(query, page, languageCode, dl).getSearchResult();
|
||||
if(result.resultList.isEmpty()) {
|
||||
if(result.suggestion.isEmpty()) {
|
||||
throw new ExtractionException("Empty result despite no error");
|
||||
} else {
|
||||
// This is used as a fallback. Do not relay on it !!!
|
||||
throw new SearchEngine.NothingFoundException(result.suggestion);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String suggestion = "";
|
||||
public final List<StreamPreviewInfo> resultList = new Vector<>();
|
||||
public List<Exception> errors = new Vector<>();
|
||||
}
|
|
@ -3,7 +3,7 @@ package org.schabi.newpipe.extractor;
|
|||
/**
|
||||
* Created by Christian Schabesberger on 10.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
|
@ -29,7 +29,6 @@ import java.util.List;
|
|||
public interface StreamExtractor {
|
||||
|
||||
public class ExctractorInitException extends ExtractionException {
|
||||
public ExctractorInitException() {}
|
||||
public ExctractorInitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
@ -42,13 +41,9 @@ public interface StreamExtractor {
|
|||
}
|
||||
|
||||
public class ContentNotAvailableException extends ParsingException {
|
||||
public ContentNotAvailableException() {}
|
||||
public ContentNotAvailableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public ContentNotAvailableException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
public ContentNotAvailableException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
@ -63,16 +58,16 @@ public interface StreamExtractor {
|
|||
public abstract String getUploadDate() throws ParsingException;
|
||||
public abstract String getThumbnailUrl() throws ParsingException;
|
||||
public abstract String getUploaderThumbnailUrl() throws ParsingException;
|
||||
public abstract List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException;
|
||||
public abstract List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException;
|
||||
public abstract List<VideoInfo.VideoStream> getVideoOnlyStreams() throws ParsingException;
|
||||
public abstract List<StreamInfo.AudioStream> getAudioStreams() throws ParsingException;
|
||||
public abstract List<StreamInfo.VideoStream> getVideoStreams() throws ParsingException;
|
||||
public abstract List<StreamInfo.VideoStream> getVideoOnlyStreams() throws ParsingException;
|
||||
public abstract String getDashMpdUrl() throws ParsingException;
|
||||
public abstract int getAgeLimit() throws ParsingException;
|
||||
public abstract String getAverageRating() throws ParsingException;
|
||||
public abstract int getLikeCount() throws ParsingException;
|
||||
public abstract int getDislikeCount() throws ParsingException;
|
||||
public abstract VideoPreviewInfo getNextVideo() throws ParsingException;
|
||||
public abstract List<VideoPreviewInfo> getRelatedVideos() throws ParsingException;
|
||||
public abstract VideoUrlIdHandler getUrlIdConverter();
|
||||
public abstract StreamPreviewInfo getNextVideo() throws ParsingException;
|
||||
public abstract List<StreamPreviewInfo> getRelatedVideos() throws ParsingException;
|
||||
public abstract StreamUrlIdHandler getUrlIdConverter();
|
||||
public abstract String getPageUrl();
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import java.util.Vector;
|
|||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoInfo.java is part of NewPipe.
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -26,181 +26,188 @@ import java.util.Vector;
|
|||
|
||||
/**Info object for opened videos, ie the video ready to play.*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class VideoInfo extends AbstractVideoInfo {
|
||||
public class StreamInfo extends AbstractVideoInfo {
|
||||
|
||||
public static class StreamExctractException extends ExtractionException {
|
||||
StreamExctractException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**Fills out the video info fields which are common to all services.
|
||||
* Probably needs to be overridden by subclasses*/
|
||||
public static VideoInfo getVideoInfo(StreamExtractor extractor, Downloader downloader)
|
||||
public static StreamInfo getVideoInfo(StreamExtractor extractor, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
VideoInfo videoInfo = new VideoInfo();
|
||||
StreamInfo streamInfo = new StreamInfo();
|
||||
|
||||
videoInfo = extractImportantData(videoInfo, extractor, downloader);
|
||||
videoInfo = extractStreams(videoInfo, extractor, downloader);
|
||||
videoInfo = extractOptionalData(videoInfo, extractor, downloader);
|
||||
streamInfo = extractImportantData(streamInfo, extractor, downloader);
|
||||
streamInfo = extractStreams(streamInfo, extractor, downloader);
|
||||
streamInfo = extractOptionalData(streamInfo, extractor, downloader);
|
||||
|
||||
return videoInfo;
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
private static VideoInfo extractImportantData(
|
||||
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader)
|
||||
private static StreamInfo extractImportantData(
|
||||
StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
/* ---- importand data, withoug the video can't be displayed goes here: ---- */
|
||||
// if one of these is not available an exception is ment to be thrown directly into the frontend.
|
||||
|
||||
VideoUrlIdHandler uiconv = extractor.getUrlIdConverter();
|
||||
StreamUrlIdHandler uiconv = extractor.getUrlIdConverter();
|
||||
|
||||
videoInfo.webpage_url = extractor.getPageUrl();
|
||||
videoInfo.id = uiconv.getVideoId(extractor.getPageUrl());
|
||||
videoInfo.title = extractor.getTitle();
|
||||
videoInfo.age_limit = extractor.getAgeLimit();
|
||||
streamInfo.webpage_url = extractor.getPageUrl();
|
||||
streamInfo.id = uiconv.getVideoId(extractor.getPageUrl());
|
||||
streamInfo.title = extractor.getTitle();
|
||||
streamInfo.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.age_limit == -1));
|
||||
if((streamInfo.webpage_url == null || streamInfo.webpage_url.isEmpty())
|
||||
|| (streamInfo.id == null || streamInfo.id.isEmpty())
|
||||
|| (streamInfo.title == null /* streamInfo.title can be empty of course */)
|
||||
|| (streamInfo.age_limit == -1));
|
||||
|
||||
return videoInfo;
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
private static VideoInfo extractStreams(
|
||||
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader)
|
||||
private static StreamInfo extractStreams(
|
||||
StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
/* ---- stream extraction goes here ---- */
|
||||
// At least one type of stream has to be available,
|
||||
// otherwise an exception will be thrown directly into the frontend.
|
||||
|
||||
try {
|
||||
videoInfo.dashMpdUrl = extractor.getDashMpdUrl();
|
||||
streamInfo.dashMpdUrl = extractor.getDashMpdUrl();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(new ExtractionException("Couldn't get Dash manifest", e));
|
||||
streamInfo.addException(new ExtractionException("Couldn't get Dash manifest", e));
|
||||
}
|
||||
|
||||
/* Load and extract audio */
|
||||
try {
|
||||
videoInfo.audio_streams = extractor.getAudioStreams();
|
||||
streamInfo.audio_streams = extractor.getAudioStreams();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(new ExtractionException("Couldn't get audio streams", e));
|
||||
streamInfo.addException(new ExtractionException("Couldn't get audio streams", e));
|
||||
}
|
||||
// also try to get streams from the dashMpd
|
||||
if(videoInfo.dashMpdUrl != null && !videoInfo.dashMpdUrl.isEmpty()) {
|
||||
if(videoInfo.audio_streams == null) {
|
||||
videoInfo.audio_streams = new Vector<AudioStream>();
|
||||
if(streamInfo.dashMpdUrl != null && !streamInfo.dashMpdUrl.isEmpty()) {
|
||||
if(streamInfo.audio_streams == null) {
|
||||
streamInfo.audio_streams = new Vector<AudioStream>();
|
||||
}
|
||||
//todo: make this quick and dirty solution a real fallback
|
||||
// same as the quick and dirty aboth
|
||||
try {
|
||||
videoInfo.audio_streams.addAll(
|
||||
DashMpdParser.getAudioStreams(videoInfo.dashMpdUrl, downloader));
|
||||
streamInfo.audio_streams.addAll(
|
||||
DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl, downloader));
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(
|
||||
streamInfo.addException(
|
||||
new ExtractionException("Couldn't get audio streams from dash mpd", e));
|
||||
}
|
||||
}
|
||||
/* Extract video stream url*/
|
||||
try {
|
||||
videoInfo.video_streams = extractor.getVideoStreams();
|
||||
streamInfo.video_streams = extractor.getVideoStreams();
|
||||
} catch (Exception e) {
|
||||
videoInfo.addException(
|
||||
streamInfo.addException(
|
||||
new ExtractionException("Couldn't get video streams", e));
|
||||
}
|
||||
/* Extract video only stream url*/
|
||||
try {
|
||||
videoInfo.video_only_streams = extractor.getVideoOnlyStreams();
|
||||
streamInfo.video_only_streams = extractor.getVideoOnlyStreams();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(
|
||||
streamInfo.addException(
|
||||
new ExtractionException("Couldn't get video only streams", e));
|
||||
}
|
||||
|
||||
// either dash_mpd audio_only or video has to be available, otherwise we didn't get a stream,
|
||||
// and therefore failed. (Since video_only_streams are just optional they don't caunt).
|
||||
if((videoInfo.video_streams == null || videoInfo.video_streams.isEmpty())
|
||||
&& (videoInfo.audio_streams == null || videoInfo.audio_streams.isEmpty())
|
||||
&& (videoInfo.dashMpdUrl == null || videoInfo.dashMpdUrl.isEmpty())) {
|
||||
throw new ExtractionException("Could not get any stream. See error variable to get further details.");
|
||||
if((streamInfo.video_streams == null || streamInfo.video_streams.isEmpty())
|
||||
&& (streamInfo.audio_streams == null || streamInfo.audio_streams.isEmpty())
|
||||
&& (streamInfo.dashMpdUrl == null || streamInfo.dashMpdUrl.isEmpty())) {
|
||||
throw new StreamExctractException(
|
||||
"Could not get any stream. See error variable to get further details.");
|
||||
}
|
||||
|
||||
return videoInfo;
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
private static VideoInfo extractOptionalData(
|
||||
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader) {
|
||||
private static StreamInfo extractOptionalData(
|
||||
StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) {
|
||||
/* ---- optional data goes here: ---- */
|
||||
// If one of these failes, the frontend neets to handle that they are not available.
|
||||
// Exceptions are therfore not thrown into the frontend, but stored into the error List,
|
||||
// so the frontend can afterwads check where errors happend.
|
||||
|
||||
try {
|
||||
videoInfo.thumbnail_url = extractor.getThumbnailUrl();
|
||||
streamInfo.thumbnail_url = extractor.getThumbnailUrl();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.duration = extractor.getLength();
|
||||
streamInfo.duration = extractor.getLength();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.uploader = extractor.getUploader();
|
||||
streamInfo.uploader = extractor.getUploader();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.description = extractor.getDescription();
|
||||
streamInfo.description = extractor.getDescription();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.view_count = extractor.getViewCount();
|
||||
streamInfo.view_count = extractor.getViewCount();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.upload_date = extractor.getUploadDate();
|
||||
streamInfo.upload_date = extractor.getUploadDate();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl();
|
||||
streamInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.start_position = extractor.getTimeStamp();
|
||||
streamInfo.start_position = extractor.getTimeStamp();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.average_rating = extractor.getAverageRating();
|
||||
streamInfo.average_rating = extractor.getAverageRating();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.like_count = extractor.getLikeCount();
|
||||
streamInfo.like_count = extractor.getLikeCount();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.dislike_count = extractor.getDislikeCount();
|
||||
streamInfo.dislike_count = extractor.getDislikeCount();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.next_video = extractor.getNextVideo();
|
||||
streamInfo.next_video = extractor.getNextVideo();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
videoInfo.related_videos = extractor.getRelatedVideos();
|
||||
streamInfo.related_videos = extractor.getRelatedVideos();
|
||||
} catch(Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
try {
|
||||
|
||||
} catch (Exception e) {
|
||||
videoInfo.addException(e);
|
||||
streamInfo.addException(e);
|
||||
}
|
||||
|
||||
return videoInfo;
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
public String uploader_thumbnail_url = "";
|
||||
|
@ -220,20 +227,20 @@ public class VideoInfo extends AbstractVideoInfo {
|
|||
public int like_count = -1;
|
||||
public int dislike_count = -1;
|
||||
public String average_rating = "";
|
||||
public VideoPreviewInfo next_video = null;
|
||||
public List<VideoPreviewInfo> related_videos = null;
|
||||
//in seconds. some metadata is not passed using a VideoInfo object!
|
||||
public StreamPreviewInfo next_video = null;
|
||||
public List<StreamPreviewInfo> related_videos = null;
|
||||
//in seconds. some metadata is not passed using a StreamInfo object!
|
||||
public int start_position = 0;
|
||||
//todo: public int service_id = -1;
|
||||
|
||||
public List<Exception> errors = new Vector<>();
|
||||
|
||||
public VideoInfo() {}
|
||||
public StreamInfo() {}
|
||||
|
||||
/**Creates a new VideoInfo object from an existing AbstractVideoInfo.
|
||||
* All the shared properties are copied to the new VideoInfo.*/
|
||||
/**Creates a new StreamInfo object from an existing AbstractVideoInfo.
|
||||
* All the shared properties are copied to the new StreamInfo.*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public VideoInfo(AbstractVideoInfo avi) {
|
||||
public StreamInfo(AbstractVideoInfo avi) {
|
||||
this.id = avi.id;
|
||||
this.title = avi.title;
|
||||
this.uploader = avi.uploader;
|
||||
|
@ -245,9 +252,9 @@ public class VideoInfo extends AbstractVideoInfo {
|
|||
this.view_count = avi.view_count;
|
||||
|
||||
//todo: better than this
|
||||
if(avi instanceof VideoPreviewInfo) {
|
||||
if(avi instanceof StreamPreviewInfo) {
|
||||
//shitty String to convert code
|
||||
String dur = ((VideoPreviewInfo)avi).duration;
|
||||
String dur = ((StreamPreviewInfo)avi).duration;
|
||||
int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":")));
|
||||
int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length()));
|
||||
this.duration = (minutes*60)+seconds;
|
|
@ -1,14 +1,10 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 26.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* VideoPreviewInfo.java is part of NewPipe.
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,6 +21,6 @@ import android.os.Parcelable;
|
|||
*/
|
||||
|
||||
/**Info object for previews of unopened videos, eg search results, related videos*/
|
||||
public class VideoPreviewInfo extends AbstractVideoInfo {
|
||||
public class StreamPreviewInfo extends AbstractVideoInfo {
|
||||
public String duration = "";
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamUrlIdHandler;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfoCollector.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class StreamPreviewInfoCollector {
|
||||
SearchResult result = new SearchResult();
|
||||
StreamUrlIdHandler urlIdHandler = null;
|
||||
|
||||
public StreamPreviewInfoCollector(StreamUrlIdHandler handler) {
|
||||
urlIdHandler = handler;
|
||||
}
|
||||
|
||||
public void setSuggestion(String suggestion) {
|
||||
result.suggestion = suggestion;
|
||||
}
|
||||
|
||||
public void addError(Exception e) {
|
||||
result.errors.add(e);
|
||||
}
|
||||
|
||||
public SearchResult getSearchResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void commit(StreamPreviewInfoExtractor extractor) throws ParsingException {
|
||||
try {
|
||||
StreamPreviewInfo resultItem = new StreamPreviewInfo();
|
||||
// importand information
|
||||
resultItem.webpage_url = extractor.getWebPageUrl();
|
||||
if (urlIdHandler == null) {
|
||||
throw new ParsingException("Error: UrlIdHandler not set");
|
||||
} else {
|
||||
resultItem.id = (new YoutubeStreamUrlIdHandler()).getVideoId(resultItem.webpage_url);
|
||||
}
|
||||
resultItem.title = extractor.getTitle();
|
||||
|
||||
// optional iformation
|
||||
try {
|
||||
resultItem.duration = extractor.getDuration();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.uploader = extractor.getUploader();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.upload_date = extractor.getUploadDate();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.view_count = extractor.getViewCount();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.thumbnail_url = extractor.getThumbnailUrl();
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
|
||||
result.resultList.add(resultItem);
|
||||
} catch (Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamPreviewInfoExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public interface StreamPreviewInfoExtractor {
|
||||
String getWebPageUrl() throws ParsingException;
|
||||
String getTitle() throws ParsingException;
|
||||
String getDuration() throws ParsingException;
|
||||
String getUploader() throws ParsingException;
|
||||
String getUploadDate() throws ParsingException;
|
||||
long getViewCount() throws ParsingException;
|
||||
String getThumbnailUrl() throws ParsingException;
|
||||
}
|
|
@ -4,7 +4,7 @@ package org.schabi.newpipe.extractor;
|
|||
* Created by Christian Schabesberger on 02.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* VideoUrlIdHandler.java is part of NewPipe.
|
||||
* StreamUrlIdHandler.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,7 +20,7 @@ package org.schabi.newpipe.extractor;
|
|||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public interface VideoUrlIdHandler {
|
||||
public interface StreamUrlIdHandler {
|
||||
String getVideoUrl(String videoId);
|
||||
String getVideoId(String siteUrl) throws ParsingException;
|
||||
String cleanUrl(String siteUrl) throws ParsingException;
|
|
@ -5,7 +5,7 @@ import java.io.IOException;
|
|||
/**
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* StreamingService.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
|
@ -31,7 +31,7 @@ public interface StreamingService {
|
|||
throws IOException, ExtractionException;
|
||||
SearchEngine getSearchEngineInstance();
|
||||
|
||||
VideoUrlIdHandler getUrlIdHandler();
|
||||
StreamUrlIdHandler getUrlIdHandler();
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -7,10 +7,13 @@ import org.jsoup.Jsoup;
|
|||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoCollector;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
@ -49,9 +52,10 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
private static final String TAG = YoutubeSearchEngine.class.toString();
|
||||
|
||||
@Override
|
||||
public Result search(String query, int page, String languageCode, Downloader downloader)
|
||||
throws IOException, ParsingException {
|
||||
Result result = new Result();
|
||||
public StreamPreviewInfoCollector search(String query, int page, String languageCode, Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
StreamPreviewInfoCollector collector = new StreamPreviewInfoCollector(
|
||||
new YoutubeStreamUrlIdHandler());
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("https")
|
||||
.authority("www.youtube.com")
|
||||
|
@ -71,7 +75,6 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
site = downloader.download(url);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Document doc = Jsoup.parse(site, url);
|
||||
Element list = doc.select("ol[class=\"item-section\"]").first();
|
||||
|
@ -92,58 +95,25 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
|
||||
// both types of spell correction item
|
||||
if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) {
|
||||
result.suggestion = el.select("a").first().text();
|
||||
collector.setSuggestion(el.select("a").first().text());
|
||||
if(list.children().size() == 1) {
|
||||
throw new NothingFoundException("Did you mean: " + el.select("a").first().text());
|
||||
}
|
||||
// search message item
|
||||
} else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) {
|
||||
result.errorMessage = el.text();
|
||||
//result.errorMessage = el.text();
|
||||
throw new NothingFoundException(el.text());
|
||||
|
||||
// video item type
|
||||
} else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
|
||||
VideoPreviewInfo resultItem = new VideoPreviewInfo();
|
||||
|
||||
// importand information
|
||||
resultItem.webpage_url = getWebpageUrl(item);
|
||||
resultItem.id = (new YoutubeVideoUrlIdHandler()).getVideoId(resultItem.webpage_url);
|
||||
resultItem.title = getTitle(item);
|
||||
|
||||
// optional iformation
|
||||
//todo: make this a proper error handling
|
||||
try {
|
||||
resultItem.duration = getDuration(item);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
resultItem.uploader = getUploader(item);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
resultItem.upload_date = getUploadDate(item);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
resultItem.view_count = getViewCount(item);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
resultItem.thumbnail_url = getThumbnailUrl(item);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
result.resultList.add(resultItem);
|
||||
collector.commit(extractPreviewInfo(el));
|
||||
} else {
|
||||
//noinspection ConstantConditions
|
||||
Log.e(TAG, "unexpected element found:\"" + el + "\"");
|
||||
collector.addError(new Exception("unexpected element found:\"" + el + "\""));
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException(e);
|
||||
}
|
||||
return result;
|
||||
|
||||
return collector;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -167,8 +137,6 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
|
||||
String response = dl.download(url);
|
||||
|
||||
try {
|
||||
|
||||
//TODO: Parse xml data using Jsoup not done
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder;
|
||||
|
@ -180,10 +148,10 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
new ByteArrayInputStream(response.getBytes("utf-8"))));
|
||||
doc.getDocumentElement().normalize();
|
||||
} catch (ParserConfigurationException | SAXException | IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new ParsingException("Could not parse document.");
|
||||
}
|
||||
|
||||
if (doc != null) {
|
||||
try {
|
||||
NodeList nList = doc.getElementsByTagName("CompleteSuggestion");
|
||||
for (int temp = 0; temp < nList.getLength(); temp++) {
|
||||
|
||||
|
@ -194,28 +162,30 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
suggestions.add(eElement.getAttribute("data"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "GREAT FUCKING ERROR");
|
||||
}
|
||||
return suggestions;
|
||||
} catch(Exception e) {
|
||||
throw new ParsingException(e);
|
||||
throw new ParsingException("Could not get suggestions form document.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getWebpageUrl(Element item) {
|
||||
private StreamPreviewInfoExtractor extractPreviewInfo(final Element item) {
|
||||
return new StreamPreviewInfoExtractor() {
|
||||
@Override
|
||||
public String getWebPageUrl() throws ParsingException {
|
||||
Element el = item.select("div[class*=\"yt-lockup-video\"").first();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
return dl.attr("abs:href");
|
||||
}
|
||||
|
||||
private String getTitle(Element item) {
|
||||
@Override
|
||||
public String getTitle() throws ParsingException {
|
||||
Element el = item.select("div[class*=\"yt-lockup-video\"").first();
|
||||
Element dl = el.select("h3").first().select("a").first();
|
||||
return dl.text();
|
||||
}
|
||||
|
||||
private String getDuration(Element item) {
|
||||
@Override
|
||||
public String getDuration() throws ParsingException {
|
||||
try {
|
||||
return item.select("span[class=\"video-time\"]").first().text();
|
||||
} catch(Exception e) {
|
||||
|
@ -224,19 +194,22 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
return "";
|
||||
}
|
||||
|
||||
private String getUploader(Element item) {
|
||||
@Override
|
||||
public String getUploader() throws ParsingException {
|
||||
return item.select("div[class=\"yt-lockup-byline\"]").first()
|
||||
.select("a").first()
|
||||
.text();
|
||||
}
|
||||
|
||||
private String getUploadDate(Element item) {
|
||||
@Override
|
||||
public String getUploadDate() throws ParsingException {
|
||||
return item.select("div[class=\"yt-lockup-meta\"]").first()
|
||||
.select("li").first()
|
||||
.text();
|
||||
}
|
||||
|
||||
private long getViewCount(Element item) throws Parser.RegexException{
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
String output;
|
||||
String input = item.select("div[class=\"yt-lockup-meta\"]").first()
|
||||
.select("li").get(1)
|
||||
|
@ -246,13 +219,20 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
.replace(".", "")
|
||||
.replace(",", "");
|
||||
|
||||
if(Long.parseLong(output) == 30) {
|
||||
Log.d(TAG, "bla");
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(output);
|
||||
} catch (NumberFormatException e) {
|
||||
// if this happens the video probably has no views
|
||||
if(!input.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
throw new ParsingException("Could not handle input: " + input, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getThumbnailUrl(Element item) {
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
String url;
|
||||
Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first()
|
||||
.select("img").first();
|
||||
|
@ -266,4 +246,6 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||
|
||||
return url;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import org.schabi.newpipe.extractor.ExtractionException;
|
|||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.StreamUrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.SearchEngine;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -40,7 +40,7 @@ public class YoutubeService implements StreamingService {
|
|||
@Override
|
||||
public StreamExtractor getExtractorInstance(String url, Downloader downloader)
|
||||
throws ExtractionException, IOException {
|
||||
VideoUrlIdHandler urlIdHandler = new YoutubeVideoUrlIdHandler();
|
||||
StreamUrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler();
|
||||
if(urlIdHandler.acceptUrl(url)) {
|
||||
return new YoutubeStreamExtractor(url, downloader) ;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public class YoutubeService implements StreamingService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public VideoUrlIdHandler getUrlIdHandler() {
|
||||
return new YoutubeVideoUrlIdHandler();
|
||||
public StreamUrlIdHandler getUrlIdHandler() {
|
||||
return new YoutubeStreamUrlIdHandler();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,11 @@ import org.schabi.newpipe.extractor.ExtractionException;
|
|||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.StreamPreviewInfo;
|
||||
import org.schabi.newpipe.extractor.StreamUrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.VideoInfo;
|
||||
import org.schabi.newpipe.extractor.VideoPreviewInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
@ -52,9 +52,6 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
// exceptions
|
||||
|
||||
public class DecryptException extends ParsingException {
|
||||
DecryptException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
DecryptException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
@ -69,8 +66,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
public class LiveStreamException extends ContentNotAvailableException {
|
||||
LiveStreamException() {
|
||||
super();
|
||||
LiveStreamException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +176,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
// cached values
|
||||
private static volatile String decryptionCode = "";
|
||||
|
||||
VideoUrlIdHandler urlidhandler = new YoutubeVideoUrlIdHandler();
|
||||
StreamUrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler();
|
||||
String pageUrl = "";
|
||||
|
||||
private Downloader downloader;
|
||||
|
@ -250,7 +247,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
throw new ParsingException("Could not parse yt player config", e);
|
||||
}
|
||||
if (isLiveStream) {
|
||||
throw new LiveStreamException();
|
||||
throw new LiveStreamException("This is a Life stream. Can't use those right now.");
|
||||
}
|
||||
|
||||
return playerArgs;
|
||||
|
@ -433,8 +430,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
|
||||
|
||||
@Override
|
||||
public List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException {
|
||||
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
|
||||
public List<StreamInfo.AudioStream> getAudioStreams() throws ParsingException {
|
||||
Vector<StreamInfo.AudioStream> audioStreams = new Vector<>();
|
||||
try{
|
||||
String encoded_url_map;
|
||||
// playerArgs could be null if the video is age restricted
|
||||
|
@ -461,7 +458,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
+ decryptSignature(tags.get("s"), decryptionCode);
|
||||
}
|
||||
|
||||
audioStreams.add(new VideoInfo.AudioStream(streamUrl,
|
||||
audioStreams.add(new StreamInfo.AudioStream(streamUrl,
|
||||
itagItem.mediaFormatId,
|
||||
itagItem.bandWidth,
|
||||
itagItem.samplingRate));
|
||||
|
@ -475,8 +472,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException {
|
||||
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
|
||||
public List<StreamInfo.VideoStream> getVideoStreams() throws ParsingException {
|
||||
Vector<StreamInfo.VideoStream> videoStreams = new Vector<>();
|
||||
|
||||
try{
|
||||
String encoded_url_map;
|
||||
|
@ -504,7 +501,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
streamUrl = streamUrl + "&signature="
|
||||
+ decryptSignature(tags.get("s"), decryptionCode);
|
||||
}
|
||||
videoStreams.add(new VideoInfo.VideoStream(
|
||||
videoStreams.add(new StreamInfo.VideoStream(
|
||||
streamUrl,
|
||||
itagItem.mediaFormatId,
|
||||
itagItem.resolutionString));
|
||||
|
@ -527,7 +524,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<VideoInfo.VideoStream> getVideoOnlyStreams() throws ParsingException {
|
||||
public List<StreamInfo.VideoStream> getVideoOnlyStreams() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -638,7 +635,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public VideoPreviewInfo getNextVideo() throws ParsingException {
|
||||
public StreamPreviewInfo getNextVideo() throws ParsingException {
|
||||
try {
|
||||
return extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
||||
.select("li").first());
|
||||
|
@ -648,9 +645,9 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Vector<VideoPreviewInfo> getRelatedVideos() throws ParsingException {
|
||||
public Vector<StreamPreviewInfo> getRelatedVideos() throws ParsingException {
|
||||
try {
|
||||
Vector<VideoPreviewInfo> relatedVideos = new Vector<>();
|
||||
Vector<StreamPreviewInfo> relatedVideos = new Vector<>();
|
||||
for (Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
|
||||
// first check if we have a playlist. If so leave them out
|
||||
if (li.select("a[class*=\"content-link\"]").first() != null) {
|
||||
|
@ -664,8 +661,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public VideoUrlIdHandler getUrlIdConverter() {
|
||||
return new YoutubeVideoUrlIdHandler();
|
||||
public StreamUrlIdHandler getUrlIdConverter() {
|
||||
return new YoutubeStreamUrlIdHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -674,10 +671,10 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
}
|
||||
|
||||
/**Provides information about links to other videos on the video page, such as related videos.
|
||||
* This is encapsulated in a VideoPreviewInfo object,
|
||||
* which is a subset of the fields in a full VideoInfo.*/
|
||||
private VideoPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException {
|
||||
VideoPreviewInfo info = new VideoPreviewInfo();
|
||||
* This is encapsulated in a StreamPreviewInfo object,
|
||||
* which is a subset of the fields in a full StreamInfo.*/
|
||||
private StreamPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException {
|
||||
StreamPreviewInfo info = new StreamPreviewInfo();
|
||||
|
||||
try {
|
||||
info.webpage_url = li.select("a.content-link").first()
|
||||
|
@ -718,7 +715,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
info.thumbnail_url = "https:" + info.thumbnail_url;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(e);
|
||||
throw new ParsingException("Could not get video preview info", e);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
@ -772,7 +769,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
|
|||
Function decryptionFunc = (Function) scope.get("decrypt", scope);
|
||||
result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig});
|
||||
} catch (Exception e) {
|
||||
throw new DecryptException(e);
|
||||
throw new DecryptException("could not get decrypt signature", e);
|
||||
} finally {
|
||||
Context.exit();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package org.schabi.newpipe.extractor.services.youtube;
|
|||
|
||||
import org.schabi.newpipe.extractor.Parser;
|
||||
import org.schabi.newpipe.extractor.ParsingException;
|
||||
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.StreamUrlIdHandler;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
|
@ -11,7 +11,7 @@ import java.net.URLDecoder;
|
|||
* Created by Christian Schabesberger on 02.02.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* YoutubeVideoUrlIdHandler.java is part of NewPipe.
|
||||
* YoutubeStreamUrlIdHandler.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,7 +27,7 @@ import java.net.URLDecoder;
|
|||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler {
|
||||
public class YoutubeStreamUrlIdHandler implements StreamUrlIdHandler {
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Override
|
||||
public String getVideoUrl(String videoId) {
|
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ErrorActivity">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorSorryView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/sorry_string"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageWhatHappenedView"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/what_happened_headline"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorMessageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/black"
|
||||
android:text="@string/info_labels"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorDeviceHeadlineView"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/what_device_headline"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/errorInfoLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorInfoLabelsView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/black"
|
||||
android:text="@string/info_labels"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:paddingLeft="16dp"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorInfosView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorDetailView"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/error_details_headline"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/horizontalScrollView"
|
||||
android:layout_gravity="center" >
|
||||
<TextView
|
||||
android:id="@+id/errorView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:typeface="monospace"/>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorYourComment"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/your_comment"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/errorCommentBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/errorReportButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/error_report_button_text" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</FrameLayout>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/menu_item_share_error"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="ifRoom"
|
||||
android:icon="@drawable/ic_share_black"/>
|
||||
</menu>
|
|
@ -0,0 +1,6 @@
|
|||
<resources>
|
||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||
(such as screen margins) for screens with more than 820dp of available width. This
|
||||
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
</resources>
|
|
@ -35,5 +35,8 @@
|
|||
<!-- Paddings & Margins -->
|
||||
<dimen name="video_item_detail_like_margin">6sp</dimen>
|
||||
<dimen name="video_item_detail_play_fab_margin">20dp</dimen>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
</resources>
|
|
@ -15,7 +15,7 @@
|
|||
<string name="download">Download</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="did_you_mean">Did you mean: </string>
|
||||
<string name="did_you_mean">Did you mean: %1$s ?</string>
|
||||
<string name="search_page">Search page: </string>
|
||||
<string name="share_dialog_title">Share with:</string>
|
||||
<string name="choose_browser">Choose browser:</string>
|
||||
|
@ -84,10 +84,25 @@
|
|||
<string name="could_not_load_thumbnails">Could not load all Thumbnails</string>
|
||||
<string name="youtube_signature_decryption_error">Could not decrypt video url signature.</string>
|
||||
<string name="parsing_error">Could not parse website.</string>
|
||||
<string name="light_parsing_error">Could not parse website complete.</string>
|
||||
<string name="content_not_available">Content not available.</string>
|
||||
<string name="blocked_by_gema">Blocked by GEMA.</string>
|
||||
<string name="could_not_setup_download_menu">Could not setup download menu.</string>
|
||||
<string name="live_streams_not_supported">This is a LIVE STREAM. These are not yet supported.</string>
|
||||
<string name="could_not_get_stream">Could not get any stream.</string>
|
||||
<!-- error activity -->
|
||||
<string name="sorry_string">Sorry that should not happen.</string>
|
||||
<string name="guru_meditation" translatable="false">Guru Meditation.</string>
|
||||
<string name="error_report_button_text">Report error via mail</string>
|
||||
<string name="error_snackbar_message">Sorry some errors occurred.</string>
|
||||
<string name="error_snackbar_action">REPORT</string>
|
||||
<string name="what_device_headline">Info:</string>
|
||||
<string name="what_happened_headline">What happened:</string>
|
||||
<string name="info_labels">What:\\nRequest:\\nContent Lang:\\nService:\\nGMT Time:\\nVersion:\\nOS version:\\nGlob. IP range:</string>
|
||||
<string name="info_searched_lbl">Searched for:</string>
|
||||
<string name="info_requested_stream_lbl">Requested stream:</string>
|
||||
<string name="your_comment">Your comment (in English):</string>
|
||||
<string name="error_details_headline">Details:</string>
|
||||
|
||||
|
||||
<!-- Content descriptions (for better accessibility) -->
|
||||
|
|
Loading…
Reference in New Issue