Merge pull request #2862 from TeamNewPipe/release_0.18.0
Release 0.18.0
|
@ -81,6 +81,7 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
|||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
|
||||
## Updates
|
||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
@ -8,8 +11,8 @@ android {
|
|||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 790
|
||||
versionName "0.17.4"
|
||||
versionCode 800
|
||||
versionName "0.18.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
@ -44,7 +47,7 @@ android {
|
|||
|
||||
ext {
|
||||
androidxLibVersion = '1.0.0'
|
||||
exoPlayerLibVersion = '2.10.6'
|
||||
exoPlayerLibVersion = '2.10.8'
|
||||
roomDbLibVersion = '2.1.0'
|
||||
leakCanaryLibVersion = '1.5.4' //1.6.1
|
||||
okHttpLibVersion = '3.12.6'
|
||||
|
@ -53,11 +56,14 @@ ext {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
|
||||
exclude module: 'support-annotations'
|
||||
})
|
||||
|
||||
implementation 'com.github.teamnewpipe:NewPipeExtractor:v0.17.4'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:8cb3250'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
|
||||
|
@ -89,13 +95,14 @@ dependencies {
|
|||
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||
implementation 'org.ocpsoft.prettytime:prettytime:4.0.1.Final'
|
||||
|
||||
implementation "androidx.room:room-runtime:${roomDbLibVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${roomDbLibVersion}"
|
||||
annotationProcessor "androidx.room:room-compiler:${roomDbLibVersion}"
|
||||
kapt "androidx.room:room-compiler:${roomDbLibVersion}"
|
||||
|
||||
implementation "frankiesardo:icepick:${icepickLibVersion}"
|
||||
annotationProcessor "frankiesardo:icepick-processor:${icepickLibVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickLibVersion}"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}"
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
#}
|
||||
|
||||
-dontobfuscate
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.ocpsoft.prettytime.i18n.** { *; }
|
||||
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
|
|
|
@ -15,7 +15,7 @@ import com.squareup.leakcanary.LeakCanary;
|
|||
import com.squareup.leakcanary.LeakDirectoryProvider;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -39,7 +39,7 @@ public class DebugApp extends App {
|
|||
|
||||
@Override
|
||||
protected Downloader getDownloader() {
|
||||
return org.schabi.newpipe.Downloader.init(new OkHttpClient.Builder()
|
||||
return DownloaderImpl.init(new OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(new StethoInterceptor()));
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ import android.app.NotificationChannel;
|
|||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
@ -21,13 +21,15 @@ import org.acra.config.ACRAConfiguration;
|
|||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.ConfigurationBuilder;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -95,10 +97,15 @@ public class App extends Application {
|
|||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(this));
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.init();
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
|
||||
|
@ -109,7 +116,7 @@ public class App extends Application {
|
|||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
return org.schabi.newpipe.Downloader.init(null);
|
||||
return DownloaderImpl.init(null);
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
|
|
|
@ -107,6 +107,7 @@ public abstract class BaseFragment extends Fragment {
|
|||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,296 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.schabi.newpipe.extractor.DownloadRequest;
|
||||
import org.schabi.newpipe.extractor.DownloadResponse;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 28.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* Downloader.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 Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
|
||||
private static Downloader instance;
|
||||
private String mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private Downloader(OkHttpClient.Builder builder) {
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
* @param builder if null, default builder will be used
|
||||
*/
|
||||
public static Downloader init(@Nullable OkHttpClient.Builder builder) {
|
||||
return instance = new Downloader(builder != null ? builder : new OkHttpClient.Builder());
|
||||
}
|
||||
|
||||
public static Downloader getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getCookies() {
|
||||
return mCookies;
|
||||
}
|
||||
|
||||
public void setCookies(String cookies) {
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(String url) throws IOException {
|
||||
Response response = null;
|
||||
try {
|
||||
final Request request = new Request.Builder()
|
||||
.head().url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
response = client.newCall(request).execute();
|
||||
|
||||
String contentLength = response.header("Content-Length");
|
||||
return contentLength == null ? -1 : Long.parseLong(contentLength);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} finally {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param localization the language and country (usually a 2-character code) to set
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Localization localization) throws IOException, ReCaptchaException {
|
||||
Map<String, String> requestProperties = new HashMap<>();
|
||||
requestProperties.put("Accept-Language", localization.getLanguage());
|
||||
return download(siteUrl, requestProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP headers included in the customProperties map.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param customProperties set request header properties
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
return getBody(siteUrl, customProperties).string();
|
||||
}
|
||||
|
||||
public InputStream stream(String siteUrl) throws IOException {
|
||||
try {
|
||||
return getBody(siteUrl, Collections.emptyMap()).byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl);
|
||||
|
||||
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (!customProperties.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request request = requestBuilder.build();
|
||||
final Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||
* Primarily intended for downloading web pages.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to download
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
return download(siteUrl, Collections.emptyMap());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl);
|
||||
|
||||
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||
// set custom headers in request
|
||||
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||
for(String value : pair.getValue()){
|
||||
requestBuilder.addHeader(pair.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestHeaders.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request okRequest = requestBuilder.build();
|
||||
final Response response = client.newCall(okRequest).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DownloadResponse(response.code(), body.string(), response.headers().toMultimap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException {
|
||||
return get(siteUrl, DownloadRequest.emptyRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||
|
||||
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||
if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){
|
||||
// content type header is required. maybe throw an exception here
|
||||
return null;
|
||||
}
|
||||
|
||||
String contentType = requestHeaders.get("Content-Type").get(0);
|
||||
|
||||
RequestBody okRequestBody = null;
|
||||
if (null != request.getRequestBody()) {
|
||||
okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody());
|
||||
}
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("POST", okRequestBody).url(siteUrl);
|
||||
|
||||
// set custom headers in request
|
||||
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||
for(String value : pair.getValue()){
|
||||
requestBuilder.addHeader(pair.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestHeaders.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request okRequest = requestBuilder.build();
|
||||
final Response response = client.newCall(okRequest).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DownloadResponse(response.code(), body.string(), response.headers().toMultimap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse head(String siteUrl) throws IOException, ReCaptchaException {
|
||||
final Request request = new Request.Builder()
|
||||
.head().url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
final Response response = client.newCall(request).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
return new DownloadResponse(response.code(), null, response.headers().toMultimap());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Request;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import okhttp3.CipherSuite;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
|
||||
private static DownloaderImpl instance;
|
||||
private String mCookies;
|
||||
private OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(OkHttpClient.Builder builder) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
enableModernTLS(builder);
|
||||
}
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
* @param builder if null, default builder will be used
|
||||
*/
|
||||
public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
|
||||
return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
|
||||
}
|
||||
|
||||
public static DownloaderImpl getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getCookies() {
|
||||
return mCookies;
|
||||
}
|
||||
|
||||
public void setCookies(String cookies) {
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(String url) throws IOException {
|
||||
try {
|
||||
final Response response = head(url);
|
||||
return Long.parseLong(response.getHeader("Content-Length"));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(String siteUrl) throws IOException {
|
||||
try {
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final okhttp3.Request request = requestBuilder.build();
|
||||
final okhttp3.Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
|
||||
final String httpMethod = request.httpMethod();
|
||||
final String url = request.url();
|
||||
final Map<String, List<String>> headers = request.headers();
|
||||
final byte[] dataToSend = request.dataToSend();
|
||||
|
||||
RequestBody requestBody = null;
|
||||
if (dataToSend != null) {
|
||||
requestBody = RequestBody.create(null, dataToSend);
|
||||
}
|
||||
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
final String headerName = pair.getKey();
|
||||
final List<String> headerValueList = pair.getValue();
|
||||
|
||||
if (headerValueList.size() > 1) {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
for (String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
requestBuilder.header(headerName, headerValueList.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
response.close();
|
||||
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
final ResponseBody body = response.body();
|
||||
String responseBodyToReturn = null;
|
||||
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
|
||||
return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of
|
||||
* OkHttpClient.Builder.sslSocketFactory(_,_)
|
||||
* <p>
|
||||
* If there is an error, the function will safely fall back to doing nothing and printing the error to the console.
|
||||
*
|
||||
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
||||
*/
|
||||
private static void enableModernTLS(OkHttpClient.Builder builder) {
|
||||
try {
|
||||
// get the default TrustManager
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException("Unexpected default trust managers:"
|
||||
+ Arrays.toString(trustManagers));
|
||||
}
|
||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||
|
||||
// insert our own TLSSocketFactory
|
||||
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
||||
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
||||
|
||||
// This will try to enable all modern CipherSuites(+2 more) that are supported on the device.
|
||||
// Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites.
|
||||
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
||||
List<CipherSuite> cipherSuites = new ArrayList<>();
|
||||
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
|
||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
||||
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
||||
.build();
|
||||
|
||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ public class ImageDownloader extends BaseImageDownloader {
|
|||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
|
||||
final Downloader downloader = (Downloader) NewPipe.getDownloader();
|
||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,14 +29,18 @@ import android.os.Handler;
|
|||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -47,12 +51,15 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
|
@ -61,11 +68,16 @@ import org.schabi.newpipe.report.ErrorActivity;
|
|||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
@ -97,6 +109,11 @@ public class MainActivity extends AppCompatActivity {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
TLSSocketFactoryCompat.setAsDefault();
|
||||
}
|
||||
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -300,13 +317,57 @@ public class MainActivity extends AppCompatActivity {
|
|||
final String title = s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
drawerItems.getMenu()
|
||||
MenuItem menuItem = drawerItems.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
|
||||
// peertube specifics
|
||||
if(s.getServiceId() == 3){
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
}
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
|
||||
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
|
||||
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for(PeertubeInstance instance: instances){
|
||||
items.add(instance.getName());
|
||||
if(instance.getUrl().equals(currentInstace.getUrl())){
|
||||
defaultSelect = items.size()-1;
|
||||
}
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setSelection(defaultSelect, false);
|
||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
PeertubeInstance newInstance = instances.get(position);
|
||||
if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||
changeService(menuItem);
|
||||
drawer.closeDrawers();
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
recreate();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
menuItem.setActionView(spinner);
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
|
||||
|
||||
|
@ -367,6 +428,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
String selectedServiceName = NewPipe.getService(
|
||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||
toggleServiceButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -112,7 +112,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
||||
if (find_access_cookies(cookies)) {
|
||||
// Give cookies to Downloader class
|
||||
Downloader.getInstance().setCookies(mCookies);
|
||||
DownloaderImpl.getInstance().setCookies(mCookies);
|
||||
|
||||
// Closing activity and return to parent
|
||||
setResult(RESULT_OK);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
|
@ -72,10 +74,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||
|
||||
@Ignore
|
||||
public boolean isIdenticalTo(final PlaylistInfo info) {
|
||||
return getServiceId() == info.getServiceId() && getName().equals(info.getName()) &&
|
||||
getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) &&
|
||||
getThumbnailUrl().equals(info.getThumbnailUrl()) &&
|
||||
getUploader().equals(info.getUploaderName());
|
||||
/*
|
||||
* Returns boolean comparing the online playlist and the local copy.
|
||||
* (False if info changed such as playlist name or track count)
|
||||
*/
|
||||
return getServiceId() == info.getServiceId()
|
||||
&& getStreamCount() == info.getStreamCount()
|
||||
&& TextUtils.equals(getName(), info.getName())
|
||||
&& TextUtils.equals(getUrl(), info.getUrl())
|
||||
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl())
|
||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
|
|
|
@ -40,12 +40,12 @@ import org.schabi.newpipe.MainActivity;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
@ -68,6 +68,7 @@ import java.util.Locale;
|
|||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
|
@ -488,35 +489,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
Localization loc = NewPipe.getPreferredLocalization();
|
||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||
|
||||
int candidate = 0;
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry());
|
||||
if (tag.equalsIgnoreCase(loc.getLanguage())) {
|
||||
return i;
|
||||
final Locale streamLocale = streams.get(i).getLocale();
|
||||
|
||||
final boolean languageEquals = streamLocale.getLanguage() != null && preferredLocalization.getLanguageCode() != null &&
|
||||
streamLocale.getLanguage().equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
|
||||
final boolean countryEquals = streamLocale.getCountry() != null && streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
|
||||
|
||||
if (languageEquals) {
|
||||
if (countryEquals) return i;
|
||||
|
||||
candidate = i;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
// 1st loop match country & language
|
||||
// 2nd loop match language only
|
||||
int index = loc.getLanguage().indexOf("-");
|
||||
String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage();
|
||||
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
|
||||
if (streamLocale.getLanguage().equalsIgnoreCase(lang)) {
|
||||
if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
StoredDirectoryHelper mainStorageAudio = null;
|
||||
|
@ -773,12 +763,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
Stream selectedStream;
|
||||
Stream secondaryStream = null;
|
||||
char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
MissionRecoveryInfo[] recoveryInfo;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
|
@ -789,18 +780,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
if (secondary != null) {
|
||||
secondaryStream = secondary.getStream();
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
|
@ -812,8 +805,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||
// does not work on slow networks but is later updated in the downloader
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondary.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -835,13 +828,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
return;
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
urls = new String[]{selectedStream.getUrl()};
|
||||
if (secondaryStream == null) {
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream)
|
||||
};
|
||||
} else {
|
||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl(), secondaryStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
|
||||
};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
DownloadManagerService.startMission(
|
||||
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
|
||||
);
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -15,7 +16,7 @@ import androidx.appcompat.app.ActionBar;
|
|||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
@ -52,32 +53,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
destroyOldFragments();
|
||||
|
||||
tabsManager = TabsManager.getManager(activity);
|
||||
tabsManager.setSavedTabsListener(() -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
||||
}
|
||||
if (isResumed()) {
|
||||
updateTabs();
|
||||
setupTabs();
|
||||
} else {
|
||||
hasTabsChanged = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void destroyOldFragments() {
|
||||
for (Fragment fragment : getChildFragmentManager().getFragments()) {
|
||||
if (fragment != null) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.remove(fragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
|
@ -90,23 +78,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
|
||||
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.addOnTabSelectedListener(this);
|
||||
updateTabs();
|
||||
|
||||
setupTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (hasTabsChanged) {
|
||||
hasTabsChanged = false;
|
||||
updateTabs();
|
||||
}
|
||||
if (hasTabsChanged) setupTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -153,45 +135,42 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
// Tabs
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void updateTabs() {
|
||||
public void setupTabs() {
|
||||
tabsList.clear();
|
||||
tabsList.addAll(tabsManager.getTabs());
|
||||
pagerAdapter.notifyDataSetChanged();
|
||||
|
||||
viewPager.setOffscreenPageLimit(pagerAdapter.getCount());
|
||||
updateTabsIcon();
|
||||
updateTabsContentDescription();
|
||||
updateCurrentTitle();
|
||||
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
|
||||
}
|
||||
// Clear previous tabs/fragments and set new adapter
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(tabsList.size());
|
||||
|
||||
updateTabsIconAndDescription();
|
||||
updateTitleForTab(viewPager.getCurrentItem());
|
||||
|
||||
hasTabsChanged = false;
|
||||
}
|
||||
|
||||
private void updateTabsIcon() {
|
||||
private void updateTabsIconAndDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity));
|
||||
final Tab tab = tabsList.get(i);
|
||||
tabToSet.setIcon(tab.getTabIconRes(requireContext()));
|
||||
tabToSet.setContentDescription(tab.getTabName(requireContext()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTabsContentDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
final Tab t = tabsList.get(i);
|
||||
tabToSet.setIcon(t.getTabIconRes(activity));
|
||||
tabToSet.setContentDescription(t.getTabName(activity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCurrentTitle() {
|
||||
setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext()));
|
||||
private void updateTitleForTab(int tabPosition) {
|
||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab selectedTab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
||||
updateCurrentTitle();
|
||||
updateTitleForTab(selectedTab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -201,29 +180,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||
updateCurrentTitle();
|
||||
updateTitleForTab(tab.getPosition());
|
||||
}
|
||||
|
||||
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
|
||||
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private final Context context;
|
||||
private final List<Tab> internalTabsList;
|
||||
|
||||
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.context = context;
|
||||
this.internalTabsList = new ArrayList<>(tabsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
final Tab tab = tabsList.get(position);
|
||||
final Tab tab = internalTabsList.get(position);
|
||||
|
||||
Throwable throwable = null;
|
||||
Fragment fragment = null;
|
||||
try {
|
||||
fragment = tab.getFragment();
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(activity, throwable, activity.getClass(), null,
|
||||
ErrorActivity.reportError(context, throwable, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
@ -244,15 +227,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return tabsList.size();
|
||||
return internalTabsList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.remove((Fragment) object)
|
||||
.commitNowAllowingStateLoss();
|
||||
public boolean sameTabs(List<Tab> tabsToCompare) {
|
||||
return internalTabsList.equals(tabsToCompare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1067,7 +1067,13 @@ public class VideoDetailFragment
|
|||
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
|
||||
|
||||
if (info.getViewCount() >= 0) {
|
||||
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||
videoCountView.setText(Localization.listeningCount(activity, info.getViewCount()));
|
||||
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
|
||||
videoCountView.setText(Localization.watchingCount(activity, info.getViewCount()));
|
||||
} else {
|
||||
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
|
||||
}
|
||||
videoCountView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
videoCountView.setVisibility(View.GONE);
|
||||
|
@ -1120,9 +1126,15 @@ public class VideoDetailFragment
|
|||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||
if (!TextUtils.isEmpty(info.getUploadDate())) {
|
||||
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
||||
|
||||
if (info.getUploadDate() != null) {
|
||||
videoUploadDateView.setText(Localization.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
|
||||
videoUploadDateView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
videoUploadDateView.setText(null);
|
||||
videoUploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
prepareDescription(info.getDescription());
|
||||
updateProgressInfo(info);
|
||||
|
||||
|
|
|
@ -111,6 +111,8 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
|||
super.startLoading(forceLoad);
|
||||
|
||||
showListFooter(false);
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
currentInfo = null;
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = loadResult(forceLoad)
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
|
@ -98,7 +99,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(activity != null
|
||||
if (activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||
|
@ -152,7 +153,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if(useAsFrontPage && supportActionBar != null) {
|
||||
if (useAsFrontPage && supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
@ -165,7 +166,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if(info != null) {
|
||||
if (info != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
@ -178,10 +179,14 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
openRssFeed();
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
@ -397,8 +402,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for(InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if(i instanceof StreamInfoItem) {
|
||||
for (InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
|
@ -432,12 +437,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
} else {
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url,
|
||||
errorId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package org.schabi.newpipe.fragments.list.kiosk;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
public class DefaultKioskFragment extends KioskFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (serviceId < 0) {
|
||||
updateSelectedDefaultKiosk();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
updateSelectedDefaultKiosk();
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSelectedDefaultKiosk() {
|
||||
try {
|
||||
serviceId = ServiceHelper.getSelectedServiceId(requireContext());
|
||||
|
||||
final KioskList kioskList = NewPipe.getService(serviceId).getKioskList();
|
||||
kioskId = kioskList.getDefaultKioskId();
|
||||
url = kioskList.getListLinkHandlerFactoryByType(kioskId).fromId(kioskId).getUrl();
|
||||
|
||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, requireContext());
|
||||
name = kioskTranslatedName;
|
||||
|
||||
currentInfo = null;
|
||||
currentNextPageUrl = null;
|
||||
} catch (ExtractionException e) {
|
||||
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import android.os.Bundle;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -17,10 +19,12 @@ import org.schabi.newpipe.extractor.StreamingService;
|
|||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
|
@ -52,6 +56,8 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||
@State
|
||||
protected String kioskId = "";
|
||||
protected String kioskTranslatedName;
|
||||
@State
|
||||
protected ContentCountry contentCountry;
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -87,6 +93,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||
|
||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
|
||||
name = kioskTranslatedName;
|
||||
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,6 +115,15 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -127,6 +143,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||
|
||||
@Override
|
||||
public Single<KioskInfo> loadResult(boolean forceReload) {
|
||||
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
||||
return ExtractorHelper.getKioskInfo(serviceId,
|
||||
url,
|
||||
forceReload);
|
||||
|
|
|
@ -259,7 +259,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||
animateView(headerRootLayout, true, 100);
|
||||
animateView(headerUploaderLayout, true, 300);
|
||||
headerUploaderLayout.setOnClickListener(null);
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui
|
||||
headerUploaderName.setText(result.getUploaderName());
|
||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||
headerUploaderLayout.setOnClickListener(v -> {
|
||||
|
@ -273,6 +273,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||
}
|
||||
});
|
||||
}
|
||||
} else { // Else : say we have no uploader
|
||||
headerUploaderName.setText(R.string.playlist_no_uploader);
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -101,10 +102,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
ellipsize();
|
||||
}
|
||||
|
||||
if (null != item.getLikeCount()) {
|
||||
if (item.getLikeCount() >= 0) {
|
||||
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
|
||||
} else {
|
||||
itemLikesCountView.setText("-");
|
||||
}
|
||||
|
||||
if (item.getPublishedTime() != null) {
|
||||
itemPublishedTime.setText(Localization.relativeTime(item.getPublishedTime().date()));
|
||||
} else {
|
||||
itemPublishedTime.setText(item.getTextualPublishedTime());
|
||||
}
|
||||
itemPublishedTime.setText(item.getPublishedTime());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
toggleEllipsize();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
@ -7,10 +8,13 @@ import android.widget.TextView;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
* <p>
|
||||
|
@ -53,15 +57,38 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
|
||||
String viewsAndDate = "";
|
||||
if (infoItem.getViewCount() >= 0) {
|
||||
if (infoItem.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||
viewsAndDate = Localization.listeningCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
} else if (infoItem.getStreamType().equals(StreamType.LIVE_STREAM)) {
|
||||
viewsAndDate = Localization.watchingCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
} else {
|
||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
}
|
||||
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
|
||||
}
|
||||
|
||||
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
|
||||
if (!TextUtils.isEmpty(uploadDate)) {
|
||||
if (viewsAndDate.isEmpty()) {
|
||||
viewsAndDate = infoItem.getUploadDate();
|
||||
} else {
|
||||
viewsAndDate += " • " + infoItem.getUploadDate();
|
||||
return uploadDate;
|
||||
}
|
||||
|
||||
return Localization.concatenateStrings(viewsAndDate, uploadDate);
|
||||
}
|
||||
|
||||
return viewsAndDate;
|
||||
}
|
||||
|
||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
||||
if (infoItem.getUploadDate() != null) {
|
||||
String formattedRelativeTime = Localization.relativeTime(infoItem.getUploadDate().date());
|
||||
|
||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
||||
.getBoolean(itemBuilder.getContext().getString(R.string.show_original_time_ago_key), false)) {
|
||||
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
|
||||
}
|
||||
return formattedRelativeTime;
|
||||
} else {
|
||||
return infoItem.getTextualUploadDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
|
@ -28,8 +30,14 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||
|
||||
itemTitleView.setText(item.getName());
|
||||
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
|
||||
// Here is where the uploader name is set in the bookmarked playlists library
|
||||
if (!TextUtils.isEmpty(item.getUploader())) {
|
||||
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
||||
NewPipe.getNameOfService(item.getServiceId())));
|
||||
} else {
|
||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
||||
}
|
||||
|
||||
|
||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
|
|
@ -55,7 +55,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason;
|
|||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
@ -178,7 +178,6 @@ public abstract class BasePlayer implements
|
|||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
|
||||
protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
||||
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
|
||||
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
|
||||
|
@ -209,7 +208,7 @@ public abstract class BasePlayer implements
|
|||
this.progressUpdateReactor = new SerialDisposable();
|
||||
this.databaseUpdateReactor = new CompositeDisposable();
|
||||
|
||||
final String userAgent = Downloader.USER_AGENT;
|
||||
final String userAgent = DownloaderImpl.USER_AGENT;
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
|
||||
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
|
@ -954,12 +953,19 @@ public abstract class BasePlayer implements
|
|||
|
||||
public void onFastRewind() {
|
||||
if (DEBUG) Log.d(TAG, "onFastRewind() called");
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
seekBy(-getSeekDuration());
|
||||
}
|
||||
|
||||
public void onFastForward() {
|
||||
if (DEBUG) Log.d(TAG, "onFastForward() called");
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
seekBy(getSeekDuration());
|
||||
}
|
||||
|
||||
private int getSeekDuration() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.seek_duration_key);
|
||||
final String value = prefs.getString(key, context.getString(R.string.seek_duration_default_value));
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
|
||||
public void onPlayPrevious() {
|
||||
|
|
|
@ -1036,7 +1036,7 @@ public final class PopupVideoPlayer extends Service {
|
|||
public boolean onTouch(View v, MotionEvent event) {
|
||||
popupGestureDetector.onTouchEvent(event);
|
||||
if (playerImpl == null) return false;
|
||||
if (event.getPointerCount() == 2 && !isResizing) {
|
||||
if (event.getPointerCount() == 2 && !isMoving && !isResizing) {
|
||||
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
|
||||
playerImpl.showAndAnimateControl(-1, true);
|
||||
playerImpl.getLoadingPanel().setVisibility(View.GONE);
|
||||
|
|
|
@ -26,14 +26,13 @@ import android.animation.PropertyValuesHolder;
|
|||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -45,6 +44,10 @@ import android.widget.ProgressBar;
|
|||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
@ -285,6 +288,17 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
if (captionPopupMenu == null) return;
|
||||
captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId);
|
||||
|
||||
String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.caption_user_set_key), null);
|
||||
/*
|
||||
* only search for autogenerated cc as fallback
|
||||
* if "(auto-generated)" was not already selected
|
||||
* we are only looking for "(" instead of "(auto-generated)" to hopefully get all
|
||||
* internationalized variants such as "(automatisch-erzeugt)" and so on
|
||||
*/
|
||||
boolean searchForAutogenerated = userPreferredLanguage != null &&
|
||||
!userPreferredLanguage.contains("(");
|
||||
|
||||
// Add option for turning off caption
|
||||
MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
|
||||
0, Menu.NONE, R.string.caption_none);
|
||||
|
@ -294,6 +308,8 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, true));
|
||||
}
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit();
|
||||
return true;
|
||||
});
|
||||
|
||||
|
@ -308,9 +324,26 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
|
||||
captionLanguage).commit();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// apply caption language from previous user preference
|
||||
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) ||
|
||||
searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) ||
|
||||
userPreferredLanguage.contains("(") &&
|
||||
captionLanguage.startsWith(userPreferredLanguage.substring(0,
|
||||
userPreferredLanguage.indexOf('('))))) {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
}
|
||||
searchForAutogenerated = false;
|
||||
}
|
||||
}
|
||||
captionPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ import com.nostra13.universalimageloader.core.ImageLoader;
|
|||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
@ -53,10 +54,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||
|
||||
private String thumbnailLoadToggleKey;
|
||||
|
||||
private Localization initialSelectedLocalization;
|
||||
private ContentCountry initialSelectedContentCountry;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
|
||||
|
||||
initialSelectedLocalization = org.schabi.newpipe.util.Localization.getPreferredLocalization(requireContext());
|
||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization.getPreferredContentCountry(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,20 +115,23 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||
startActivityForResult(i, REQUEST_EXPORT_PATH);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Preference setPreferredLanguage = findPreference(getString(R.string.content_language_key));
|
||||
setPreferredLanguage.setOnPreferenceChangeListener((Preference p, Object newLanguage) -> {
|
||||
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
|
||||
NewPipe.setLocalization(new Localization(oldLocal.getCountry(), (String) newLanguage));
|
||||
return true;
|
||||
});
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
Preference setPreferredCountry = findPreference(getString(R.string.content_country_key));
|
||||
setPreferredCountry.setOnPreferenceChangeListener((Preference p, Object newCountry) -> {
|
||||
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
|
||||
NewPipe.setLocalization(new Localization((String) newCountry, oldLocal.getLanguage()));
|
||||
return true;
|
||||
});
|
||||
final Localization selectedLocalization = org.schabi.newpipe.util.Localization
|
||||
.getPreferredLocalization(requireContext());
|
||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
|
||||
if (!selectedLocalization.equals(initialSelectedLocalization)
|
||||
|| !selectedContentCountry.equals(initialSelectedContentCountry)) {
|
||||
Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart, Toast.LENGTH_LONG).show();
|
||||
|
||||
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,420 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class PeertubeInstanceListFragment extends Fragment {
|
||||
|
||||
private List<PeertubeInstance> instanceList = new ArrayList<>();
|
||||
private PeertubeInstance selectedInstance;
|
||||
private String savedInstanceListKey;
|
||||
public InstanceListAdapter instanceListAdapter;
|
||||
|
||||
private ProgressBar progressBar;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
savedInstanceListKey = getString(R.string.peertube_instance_list_key);
|
||||
selectedInstance = PeertubeHelper.getCurrentInstance();
|
||||
updateInstanceList();
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_instance_list, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
|
||||
initButton(rootView);
|
||||
|
||||
RecyclerView listInstances = rootView.findViewById(R.id.instances);
|
||||
listInstances.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(listInstances);
|
||||
|
||||
instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper);
|
||||
listInstances.setAdapter(instanceListAdapter);
|
||||
|
||||
progressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
saveChanges();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
disposables = null;
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private final int MENU_ITEM_RESTORE_ID = 123456;
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults);
|
||||
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
|
||||
final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults);
|
||||
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
|
||||
restoreDefaults();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateInstanceList() {
|
||||
instanceList.clear();
|
||||
instanceList.addAll(PeertubeHelper.getInstanceList(requireContext()));
|
||||
}
|
||||
|
||||
private void selectInstance(PeertubeInstance instance) {
|
||||
selectedInstance = PeertubeHelper.selectInstance(instance, requireContext());
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
if (getActivity() instanceof AppCompatActivity) {
|
||||
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) actionBar.setTitle(R.string.peertube_instance_url_title);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChanges() {
|
||||
JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
|
||||
for (PeertubeInstance instance : instanceList) {
|
||||
jsonWriter.object();
|
||||
jsonWriter.value("name", instance.getName());
|
||||
jsonWriter.value("url", instance.getUrl());
|
||||
jsonWriter.end();
|
||||
}
|
||||
String jsonToSave = jsonWriter.end().end().done();
|
||||
sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply();
|
||||
}
|
||||
|
||||
private void restoreDefaults() {
|
||||
new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext()))
|
||||
.setTitle(R.string.restore_defaults)
|
||||
.setMessage(R.string.restore_defaults_confirmation)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
sharedPreferences.edit().remove(savedInstanceListKey).apply();
|
||||
selectInstance(PeertubeInstance.defaultInstance);
|
||||
updateInstanceList();
|
||||
instanceListAdapter.notifyDataSetChanged();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void initButton(View rootView) {
|
||||
final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton);
|
||||
fab.setOnClickListener(v -> {
|
||||
showAddItemDialog(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void showAddItemDialog(Context c) {
|
||||
final EditText urlET = new EditText(c);
|
||||
urlET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
urlET.setHint(R.string.peertube_instance_add_help);
|
||||
AlertDialog dialog = new AlertDialog.Builder(c)
|
||||
.setTitle(R.string.peertube_instance_add_title)
|
||||
.setIcon(R.drawable.place_holder_peertube)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.finish, (dialog1, which) -> {
|
||||
String url = urlET.getText().toString();
|
||||
addInstance(url);
|
||||
})
|
||||
.create();
|
||||
dialog.setView(urlET, 50, 0, 50, 0);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void addInstance(String url) {
|
||||
String cleanUrl = cleanUrl(url);
|
||||
if(null == cleanUrl) return;
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
Disposable disposable = Single.fromCallable(() -> {
|
||||
PeertubeInstance instance = new PeertubeInstance(cleanUrl);
|
||||
instance.fetchInstanceMetaData();
|
||||
return instance;
|
||||
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe((instance) -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
add(instance);
|
||||
}, e -> {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String cleanUrl(String url){
|
||||
url = url.trim();
|
||||
// if protocol not present, add https
|
||||
if(!url.startsWith("http")){
|
||||
url = "https://" + url;
|
||||
}
|
||||
// remove trailing slash
|
||||
url = url.replaceAll("/$", "");
|
||||
// only allow https
|
||||
if (!url.startsWith("https://")) {
|
||||
Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
}
|
||||
// only allow if not already exists
|
||||
for (PeertubeInstance instance : instanceList) {
|
||||
if (instance.getUrl().equals(url)) {
|
||||
Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private void add(final PeertubeInstance instance) {
|
||||
instanceList.add(instance);
|
||||
instanceListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// List Handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private class InstanceListAdapter extends RecyclerView.Adapter<InstanceListAdapter.TabViewHolder> {
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
private final LayoutInflater inflater;
|
||||
private RadioButton lastChecked;
|
||||
|
||||
InstanceListAdapter(Context context, ItemTouchHelper itemTouchHelper) {
|
||||
this.itemTouchHelper = itemTouchHelper;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
public void swapItems(int fromPosition, int toPosition) {
|
||||
Collections.swap(instanceList, fromPosition, toPosition);
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = inflater.inflate(R.layout.item_instance, parent, false);
|
||||
return new InstanceListAdapter.TabViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull InstanceListAdapter.TabViewHolder holder, int position) {
|
||||
holder.bind(position, holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return instanceList.size();
|
||||
}
|
||||
|
||||
class TabViewHolder extends RecyclerView.ViewHolder {
|
||||
private AppCompatImageView instanceIconView;
|
||||
private TextView instanceNameView;
|
||||
private TextView instanceUrlView;
|
||||
private RadioButton instanceRB;
|
||||
private ImageView handle;
|
||||
|
||||
TabViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
instanceIconView = itemView.findViewById(R.id.instanceIcon);
|
||||
instanceNameView = itemView.findViewById(R.id.instanceName);
|
||||
instanceUrlView = itemView.findViewById(R.id.instanceUrl);
|
||||
instanceRB = itemView.findViewById(R.id.selectInstanceRB);
|
||||
handle = itemView.findViewById(R.id.handle);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
void bind(int position, TabViewHolder holder) {
|
||||
handle.setOnTouchListener(getOnTouchListener(holder));
|
||||
|
||||
final PeertubeInstance instance = instanceList.get(position);
|
||||
instanceNameView.setText(instance.getName());
|
||||
instanceUrlView.setText(instance.getUrl());
|
||||
instanceRB.setOnCheckedChangeListener(null);
|
||||
if (selectedInstance.getUrl().equals(instance.getUrl())) {
|
||||
if (lastChecked != null && lastChecked != instanceRB) {
|
||||
lastChecked.setChecked(false);
|
||||
}
|
||||
instanceRB.setChecked(true);
|
||||
lastChecked = instanceRB;
|
||||
}
|
||||
instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
selectInstance(instance);
|
||||
if (lastChecked != null && lastChecked != instanceRB) {
|
||||
lastChecked.setChecked(false);
|
||||
}
|
||||
lastChecked = instanceRB;
|
||||
}
|
||||
});
|
||||
instanceIconView.setImageResource(R.drawable.place_holder_peertube);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) {
|
||||
return (view, motionEvent) -> {
|
||||
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
if (itemTouchHelper != null && getItemCount() > 1) {
|
||||
itemTouchHelper.startDrag(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||
int viewSizeOutOfBounds, int totalSize,
|
||||
long msSinceStartScroll) {
|
||||
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
|
||||
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||
final int minimumAbsVelocity = Math.max(12,
|
||||
Math.abs(standardSpeed));
|
||||
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
|
||||
RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType() ||
|
||||
instanceListAdapter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getAdapterPosition();
|
||||
final int targetIndex = target.getAdapterPosition();
|
||||
instanceListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
// do not allow swiping the selected instance
|
||||
if(instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {
|
||||
instanceListAdapter.notifyItemChanged(position);
|
||||
return;
|
||||
}
|
||||
instanceList.remove(position);
|
||||
instanceListAdapter.notifyItemRemoved(position);
|
||||
|
||||
if (instanceList.isEmpty()) {
|
||||
instanceList.add(selectedInstance);
|
||||
instanceListAdapter.notifyItemInserted(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -231,7 +231,7 @@ public class ChooseTabsFragment extends Fragment {
|
|||
break;
|
||||
case DEFAULT_KIOSK:
|
||||
if (!tabList.contains(tab)) {
|
||||
returnList.add(new ChooseTabListItem(tab.getTabId(), "Default Kiosk",
|
||||
returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.default_kiosk_page_summary),
|
||||
ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot)));
|
||||
}
|
||||
break;
|
||||
|
@ -305,23 +305,25 @@ public class ChooseTabsFragment extends Fragment {
|
|||
return;
|
||||
}
|
||||
|
||||
String tabName = tab.getTabName(requireContext());
|
||||
final String tabName;
|
||||
switch (type) {
|
||||
case BLANK:
|
||||
tabName = requireContext().getString(R.string.blank_page_summary);
|
||||
break;
|
||||
case KIOSK:
|
||||
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tabName;
|
||||
break;
|
||||
case CHANNEL:
|
||||
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName;
|
||||
tabName = getString(R.string.blank_page_summary);
|
||||
break;
|
||||
case DEFAULT_KIOSK:
|
||||
tabName = "Default Kiosk";
|
||||
tabName = getString(R.string.default_kiosk_page_summary);
|
||||
break;
|
||||
case KIOSK:
|
||||
tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab).getKioskServiceId()) + "/" + tab.getTabName(requireContext());
|
||||
break;
|
||||
case CHANNEL:
|
||||
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tab.getTabName(requireContext());
|
||||
break;
|
||||
default:
|
||||
tabName = tab.getTabName(requireContext());
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
tabNameView.setText(tabName);
|
||||
tabIconView.setImageResource(tab.getTabIconRes(requireContext()));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.settings.tabs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -9,22 +10,26 @@ import androidx.fragment.app.Fragment;
|
|||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonSink;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.BlankFragment;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class Tab {
|
||||
Tab() {
|
||||
}
|
||||
|
@ -40,10 +45,12 @@ public abstract class Tab {
|
|||
/**
|
||||
* Return a instance of the fragment that this tab represent.
|
||||
*/
|
||||
public abstract Fragment getFragment() throws ExtractionException;
|
||||
public abstract Fragment getFragment(Context context) throws ExtractionException;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
|
||||
return obj instanceof Tab && obj.getClass().equals(this.getClass())
|
||||
&& ((Tab) obj).getTabId() == this.getTabId();
|
||||
}
|
||||
|
@ -115,12 +122,6 @@ public abstract class Tab {
|
|||
return new KioskTab(jsonObject);
|
||||
case CHANNEL:
|
||||
return new ChannelTab(jsonObject);
|
||||
case DEFAULT_KIOSK:
|
||||
DefaultKioskTab tab = new DefaultKioskTab();
|
||||
if(!StringUtil.isBlank(tab.getKioskId())){
|
||||
return tab;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,13 +134,13 @@ public abstract class Tab {
|
|||
|
||||
public enum Type {
|
||||
BLANK(new BlankTab()),
|
||||
DEFAULT_KIOSK(new DefaultKioskTab()),
|
||||
SUBSCRIPTIONS(new SubscriptionsTab()),
|
||||
FEED(new FeedTab()),
|
||||
BOOKMARKS(new BookmarksTab()),
|
||||
HISTORY(new HistoryTab()),
|
||||
KIOSK(new KioskTab()),
|
||||
CHANNEL(new ChannelTab()),
|
||||
DEFAULT_KIOSK(new DefaultKioskTab());
|
||||
CHANNEL(new ChannelTab());
|
||||
|
||||
private Tab tab;
|
||||
|
||||
|
@ -176,7 +177,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public BlankFragment getFragment() {
|
||||
public BlankFragment getFragment(Context context) {
|
||||
return new BlankFragment();
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +202,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionFragment getFragment() {
|
||||
public SubscriptionFragment getFragment(Context context) {
|
||||
return new SubscriptionFragment();
|
||||
}
|
||||
|
||||
|
@ -227,7 +228,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public FeedFragment getFragment() {
|
||||
public FeedFragment getFragment(Context context) {
|
||||
return new FeedFragment();
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +253,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public BookmarkFragment getFragment() {
|
||||
public BookmarkFragment getFragment(Context context) {
|
||||
return new BookmarkFragment();
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +278,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StatisticsPlaylistFragment getFragment() {
|
||||
public StatisticsPlaylistFragment getFragment(Context context) {
|
||||
return new StatisticsPlaylistFragment();
|
||||
}
|
||||
}
|
||||
|
@ -327,7 +328,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public KioskFragment getFragment() throws ExtractionException {
|
||||
public KioskFragment getFragment(Context context) throws ExtractionException {
|
||||
return KioskFragment.getInstance(kioskServiceId, kioskId);
|
||||
}
|
||||
|
||||
|
@ -343,6 +344,13 @@ public abstract class Tab {
|
|||
kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "<no-id>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj) &&
|
||||
kioskServiceId == ((KioskTab) obj).kioskServiceId
|
||||
&& Objects.equals(kioskId, ((KioskTab) obj).kioskId);
|
||||
}
|
||||
|
||||
public int getKioskServiceId() {
|
||||
return kioskServiceId;
|
||||
}
|
||||
|
@ -394,7 +402,7 @@ public abstract class Tab {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ChannelFragment getFragment() {
|
||||
public ChannelFragment getFragment(Context context) {
|
||||
return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
|
||||
}
|
||||
|
||||
|
@ -412,6 +420,14 @@ public abstract class Tab {
|
|||
channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj) &&
|
||||
channelServiceId == ((ChannelTab) obj).channelServiceId
|
||||
&& Objects.equals(channelUrl, ((ChannelTab) obj).channelUrl)
|
||||
&& Objects.equals(channelName, ((ChannelTab) obj).channelName);
|
||||
}
|
||||
|
||||
public int getChannelServiceId() {
|
||||
return channelServiceId;
|
||||
}
|
||||
|
@ -428,22 +444,6 @@ public abstract class Tab {
|
|||
public static class DefaultKioskTab extends Tab {
|
||||
public static final int ID = 7;
|
||||
|
||||
private int kioskServiceId;
|
||||
private String kioskId;
|
||||
|
||||
protected DefaultKioskTab() {
|
||||
initKiosk();
|
||||
}
|
||||
|
||||
public void initKiosk() {
|
||||
this.kioskServiceId = ServiceHelper.getSelectedServiceId(App.getApp());
|
||||
try {
|
||||
this.kioskId = NewPipe.getService(this.kioskServiceId).getKioskList().getDefaultKioskId();
|
||||
} catch (ExtractionException e) {
|
||||
this.kioskId = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTabId() {
|
||||
return ID;
|
||||
|
@ -451,27 +451,31 @@ public abstract class Tab {
|
|||
|
||||
@Override
|
||||
public String getTabName(Context context) {
|
||||
return KioskTranslator.getTranslatedKioskName(kioskId, context);
|
||||
return KioskTranslator.getTranslatedKioskName(getDefaultKioskId(context), context);
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
@Override
|
||||
public int getTabIconRes(Context context) {
|
||||
final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context);
|
||||
|
||||
if (kioskIcon <= 0) {
|
||||
throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
|
||||
}
|
||||
|
||||
return kioskIcon;
|
||||
return KioskTranslator.getKioskIcons(getDefaultKioskId(context), context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KioskFragment getFragment() throws ExtractionException {
|
||||
return KioskFragment.getInstance(kioskServiceId, kioskId);
|
||||
public DefaultKioskFragment getFragment(Context context) throws ExtractionException {
|
||||
return new DefaultKioskFragment();
|
||||
}
|
||||
|
||||
public String getKioskId() {
|
||||
private String getDefaultKioskId(Context context) {
|
||||
final int kioskServiceId = ServiceHelper.getSelectedServiceId(context);
|
||||
|
||||
String kioskId = "";
|
||||
try {
|
||||
final StreamingService service = NewPipe.getService(kioskServiceId);
|
||||
kioskId = service.getKioskList().getDefaultKioskId();
|
||||
} catch (ExtractionException e) {
|
||||
ErrorActivity.reportError(context, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0));
|
||||
}
|
||||
return kioskId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package org.schabi.newpipe.settings.tabs;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
|
@ -9,18 +7,25 @@ import com.grack.nanojson.JsonParserException;
|
|||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class to get a JSON representation of a list of tabs, and the other way around.
|
||||
*/
|
||||
public class TabsJsonHelper {
|
||||
private static final String JSON_TABS_ARRAY_KEY = "tabs";
|
||||
|
||||
private static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList(
|
||||
Tab.Type.DEFAULT_KIOSK.getTab(),
|
||||
Tab.Type.SUBSCRIPTIONS.getTab(),
|
||||
Tab.Type.BOOKMARKS.getTab()
|
||||
));
|
||||
|
||||
public static class InvalidJsonException extends Exception {
|
||||
private InvalidJsonException() {
|
||||
super();
|
||||
|
@ -83,16 +88,6 @@ public class TabsJsonHelper {
|
|||
return returnTabs;
|
||||
}
|
||||
|
||||
public static List<Tab> getDefaultTabs(){
|
||||
List<Tab> tabs = new ArrayList<>();
|
||||
Tab.DefaultKioskTab tab = new Tab.DefaultKioskTab();
|
||||
if(!StringUtil.isBlank(tab.getKioskId())){
|
||||
tabs.add(tab);
|
||||
}
|
||||
tabs.add(Tab.Type.SUBSCRIPTIONS.getTab());
|
||||
tabs.add(Tab.Type.BOOKMARKS.getTab());
|
||||
return Collections.unmodifiableList(tabs);
|
||||
}
|
||||
/**
|
||||
* Get a JSON representation from a list of tabs.
|
||||
*
|
||||
|
@ -112,4 +107,8 @@ public class TabsJsonHelper {
|
|||
jsonWriter.end();
|
||||
return jsonWriter.done();
|
||||
}
|
||||
|
||||
public static List<Tab> getDefaultTabs(){
|
||||
return FALLBACK_INITIAL_TABS_LIST;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
|
|||
*/
|
||||
public class Mp4DashReader {
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Constants">
|
||||
private static final int ATOM_MOOF = 0x6D6F6F66;
|
||||
private static final int ATOM_MFHD = 0x6D666864;
|
||||
private static final int ATOM_TRAF = 0x74726166;
|
||||
|
@ -50,7 +49,7 @@ public class Mp4DashReader {
|
|||
private static final int HANDLER_VIDE = 0x76696465;
|
||||
private static final int HANDLER_SOUN = 0x736F756E;
|
||||
private static final int HANDLER_SUBT = 0x73756274;
|
||||
// </editor-fold>
|
||||
|
||||
|
||||
private final DataReader stream;
|
||||
|
||||
|
@ -293,7 +292,8 @@ public class Mp4DashReader {
|
|||
return null;
|
||||
}
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Utils">
|
||||
|
||||
|
||||
private long readUint() throws IOException {
|
||||
return stream.readInt() & 0xffffffffL;
|
||||
}
|
||||
|
@ -392,9 +392,7 @@ public class Mp4DashReader {
|
|||
return readBox();
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||
|
||||
private Moof parse_moof(Box ref, int trackId) throws IOException {
|
||||
Moof obj = new Moof();
|
||||
|
@ -795,9 +793,8 @@ public class Mp4DashReader {
|
|||
return readFullBox(b);
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Helper classes">
|
||||
|
||||
class Box {
|
||||
|
||||
int type;
|
||||
|
@ -1013,5 +1010,5 @@ public class Mp4DashReader {
|
|||
public TrunEntry info;
|
||||
public byte[] data;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
|
|||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.TrackKind;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -22,6 +23,7 @@ public class Mp4FromDashWriter {
|
|||
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
|
||||
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
|
||||
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
|
||||
private final static short SINGLE_CHUNK_SAMPLE_BUFFER = 256;
|
||||
|
||||
private final long time;
|
||||
|
||||
|
@ -145,7 +147,7 @@ public class Mp4FromDashWriter {
|
|||
// not allowed for very short tracks (less than 0.5 seconds)
|
||||
//
|
||||
outStream = output;
|
||||
int read = 8;// mdat box header size
|
||||
long read = 8;// mdat box header size
|
||||
long totalSampleSize = 0;
|
||||
int[] sampleExtra = new int[readers.length];
|
||||
int[] defaultMediaTime = new int[readers.length];
|
||||
|
@ -157,7 +159,9 @@ public class Mp4FromDashWriter {
|
|||
tablesInfo[i] = new TablesInfo();
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
||||
boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio;
|
||||
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
int samplesSize = 0;
|
||||
int sampleSizeChanges = 0;
|
||||
|
@ -210,14 +214,21 @@ public class Mp4FromDashWriter {
|
|||
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
|
||||
|
||||
tmp = tmp % SAMPLES_PER_CHUNK;
|
||||
if (tmp == 0) {
|
||||
if (singleChunk) {
|
||||
// avoid split audio streams in chunks
|
||||
tablesInfo[i].stsc = 1;
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, tablesInfo[i].stsz, 1
|
||||
};
|
||||
tablesInfo[i].stco = 1;
|
||||
} else if (tmp == 0) {
|
||||
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1
|
||||
};
|
||||
} else {
|
||||
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
|
||||
tablesInfo[i].stsc = 3;// first chunk (init) and successive chunks and remain chunk
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1,
|
||||
|
@ -244,7 +255,7 @@ public class Mp4FromDashWriter {
|
|||
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
|
||||
boolean is64 = read > THRESHOLD_FOR_CO64;
|
||||
|
||||
|
@ -268,10 +279,10 @@ public class Mp4FromDashWriter {
|
|||
} else {*/
|
||||
if (auxSize > 0) {
|
||||
int length = auxSize;
|
||||
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
||||
byte[] buffer = new byte[64 * 1024];// 64 KiB
|
||||
while (length > 0) {
|
||||
int count = Math.min(length, buffer.length);
|
||||
outWrite(buffer, 0, count);
|
||||
outWrite(buffer, count);
|
||||
length -= count;
|
||||
}
|
||||
}
|
||||
|
@ -280,7 +291,7 @@ public class Mp4FromDashWriter {
|
|||
outSeek(ftyp_size);
|
||||
}
|
||||
|
||||
// tablesInfo contais row counts
|
||||
// tablesInfo contains row counts
|
||||
// and after returning from make_moov() will contain table offsets
|
||||
make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
|
@ -291,7 +302,7 @@ public class Mp4FromDashWriter {
|
|||
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
|
||||
tablesInfo[i].stsc_bEntries = null;
|
||||
if (tablesInfo[i].ctts > 0) {
|
||||
sampleCount[i] = 1;// index is not base zero
|
||||
sampleCount[i] = 1;// the index is not base zero
|
||||
sampleExtra[i] = -1;
|
||||
}
|
||||
}
|
||||
|
@ -303,8 +314,8 @@ public class Mp4FromDashWriter {
|
|||
outWrite(make_mdat(totalSampleSize, is64));
|
||||
|
||||
int[] sampleIndex = new int[readers.length];
|
||||
int[] sizes = new int[SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[SAMPLES_PER_CHUNK];
|
||||
int[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK];
|
||||
|
||||
int written = readers.length;
|
||||
while (written > 0) {
|
||||
|
@ -317,7 +328,12 @@ public class Mp4FromDashWriter {
|
|||
|
||||
long chunkOffset = writeOffset;
|
||||
int syncCount = 0;
|
||||
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
int limit;
|
||||
if (singleChunk) {
|
||||
limit = SINGLE_CHUNK_SAMPLE_BUFFER;
|
||||
} else {
|
||||
limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
}
|
||||
|
||||
int j = 0;
|
||||
for (; j < limit; j++) {
|
||||
|
@ -354,7 +370,7 @@ public class Mp4FromDashWriter {
|
|||
sizes[j] = sample.data.length;
|
||||
}
|
||||
|
||||
outWrite(sample.data, 0, sample.data.length);
|
||||
outWrite(sample.data, sample.data.length);
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
|
@ -368,12 +384,18 @@ public class Mp4FromDashWriter {
|
|||
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stco > 0) {
|
||||
if (is64) {
|
||||
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
||||
} else {
|
||||
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
||||
}
|
||||
|
||||
if (singleChunk) {
|
||||
tablesInfo[i].stco = -1;
|
||||
}
|
||||
}
|
||||
|
||||
outRestore();
|
||||
}
|
||||
}
|
||||
|
@ -404,7 +426,7 @@ public class Mp4FromDashWriter {
|
|||
}
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
||||
|
||||
private int writeEntry64(int offset, long value) throws IOException {
|
||||
outBackup();
|
||||
|
||||
|
@ -447,16 +469,16 @@ public class Mp4FromDashWriter {
|
|||
lastWriteOffset = -1;
|
||||
}
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Utils">
|
||||
|
||||
|
||||
private void outWrite(byte[] buffer) throws IOException {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
outWrite(buffer, buffer.length);
|
||||
}
|
||||
|
||||
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||
private void outWrite(byte[] buffer, int count) throws IOException {
|
||||
writeOffset += count;
|
||||
outStream.write(buffer, offset, count);
|
||||
outStream.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
private void outSeek(long offset) throws IOException {
|
||||
|
@ -509,7 +531,6 @@ public class Mp4FromDashWriter {
|
|||
);
|
||||
|
||||
if (extra >= 0) {
|
||||
//size += 4;// commented for auxiliar buffer !!!
|
||||
offset += 4;
|
||||
auxWrite(extra);
|
||||
}
|
||||
|
@ -531,7 +552,7 @@ public class Mp4FromDashWriter {
|
|||
if (moovSimulation) {
|
||||
writeOffset += buffer.length;
|
||||
} else if (auxBuffer == null) {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
outWrite(buffer, buffer.length);
|
||||
} else {
|
||||
auxBuffer.put(buffer);
|
||||
}
|
||||
|
@ -560,9 +581,9 @@ public class Mp4FromDashWriter {
|
|||
private int auxOffset() {
|
||||
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Box makers">
|
||||
|
||||
|
||||
private int make_ftyp() throws IOException {
|
||||
byte[] buffer = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
|
||||
|
@ -703,7 +724,7 @@ public class Mp4FromDashWriter {
|
|||
int mediaTime;
|
||||
|
||||
if (tracks[index].trak.edst_elst == null) {
|
||||
// is a audio track ¿is edst/elst opcional for audio tracks?
|
||||
// is a audio track ¿is edst/elst optional for audio tracks?
|
||||
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
|
||||
bMediaRate = 0x00010000;
|
||||
} else {
|
||||
|
@ -794,17 +815,17 @@ public class Mp4FromDashWriter {
|
|||
|
||||
return buffer.array();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
|
||||
class TablesInfo {
|
||||
|
||||
public int stts;
|
||||
public int stsc;
|
||||
public int[] stsc_bEntries;
|
||||
public int ctts;
|
||||
public int stsz;
|
||||
public int stsz_default;
|
||||
public int stss;
|
||||
public int stco;
|
||||
int stts;
|
||||
int stsc;
|
||||
int[] stsc_bEntries;
|
||||
int ctts;
|
||||
int stsz;
|
||||
int stsz_default;
|
||||
int stss;
|
||||
int stco;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,431 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
private static final byte FLAG_UNSET = 0x00;
|
||||
//private static final byte FLAG_CONTINUED = 0x01;
|
||||
private static final byte FLAG_FIRST = 0x02;
|
||||
private static final byte FLAG_LAST = 0x04;
|
||||
|
||||
private final static byte HEADER_CHECKSUM_OFFSET = 22;
|
||||
private final static byte HEADER_SIZE = 27;
|
||||
|
||||
private final static int TIME_SCALE_NS = 1000000000;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private SharpStream source;
|
||||
private SharpStream output;
|
||||
|
||||
private int sequence_count = 0;
|
||||
private final int STREAM_ID;
|
||||
private byte packet_flag = FLAG_FIRST;
|
||||
|
||||
private WebMReader webm = null;
|
||||
private WebMTrack webm_track = null;
|
||||
private Segment webm_segment = null;
|
||||
private Cluster webm_cluster = null;
|
||||
private SimpleBlock webm_block = null;
|
||||
|
||||
private long webm_block_last_timecode = 0;
|
||||
private long webm_block_near_duration = 0;
|
||||
|
||||
private short segment_table_size = 0;
|
||||
private final byte[] segment_table = new byte[255];
|
||||
private long segment_table_next_timestamp = TIME_SCALE_NS;
|
||||
|
||||
private final int[] crc32_table = new int[256];
|
||||
|
||||
public OggFromWebMWriter(@NonNull SharpStream source, @NonNull SharpStream target) {
|
||||
if (!source.canRead() || !source.canRewind()) {
|
||||
throw new IllegalArgumentException("source stream must be readable and allows seeking");
|
||||
}
|
||||
if (!target.canWrite() || !target.canRewind()) {
|
||||
throw new IllegalArgumentException("output stream must be writable and allows seeking");
|
||||
}
|
||||
|
||||
this.source = source;
|
||||
this.output = target;
|
||||
|
||||
this.STREAM_ID = (int) System.currentTimeMillis();
|
||||
|
||||
populate_crc32_table();
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public WebMTrack[] getTracksFromSource() throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("source must be parsed first");
|
||||
}
|
||||
|
||||
return webm.getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSource() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
webm = new WebMReader(source);
|
||||
webm.parse();
|
||||
webm_segment = webm.getNextSegment();
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTrack(int trackIndex) throws IOException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("source must be parsed first");
|
||||
}
|
||||
if (done) {
|
||||
throw new IOException("already done");
|
||||
}
|
||||
if (webm_track != null) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
switch (webm.getAvailableTracks()[trackIndex].kind) {
|
||||
case Audio:
|
||||
case Video:
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("the track must an audio or video stream");
|
||||
}
|
||||
|
||||
try {
|
||||
webm_track = webm.selectTrack(trackIndex);
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
webm_track = null;
|
||||
webm = null;
|
||||
|
||||
if (!output.isClosed()) {
|
||||
output.flush();
|
||||
}
|
||||
|
||||
source.close();
|
||||
output.close();
|
||||
}
|
||||
|
||||
public void build() throws IOException {
|
||||
float resolution;
|
||||
SimpleBlock bloq;
|
||||
ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255));
|
||||
ByteBuffer page = ByteBuffer.allocate(64 * 1024);
|
||||
|
||||
header.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
/* step 1: get the amount of frames per seconds */
|
||||
switch (webm_track.kind) {
|
||||
case Audio:
|
||||
resolution = getSampleFrequencyFromTrack(webm_track.bMetadata);
|
||||
if (resolution == 0f) {
|
||||
throw new RuntimeException("cannot get the audio sample rate");
|
||||
}
|
||||
break;
|
||||
case Video:
|
||||
// WARNING: untested
|
||||
if (webm_track.defaultDuration == 0) {
|
||||
throw new RuntimeException("missing default frame time");
|
||||
}
|
||||
resolution = 1000f / ((float) webm_track.defaultDuration / webm_segment.info.timecodeScale);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("not implemented");
|
||||
}
|
||||
|
||||
/* step 2: create packet with code init data */
|
||||
if (webm_track.codecPrivate != null) {
|
||||
addPacketSegment(webm_track.codecPrivate.length);
|
||||
make_packetHeader(0x00, header, webm_track.codecPrivate);
|
||||
write(header);
|
||||
output.write(webm_track.codecPrivate);
|
||||
}
|
||||
|
||||
/* step 3: create packet with metadata */
|
||||
byte[] buffer = make_metadata();
|
||||
if (buffer != null) {
|
||||
addPacketSegment(buffer.length);
|
||||
make_packetHeader(0x00, header, buffer);
|
||||
write(header);
|
||||
output.write(buffer);
|
||||
}
|
||||
|
||||
/* step 4: calculate amount of packets */
|
||||
while (webm_segment != null) {
|
||||
bloq = getNextBlock();
|
||||
|
||||
if (bloq != null && addPacketSegment(bloq)) {
|
||||
int pos = page.position();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
bloq.data.read(page.array(), pos, bloq.dataSize);
|
||||
page.position(pos + bloq.dataSize);
|
||||
continue;
|
||||
}
|
||||
|
||||
// calculate the current packet duration using the next block
|
||||
double elapsed_ns = webm_track.codecDelay;
|
||||
|
||||
if (bloq == null) {
|
||||
packet_flag = FLAG_LAST;// note: if the flag is FLAG_CONTINUED, is changed
|
||||
elapsed_ns += webm_block_last_timecode;
|
||||
|
||||
if (webm_track.defaultDuration > 0) {
|
||||
elapsed_ns += webm_track.defaultDuration;
|
||||
} else {
|
||||
// hardcoded way, guess the sample duration
|
||||
elapsed_ns += webm_block_near_duration;
|
||||
}
|
||||
} else {
|
||||
elapsed_ns += bloq.absoluteTimeCodeNs;
|
||||
}
|
||||
|
||||
// get the sample count in the page
|
||||
elapsed_ns = elapsed_ns / TIME_SCALE_NS;
|
||||
elapsed_ns = Math.ceil(elapsed_ns * resolution);
|
||||
|
||||
// create header and calculate page checksum
|
||||
int checksum = make_packetHeader((long) elapsed_ns, header, null);
|
||||
checksum = calc_crc32(checksum, page.array(), page.position());
|
||||
|
||||
header.putInt(HEADER_CHECKSUM_OFFSET, checksum);
|
||||
|
||||
// dump data
|
||||
write(header);
|
||||
write(page);
|
||||
|
||||
webm_block = bloq;
|
||||
}
|
||||
}
|
||||
|
||||
private int make_packetHeader(long gran_pos, @NonNull ByteBuffer buffer, byte[] immediate_page) {
|
||||
short length = HEADER_SIZE;
|
||||
|
||||
buffer.putInt(0x5367674f);// "OggS" binary string in little-endian
|
||||
buffer.put((byte) 0x00);// version
|
||||
buffer.put(packet_flag);// type
|
||||
|
||||
buffer.putLong(gran_pos);// granulate position
|
||||
|
||||
buffer.putInt(STREAM_ID);// bitstream serial number
|
||||
buffer.putInt(sequence_count++);// page sequence number
|
||||
|
||||
buffer.putInt(0x00);// page checksum
|
||||
|
||||
buffer.put((byte) segment_table_size);// segment table
|
||||
buffer.put(segment_table, 0, segment_table_size);// segment size
|
||||
|
||||
length += segment_table_size;
|
||||
|
||||
clearSegmentTable();// clear segment table for next header
|
||||
|
||||
int checksum_crc32 = calc_crc32(0x00, buffer.array(), length);
|
||||
|
||||
if (immediate_page != null) {
|
||||
checksum_crc32 = calc_crc32(checksum_crc32, immediate_page, immediate_page.length);
|
||||
buffer.putInt(HEADER_CHECKSUM_OFFSET, checksum_crc32);
|
||||
segment_table_next_timestamp -= TIME_SCALE_NS;
|
||||
}
|
||||
|
||||
return checksum_crc32;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private byte[] make_metadata() {
|
||||
if ("A_OPUS".equals(webm_track.codecId)) {
|
||||
return new byte[]{
|
||||
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73,// "OpusTags" binary string
|
||||
0x07, 0x00, 0x00, 0x00,// writting application string size
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
|
||||
0x00, 0x00, 0x00, 0x00// additional tags count (zero means no tags)
|
||||
};
|
||||
} else if ("A_VORBIS".equals(webm_track.codecId)) {
|
||||
return new byte[]{
|
||||
0x03,// ????????
|
||||
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73,// "vorbis" binary string
|
||||
0x07, 0x00, 0x00, 0x00,// writting application string size
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
|
||||
0x01, 0x00, 0x00, 0x00,// additional tags count (zero means no tags)
|
||||
|
||||
/*
|
||||
// whole file duration (not implemented)
|
||||
0x44,// tag string size
|
||||
0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30,
|
||||
0x30, 0x2E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30
|
||||
*/
|
||||
0x0F,// tag string size
|
||||
0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, 0x44, 0x45, 0x52, 0x3D,// "ENCODER=" binary string
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65,// "NewPipe" binary string
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// ????????
|
||||
};
|
||||
}
|
||||
|
||||
// not implemented for the desired codec
|
||||
return null;
|
||||
}
|
||||
|
||||
private void write(ByteBuffer buffer) throws IOException {
|
||||
output.write(buffer.array(), 0, buffer.position());
|
||||
buffer.position(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Nullable
|
||||
private SimpleBlock getNextBlock() throws IOException {
|
||||
SimpleBlock res;
|
||||
|
||||
if (webm_block != null) {
|
||||
res = webm_block;
|
||||
webm_block = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (webm_segment == null) {
|
||||
webm_segment = webm.getNextSegment();
|
||||
if (webm_segment == null) {
|
||||
return null;// no more blocks in the selected track
|
||||
}
|
||||
}
|
||||
|
||||
if (webm_cluster == null) {
|
||||
webm_cluster = webm_segment.getNextCluster();
|
||||
if (webm_cluster == null) {
|
||||
webm_segment = null;
|
||||
return getNextBlock();
|
||||
}
|
||||
}
|
||||
|
||||
res = webm_cluster.getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
webm_cluster = null;
|
||||
return getNextBlock();
|
||||
}
|
||||
|
||||
webm_block_near_duration = res.absoluteTimeCodeNs - webm_block_last_timecode;
|
||||
webm_block_last_timecode = res.absoluteTimeCodeNs;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private float getSampleFrequencyFromTrack(byte[] bMetadata) {
|
||||
// hardcoded way
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bMetadata);
|
||||
|
||||
while (buffer.remaining() >= 6) {
|
||||
int id = buffer.getShort() & 0xFFFF;
|
||||
if (id == 0x0000B584) {
|
||||
return buffer.getFloat();
|
||||
}
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void clearSegmentTable() {
|
||||
segment_table_next_timestamp += TIME_SCALE_NS;
|
||||
packet_flag = FLAG_UNSET;
|
||||
segment_table_size = 0;
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(SimpleBlock block) {
|
||||
long timestamp = block.absoluteTimeCodeNs + webm_track.codecDelay;
|
||||
|
||||
if (timestamp >= segment_table_next_timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return addPacketSegment(block.dataSize);
|
||||
}
|
||||
|
||||
private boolean addPacketSegment(int size) {
|
||||
if (size > 65025) {
|
||||
throw new UnsupportedOperationException("page size cannot be larger than 65025");
|
||||
}
|
||||
|
||||
int available = (segment_table.length - segment_table_size) * 255;
|
||||
boolean extra = (size % 255) == 0;
|
||||
|
||||
if (extra) {
|
||||
// add a zero byte entry in the table
|
||||
// required to indicate the sample size is multiple of 255
|
||||
available -= 255;
|
||||
}
|
||||
|
||||
// check if possible add the segment, without overflow the table
|
||||
if (available < size) {
|
||||
return false;// not enough space on the page
|
||||
}
|
||||
|
||||
for (; size > 0; size -= 255) {
|
||||
segment_table[segment_table_size++] = (byte) Math.min(size, 255);
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
segment_table[segment_table_size++] = 0x00;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void populate_crc32_table() {
|
||||
for (int i = 0; i < 0x100; i++) {
|
||||
int crc = i << 24;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
long b = crc >>> 31;
|
||||
crc <<= 1;
|
||||
crc ^= (int) (0x100000000L - b) & 0x04c11db7;
|
||||
}
|
||||
crc32_table[i] = crc;
|
||||
}
|
||||
}
|
||||
|
||||
private int calc_crc32(int initial_crc, byte[] buffer, int size) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
int reg = (initial_crc >>> 24) & 0xff;
|
||||
initial_crc = (initial_crc << 8) ^ crc32_table[reg ^ (buffer[i] & 0xff)];
|
||||
}
|
||||
|
||||
return initial_crc;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
|
|||
*/
|
||||
public class WebMReader {
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="constants">
|
||||
private final static int ID_EMBL = 0x0A45DFA3;
|
||||
private final static int ID_EMBLReadVersion = 0x02F7;
|
||||
private final static int ID_EMBLDocType = 0x0282;
|
||||
|
@ -37,11 +36,14 @@ public class WebMReader {
|
|||
private final static int ID_Audio = 0x61;
|
||||
private final static int ID_DefaultDuration = 0x3E383;
|
||||
private final static int ID_FlagLacing = 0x1C;
|
||||
private final static int ID_CodecDelay = 0x16AA;
|
||||
|
||||
private final static int ID_Cluster = 0x0F43B675;
|
||||
private final static int ID_Timecode = 0x67;
|
||||
private final static int ID_SimpleBlock = 0x23;
|
||||
//</editor-fold>
|
||||
private final static int ID_Block = 0x21;
|
||||
private final static int ID_GroupBlock = 0x20;
|
||||
|
||||
|
||||
public enum TrackKind {
|
||||
Audio/*2*/, Video/*1*/, Other
|
||||
|
@ -96,7 +98,7 @@ public class WebMReader {
|
|||
}
|
||||
|
||||
ensure(segment.ref);
|
||||
|
||||
// WARNING: track cannot be the same or have different index in new segments
|
||||
Element elem = untilElement(null, ID_Segment);
|
||||
if (elem == null) {
|
||||
done = true;
|
||||
|
@ -107,7 +109,8 @@ public class WebMReader {
|
|||
return segment;
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="utils">
|
||||
|
||||
|
||||
private long readNumber(Element parent) throws IOException {
|
||||
int length = (int) parent.contentSize;
|
||||
long value = 0;
|
||||
|
@ -189,6 +192,9 @@ public class WebMReader {
|
|||
Element elem;
|
||||
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
||||
elem = readElement();
|
||||
if (expected.length < 1) {
|
||||
return elem;
|
||||
}
|
||||
for (int type : expected) {
|
||||
if (elem.type == type) {
|
||||
return elem;
|
||||
|
@ -219,9 +225,9 @@ public class WebMReader {
|
|||
|
||||
stream.skipBytes(skip);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="elements readers">
|
||||
|
||||
|
||||
private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException {
|
||||
Element elem = untilElement(ref, ID_EMBLReadVersion);
|
||||
if (elem == null) {
|
||||
|
@ -300,9 +306,7 @@ public class WebMReader {
|
|||
WebMTrack entry = new WebMTrack();
|
||||
boolean drop = false;
|
||||
Element elem;
|
||||
while ((elem = untilElement(elem_trackEntry,
|
||||
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
|
||||
)) != null) {
|
||||
while ((elem = untilElement(elem_trackEntry)) != null) {
|
||||
switch (elem.type) {
|
||||
case ID_TrackNumber:
|
||||
entry.trackNumber = readNumber(elem);
|
||||
|
@ -326,8 +330,9 @@ public class WebMReader {
|
|||
case ID_FlagLacing:
|
||||
drop = readNumber(elem) != lacingExpected;
|
||||
break;
|
||||
case ID_CodecDelay:
|
||||
entry.codecDelay = readNumber(elem);
|
||||
default:
|
||||
System.out.println();
|
||||
break;
|
||||
}
|
||||
ensure(elem);
|
||||
|
@ -360,12 +365,13 @@ public class WebMReader {
|
|||
|
||||
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
|
||||
SimpleBlock obj = new SimpleBlock(ref);
|
||||
obj.dataSize = stream.position();
|
||||
obj.trackNumber = readEncodedNumber();
|
||||
obj.relativeTimeCode = stream.readShort();
|
||||
obj.flags = (byte) stream.read();
|
||||
obj.dataSize = (ref.offset + ref.size) - stream.position();
|
||||
obj.dataSize = (int) ((ref.offset + ref.size) - stream.position());
|
||||
obj.createdFromBlock = ref.type == ID_Block;
|
||||
|
||||
// NOTE: lacing is not implemented, and will be mixed with the stream data
|
||||
if (obj.dataSize < 0) {
|
||||
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
||||
}
|
||||
|
@ -383,9 +389,9 @@ public class WebMReader {
|
|||
|
||||
return obj;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="class helpers">
|
||||
|
||||
|
||||
class Element {
|
||||
|
||||
int type;
|
||||
|
@ -409,6 +415,7 @@ public class WebMReader {
|
|||
public byte[] bMetadata;
|
||||
public TrackKind kind;
|
||||
public long defaultDuration;
|
||||
public long codecDelay;
|
||||
}
|
||||
|
||||
public class Segment {
|
||||
|
@ -448,6 +455,7 @@ public class WebMReader {
|
|||
public class SimpleBlock {
|
||||
|
||||
public InputStream data;
|
||||
public boolean createdFromBlock;
|
||||
|
||||
SimpleBlock(Element ref) {
|
||||
this.ref = ref;
|
||||
|
@ -455,8 +463,9 @@ public class WebMReader {
|
|||
|
||||
public long trackNumber;
|
||||
public short relativeTimeCode;
|
||||
public long absoluteTimeCodeNs;
|
||||
public byte flags;
|
||||
public long dataSize;
|
||||
public int dataSize;
|
||||
private final Element ref;
|
||||
|
||||
public boolean isKeyframe() {
|
||||
|
@ -468,33 +477,55 @@ public class WebMReader {
|
|||
|
||||
Element ref;
|
||||
SimpleBlock currentSimpleBlock = null;
|
||||
Element currentBlockGroup = null;
|
||||
public long timecode;
|
||||
|
||||
Cluster(Element ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
boolean check() {
|
||||
boolean insideClusterBounds() {
|
||||
return stream.position() >= (ref.offset + ref.size);
|
||||
}
|
||||
|
||||
public SimpleBlock getNextSimpleBlock() throws IOException {
|
||||
if (check()) {
|
||||
if (insideClusterBounds()) {
|
||||
return null;
|
||||
}
|
||||
if (currentSimpleBlock != null) {
|
||||
|
||||
if (currentBlockGroup != null) {
|
||||
ensure(currentBlockGroup);
|
||||
currentBlockGroup = null;
|
||||
currentSimpleBlock = null;
|
||||
} else if (currentSimpleBlock != null) {
|
||||
ensure(currentSimpleBlock.ref);
|
||||
}
|
||||
|
||||
while (!check()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock);
|
||||
while (!insideClusterBounds()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock, ID_GroupBlock);
|
||||
if (elem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (elem.type == ID_GroupBlock) {
|
||||
currentBlockGroup = elem;
|
||||
elem = untilElement(currentBlockGroup, ID_Block);
|
||||
|
||||
if (elem == null) {
|
||||
ensure(currentBlockGroup);
|
||||
currentBlockGroup = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentSimpleBlock = readSimpleBlock(elem);
|
||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
||||
|
||||
// calculate the timestamp in nanoseconds
|
||||
currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode + this.timecode;
|
||||
currentSimpleBlock.absoluteTimeCodeNs *= segment.info.timecodeScale;
|
||||
|
||||
return currentSimpleBlock;
|
||||
}
|
||||
|
||||
|
@ -505,5 +536,5 @@ public class WebMReader {
|
|||
}
|
||||
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
|||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
@ -17,7 +18,7 @@ import java.util.ArrayList;
|
|||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMWriter {
|
||||
public class WebMWriter implements Closeable {
|
||||
|
||||
private final static int BUFFER_SIZE = 8 * 1024;
|
||||
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
|
||||
|
@ -35,7 +36,7 @@ public class WebMWriter {
|
|||
private long written = 0;
|
||||
|
||||
private Segment[] readersSegment;
|
||||
private Cluster[] readersCluter;
|
||||
private Cluster[] readersCluster;
|
||||
|
||||
private int[] predefinedDurations;
|
||||
|
||||
|
@ -81,7 +82,7 @@ public class WebMWriter {
|
|||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
try {
|
||||
readersSegment = new Segment[readers.length];
|
||||
readersCluter = new Cluster[readers.length];
|
||||
readersCluster = new Cluster[readers.length];
|
||||
predefinedDurations = new int[readers.length];
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
|
@ -102,6 +103,7 @@ public class WebMWriter {
|
|||
return parsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
@ -114,7 +116,7 @@ public class WebMWriter {
|
|||
readers = null;
|
||||
infoTracks = null;
|
||||
readersSegment = null;
|
||||
readersCluter = null;
|
||||
readersCluster = null;
|
||||
outBuffer = null;
|
||||
}
|
||||
|
||||
|
@ -247,7 +249,7 @@ public class WebMWriter {
|
|||
nextCueTime += DEFAULT_CUES_EACH_MS;
|
||||
}
|
||||
keyFrames.add(
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 8, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -334,17 +336,17 @@ public class WebMWriter {
|
|||
}
|
||||
}
|
||||
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
if (readersCluster[internalTrackId] == null) {
|
||||
readersCluster[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluster[internalTrackId] == null) {
|
||||
readersSegment[internalTrackId] = null;
|
||||
return getNextBlockFrom(internalTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
|
||||
SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
readersCluter[internalTrackId] = null;
|
||||
readersCluster[internalTrackId] = null;
|
||||
return new Block();// fake block to indicate the end of the cluster
|
||||
}
|
||||
|
||||
|
@ -353,16 +355,11 @@ public class WebMWriter {
|
|||
bloq.dataSize = (int) res.dataSize;
|
||||
bloq.trackNumber = internalTrackId;
|
||||
bloq.flags = res.flags;
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
||||
bloq.absoluteTimecode = res.absoluteTimeCodeNs / DEFAULT_TIMECODE_SCALE;
|
||||
|
||||
return bloq;
|
||||
}
|
||||
|
||||
private short convertTimecode(int time, long oldTimeScale) {
|
||||
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
|
||||
}
|
||||
|
||||
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||
if (stream.canSeek()) {
|
||||
stream.seek(offset);
|
||||
|
|
|
@ -32,7 +32,7 @@ import org.schabi.newpipe.extractor.Info;
|
|||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
|
|
|
@ -31,6 +31,12 @@ public class KioskTranslator {
|
|||
return c.getString(R.string.top_50);
|
||||
case "New & hot":
|
||||
return c.getString(R.string.new_and_hot);
|
||||
case "Local":
|
||||
return c.getString(R.string.local);
|
||||
case "Recently added":
|
||||
return c.getString(R.string.recently_added);
|
||||
case "Most liked":
|
||||
return c.getString(R.string.most_liked);
|
||||
case "conferences":
|
||||
return c.getString(R.string.conferences);
|
||||
default:
|
||||
|
@ -46,6 +52,12 @@ public class KioskTranslator {
|
|||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "New & hot":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "Local":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local);
|
||||
case "Recently added":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent);
|
||||
case "Most liked":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.thumbs_up);
|
||||
case "conferences":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
default:
|
||||
|
|
|
@ -2,24 +2,26 @@ package org.schabi.newpipe.util;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
import org.ocpsoft.prettytime.units.Decade;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
/*
|
||||
* Created by chschtsch on 12/29/15.
|
||||
*
|
||||
|
@ -42,11 +44,16 @@ import java.util.Locale;
|
|||
|
||||
public class Localization {
|
||||
|
||||
public final static String DOT_SEPARATOR = " • ";
|
||||
private static PrettyTime prettyTime;
|
||||
private static final String DOT_SEPARATOR = " • ";
|
||||
|
||||
private Localization() {
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
initPrettyTime();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String concatenateStrings(final String... strings) {
|
||||
return concatenateStrings(Arrays.asList(strings));
|
||||
|
@ -69,16 +76,18 @@ public class Localization {
|
|||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static org.schabi.newpipe.extractor.utils.Localization getPreferredExtractorLocal(Context context) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization(final Context context) {
|
||||
final String contentLanguage = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.content_language_key), context.getString(R.string.default_language_value));
|
||||
return org.schabi.newpipe.extractor.localization.Localization.fromLocalizationCode(contentLanguage);
|
||||
}
|
||||
|
||||
String languageCode = sp.getString(context.getString(R.string.content_language_key),
|
||||
context.getString(R.string.default_language_value));
|
||||
|
||||
String countryCode = sp.getString(context.getString(R.string.content_country_key),
|
||||
context.getString(R.string.default_country_value));
|
||||
|
||||
return new org.schabi.newpipe.extractor.utils.Localization(countryCode, languageCode);
|
||||
public static ContentCountry getPreferredContentCountry(final Context context) {
|
||||
final String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.content_country_key), context.getString(R.string.default_country_value));
|
||||
return new ContentCountry(contentCountry);
|
||||
}
|
||||
|
||||
public static Locale getPreferredLocale(Context context) {
|
||||
|
@ -106,27 +115,12 @@ public class Localization {
|
|||
return nf.format(number);
|
||||
}
|
||||
|
||||
private static String formatDate(Context context, String date) {
|
||||
Locale locale = getPreferredLocale(context);
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Date datum = null;
|
||||
try {
|
||||
datum = formatter.parse(date);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
public static String formatDate(Date date) {
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(date);
|
||||
}
|
||||
|
||||
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
|
||||
|
||||
return df.format(datum);
|
||||
}
|
||||
|
||||
public static String localizeDate(Context context, String date) {
|
||||
Resources res = context.getResources();
|
||||
String dateString = res.getString(R.string.upload_date_text);
|
||||
|
||||
String formattedDate = formatDate(context, date);
|
||||
return String.format(dateString, formattedDate);
|
||||
public static String localizeUploadDate(Context context, Date date) {
|
||||
return context.getString(R.string.upload_date_text, formatDate(date));
|
||||
}
|
||||
|
||||
public static String localizeViewCount(Context context, long viewCount) {
|
||||
|
@ -153,6 +147,14 @@ public class Localization {
|
|||
}
|
||||
}
|
||||
|
||||
public static String listeningCount(Context context, long listeningCount) {
|
||||
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount, shortCount(context, listeningCount));
|
||||
}
|
||||
|
||||
public static String watchingCount(Context context, long watchingCount) {
|
||||
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, shortCount(context, watchingCount));
|
||||
}
|
||||
|
||||
public static String shortViewCount(Context context, long viewCount) {
|
||||
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, shortCount(context, viewCount));
|
||||
}
|
||||
|
@ -192,4 +194,26 @@ public class Localization {
|
|||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Pretty Time
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static void initPrettyTime() {
|
||||
prettyTime = new PrettyTime(Locale.getDefault());
|
||||
// Do not use decades as YouTube doesn't either.
|
||||
prettyTime.removeUnit(Decade.class);
|
||||
}
|
||||
|
||||
private static PrettyTime getPrettyTime() {
|
||||
// If pretty time's Locale is different, init again with the new one.
|
||||
if (!prettyTime.getLocale().equals(Locale.getDefault())) {
|
||||
initPrettyTime();
|
||||
}
|
||||
return prettyTime;
|
||||
}
|
||||
|
||||
public static String relativeTime(Calendar calendarTime) {
|
||||
return getPrettyTime().formatUnrounded(calendarTime);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PeertubeHelper {
|
||||
|
||||
public static List<PeertubeInstance> getInstanceList(Context context) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
|
||||
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
|
||||
if (null == savedJson) {
|
||||
return Collections.singletonList(getCurrentInstance());
|
||||
}
|
||||
|
||||
try {
|
||||
JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
|
||||
List<PeertubeInstance> result = new ArrayList<>();
|
||||
for (Object o : array) {
|
||||
if (o instanceof JsonObject) {
|
||||
JsonObject instance = (JsonObject) o;
|
||||
String name = instance.getString("name");
|
||||
String url = instance.getString("url");
|
||||
result.add(new PeertubeInstance(url, name));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (JsonParserException e) {
|
||||
return Collections.singletonList(getCurrentInstance());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static PeertubeInstance selectInstance(PeertubeInstance instance, Context context) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key);
|
||||
JsonStringWriter jsonWriter = JsonWriter.string().object();
|
||||
jsonWriter.value("name", instance.getName());
|
||||
jsonWriter.value("url", instance.getUrl());
|
||||
String jsonToSave = jsonWriter.end().done();
|
||||
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
|
||||
ServiceList.PeerTube.setInstance(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static PeertubeInstance getCurrentInstance(){
|
||||
return ServiceList.PeerTube.getInstance();
|
||||
}
|
||||
}
|
|
@ -52,10 +52,12 @@ public class SecondaryStreamHelper<T extends Stream> {
|
|||
}
|
||||
}
|
||||
|
||||
if (m4v) return null;
|
||||
|
||||
// retry, but this time in reverse order
|
||||
for (int i = audioStreams.size() - 1; i >= 0; i--) {
|
||||
AudioStream audio = audioStreams.get(i);
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
|
||||
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -27,13 +34,15 @@ public class ServiceHelper {
|
|||
return R.drawable.place_holder_cloud;
|
||||
case 2:
|
||||
return R.drawable.place_holder_gadse;
|
||||
case 3:
|
||||
return R.drawable.place_holder_peertube;
|
||||
default:
|
||||
return R.drawable.place_holder_circle;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTranslatedFilterString(String filter, Context c) {
|
||||
switch(filter) {
|
||||
switch (filter) {
|
||||
case "all": return c.getString(R.string.all);
|
||||
case "videos": return c.getString(R.string.videos);
|
||||
case "channels": return c.getString(R.string.channels);
|
||||
|
@ -126,9 +135,36 @@ public class ServiceHelper {
|
|||
}
|
||||
|
||||
public static boolean isBeta(final StreamingService s) {
|
||||
switch(s.getServiceInfo().getName()) {
|
||||
switch (s.getServiceInfo().getName()) {
|
||||
case "YouTube": return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initService(Context context, int serviceId) {
|
||||
if (serviceId == ServiceList.PeerTube.getServiceId()) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String json = sharedPreferences.getString(context.getString(R.string.peertube_selected_instance_key), null);
|
||||
if (null == json) {
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject jsonObject = null;
|
||||
try {
|
||||
jsonObject = JsonParser.object().from(json);
|
||||
} catch (JsonParserException e) {
|
||||
return;
|
||||
}
|
||||
String name = jsonObject.getString("name");
|
||||
String url = jsonObject.getString("url");
|
||||
PeertubeInstance instance = new PeertubeInstance(url, name);
|
||||
ServiceList.PeerTube.setInstance(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public static void initServices(Context context) {
|
||||
for (StreamingService s : ServiceList.all()) {
|
||||
initService(context, s.getServiceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.widget.ImageView;
|
|||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
|
@ -182,7 +182,7 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
|||
continue;
|
||||
}
|
||||
|
||||
final long contentLength = Downloader.getInstance().getContentLength(stream.getUrl());
|
||||
final long contentLength = DownloaderImpl.getInstance().getContentLength(stream.getUrl());
|
||||
streamsWrapper.setSize(stream, contentLength);
|
||||
hasChanged = true;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
|
||||
/**
|
||||
* This is an extension of the SSLSocketFactory which enables TLS 1.2 and 1.1.
|
||||
* Created for usage on Android 4.1-4.4 devices, which haven't enabled those by default.
|
||||
*/
|
||||
public class TLSSocketFactoryCompat extends SSLSocketFactory {
|
||||
|
||||
|
||||
private static TLSSocketFactoryCompat instance = null;
|
||||
|
||||
private SSLSocketFactory internalSSLSocketFactory;
|
||||
|
||||
public static TLSSocketFactoryCompat getInstance() throws NoSuchAlgorithmException, KeyManagementException {
|
||||
if (instance != null) {
|
||||
return instance;
|
||||
}
|
||||
return instance = new TLSSocketFactoryCompat();
|
||||
}
|
||||
|
||||
|
||||
public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmException {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, null, null);
|
||||
internalSSLSocketFactory = context.getSocketFactory();
|
||||
}
|
||||
|
||||
public TLSSocketFactoryCompat(TrustManager[] tm) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, tm, new java.security.SecureRandom());
|
||||
internalSSLSocketFactory = context.getSocketFactory();
|
||||
}
|
||||
|
||||
public static void setAsDefault() {
|
||||
try {
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(getInstance());
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return internalSSLSocketFactory.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return internalSSLSocketFactory.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private Socket enableTLSOnSocket(Socket socket) {
|
||||
if (socket != null && (socket instanceof SSLSocket)) {
|
||||
((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"});
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -13,6 +15,7 @@ import java.nio.channels.ClosedByInterruptException;
|
|||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
public class DownloadInitializer extends Thread {
|
||||
private final static String TAG = "DownloadInitializer";
|
||||
|
@ -28,9 +31,9 @@ public class DownloadInitializer extends Thread {
|
|||
mConn = null;
|
||||
}
|
||||
|
||||
private static void safeClose(HttpURLConnection con) {
|
||||
private void dispose() {
|
||||
try {
|
||||
con.getInputStream().close();
|
||||
mConn.getInputStream().close();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
@ -51,9 +54,9 @@ public class DownloadInitializer extends Thread {
|
|||
long lowestSize = Long.MAX_VALUE;
|
||||
|
||||
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
|
||||
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
|
||||
mConn = mMission.openConnection(mMission.urls[i], true, -1, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (Thread.interrupted()) return;
|
||||
long length = Utility.getContentLength(mConn);
|
||||
|
@ -81,9 +84,9 @@ public class DownloadInitializer extends Thread {
|
|||
}
|
||||
} else {
|
||||
// ask for the current resource length
|
||||
mConn = mMission.openConnection(mId, -1, -1);
|
||||
mConn = mMission.openConnection(true, -1, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
|
@ -107,9 +110,9 @@ public class DownloadInitializer extends Thread {
|
|||
}
|
||||
} else {
|
||||
// Open again
|
||||
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
||||
mConn = mMission.openConnection(true, mMission.length - 10, mMission.length);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
safeClose(mConn);
|
||||
dispose();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
|
@ -151,12 +154,33 @@ public class DownloadInitializer extends Thread {
|
|||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
if (!mMission.unknownLength && mMission.recoveryInfo != null) {
|
||||
String entityTag = mConn.getHeaderField("ETAG");
|
||||
String lastModified = mConn.getHeaderField("Last-Modified");
|
||||
MissionRecoveryInfo recovery = mMission.recoveryInfo[mMission.current];
|
||||
|
||||
if (!TextUtils.isEmpty(entityTag)) {
|
||||
recovery.validateCondition = entityTag;
|
||||
} else if (!TextUtils.isEmpty(lastModified)) {
|
||||
recovery.validateCondition = lastModified;// Note: this is less precise
|
||||
} else {
|
||||
recovery.validateCondition = null;
|
||||
}
|
||||
}
|
||||
|
||||
mMission.running = false;
|
||||
break;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running) return;
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired
|
||||
interrupt();
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
||||
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
||||
|
@ -179,13 +203,6 @@ public class DownloadInitializer extends Thread {
|
|||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
|
||||
if (mConn != null) {
|
||||
try {
|
||||
mConn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
if (mConn != null) dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.ConnectException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
|
@ -27,14 +33,11 @@ import us.shandian.giga.util.Utility;
|
|||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadMission extends Mission {
|
||||
private static final long serialVersionUID = 5L;// last bump: 30 june 2019
|
||||
private static final long serialVersionUID = 6L;// last bump: 07 october 2019
|
||||
|
||||
static final int BUFFER_SIZE = 64 * 1024;
|
||||
static final int BLOCK_SIZE = 512 * 1024;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private static final String INSUFFICIENT_STORAGE = "ENOSPC";
|
||||
|
||||
private static final String TAG = "DownloadMission";
|
||||
|
||||
public static final int ERROR_NOTHING = -1;
|
||||
|
@ -51,8 +54,9 @@ public class DownloadMission extends Mission {
|
|||
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
||||
public static final int ERROR_PROGRESS_LOST = 1011;
|
||||
public static final int ERROR_TIMEOUT = 1012;
|
||||
public static final int ERROR_RESOURCE_GONE = 1013;
|
||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||
static final int ERROR_HTTP_FORBIDDEN = 403;
|
||||
|
||||
/**
|
||||
* The urls of the file to download
|
||||
|
@ -60,9 +64,9 @@ public class DownloadMission extends Mission {
|
|||
public String[] urls;
|
||||
|
||||
/**
|
||||
* Number of bytes downloaded
|
||||
* Number of bytes downloaded and written
|
||||
*/
|
||||
public long done;
|
||||
public volatile long done;
|
||||
|
||||
/**
|
||||
* Indicates a file generated dynamically on the web server
|
||||
|
@ -118,31 +122,36 @@ public class DownloadMission extends Mission {
|
|||
/**
|
||||
* Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback}
|
||||
*/
|
||||
long fallbackResumeOffset;
|
||||
volatile long fallbackResumeOffset;
|
||||
|
||||
/**
|
||||
* Maximum of download threads running, chosen by the user
|
||||
*/
|
||||
public int threadCount = 3;
|
||||
|
||||
/**
|
||||
* information required to recover a download
|
||||
*/
|
||||
public MissionRecoveryInfo[] recoveryInfo;
|
||||
|
||||
private transient int finishCount;
|
||||
public transient boolean running;
|
||||
public transient volatile boolean running;
|
||||
public boolean enqueued;
|
||||
|
||||
public int errCode = ERROR_NOTHING;
|
||||
public Exception errObject = null;
|
||||
|
||||
public transient boolean recovered;
|
||||
public transient Handler mHandler;
|
||||
private transient boolean mWritingToFile;
|
||||
private transient boolean[] blockAcquired;
|
||||
|
||||
private transient long writingToFileNext;
|
||||
private transient volatile boolean writingToFile;
|
||||
|
||||
final Object LOCK = new Lock();
|
||||
|
||||
private transient boolean deleted;
|
||||
|
||||
public transient volatile Thread[] threads = new Thread[0];
|
||||
private transient Thread init = null;
|
||||
@NonNull
|
||||
public transient Thread[] threads = new Thread[0];
|
||||
public transient Thread init = null;
|
||||
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||
if (urls == null) throw new NullPointerException("urls is null");
|
||||
|
@ -197,37 +206,34 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
|
||||
/**
|
||||
* Open connection
|
||||
* Opens a connection
|
||||
*
|
||||
* @param threadId id of the calling thread, used only for debug
|
||||
* @param headRequest {@code true} for use {@code HEAD} request method, otherwise, {@code GET} is used
|
||||
* @param rangeStart range start
|
||||
* @param rangeEnd range end
|
||||
* @return a {@link java.net.URLConnection URLConnection} linking to the URL.
|
||||
* @throws IOException if an I/O exception occurs.
|
||||
*/
|
||||
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||
return openConnection(urls[current], threadId, rangeStart, rangeEnd);
|
||||
HttpURLConnection openConnection(boolean headRequest, long rangeStart, long rangeEnd) throws IOException {
|
||||
return openConnection(urls[current], headRequest, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||
HttpURLConnection openConnection(String url, boolean headRequest, long rangeStart, long rangeEnd) throws IOException {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
conn.setRequestProperty("User-Agent", Downloader.USER_AGENT);
|
||||
conn.setRequestProperty("User-Agent", DownloaderImpl.USER_AGENT);
|
||||
conn.setRequestProperty("Accept", "*/*");
|
||||
|
||||
if (headRequest) conn.setRequestMethod("HEAD");
|
||||
|
||||
// BUG workaround: switching between networks can freeze the download forever
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(10000);
|
||||
|
||||
if (rangeStart >= 0) {
|
||||
String req = "bytes=" + rangeStart + "-";
|
||||
if (rangeEnd > 0) req += rangeEnd;
|
||||
|
||||
conn.setRequestProperty("Range", req);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range"));
|
||||
}
|
||||
}
|
||||
|
||||
return conn;
|
||||
|
@ -240,18 +246,21 @@ public class DownloadMission extends Mission {
|
|||
* @throws HttpError if the HTTP Status-Code is not satisfiable
|
||||
*/
|
||||
void establishConnection(int threadId, HttpURLConnection conn) throws IOException, HttpError {
|
||||
conn.connect();
|
||||
int statusCode = conn.getResponseCode();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode);
|
||||
Log.d(TAG, threadId + ":[request] Range=" + conn.getRequestProperty("Range"));
|
||||
Log.d(TAG, threadId + ":[response] Code=" + statusCode);
|
||||
Log.d(TAG, threadId + ":[response] Content-Length=" + conn.getContentLength());
|
||||
Log.d(TAG, threadId + ":[response] Content-Range=" + conn.getHeaderField("Content-Range"));
|
||||
}
|
||||
|
||||
|
||||
switch (statusCode) {
|
||||
case 204:
|
||||
case 205:
|
||||
case 207:
|
||||
throw new HttpError(conn.getResponseCode());
|
||||
throw new HttpError(statusCode);
|
||||
case 416:
|
||||
return;// let the download thread handle this error
|
||||
default:
|
||||
|
@ -268,28 +277,19 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
|
||||
synchronized void notifyProgress(long deltaLen) {
|
||||
if (!running) return;
|
||||
|
||||
if (recovered) {
|
||||
recovered = false;
|
||||
}
|
||||
|
||||
if (unknownLength) {
|
||||
length += deltaLen;// Update length before proceeding
|
||||
}
|
||||
|
||||
done += deltaLen;
|
||||
|
||||
if (done > length) {
|
||||
done = length;
|
||||
}
|
||||
if (metadata == null) return;
|
||||
|
||||
if (done != length && !deleted && !mWritingToFile) {
|
||||
mWritingToFile = true;
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) {
|
||||
writingToFile = true;
|
||||
writingToFileNext = done + BLOCK_SIZE;
|
||||
writeThisToFileAsync();
|
||||
}
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
synchronized void notifyError(Exception err) {
|
||||
|
@ -314,13 +314,29 @@ public class DownloadMission extends Mission {
|
|||
|
||||
public synchronized void notifyError(int code, Exception err) {
|
||||
Log.e(TAG, "notifyError() code = " + code, err);
|
||||
if (err instanceof IOException) {
|
||||
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (err != null && err.getCause() instanceof ErrnoException) {
|
||||
int errno = ((ErrnoException) err.getCause()).errno;
|
||||
if (errno == OsConstants.ENOSPC) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
} else if (errno == OsConstants.EACCES) {
|
||||
code = ERROR_PERMISSION_DENIED;
|
||||
err = null;
|
||||
} else if (err.getMessage().contains(INSUFFICIENT_STORAGE)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof IOException) {
|
||||
if (err.getMessage().contains("Permission denied")) {
|
||||
code = ERROR_PERMISSION_DENIED;
|
||||
err = null;
|
||||
} else if (err.getMessage().contains("ENOSPC")) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
} else if (!storage.canWrite()) {
|
||||
code = ERROR_FILE_CREATION;
|
||||
err = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,45 +358,43 @@ public class DownloadMission extends Mission {
|
|||
|
||||
notify(DownloadManagerService.MESSAGE_ERROR);
|
||||
|
||||
if (running) {
|
||||
running = false;
|
||||
recovered = true;
|
||||
if (threads != null) selfPause();
|
||||
}
|
||||
if (running) pauseThreads();
|
||||
}
|
||||
|
||||
synchronized void notifyFinished() {
|
||||
if (errCode > ERROR_NOTHING) return;
|
||||
|
||||
finishCount++;
|
||||
|
||||
if (blocks.length < 1 || threads == null || finishCount == threads.length) {
|
||||
if (errCode != ERROR_NOTHING) return;
|
||||
if (current < urls.length) {
|
||||
if (++finishCount < threads.length) return;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length);
|
||||
}
|
||||
|
||||
if ((current + 1) < urls.length) {
|
||||
// prepare next sub-mission
|
||||
long current_offset = offsets[current++];
|
||||
offsets[current] = current_offset + length;
|
||||
initializer();
|
||||
return;
|
||||
Log.d(TAG, "onFinish: downloaded " + (current + 1) + "/" + urls.length);
|
||||
}
|
||||
|
||||
current++;
|
||||
if (current < urls.length) {
|
||||
// prepare next sub-mission
|
||||
offsets[current] = offsets[current - 1] + length;
|
||||
initializer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (psAlgorithm != null && psState == 0) {
|
||||
threads = new Thread[]{
|
||||
runAsync(1, this::doPostprocessing)
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// this mission is fully finished
|
||||
|
||||
unknownLength = false;
|
||||
|
||||
if (!doPostprocessing()) return;
|
||||
|
||||
enqueued = false;
|
||||
running = false;
|
||||
deleteThisFromFile();
|
||||
|
||||
deleteThisFromFile();
|
||||
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyPostProcessing(int state) {
|
||||
String action;
|
||||
|
@ -397,10 +411,15 @@ public class DownloadMission extends Mission {
|
|||
|
||||
Log.d(TAG, action + " postprocessing on " + storage.getName());
|
||||
|
||||
if (state == 2) {
|
||||
psState = state;
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
// don't return without fully write the current state
|
||||
psState = state;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
writeThisToFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,14 +428,10 @@ public class DownloadMission extends Mission {
|
|||
* Start downloading with multiple threads.
|
||||
*/
|
||||
public void start() {
|
||||
if (running || isFinished()) return;
|
||||
if (running || isFinished() || urls.length < 1) return;
|
||||
|
||||
// ensure that the previous state is completely paused.
|
||||
joinForThread(init);
|
||||
if (threads != null) {
|
||||
for (Thread thread : threads) joinForThread(thread);
|
||||
threads = null;
|
||||
}
|
||||
joinForThreads(10000);
|
||||
|
||||
running = true;
|
||||
errCode = ERROR_NOTHING;
|
||||
|
@ -427,7 +442,14 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
|
||||
if (current >= urls.length) {
|
||||
runAsync(1, this::notifyFinished);
|
||||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_RUNNING);
|
||||
|
||||
if (urls[current] == null) {
|
||||
doRecover(ERROR_RESOURCE_GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -441,18 +463,13 @@ public class DownloadMission extends Mission {
|
|||
blockAcquired = new boolean[blocks.length];
|
||||
|
||||
if (blocks.length < 1) {
|
||||
if (unknownLength) {
|
||||
done = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))};
|
||||
} else {
|
||||
int remainingBlocks = 0;
|
||||
for (int block : blocks) if (block >= 0) remainingBlocks++;
|
||||
|
||||
if (remainingBlocks < 1) {
|
||||
runAsync(1, this::notifyFinished);
|
||||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -478,7 +495,7 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
|
||||
running = false;
|
||||
recovered = true;
|
||||
notify(DownloadManagerService.MESSAGE_PAUSED);
|
||||
|
||||
if (init != null && init.isAlive()) {
|
||||
// NOTE: if start() method is running ¡will no have effect!
|
||||
|
@ -493,39 +510,25 @@ public class DownloadMission extends Mission {
|
|||
Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server).");
|
||||
}
|
||||
|
||||
// check if the calling thread (alias UI thread) is interrupted
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
writeThisToFile();
|
||||
return;
|
||||
init = null;
|
||||
pauseThreads();
|
||||
}
|
||||
|
||||
// wait for all threads are suspended before save the state
|
||||
if (threads != null) runAsync(-1, this::selfPause);
|
||||
}
|
||||
|
||||
private void selfPause() {
|
||||
try {
|
||||
for (Thread thread : threads) {
|
||||
if (thread.isAlive()) {
|
||||
thread.interrupt();
|
||||
thread.join(5000);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
private void pauseThreads() {
|
||||
running = false;
|
||||
joinForThreads(-1);
|
||||
writeThisToFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the downloaded file and the meta file
|
||||
*/
|
||||
@Override
|
||||
public boolean delete() {
|
||||
deleted = true;
|
||||
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
|
||||
|
||||
notify(DownloadManagerService.MESSAGE_DELETED);
|
||||
|
||||
boolean res = deleteThisFromFile();
|
||||
|
||||
if (!super.delete()) return false;
|
||||
|
@ -540,35 +543,37 @@ public class DownloadMission extends Mission {
|
|||
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
|
||||
*/
|
||||
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
|
||||
done = 0;
|
||||
length = 0;
|
||||
errCode = errorCode;
|
||||
errObject = null;
|
||||
unknownLength = false;
|
||||
threads = null;
|
||||
threads = new Thread[0];
|
||||
fallbackResumeOffset = 0;
|
||||
blocks = null;
|
||||
blockAcquired = null;
|
||||
|
||||
if (rollback) current = 0;
|
||||
|
||||
if (persistChanges)
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (persistChanges) writeThisToFile();
|
||||
}
|
||||
|
||||
private void initializer() {
|
||||
init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this));
|
||||
}
|
||||
|
||||
private void writeThisToFileAsync() {
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this {@link DownloadMission} to the meta file asynchronously
|
||||
* if no thread is already running.
|
||||
*/
|
||||
private void writeThisToFile() {
|
||||
void writeThisToFile() {
|
||||
synchronized (LOCK) {
|
||||
if (deleted) return;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (metadata == null) return;
|
||||
Utility.writeToFile(metadata, this);
|
||||
writingToFile = false;
|
||||
}
|
||||
mWritingToFile = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -621,11 +626,10 @@ public class DownloadMission extends Mission {
|
|||
public long getLength() {
|
||||
long calculated;
|
||||
if (psState == 1 || psState == 3) {
|
||||
calculated = length;
|
||||
} else {
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
return length;
|
||||
}
|
||||
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
calculated -= offsets[0];// don't count reserved space
|
||||
|
||||
return calculated > nearLength ? calculated : nearLength;
|
||||
|
@ -638,7 +642,7 @@ public class DownloadMission extends Mission {
|
|||
*/
|
||||
public void setEnqueued(boolean queue) {
|
||||
enqueued = queue;
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
writeThisToFileAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -667,24 +671,29 @@ public class DownloadMission extends Mission {
|
|||
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
|
||||
*/
|
||||
public boolean isCorrupt() {
|
||||
if (urls.length < 1) return false;
|
||||
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished();
|
||||
}
|
||||
|
||||
private boolean doPostprocessing() {
|
||||
if (psAlgorithm == null || psState == 2) return true;
|
||||
/**
|
||||
* Indicates if mission urls has expired and there an attempt to renovate them
|
||||
*
|
||||
* @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false}
|
||||
*/
|
||||
public boolean isRecovering() {
|
||||
return threads.length > 0 && threads[0] instanceof DownloadMissionRecover && threads[0].isAlive();
|
||||
}
|
||||
|
||||
private void doPostprocessing() {
|
||||
errCode = ERROR_NOTHING;
|
||||
errObject = null;
|
||||
Thread thread = Thread.currentThread();
|
||||
|
||||
notifyPostProcessing(1);
|
||||
notifyProgress(0);
|
||||
|
||||
if (DEBUG)
|
||||
Thread.currentThread().setName("[" + TAG + "] ps = " +
|
||||
psAlgorithm.getClass().getSimpleName() +
|
||||
" filename = " + storage.getName()
|
||||
);
|
||||
|
||||
threads = new Thread[]{Thread.currentThread()};
|
||||
if (DEBUG) {
|
||||
thread.setName("[" + TAG + "] ps = " + psAlgorithm + " filename = " + storage.getName());
|
||||
}
|
||||
|
||||
Exception exception = null;
|
||||
|
||||
|
@ -693,6 +702,11 @@ public class DownloadMission extends Mission {
|
|||
} catch (Exception err) {
|
||||
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||
|
||||
if (err instanceof InterruptedIOException || err instanceof ClosedByInterruptException || thread.isInterrupted()) {
|
||||
notifyError(DownloadMission.ERROR_POSTPROCESSING_STOPPED, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||
|
||||
exception = err;
|
||||
|
@ -703,16 +717,38 @@ public class DownloadMission extends Mission {
|
|||
if (errCode != ERROR_NOTHING) {
|
||||
if (exception == null) exception = errObject;
|
||||
notifyError(ERROR_POSTPROCESSING, exception);
|
||||
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
notifyFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover the download
|
||||
*
|
||||
* @param errorCode error code which trigger the recovery procedure
|
||||
*/
|
||||
void doRecover(int errorCode) {
|
||||
Log.i(TAG, "Attempting to recover the mission: " + storage.getName());
|
||||
|
||||
if (recoveryInfo == null) {
|
||||
notifyError(errorCode, null);
|
||||
urls = new String[0];// mark this mission as dead
|
||||
return;
|
||||
}
|
||||
|
||||
joinForThreads(0);
|
||||
|
||||
threads = new Thread[]{
|
||||
runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode))
|
||||
};
|
||||
}
|
||||
|
||||
private boolean deleteThisFromFile() {
|
||||
synchronized (LOCK) {
|
||||
return metadata.delete();
|
||||
boolean res = metadata.delete();
|
||||
metadata = null;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -722,8 +758,8 @@ public class DownloadMission extends Mission {
|
|||
* @param id id of new thread (used for debugging only)
|
||||
* @param who the Runnable whose {@code run} method is invoked.
|
||||
*/
|
||||
private void runAsync(int id, Runnable who) {
|
||||
runAsync(id, new Thread(who));
|
||||
private Thread runAsync(int id, Runnable who) {
|
||||
return runAsync(id, new Thread(who));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -749,25 +785,47 @@ public class DownloadMission extends Mission {
|
|||
return who;
|
||||
}
|
||||
|
||||
private void joinForThread(Thread thread) {
|
||||
if (thread == null || !thread.isAlive()) return;
|
||||
if (thread == Thread.currentThread()) return;
|
||||
/**
|
||||
* Waits at most {@code millis} milliseconds for the thread to die
|
||||
*
|
||||
* @param millis the time to wait in milliseconds
|
||||
*/
|
||||
private void joinForThreads(int millis) {
|
||||
final Thread currentThread = Thread.currentThread();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "a thread is !still alive!: " + thread.getName());
|
||||
if (init != null && init != currentThread && init.isAlive()) {
|
||||
init.interrupt();
|
||||
|
||||
if (millis > 0) {
|
||||
try {
|
||||
init.join(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Initializer thread is still running", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// still alive, this should not happen.
|
||||
// Possible reasons:
|
||||
// if a thread is still alive, possible reasons:
|
||||
// slow device
|
||||
// the user is spamming start/pause buttons
|
||||
// start() method called quickly after pause()
|
||||
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive() || thread == Thread.currentThread()) continue;
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
try {
|
||||
thread.join(10000);
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive()) continue;
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "thread alive: " + thread.getName());
|
||||
}
|
||||
if (millis > 0) thread.join(millis);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "timeout on join : " + thread.getName());
|
||||
throw new RuntimeException("A thread is still running:\n" + thread.getName());
|
||||
throw new RuntimeException("A download thread is still running", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -785,9 +843,9 @@ public class DownloadMission extends Mission {
|
|||
}
|
||||
}
|
||||
|
||||
static class Block {
|
||||
int position;
|
||||
int done;
|
||||
public static class Block {
|
||||
public int position;
|
||||
public int done;
|
||||
}
|
||||
|
||||
private static class Lock implements Serializable {
|
||||
|
|
|
@ -0,0 +1,313 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||
|
||||
public class DownloadMissionRecover extends Thread {
|
||||
private static final String TAG = "DownloadMissionRecover";
|
||||
static final int mID = -3;
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final boolean mNotInitialized;
|
||||
|
||||
private final int mErrCode;
|
||||
|
||||
private HttpURLConnection mConn;
|
||||
private MissionRecoveryInfo mRecovery;
|
||||
private StreamExtractor mExtractor;
|
||||
|
||||
DownloadMissionRecover(DownloadMission mission, int errCode) {
|
||||
mMission = mission;
|
||||
mNotInitialized = mission.blocks == null && mission.current == 0;
|
||||
mErrCode = errCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMission.source == null) {
|
||||
mMission.notifyError(mErrCode, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception err = null;
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt++ < mMission.maxRetry) {
|
||||
try {
|
||||
tryRecover();
|
||||
return;
|
||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
|
||||
// give up
|
||||
mMission.notifyError(mErrCode, err);
|
||||
}
|
||||
|
||||
private void tryRecover() throws ExtractionException, IOException, HttpError {
|
||||
if (mExtractor == null) {
|
||||
try {
|
||||
StreamingService svr = NewPipe.getServiceByUrl(mMission.source);
|
||||
mExtractor = svr.getStreamExtractor(mMission.source);
|
||||
mExtractor.fetchPage();
|
||||
} catch (ExtractionException e) {
|
||||
mExtractor = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// maybe the following check is redundant
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
if (!mNotInitialized) {
|
||||
// set the current download url to null in case if the recovery
|
||||
// process is canceled. Next time start() method is called the
|
||||
// recovery will be executed, saving time
|
||||
mMission.urls[mMission.current] = null;
|
||||
|
||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||
resolveStream();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "mission is not fully initialized, this will take a while");
|
||||
|
||||
try {
|
||||
for (; mMission.current < mMission.urls.length; mMission.current++) {
|
||||
mRecovery = mMission.recoveryInfo[mMission.current];
|
||||
|
||||
if (test()) continue;
|
||||
if (!mMission.running) return;
|
||||
|
||||
resolveStream();
|
||||
if (!mMission.running) return;
|
||||
|
||||
// before continue, check if the current stream was resolved
|
||||
if (mMission.urls[mMission.current] == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mMission.current = 0;
|
||||
}
|
||||
|
||||
mMission.writeThisToFile();
|
||||
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private void resolveStream() throws IOException, ExtractionException, HttpError {
|
||||
// FIXME: this getErrorMessage() always returns "video is unavailable"
|
||||
/*if (mExtractor.getErrorMessage() != null) {
|
||||
mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage()));
|
||||
return;
|
||||
}*/
|
||||
|
||||
String url = null;
|
||||
|
||||
switch (mRecovery.kind) {
|
||||
case 'a':
|
||||
for (AudioStream audio : mExtractor.getAudioStreams()) {
|
||||
if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) {
|
||||
url = audio.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
List<VideoStream> videoStreams;
|
||||
if (mRecovery.desired2)
|
||||
videoStreams = mExtractor.getVideoOnlyStreams();
|
||||
else
|
||||
videoStreams = mExtractor.getVideoStreams();
|
||||
for (VideoStream video : videoStreams) {
|
||||
if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) {
|
||||
url = video.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) {
|
||||
String tag = subtitles.getLanguageTag();
|
||||
if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) {
|
||||
url = subtitles.getURL();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown stream type");
|
||||
}
|
||||
|
||||
resolve(url);
|
||||
}
|
||||
|
||||
private void resolve(String url) throws IOException, HttpError {
|
||||
if (mRecovery.validateCondition == null) {
|
||||
Log.w(TAG, "validation condition not defined, the resource can be stale");
|
||||
}
|
||||
|
||||
if (mMission.unknownLength || mRecovery.validateCondition == null) {
|
||||
recover(url, false);
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
////// Validate the http resource doing a range request
|
||||
/////////////////////
|
||||
try {
|
||||
mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length);
|
||||
mConn.setRequestProperty("If-Range", mRecovery.validateCondition);
|
||||
mMission.establishConnection(mID, mConn);
|
||||
|
||||
int code = mConn.getResponseCode();
|
||||
|
||||
switch (code) {
|
||||
case 200:
|
||||
case 413:
|
||||
// stale
|
||||
recover(url, true);
|
||||
return;
|
||||
case 206:
|
||||
// in case of validation using the Last-Modified date, check the resource length
|
||||
long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range"));
|
||||
boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length;
|
||||
|
||||
recover(url, lengthMismatch);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new HttpError(code);
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void recover(String url, boolean stale) {
|
||||
Log.i(TAG,
|
||||
String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url)
|
||||
);
|
||||
|
||||
mMission.urls[mMission.current] = url;
|
||||
|
||||
if (url == null) {
|
||||
mMission.urls = new String[0];
|
||||
mMission.notifyError(ERROR_RESOURCE_GONE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mNotInitialized) return;
|
||||
|
||||
if (stale) {
|
||||
mMission.resetState(false, false, DownloadMission.ERROR_NOTHING);
|
||||
}
|
||||
|
||||
mMission.writeThisToFile();
|
||||
|
||||
if (!mMission.running || super.isInterrupted()) return;
|
||||
|
||||
mMission.running = false;
|
||||
mMission.start();
|
||||
}
|
||||
|
||||
private long[] parseContentRange(String value) {
|
||||
long[] range = new long[3];
|
||||
|
||||
if (value == null) {
|
||||
// this never should happen
|
||||
return range;
|
||||
}
|
||||
|
||||
try {
|
||||
value = value.trim();
|
||||
|
||||
if (!value.startsWith("bytes")) {
|
||||
return range;// unknown range type
|
||||
}
|
||||
|
||||
int space = value.lastIndexOf(' ') + 1;
|
||||
int dash = value.indexOf('-', space) + 1;
|
||||
int bar = value.indexOf('/', dash);
|
||||
|
||||
// start
|
||||
range[0] = Long.parseLong(value.substring(space, dash - 1));
|
||||
|
||||
// end
|
||||
range[1] = Long.parseLong(value.substring(dash, bar));
|
||||
|
||||
// resource length
|
||||
value = value.substring(bar + 1);
|
||||
if (value.equals("*")) {
|
||||
range[2] = -1;// unknown length received from the server but should be valid
|
||||
} else {
|
||||
range[2] = Long.parseLong(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private boolean test() {
|
||||
if (mMission.urls[mMission.current] == null) return false;
|
||||
|
||||
try {
|
||||
mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1);
|
||||
mMission.establishConnection(mID, mConn);
|
||||
|
||||
if (mConn.getResponseCode() == 200) return true;
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
try {
|
||||
try {
|
||||
mConn.getInputStream().close();
|
||||
} finally {
|
||||
mConn.disconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do
|
||||
} finally {
|
||||
mConn = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
super.interrupt();
|
||||
if (mConn != null) disconnect();
|
||||
}
|
||||
}
|
|
@ -10,8 +10,10 @@ import java.net.HttpURLConnection;
|
|||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.Block;
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -19,7 +21,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
|||
* an error occurs or the process is stopped.
|
||||
*/
|
||||
public class DownloadRunnable extends Thread {
|
||||
private static final String TAG = DownloadRunnable.class.getSimpleName();
|
||||
private static final String TAG = "DownloadRunnable";
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final int mId;
|
||||
|
@ -41,13 +43,7 @@ public class DownloadRunnable extends Thread {
|
|||
public void run() {
|
||||
boolean retry = false;
|
||||
Block block = null;
|
||||
|
||||
int retryCount = 0;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||
}
|
||||
|
||||
SharpStream f;
|
||||
|
||||
try {
|
||||
|
@ -84,13 +80,14 @@ public class DownloadRunnable extends Thread {
|
|||
}
|
||||
|
||||
try {
|
||||
mConn = mMission.openConnection(mId, start, end);
|
||||
mConn = mMission.openConnection(false, start, end);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416) {
|
||||
if (block.done > 0) {
|
||||
// try again from the start (of the block)
|
||||
mMission.notifyProgress(-block.done);
|
||||
block.done = 0;
|
||||
retry = true;
|
||||
mConn.disconnect();
|
||||
|
@ -118,7 +115,7 @@ public class DownloadRunnable extends Thread {
|
|||
int len;
|
||||
|
||||
// use always start <= end
|
||||
// fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly
|
||||
// fixes a deadlock because in some videos, youtube is sending one byte alone
|
||||
while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) {
|
||||
f.write(buf, 0, len);
|
||||
start += len;
|
||||
|
@ -133,6 +130,17 @@ public class DownloadRunnable extends Thread {
|
|||
} catch (Exception e) {
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
f.close();
|
||||
|
||||
if (mId == 1) {
|
||||
// only the first thread will execute the recovery procedure
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
break;
|
||||
|
@ -144,11 +152,7 @@ public class DownloadRunnable extends Thread {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
f.close();
|
||||
} catch (Exception err) {
|
||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "thread " + mId + " exited from main download loop");
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -10,9 +11,11 @@ import java.io.InputStream;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission.HttpError;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
||||
|
||||
/**
|
||||
* Single-threaded fallback mode
|
||||
|
@ -32,8 +35,12 @@ public class DownloadRunnableFallback extends Thread {
|
|||
}
|
||||
|
||||
private void dispose() {
|
||||
try {
|
||||
try {
|
||||
if (mIs != null) mIs.close();
|
||||
} finally {
|
||||
mConn.disconnect();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
@ -41,22 +48,10 @@ public class DownloadRunnableFallback extends Thread {
|
|||
if (mF != null) mF.close();
|
||||
}
|
||||
|
||||
private long loadPosition() {
|
||||
synchronized (mMission.LOCK) {
|
||||
return mMission.fallbackResumeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private void savePosition(long position) {
|
||||
synchronized (mMission.LOCK) {
|
||||
mMission.fallbackResumeOffset = position;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean done;
|
||||
long start = loadPosition();
|
||||
long start = mMission.fallbackResumeOffset;
|
||||
|
||||
if (DEBUG && !mMission.unknownLength && start > 0) {
|
||||
Log.i(TAG, "Resuming a single-thread download at " + start);
|
||||
|
@ -66,11 +61,18 @@ public class DownloadRunnableFallback extends Thread {
|
|||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||
|
||||
int mId = 1;
|
||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||
mConn = mMission.openConnection(false, rangeStart, -1);
|
||||
|
||||
if (mRetryCount == 0 && rangeStart == -1) {
|
||||
// workaround: bypass android connection pool
|
||||
mConn.setRequestProperty("Range", "bytes=0-");
|
||||
}
|
||||
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
// check if the download can be resumed
|
||||
if (mConn.getResponseCode() == 416 && start > 0) {
|
||||
mMission.notifyProgress(-start);
|
||||
start = 0;
|
||||
mRetryCount--;
|
||||
throw new DownloadMission.HttpError(416);
|
||||
|
@ -80,12 +82,17 @@ public class DownloadRunnableFallback extends Thread {
|
|||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
if (mMission.unknownLength || mConn.getResponseCode() == 200) {
|
||||
// restart amount of bytes downloaded
|
||||
mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0];
|
||||
}
|
||||
|
||||
mF = mMission.storage.getStream();
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
mIs = mConn.getInputStream();
|
||||
|
||||
byte[] buf = new byte[64 * 1024];
|
||||
byte[] buf = new byte[DownloadMission.BUFFER_SIZE];
|
||||
int len = 0;
|
||||
|
||||
while (mMission.running && (len = mIs.read(buf, 0, buf.length)) != -1) {
|
||||
|
@ -94,15 +101,24 @@ public class DownloadRunnableFallback extends Thread {
|
|||
mMission.notifyProgress(len);
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
// if thread goes interrupted check if the last part is written. This avoid re-download the whole file
|
||||
done = len == -1;
|
||||
} catch (Exception e) {
|
||||
dispose();
|
||||
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) return;
|
||||
|
||||
if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) {
|
||||
// for youtube streams. The url has expired, recover
|
||||
dispose();
|
||||
mMission.doRecover(ERROR_HTTP_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRetryCount++ >= mMission.maxRetry) {
|
||||
mMission.notifyError(e);
|
||||
return;
|
||||
|
@ -116,12 +132,10 @@ public class DownloadRunnableFallback extends Thread {
|
|||
return;
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
if (done) {
|
||||
mMission.notifyFinished();
|
||||
} else {
|
||||
savePosition(start);
|
||||
mMission.fallbackResumeOffset = start;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ public class FinishedMission extends Mission {
|
|||
|
||||
public FinishedMission(@NonNull DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
length = mission.length;
|
||||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class MissionRecoveryInfo implements Serializable, Parcelable {
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
MediaFormat format;
|
||||
String desired;
|
||||
boolean desired2;
|
||||
int desiredBitrate;
|
||||
byte kind;
|
||||
String validateCondition = null;
|
||||
|
||||
public MissionRecoveryInfo(@NonNull Stream stream) {
|
||||
if (stream instanceof AudioStream) {
|
||||
desiredBitrate = ((AudioStream) stream).average_bitrate;
|
||||
desired2 = false;
|
||||
kind = 'a';
|
||||
} else if (stream instanceof VideoStream) {
|
||||
desired = ((VideoStream) stream).getResolution();
|
||||
desired2 = ((VideoStream) stream).isVideoOnly();
|
||||
kind = 'v';
|
||||
} else if (stream instanceof SubtitlesStream) {
|
||||
desired = ((SubtitlesStream) stream).getLanguageTag();
|
||||
desired2 = ((SubtitlesStream) stream).isAutoGenerated();
|
||||
kind = 's';
|
||||
} else {
|
||||
throw new RuntimeException("Unknown stream kind");
|
||||
}
|
||||
|
||||
format = stream.getFormat();
|
||||
if (format == null) throw new NullPointerException("Stream format cannot be null");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
String info;
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("{type=");
|
||||
switch (kind) {
|
||||
case 'a':
|
||||
str.append("audio");
|
||||
info = "bitrate=" + desiredBitrate;
|
||||
break;
|
||||
case 'v':
|
||||
str.append("video");
|
||||
info = "quality=" + desired + " videoOnly=" + desired2;
|
||||
break;
|
||||
case 's':
|
||||
str.append("subtitles");
|
||||
info = "language=" + desired + " autoGenerated=" + desired2;
|
||||
break;
|
||||
default:
|
||||
info = "";
|
||||
str.append("other");
|
||||
}
|
||||
|
||||
str.append(" format=")
|
||||
.append(format.getName())
|
||||
.append(' ')
|
||||
.append(info)
|
||||
.append('}');
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeInt(this.format.ordinal());
|
||||
parcel.writeString(this.desired);
|
||||
parcel.writeInt(this.desired2 ? 0x01 : 0x00);
|
||||
parcel.writeInt(this.desiredBitrate);
|
||||
parcel.writeByte(this.kind);
|
||||
parcel.writeString(this.validateCondition);
|
||||
}
|
||||
|
||||
private MissionRecoveryInfo(Parcel parcel) {
|
||||
this.format = MediaFormat.values()[parcel.readInt()];
|
||||
this.desired = parcel.readString();
|
||||
this.desired2 = parcel.readInt() != 0x00;
|
||||
this.desiredBitrate = parcel.readInt();
|
||||
this.kind = parcel.readByte();
|
||||
this.validateCondition = parcel.readString();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MissionRecoveryInfo> CREATOR = new Parcelable.Creator<MissionRecoveryInfo>() {
|
||||
@Override
|
||||
public MissionRecoveryInfo createFromParcel(Parcel source) {
|
||||
return new MissionRecoveryInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MissionRecoveryInfo[] newArray(int size) {
|
||||
return new MissionRecoveryInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
import java.io.IOException;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
private static final int REPORT_INTERVAL = 256 * 1024;
|
||||
|
||||
private SharpStream source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||
this(target, start, target.length());
|
||||
}
|
||||
private long progressReport;
|
||||
private final ProgressReport onProgress;
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException {
|
||||
source = target;
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
onProgress = callback;
|
||||
progressReport = REPORT_INTERVAL;
|
||||
|
||||
if (length < 1) {
|
||||
source.close();
|
||||
|
@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[]) throws IOException {
|
||||
public int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte b[], int off, int len) throws IOException {
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if ((position + len) > length) {
|
||||
len = (int) (length - position);
|
||||
}
|
||||
|
@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream {
|
|||
int res = source.read(b, off, len);
|
||||
position += res;
|
||||
|
||||
if (onProgress != null && position > progressReport) {
|
||||
onProgress.report(position);
|
||||
progressReport = position + REPORT_INTERVAL;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
|
@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[]) throws IOException {
|
||||
public void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
@Override
|
||||
public void rewind() throws IOException {
|
||||
if (onProgress != null) {
|
||||
onProgress.report(-out.length - aux.length);// rollback the whole progress
|
||||
onProgress.report(0);// rollback the whole progress
|
||||
}
|
||||
|
||||
seek(0);
|
||||
|
@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream {
|
|||
long check();
|
||||
}
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
||||
|
||||
public interface WriteErrorHandle {
|
||||
|
||||
/**
|
||||
|
@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream {
|
|||
|
||||
class BufferedFile {
|
||||
|
||||
protected final SharpStream target;
|
||||
final SharpStream target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
long length;
|
||||
|
||||
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
private int queueSize;
|
||||
|
@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream {
|
|||
this.target = target;
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void close() {
|
||||
void close() {
|
||||
queue = null;
|
||||
target.close();
|
||||
}
|
||||
|
||||
protected void write(byte b[], int off, int len) throws IOException {
|
||||
void write(byte[] b, int off, int len) throws IOException {
|
||||
while (len > 0) {
|
||||
// if the queue is full, the method available() will flush the queue
|
||||
int read = Math.min(available(), len);
|
||||
|
@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
target.seek(0);
|
||||
}
|
||||
|
||||
protected int available() throws IOException {
|
||||
int available() throws IOException {
|
||||
if (queueSize >= queue.length) {
|
||||
flush();
|
||||
return queue.length;
|
||||
|
@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
target.seek(0);
|
||||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
void seek(long absoluteOffset) throws IOException {
|
||||
if (absoluteOffset == offset) {
|
||||
return;// nothing to do
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package us.shandian.giga.io;
|
||||
|
||||
public interface ProgressReport {
|
||||
|
||||
/**
|
||||
* Report the size of the new file
|
||||
*
|
||||
* @param progress the new size
|
||||
*/
|
||||
void report(long progress);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.OggFromWebMWriter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
class OggFromWebmDemuxer extends Postprocessing {
|
||||
|
||||
OggFromWebmDemuxer() {
|
||||
super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean test(SharpStream... sources) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
sources[0].read(buffer.array());
|
||||
|
||||
// youtube uses WebM as container, but the file extension (format suffix) is "*.opus"
|
||||
// check if the file is a webm/mkv file before proceed
|
||||
|
||||
switch (buffer.getInt()) {
|
||||
case 0x1a45dfa3:
|
||||
return true;// webm/mkv
|
||||
case 0x4F676753:
|
||||
return false;// ogg
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream");
|
||||
}
|
||||
|
||||
@Override
|
||||
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
|
||||
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
|
||||
demuxer.parseSource();
|
||||
demuxer.selectTrack(0);
|
||||
demuxer.build();
|
||||
|
||||
return OK_RESULT;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission;
|
|||
import us.shandian.giga.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.io.CircularFileWriter;
|
||||
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.io.ProgressReport;
|
||||
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
|
||||
public abstract class Postprocessing implements Serializable {
|
||||
|
||||
|
@ -28,6 +28,7 @@ public abstract class Postprocessing implements Serializable {
|
|||
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";
|
||||
|
||||
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
|
||||
Postprocessing instance;
|
||||
|
@ -45,6 +46,9 @@ public abstract class Postprocessing implements Serializable {
|
|||
case ALGORITHM_M4A_NO_DASH:
|
||||
instance = new M4aNoDash();
|
||||
break;
|
||||
case ALGORITHM_OGG_FROM_WEBM_DEMUXER:
|
||||
instance = new OggFromWebmDemuxer();
|
||||
break;
|
||||
/*case "example-algorithm":
|
||||
instance = new ExampleAlgorithm();*/
|
||||
default:
|
||||
|
@ -59,22 +63,22 @@ public abstract class Postprocessing implements Serializable {
|
|||
* Get a boolean value that indicate if the given algorithm work on the same
|
||||
* file
|
||||
*/
|
||||
public final boolean worksOnSameFile;
|
||||
public boolean worksOnSameFile;
|
||||
|
||||
/**
|
||||
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
|
||||
*/
|
||||
public final boolean reserveSpace;
|
||||
public boolean reserveSpace;
|
||||
|
||||
/**
|
||||
* Gets the given algorithm short name
|
||||
*/
|
||||
private final String name;
|
||||
private String name;
|
||||
|
||||
|
||||
private String[] args;
|
||||
|
||||
protected transient DownloadMission mission;
|
||||
private transient DownloadMission mission;
|
||||
|
||||
private File tempFile;
|
||||
|
||||
|
@ -105,16 +109,24 @@ public abstract class Postprocessing implements Serializable {
|
|||
long finalLength = -1;
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = mission.storage.length();
|
||||
|
||||
long length = mission.storage.length() - mission.offsets[0];
|
||||
mission.length = length > mission.nearLength ? length : mission.nearLength;
|
||||
|
||||
final ProgressReport readProgress = (long position) -> {
|
||||
position -= mission.offsets[0];
|
||||
if (position > mission.done) mission.done = position;
|
||||
};
|
||||
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||
for (int i = 0, j = 1; i < sources.length; i++, j++) {
|
||||
SharpStream source = mission.storage.getStream();
|
||||
long end = j < sources.length ? mission.offsets[j] : source.length();
|
||||
|
||||
sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress);
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||
|
||||
if (test(sources)) {
|
||||
for (SharpStream source : sources) source.rewind();
|
||||
|
@ -136,7 +148,7 @@ public abstract class Postprocessing implements Serializable {
|
|||
};
|
||||
|
||||
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
|
||||
out.onProgress = this::progressReport;
|
||||
out.onProgress = (long position) -> mission.done = position;
|
||||
|
||||
out.onWriteError = (err) -> {
|
||||
mission.psState = 3;
|
||||
|
@ -183,11 +195,10 @@ public abstract class Postprocessing implements Serializable {
|
|||
|
||||
if (result == OK_RESULT) {
|
||||
if (finalLength != -1) {
|
||||
mission.done = finalLength;
|
||||
mission.length = finalLength;
|
||||
}
|
||||
} else {
|
||||
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
|
||||
mission.errCode = ERROR_POSTPROCESSING;
|
||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
|
@ -212,7 +223,7 @@ public abstract class Postprocessing implements Serializable {
|
|||
*
|
||||
* @param out output stream
|
||||
* @param sources files to be processed
|
||||
* @return a error code, 0 means the operation was successful
|
||||
* @return an error code, {@code OK_RESULT} means the operation was successful
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||
|
@ -225,23 +236,12 @@ public abstract class Postprocessing implements Serializable {
|
|||
return args[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
mission.done = done;
|
||||
if (mission.length < mission.done) mission.length = mission.done;
|
||||
|
||||
Message m = new Message();
|
||||
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
||||
m.obj = mission;
|
||||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append("name=").append(name).append('[');
|
||||
str.append("{ name=").append(name).append('[');
|
||||
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
|
@ -251,6 +251,6 @@ public abstract class Postprocessing implements Serializable {
|
|||
str.delete(0, 1);
|
||||
}
|
||||
|
||||
return str.append(']').toString();
|
||||
return str.append("] }").toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,11 @@ package us.shandian.giga.service;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
@ -37,6 +35,7 @@ public class DownloadManager {
|
|||
|
||||
public static final String TAG_AUDIO = "audio";
|
||||
public static final String TAG_VIDEO = "video";
|
||||
private static final String DOWNLOADS_METADATA_FOLDER = "pending_downloads";
|
||||
|
||||
private final FinishedMissionStore mFinishedMissionStore;
|
||||
|
||||
|
@ -74,25 +73,35 @@ public class DownloadManager {
|
|||
mMissionsFinished = loadFinishedMissions();
|
||||
mPendingMissionsDir = getPendingDir(context);
|
||||
|
||||
if (!Utility.mkdir(mPendingMissionsDir, false)) {
|
||||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||
}
|
||||
|
||||
loadPendingMissions(context);
|
||||
}
|
||||
|
||||
private static File getPendingDir(@NonNull Context context) {
|
||||
//File dir = new File(ContextCompat.getDataDir(context), "pending_downloads");
|
||||
File dir = context.getExternalFilesDir("pending_downloads");
|
||||
File dir = context.getExternalFilesDir(DOWNLOADS_METADATA_FOLDER);
|
||||
if (testDir(dir)) return dir;
|
||||
|
||||
if (dir == null) {
|
||||
// One of the following paths are not accessible ¿unmounted internal memory?
|
||||
// /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
// /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
Log.w(TAG, "path to pending downloads are not accessible");
|
||||
dir = new File(context.getFilesDir(), DOWNLOADS_METADATA_FOLDER);
|
||||
if (testDir(dir)) return dir;
|
||||
|
||||
throw new RuntimeException("path to pending downloads are not accessible");
|
||||
}
|
||||
|
||||
return dir;
|
||||
private static boolean testDir(@Nullable File dir) {
|
||||
if (dir == null) return false;
|
||||
|
||||
try {
|
||||
if (!Utility.mkdir(dir, false)) {
|
||||
Log.e(TAG, "testDir() cannot create the directory in path: " + dir.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
|
||||
File tmp = new File(dir, ".tmp");
|
||||
if (!tmp.createNewFile()) return false;
|
||||
return tmp.delete();// if the file was created, SHOULD BE deleted too
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "testDir() failed: " + dir.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,6 +141,7 @@ public class DownloadManager {
|
|||
|
||||
for (File sub : subs) {
|
||||
if (!sub.isFile()) continue;
|
||||
if (sub.getName().equals(".tmp")) continue;
|
||||
|
||||
DownloadMission mis = Utility.readFromFile(sub);
|
||||
if (mis == null || mis.isFinished()) {
|
||||
|
@ -140,6 +150,8 @@ public class DownloadManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
mis.threads = new Thread[0];
|
||||
|
||||
boolean exists;
|
||||
try {
|
||||
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||
|
@ -158,8 +170,6 @@ public class DownloadManager {
|
|||
// is Java IO (avoid showing the "Save as..." dialog)
|
||||
if (exists && mis.storage.isDirect() && !mis.storage.delete())
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
|
||||
mis.psState = 0;
|
||||
|
@ -177,7 +187,6 @@ public class DownloadManager {
|
|||
mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
|
||||
}
|
||||
|
||||
mis.recovered = exists;
|
||||
mis.metadata = sub;
|
||||
mis.maxRetry = mPrefMaxRetry;
|
||||
mis.mHandler = mHandler;
|
||||
|
@ -232,7 +241,6 @@ public class DownloadManager {
|
|||
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||
|
||||
if (canDownloadInCurrentNetwork() && start) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +249,6 @@ public class DownloadManager {
|
|||
|
||||
public void resumeMission(DownloadMission mission) {
|
||||
if (!mission.running) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
|
@ -250,7 +257,6 @@ public class DownloadManager {
|
|||
if (mission.running) {
|
||||
mission.setEnqueued(false);
|
||||
mission.pause();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,7 +269,6 @@ public class DownloadManager {
|
|||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.delete();
|
||||
}
|
||||
}
|
||||
|
@ -280,7 +285,6 @@ public class DownloadManager {
|
|||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.storage = null;
|
||||
mission.delete();
|
||||
}
|
||||
|
@ -363,35 +367,29 @@ public class DownloadManager {
|
|||
}
|
||||
|
||||
public void pauseAllMissions(boolean force) {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
|
||||
|
||||
if (force) mission.threads = null;// avoid waiting for threads
|
||||
if (force) {
|
||||
// avoid waiting for threads
|
||||
mission.init = null;
|
||||
mission.threads = new Thread[0];
|
||||
}
|
||||
|
||||
mission.pause();
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
public void startAllMissions() {
|
||||
boolean flag = false;
|
||||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || mission.isCorrupt()) continue;
|
||||
|
||||
flag = true;
|
||||
mission.start();
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -472,28 +470,18 @@ public class DownloadManager {
|
|||
|
||||
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
|
||||
|
||||
int running = 0;
|
||||
int paused = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.isCorrupt() || mission.isPsRunning()) continue;
|
||||
|
||||
if (mission.running && isMetered) {
|
||||
paused++;
|
||||
mission.pause();
|
||||
} else if (!mission.running && !isMetered && mission.enqueued) {
|
||||
running++;
|
||||
mission.start();
|
||||
if (mPrefQueueLimit) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (running > 0) {
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
return;
|
||||
}
|
||||
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
void updateMaximumAttempts() {
|
||||
|
@ -502,22 +490,6 @@ public class DownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast check for pending downloads. If exists, the user will be notified
|
||||
* TODO: call this method in somewhere
|
||||
*
|
||||
* @param context the application context
|
||||
*/
|
||||
public static void notifyUserPendingDownloads(Context context) {
|
||||
int pending = getPendingDir(context).list().length;
|
||||
if (pending < 1) return;
|
||||
|
||||
Toast.makeText(context, context.getString(
|
||||
R.string.msg_pending_downloads,
|
||||
String.valueOf(pending)
|
||||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(storage);
|
||||
|
|
|
@ -23,15 +23,17 @@ import android.os.Handler;
|
|||
import android.os.Handler.Callback;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
|
@ -42,6 +44,7 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
|
@ -54,11 +57,11 @@ public class DownloadManagerService extends Service {
|
|||
|
||||
private static final String TAG = "DownloadManagerService";
|
||||
|
||||
public static final int MESSAGE_RUNNING = 0;
|
||||
public static final int MESSAGE_PAUSED = 1;
|
||||
public static final int MESSAGE_FINISHED = 2;
|
||||
public static final int MESSAGE_PROGRESS = 3;
|
||||
public static final int MESSAGE_ERROR = 4;
|
||||
public static final int MESSAGE_DELETED = 5;
|
||||
public static final int MESSAGE_ERROR = 3;
|
||||
public static final int MESSAGE_DELETED = 4;
|
||||
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1000;
|
||||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||
|
@ -73,6 +76,7 @@ public class DownloadManagerService extends Service {
|
|||
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
|
||||
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
||||
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
||||
|
||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||
|
@ -212,10 +216,12 @@ public class DownloadManagerService extends Service {
|
|||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
|
@ -245,6 +251,7 @@ public class DownloadManagerService extends Service {
|
|||
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
||||
if (icLauncher != null) icLauncher.recycle();
|
||||
|
||||
mHandler = null;
|
||||
mManager.pauseAllMissions(true);
|
||||
}
|
||||
|
||||
|
@ -269,6 +276,8 @@ public class DownloadManagerService extends Service {
|
|||
}
|
||||
|
||||
private boolean handleMessage(@NonNull Message msg) {
|
||||
if (mHandler == null) return true;
|
||||
|
||||
DownloadMission mission = (DownloadMission) msg.obj;
|
||||
|
||||
switch (msg.what) {
|
||||
|
@ -279,7 +288,7 @@ public class DownloadManagerService extends Service {
|
|||
handleConnectivityState(false);
|
||||
updateForegroundState(mManager.runMissions());
|
||||
break;
|
||||
case MESSAGE_PROGRESS:
|
||||
case MESSAGE_RUNNING:
|
||||
updateForegroundState(true);
|
||||
break;
|
||||
case MESSAGE_ERROR:
|
||||
|
@ -295,11 +304,8 @@ public class DownloadManagerService extends Service {
|
|||
if (msg.what != MESSAGE_ERROR)
|
||||
mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission));
|
||||
|
||||
synchronized (mEchoObservers) {
|
||||
for (Callback observer : mEchoObservers) {
|
||||
for (Callback observer : mEchoObservers)
|
||||
observer.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -365,7 +371,7 @@ public class DownloadManagerService extends Service {
|
|||
* Start a new download mission
|
||||
*
|
||||
* @param context the activity context
|
||||
* @param urls the list of urls to download
|
||||
* @param urls array of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
|
@ -373,9 +379,11 @@ public class DownloadManagerService extends Service {
|
|||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
* @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download
|
||||
*/
|
||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind,
|
||||
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
||||
char kind, int threads, String source, String psName,
|
||||
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.putExtra(EXTRA_URLS, urls);
|
||||
|
@ -385,6 +393,7 @@ public class DownloadManagerService extends Service {
|
|||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
||||
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
||||
intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo);
|
||||
|
||||
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
|
||||
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||
|
@ -404,6 +413,7 @@ public class DownloadManagerService extends Service {
|
|||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
|
||||
|
||||
StoredFileHelper storage;
|
||||
try {
|
||||
|
@ -418,10 +428,15 @@ public class DownloadManagerService extends Service {
|
|||
else
|
||||
ps = Postprocessing.getAlgorithm(psName, psArgs);
|
||||
|
||||
MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length];
|
||||
for (int i = 0; i < parcelRecovery.length; i++)
|
||||
recovery[i] = (MissionRecoveryInfo) parcelRecovery[i];
|
||||
|
||||
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
|
||||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.nearLength = nearLength;
|
||||
mission.recoveryInfo = recovery;
|
||||
|
||||
if (ps != null)
|
||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||
|
@ -509,16 +524,6 @@ public class DownloadManagerService extends Service {
|
|||
return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private void manageObservers(Callback handler, boolean add) {
|
||||
synchronized (mEchoObservers) {
|
||||
if (add) {
|
||||
mEchoObservers.add(handler);
|
||||
} else {
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void manageLock(boolean acquire) {
|
||||
if (acquire == mLockAcquired) return;
|
||||
|
||||
|
@ -591,11 +596,11 @@ public class DownloadManagerService extends Service {
|
|||
}
|
||||
|
||||
public void addMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, true);
|
||||
mEchoObservers.add(handler);
|
||||
}
|
||||
|
||||
public void removeMissionEventListener(Callback handler) {
|
||||
manageObservers(handler, false);
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
|
||||
public void clearDownloadNotifications() {
|
||||
|
|
|
@ -10,16 +10,6 @@ import android.os.AsyncTask;
|
|||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
|
@ -34,8 +24,20 @@ import android.widget.PopupMenu;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
@ -44,11 +46,11 @@ import java.io.File;
|
|||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
@ -62,7 +64,6 @@ import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
|||
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
||||
|
@ -71,6 +72,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
|||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
|
@ -81,6 +83,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
private static final String TAG = "MissionAdapter";
|
||||
private static final String UNDEFINED_PROGRESS = "--.-%";
|
||||
private static final String DEFAULT_MIME_TYPE = "*/*";
|
||||
private static final String UNDEFINED_ETA = "--:--";
|
||||
|
||||
|
||||
static {
|
||||
|
@ -102,10 +105,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
private View mEmptyMessage;
|
||||
private RecoverHelper mRecover;
|
||||
|
||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
|
||||
private final Runnable rUpdater = this::updater;
|
||||
|
||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
|
||||
mContext = context;
|
||||
mDownloadManager = downloadManager;
|
||||
mDeleter = null;
|
||||
|
||||
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mLayout = R.layout.mission_item;
|
||||
|
@ -116,7 +120,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
|
||||
mIterator = downloadManager.getIterator();
|
||||
|
||||
mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
|
||||
checkEmptyMessageVisibility();
|
||||
onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -141,17 +148,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
if (h.item.mission instanceof DownloadMission) {
|
||||
mPendingDownloadsItems.remove(h);
|
||||
if (mPendingDownloadsItems.size() < 1) {
|
||||
setAutoRefresh(false);
|
||||
checkMasterButtonsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
h.popupMenu.dismiss();
|
||||
h.item = null;
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
h.lastCurrent = -1;
|
||||
h.state = 0;
|
||||
h.resetSpeedMeasure();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -190,7 +193,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
|
||||
h.size.setText(length);
|
||||
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
||||
h.lastCurrent = mission.current;
|
||||
updateProgress(h);
|
||||
mPendingDownloadsItems.add(h);
|
||||
} else {
|
||||
|
@ -215,40 +217,27 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
private void updateProgress(ViewHolderItem h) {
|
||||
if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
DownloadMission mission = (DownloadMission) h.item.mission;
|
||||
|
||||
if (h.lastCurrent != mission.current) {
|
||||
h.lastCurrent = mission.current;
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = 0;
|
||||
} else {
|
||||
if (h.lastTimeStamp == -1) h.lastTimeStamp = now;
|
||||
if (h.lastDone == -1) h.lastDone = mission.done;
|
||||
}
|
||||
|
||||
long deltaTime = now - h.lastTimeStamp;
|
||||
long deltaDone = mission.done - h.lastDone;
|
||||
double done = mission.done;
|
||||
long length = mission.getLength();
|
||||
long now = System.currentTimeMillis();
|
||||
boolean hasError = mission.errCode != ERROR_NOTHING;
|
||||
|
||||
// hide on error
|
||||
// show if current resource length is not fetched
|
||||
// show if length is unknown
|
||||
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
|
||||
h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength));
|
||||
|
||||
float progress;
|
||||
double progress;
|
||||
if (mission.unknownLength) {
|
||||
progress = Float.NaN;
|
||||
progress = Double.NaN;
|
||||
h.progress.setProgress(0f);
|
||||
} else {
|
||||
progress = (float) ((double) mission.done / mission.length);
|
||||
if (mission.urls.length > 1 && mission.current < mission.urls.length) {
|
||||
progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length);
|
||||
}
|
||||
progress = done / length;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
|
||||
h.progress.setProgress(isNotFinite(progress) ? 1d : progress);
|
||||
h.status.setText(R.string.msg_error);
|
||||
} else if (isNotFinite(progress)) {
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
|
@ -257,59 +246,78 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
h.progress.setProgress(progress);
|
||||
}
|
||||
|
||||
long length = mission.getLength();
|
||||
@StringRes int state;
|
||||
String sizeStr = Utility.formatBytes(length).concat(" ");
|
||||
|
||||
int state;
|
||||
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
|
||||
state = 0;
|
||||
h.size.setText(sizeStr);
|
||||
return;
|
||||
} else if (!mission.running) {
|
||||
state = mission.enqueued ? 1 : 2;
|
||||
state = mission.enqueued ? R.string.queued : R.string.paused;
|
||||
} else if (mission.isPsRunning()) {
|
||||
state = 3;
|
||||
state = R.string.post_processing;
|
||||
} else if (mission.isRecovering()) {
|
||||
state = R.string.recovering;
|
||||
} else {
|
||||
state = 0;
|
||||
}
|
||||
|
||||
if (state != 0) {
|
||||
// update state without download speed
|
||||
if (h.state != state) {
|
||||
String statusStr;
|
||||
h.state = state;
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
statusStr = mContext.getString(R.string.queued);
|
||||
break;
|
||||
case 2:
|
||||
statusStr = mContext.getString(R.string.paused);
|
||||
break;
|
||||
case 3:
|
||||
statusStr = mContext.getString(R.string.post_processing);
|
||||
break;
|
||||
default:
|
||||
statusStr = "?";
|
||||
break;
|
||||
h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
|
||||
h.resetSpeedMeasure();
|
||||
return;
|
||||
}
|
||||
|
||||
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")"));
|
||||
} else if (deltaDone > 0) {
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = mission.done;
|
||||
if (h.lastTimestamp < 0) {
|
||||
h.size.setText(sizeStr);
|
||||
h.lastTimestamp = now;
|
||||
h.lastDone = done;
|
||||
return;
|
||||
}
|
||||
|
||||
long deltaTime = now - h.lastTimestamp;
|
||||
double deltaDone = done - h.lastDone;
|
||||
|
||||
if (h.lastDone > done) {
|
||||
h.lastDone = done;
|
||||
h.size.setText(sizeStr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (deltaDone > 0 && deltaTime > 0) {
|
||||
float speed = (deltaDone * 1000f) / deltaTime;
|
||||
float speed = (float) ((deltaDone * 1000d) / deltaTime);
|
||||
float averageSpeed = speed;
|
||||
|
||||
String speedStr = Utility.formatSpeed(speed);
|
||||
String sizeStr = Utility.formatBytes(length);
|
||||
if (h.lastSpeedIdx < 0) {
|
||||
for (int i = 0; i < h.lastSpeed.length; i++) {
|
||||
h.lastSpeed[i] = speed;
|
||||
}
|
||||
h.lastSpeedIdx = 0;
|
||||
} else {
|
||||
for (int i = 0; i < h.lastSpeed.length; i++) {
|
||||
averageSpeed += h.lastSpeed[i];
|
||||
}
|
||||
averageSpeed /= h.lastSpeed.length + 1f;
|
||||
}
|
||||
|
||||
h.size.setText(sizeStr.concat(" ").concat(speedStr));
|
||||
String speedStr = Utility.formatSpeed(averageSpeed);
|
||||
String etaStr;
|
||||
|
||||
h.lastTimeStamp = now;
|
||||
h.lastDone = mission.done;
|
||||
if (mission.unknownLength) {
|
||||
etaStr = "";
|
||||
} else {
|
||||
long eta = (long) Math.ceil((length - done) / averageSpeed);
|
||||
etaStr = Utility.formatBytes((long) done) + "/" + Utility.stringifySeconds(eta) + " ";
|
||||
}
|
||||
|
||||
h.size.setText(sizeStr.concat(etaStr).concat(speedStr));
|
||||
|
||||
h.lastTimestamp = now;
|
||||
h.lastDone = done;
|
||||
h.lastSpeed[h.lastSpeedIdx++] = speed;
|
||||
|
||||
if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,6 +396,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
return true;
|
||||
}
|
||||
|
||||
private ViewHolderItem getViewHolder(Object mission) {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (h.item.mission == mission) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(@NonNull Message msg) {
|
||||
if (mStartButton != null && mPauseButton != null) {
|
||||
|
@ -395,23 +410,21 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
}
|
||||
|
||||
switch (msg.what) {
|
||||
case DownloadManagerService.MESSAGE_PROGRESS:
|
||||
case DownloadManagerService.MESSAGE_ERROR:
|
||||
case DownloadManagerService.MESSAGE_FINISHED:
|
||||
case DownloadManagerService.MESSAGE_DELETED:
|
||||
case DownloadManagerService.MESSAGE_PAUSED:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
|
||||
setAutoRefresh(true);
|
||||
return true;
|
||||
}
|
||||
ViewHolderItem h = getViewHolder(msg.obj);
|
||||
if (h == null) return false;
|
||||
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (h.item.mission != msg.obj) continue;
|
||||
|
||||
if (msg.what == DownloadManagerService.MESSAGE_FINISHED) {
|
||||
switch (msg.what) {
|
||||
case DownloadManagerService.MESSAGE_FINISHED:
|
||||
case DownloadManagerService.MESSAGE_DELETED:
|
||||
// DownloadManager should mark the download as finished
|
||||
applyChanges();
|
||||
return true;
|
||||
|
@ -421,16 +434,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showError(@NonNull DownloadMission mission) {
|
||||
@StringRes int msg = R.string.general_error;
|
||||
String msgEx = null;
|
||||
|
||||
switch (mission.errCode) {
|
||||
case 416:
|
||||
msg = R.string.error_http_requested_range_not_satisfiable;
|
||||
msg = R.string.error_http_unsupported_range;
|
||||
break;
|
||||
case 404:
|
||||
msg = R.string.error_http_not_found;
|
||||
|
@ -443,9 +453,6 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
case ERROR_HTTP_NO_CONTENT:
|
||||
msg = R.string.error_http_no_content;
|
||||
break;
|
||||
case ERROR_HTTP_UNSUPPORTED_RANGE:
|
||||
msg = R.string.error_http_unsupported_range;
|
||||
break;
|
||||
case ERROR_PATH_CREATION:
|
||||
msg = R.string.error_path_creation;
|
||||
break;
|
||||
|
@ -466,27 +473,35 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
break;
|
||||
case ERROR_POSTPROCESSING:
|
||||
case ERROR_POSTPROCESSING_HOLD:
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
|
||||
showError(mission, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
|
||||
return;
|
||||
case ERROR_INSUFFICIENT_STORAGE:
|
||||
msg = R.string.error_insufficient_storage;
|
||||
break;
|
||||
case ERROR_UNKNOWN_EXCEPTION:
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
if (mission.errObject != null) {
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
return;
|
||||
} else {
|
||||
msg = R.string.msg_error;
|
||||
break;
|
||||
}
|
||||
case ERROR_PROGRESS_LOST:
|
||||
msg = R.string.error_progress_lost;
|
||||
break;
|
||||
case ERROR_TIMEOUT:
|
||||
msg = R.string.error_timeout;
|
||||
break;
|
||||
case ERROR_RESOURCE_GONE:
|
||||
msg = R.string.error_download_resource_gone;
|
||||
break;
|
||||
default:
|
||||
if (mission.errCode >= 100 && mission.errCode < 600) {
|
||||
msgEx = "HTTP " + mission.errCode;
|
||||
} else if (mission.errObject == null) {
|
||||
msgEx = "(not_decelerated_error_code)";
|
||||
} else {
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, msg);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
@ -503,7 +518,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
|
||||
@StringRes final int mMsg = msg;
|
||||
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
|
||||
showError(mission, UserAction.DOWNLOAD_FAILED, mMsg)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -513,13 +528,32 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
.show();
|
||||
}
|
||||
|
||||
private void showError(Exception exception, UserAction action, @StringRes int reason) {
|
||||
private void showError(DownloadMission mission, UserAction action, @StringRes int reason) {
|
||||
StringBuilder request = new StringBuilder(256);
|
||||
request.append(mission.source);
|
||||
|
||||
request.append(" [");
|
||||
if (mission.recoveryInfo != null) {
|
||||
for (MissionRecoveryInfo recovery : mission.recoveryInfo)
|
||||
request.append(' ')
|
||||
.append(recovery.toString())
|
||||
.append(' ');
|
||||
}
|
||||
request.append("]");
|
||||
|
||||
String service;
|
||||
try {
|
||||
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
|
||||
} catch (Exception e) {
|
||||
service = "-";
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(
|
||||
mContext,
|
||||
Collections.singletonList(exception),
|
||||
mission.errObject,
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
|
||||
ErrorActivity.ErrorInfo.make(action, service, request.toString(), reason)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -538,16 +572,10 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
switch (id) {
|
||||
case R.id.start:
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
mDownloadManager.resumeMission(mission);
|
||||
return true;
|
||||
case R.id.pause:
|
||||
h.state = -1;
|
||||
mDownloadManager.pauseMission(mission);
|
||||
updateProgress(h);
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
return true;
|
||||
case R.id.error_message_view:
|
||||
showError(mission);
|
||||
|
@ -580,12 +608,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
shareFile(h.item.mission);
|
||||
return true;
|
||||
case R.id.delete:
|
||||
if (mDeleter == null) {
|
||||
mDownloadManager.deleteMission(h.item.mission);
|
||||
} else {
|
||||
mDeleter.append(h.item.mission);
|
||||
}
|
||||
applyChanges();
|
||||
checkMasterButtonsVisibility();
|
||||
return true;
|
||||
case R.id.md5:
|
||||
case R.id.sha1:
|
||||
|
@ -621,7 +646,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
mIterator.end();
|
||||
|
||||
for (ViewHolderItem item : mPendingDownloadsItems) {
|
||||
item.lastTimeStamp = -1;
|
||||
item.resetSpeedMeasure();
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
|
@ -654,6 +679,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
|
||||
public void checkMasterButtonsVisibility() {
|
||||
boolean[] state = mIterator.hasValidPendingMissions();
|
||||
Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]);
|
||||
setButtonVisible(mPauseButton, state[0]);
|
||||
setButtonVisible(mStartButton, state[1]);
|
||||
}
|
||||
|
@ -663,86 +689,57 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
button.setVisible(visible);
|
||||
}
|
||||
|
||||
public void ensurePausedMissions() {
|
||||
public void refreshMissionItems() {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (((DownloadMission) h.item.mission).running) continue;
|
||||
updateProgress(h);
|
||||
h.lastTimeStamp = -1;
|
||||
h.lastDone = -1;
|
||||
h.resetSpeedMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void deleterDispose(boolean commitChanges) {
|
||||
if (mDeleter != null) mDeleter.dispose(commitChanges);
|
||||
public void onDestroy() {
|
||||
mDeleter.dispose();
|
||||
}
|
||||
|
||||
public void deleterLoad(View view) {
|
||||
if (mDeleter == null)
|
||||
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
public void onResume() {
|
||||
mDeleter.resume();
|
||||
mHandler.post(rUpdater);
|
||||
}
|
||||
|
||||
public void deleterResume() {
|
||||
if (mDeleter != null) mDeleter.resume();
|
||||
public void onPaused() {
|
||||
mDeleter.pause();
|
||||
mHandler.removeCallbacks(rUpdater);
|
||||
}
|
||||
|
||||
|
||||
public void recoverMission(DownloadMission mission) {
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
if (mission != h.item.mission) continue;
|
||||
ViewHolderItem h = getViewHolder(mission);
|
||||
if (h == null) return;
|
||||
|
||||
mission.errObject = null;
|
||||
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
|
||||
|
||||
h.status.setText(UNDEFINED_PROGRESS);
|
||||
h.state = -1;
|
||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||
h.progress.setMarquee(true);
|
||||
|
||||
mDownloadManager.resumeMission(mission);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private boolean mUpdaterRunning = false;
|
||||
private final Runnable rUpdater = this::updater;
|
||||
|
||||
public void onPaused() {
|
||||
setAutoRefresh(false);
|
||||
}
|
||||
|
||||
private void setAutoRefresh(boolean enabled) {
|
||||
if (enabled && !mUpdaterRunning) {
|
||||
mUpdaterRunning = true;
|
||||
updater();
|
||||
} else if (!enabled && mUpdaterRunning) {
|
||||
mUpdaterRunning = false;
|
||||
mHandler.removeCallbacks(rUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
private void updater() {
|
||||
if (!mUpdaterRunning) return;
|
||||
|
||||
boolean running = false;
|
||||
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||
// check if the mission is running first
|
||||
if (!((DownloadMission) h.item.mission).running) continue;
|
||||
|
||||
updateProgress(h);
|
||||
running = true;
|
||||
}
|
||||
|
||||
if (running) {
|
||||
mHandler.postDelayed(rUpdater, 1000);
|
||||
} else {
|
||||
mUpdaterRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNotFinite(Float value) {
|
||||
return Float.isNaN(value) || Float.isInfinite(value);
|
||||
private boolean isNotFinite(double value) {
|
||||
return Double.isNaN(value) || Double.isInfinite(value);
|
||||
}
|
||||
|
||||
public void setRecover(@NonNull RecoverHelper callback) {
|
||||
|
@ -771,10 +768,11 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
MenuItem source;
|
||||
MenuItem checksum;
|
||||
|
||||
long lastTimeStamp = -1;
|
||||
long lastDone = -1;
|
||||
int lastCurrent = -1;
|
||||
int state = 0;
|
||||
long lastTimestamp = -1;
|
||||
double lastDone;
|
||||
int lastSpeedIdx;
|
||||
float[] lastSpeed = new float[3];
|
||||
String estimatedTimeArrival = UNDEFINED_ETA;
|
||||
|
||||
ViewHolderItem(View view) {
|
||||
super(view);
|
||||
|
@ -859,7 +857,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
|
||||
delete.setVisible(true);
|
||||
|
||||
boolean flag = !mission.isPsFailed();
|
||||
boolean flag = !mission.isPsFailed() && mission.urls.length > 0;
|
||||
start.setVisible(flag);
|
||||
queue.setVisible(flag);
|
||||
}
|
||||
|
@ -884,6 +882,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||
|
||||
return popup;
|
||||
}
|
||||
|
||||
private void resetSpeedMeasure() {
|
||||
estimatedTimeArrival = UNDEFINED_ETA;
|
||||
lastTimestamp = -1;
|
||||
lastSpeedIdx = -1;
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolderHeader extends RecyclerView.ViewHolder {
|
||||
|
|
|
@ -4,9 +4,10 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -113,7 +114,7 @@ public class Deleter {
|
|||
show();
|
||||
}
|
||||
|
||||
private void pause() {
|
||||
public void pause() {
|
||||
running = false;
|
||||
mHandler.removeCallbacks(rNext);
|
||||
mHandler.removeCallbacks(rShow);
|
||||
|
@ -126,13 +127,11 @@ public class Deleter {
|
|||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||
}
|
||||
|
||||
public void dispose(boolean commitChanges) {
|
||||
public void dispose() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
|
||||
if (!commitChanges) return;
|
||||
|
||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||
items = null;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.graphics.Rect;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable {
|
|||
mForegroundColor = foreground;
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
mProgress = progress;
|
||||
public void setProgress(double progress) {
|
||||
mProgress = (float) progress;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,6 @@ import android.os.Bundle;
|
|||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -24,6 +19,12 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
@ -72,8 +73,7 @@ public class MissionsFragment extends Fragment {
|
|||
mBinder = (DownloadManagerBinder) binder;
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
||||
mAdapter.deleterLoad(getView());
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView());
|
||||
|
||||
mAdapter.setRecover(MissionsFragment.this::recoverMission);
|
||||
|
||||
|
@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment {
|
|||
* Added in API level 23.
|
||||
*/
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
// Bug: in api< 23 this is never called
|
||||
|
@ -147,7 +147,7 @@ public class MissionsFragment extends Fragment {
|
|||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
public void onAttach(@NonNull Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
mContext = activity;
|
||||
|
@ -162,7 +162,7 @@ public class MissionsFragment extends Fragment {
|
|||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mBinder.enableNotifications(true);
|
||||
mContext.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(true);
|
||||
mAdapter.onDestroy();
|
||||
|
||||
mBinder = null;
|
||||
mAdapter = null;
|
||||
|
@ -196,13 +196,11 @@ public class MissionsFragment extends Fragment {
|
|||
prompt.create().show();
|
||||
return true;
|
||||
case R.id.start_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().startAllMissions();
|
||||
return true;
|
||||
case R.id.pause_downloads:
|
||||
item.setVisible(false);
|
||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||
mAdapter.ensurePausedMissions();// update items view
|
||||
mAdapter.refreshMissionItems();// update items view
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
@ -271,23 +269,12 @@ public class MissionsFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterDispose(false);
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterResume();
|
||||
mAdapter.onResume();
|
||||
|
||||
if (mForceUpdate) {
|
||||
mForceUpdate = false;
|
||||
|
@ -303,7 +290,13 @@ public class MissionsFragment extends Fragment {
|
|||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (mAdapter != null) mAdapter.onPaused();
|
||||
|
||||
if (mAdapter != null) {
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter);
|
||||
mAdapter.onPaused();
|
||||
}
|
||||
|
||||
if (mBinder != null) mBinder.enableNotifications(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,14 @@ import android.content.ClipData;
|
|||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
@ -26,6 +27,7 @@ import java.io.Serializable;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
|
@ -39,26 +41,28 @@ public class Utility {
|
|||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (bytes < 1024) {
|
||||
return String.format("%d B", bytes);
|
||||
return String.format(locale, "%d B", bytes);
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f kB", bytes / 1024d);
|
||||
return String.format(locale, "%.2f kB", bytes / 1024d);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", bytes / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f MB", bytes / 1024d / 1024d);
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatSpeed(float speed) {
|
||||
public static String formatSpeed(double speed) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (speed < 1024) {
|
||||
return String.format("%.2f B/s", speed);
|
||||
return String.format(locale, "%.2f B/s", speed);
|
||||
} else if (speed < 1024 * 1024) {
|
||||
return String.format("%.2f kB/s", speed / 1024);
|
||||
return String.format(locale, "%.2f kB/s", speed / 1024);
|
||||
} else if (speed < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB/s", speed / 1024 / 1024);
|
||||
return String.format(locale, "%.2f MB/s", speed / 1024 / 1024);
|
||||
} else {
|
||||
return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,12 +192,11 @@ public class Utility {
|
|||
switch (type) {
|
||||
case MUSIC:
|
||||
return R.drawable.music;
|
||||
default:
|
||||
case VIDEO:
|
||||
return R.drawable.video;
|
||||
case SUBTITLE:
|
||||
return R.drawable.subtitle;
|
||||
default:
|
||||
return R.drawable.video;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,4 +277,25 @@ public class Utility {
|
|||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String pad(int number) {
|
||||
return number < 10 ? ("0" + number) : String.valueOf(number);
|
||||
}
|
||||
|
||||
public static String stringifySeconds(double seconds) {
|
||||
int h = (int) Math.floor(seconds / 3600);
|
||||
int m = (int) Math.floor((seconds - (h * 3600)) / 60);
|
||||
int s = (int) (seconds - (h * 3600) - (m * 60));
|
||||
|
||||
String str = "";
|
||||
|
||||
if (h < 1 && m < 1) {
|
||||
str = "00:";
|
||||
} else {
|
||||
if (h > 0) str = pad(h) + ":";
|
||||
if (m > 0) str += pad(m) + ":";
|
||||
}
|
||||
|
||||
return str + pad(s);
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 475 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 311 B |
After Width: | Height: | Size: 312 B |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 235 B |
After Width: | Height: | Size: 226 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 588 B |
After Width: | Height: | Size: 291 B |
After Width: | Height: | Size: 284 B |
After Width: | Height: | Size: 856 B |
After Width: | Height: | Size: 835 B |
After Width: | Height: | Size: 344 B |
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
|
@ -47,15 +47,22 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/drawer_header_service_view"
|
||||
android:layout_width="100dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="100dp"
|
||||
android:layout_alignLeft="@id/drawer_header_np_text_view"
|
||||
android:layout_alignStart="@id/drawer_header_np_text_view"
|
||||
android:layout_below="@id/drawer_header_np_text_view"
|
||||
android:layout_toLeftOf="@id/drawer_arrow"
|
||||
android:layout_marginRight="5dp"
|
||||
android:text="YouTube"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/drawer_header_font_color"
|
||||
android:textStyle="italic" />
|
||||
android:textStyle="italic"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/drawer_arrow"
|
||||
|
|
|
@ -46,15 +46,22 @@ android:focusable="true">
|
|||
|
||||
<TextView
|
||||
android:id="@+id/drawer_header_service_view"
|
||||
android:layout_width="100dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="100dp"
|
||||
android:text="YouTube"
|
||||
android:layout_below="@id/drawer_header_np_text_view"
|
||||
android:layout_alignLeft="@id/drawer_header_np_text_view"
|
||||
android:layout_alignStart="@id/drawer_header_np_text_view"
|
||||
android:layout_toLeftOf="@id/drawer_arrow"
|
||||
android:layout_marginRight="5dp"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/drawer_header_font_color"
|
||||
android:textStyle="italic"/>
|
||||
android:textStyle="italic"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/drawer_arrow"
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:elevation="0dp"
|
||||
android:background="?attr/android:windowBackground"
|
||||
app:headerLayout="@layout/drawer_header"/>
|
||||
app:headerLayout="@layout/drawer_header"
|
||||
android:theme="@style/NavViewTextStyle"/>
|
||||
<!-- app:menu="@menu/drawer_items" -->
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instanceHelpTV"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="15dp"
|
||||
android:autoLink="web"
|
||||
android:text="@string/peertube_instance_url_help"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/instances"
|
||||
android:layout_below="@id/instanceHelpTV"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/item_instance" />
|
||||
|
||||
<!-- LOADING INDICATOR-->
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_progress_bar"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/addInstanceButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="?attr/colorPrimary"
|
||||
app:fabSize="auto"
|
||||
app:srcCompat="?attr/ic_add" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:maxLength="0" />
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/spinner"
|
||||
tools:listitem="@layout/instance_spinner_item"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:prompt="@string/choose_instance_prompt" />
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layoutCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal"
|
||||
app:cardCornerRadius="5dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/instanceIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginLeft="10dp"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"
|
||||
tools:src="@drawable/place_holder_peertube"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instanceName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_toRightOf="@+id/instanceIcon"
|
||||
android:layout_toLeftOf="@id/selectInstanceRB"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?textAppearanceListItem"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="Framatube"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instanceUrl"
|
||||
android:autoLink="web"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_toRightOf="@id/instanceIcon"
|
||||
android:layout_toLeftOf="@id/selectInstanceRB"
|
||||
android:layout_below="@id/instanceName"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?textAppearanceListItemSecondary"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="https://framatube.org"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/selectInstanceRB"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@id/handle"
|
||||
android:layout_centerVertical="true"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/handle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingTop="12dp"
|
||||
android:src="?attr/drag_handle"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"/>
|
||||
</RelativeLayout>
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -20,7 +20,7 @@
|
|||
<string name="download_path_summary">يتم تخزين ملفات الفيديو التي تم تنزيلها هنا</string>
|
||||
<string name="download_path_title">مجلد تحميل الفيديو</string>
|
||||
<string name="err_dir_create">"لا يمكن إنشاء مجلد للتنزيلات في '%1$s'"</string>
|
||||
<string name="info_dir_created">إنشاء دليل التنزيل \'%1$s\'</string>
|
||||
<string name="info_dir_created">دليل التنزيل الذي تم إنشاؤه \'%1$s\'</string>
|
||||
<string name="install">تثبيت</string>
|
||||
<string name="kore_not_found">تطبيق Kore غير موجود. هل تريد تثبيته ؟</string>
|
||||
<string name="light_theme_title">مضيء</string>
|
||||
|
@ -42,7 +42,7 @@
|
|||
<string name="share">مشاركة</string>
|
||||
<string name="share_dialog_title">مشاركة بواسطة</string>
|
||||
<string name="show_next_and_similar_title">عرض مقاطع الفيديو \"التالية\" و \"المشابهة\"</string>
|
||||
<string name="show_play_with_kodi_summary">عرض خيار تشغيل الفيديو عبر وسائط Kodi</string>
|
||||
<string name="show_play_with_kodi_summary">عرض خيارات تشغيل الفيديو من خلال مركز كودي ميديا</string>
|
||||
<string name="show_play_with_kodi_title">عرض خيار التشغيل بواسطة كودي</string>
|
||||
<string name="theme_title">السمة</string>
|
||||
<string name="upload_date_text">تم النشر يوم %1$s</string>
|
||||
|
@ -80,7 +80,7 @@
|
|||
<string name="title_activity_history">التاريخ</string>
|
||||
<string name="action_history">التاريخ</string>
|
||||
<string name="open_in_popup_mode">فتح في وضع منبثق</string>
|
||||
<string name="use_external_video_player_summary">يزيل الصوت في بعض مستوى الدقة</string>
|
||||
<string name="use_external_video_player_summary">إزالة الصوت في بعض مستوى الدقة</string>
|
||||
<string name="popup_mode_share_menu_title">وضع النوافذ المنبثقة NewPipe</string>
|
||||
<string name="channel_unsubscribed">تم إلغاء الاشتراك في القناة</string>
|
||||
<string name="subscription_change_failed">تعذر تغيير حالة الاشتراك</string>
|
||||
|
@ -153,17 +153,17 @@
|
|||
<string name="audio">الصوت</string>
|
||||
<string name="retry">إعادة المحاولة</string>
|
||||
<string name="storage_permission_denied">تم رفض إذن الوصول إلى التخزين</string>
|
||||
<string name="short_thousand">K</string>
|
||||
<string name="short_thousand">ألف</string>
|
||||
<string name="short_million">مليون</string>
|
||||
<string name="short_billion">G</string>
|
||||
<string name="no_subscribers">ليس هناك مشترِكون</string>
|
||||
<plurals name="subscribers">
|
||||
<item quantity="zero">%s لا يوجد مشاركين</item>
|
||||
<item quantity="one">%s مشترك</item>
|
||||
<item quantity="two">"%s مشتركتين"</item>
|
||||
<item quantity="two">%s مشاريكان</item>
|
||||
<item quantity="few">%s اشتراكات</item>
|
||||
<item quantity="many">%s مشاركين</item>
|
||||
<item quantity="other">%s مشتركون</item>
|
||||
<item quantity="many">%s مشاركون</item>
|
||||
<item quantity="other">%s اشتراك</item>
|
||||
</plurals>
|
||||
<string name="no_views">دون مشاهدات</string>
|
||||
<string name="no_videos">لاتوجد فيديوهات</string>
|
||||
|
@ -234,24 +234,24 @@
|
|||
<string name="play_queue_stream_detail">التفاصيل</string>
|
||||
<string name="play_queue_audio_settings">الإعدادات الصوتية</string>
|
||||
<string name="start_here_on_main">تشغيل هنا</string>
|
||||
<string name="start_here_on_popup">تشغيل في وضع نافذة منبثقة</string>
|
||||
<string name="start_here_on_popup">بدأ التشغيل في نافذة منبثقة جديدة</string>
|
||||
<string name="reCaptcha_title">تحدي الكابتشا</string>
|
||||
<string name="hold_to_append">ضغط مطول للإدراج الى قائمة الانتظار</string>
|
||||
<plurals name="views">
|
||||
<item quantity="zero">بدون مشاهدات</item>
|
||||
<item quantity="one">%s مشاهدة</item>
|
||||
<item quantity="two">%s مشاهدتين</item>
|
||||
<item quantity="few">%s مشاهدون</item>
|
||||
<item quantity="many">%s مشاهدات</item>
|
||||
<item quantity="other">%s مشاهدين</item>
|
||||
<item quantity="zero">%s بدون مشهد</item>
|
||||
<item quantity="one">%s شاهد</item>
|
||||
<item quantity="two">%s مشاهدتان</item>
|
||||
<item quantity="few">%s مشاهدات</item>
|
||||
<item quantity="many">%s مشاهدون</item>
|
||||
<item quantity="other">%s شاهدو</item>
|
||||
</plurals>
|
||||
<plurals name="videos">
|
||||
<item quantity="zero">%s لا يوجد فيديو</item>
|
||||
<item quantity="one">%s فيديو</item>
|
||||
<item quantity="two">%s فيديوان</item>
|
||||
<item quantity="few">%s فيديوات</item>
|
||||
<item quantity="many">%s فيديوهات</item>
|
||||
<item quantity="other">%s مزيد من الفيديوات</item>
|
||||
<item quantity="zero">فيديو%s video</item>
|
||||
<item quantity="one">%s videosفيديوهات</item>
|
||||
<item quantity="two">%s videosفيديوهات</item>
|
||||
<item quantity="few">%s videosفيديوهات</item>
|
||||
<item quantity="many">%s videosفيديوهات</item>
|
||||
<item quantity="other">%s videosفيديوهات</item>
|
||||
</plurals>
|
||||
<string name="recaptcha_request_toast">طلب اختبار الكابتشا مطلوب</string>
|
||||
<string name="copyright" formatted="true">© %1$sبواسطة%2$sتحت%3$s</string>
|
||||
|
@ -425,10 +425,10 @@
|
|||
<string name="app_update_notification_channel_name">تتبيه تحديث التطبيق</string>
|
||||
<string name="volume_gesture_control_title">إيماءة التحكم بالصوت</string>
|
||||
<string name="events">الأحداث</string>
|
||||
<string name="app_update_notification_channel_description">إخطارات NewPipe جديدة الإصدار</string>
|
||||
<string name="download_to_sdcard_error_title">ذاكرة التخزين الخارجية غير متوفرة</string>
|
||||
<string name="app_update_notification_channel_description">"تنبيه عند تواجد إصدار جديد newpipe "</string>
|
||||
<string name="download_to_sdcard_error_title">وحدة التخزين الخارجية غير متاحة</string>
|
||||
<string name="download_to_sdcard_error_message">"التنزيل على بطاقة SD الخارجية غير ممكن. إعادة تعيين موقع مجلد التحميل؟"</string>
|
||||
<string name="saved_tabs_invalid_json">استخدام خطأ علامات التبويب الافتراضية, أثناء قراءة علامات التبويب المحفوظة</string>
|
||||
<string name="saved_tabs_invalid_json">باستخدام علامات التبويب الافتراضية ، خطأ أثناء قراءة علامات التبويب المحفوظة</string>
|
||||
<string name="restore_defaults">استعادة الضبط الافتراضي</string>
|
||||
<string name="restore_defaults_confirmation">هل تريد استعادة الإعدادات الافتراضية؟</string>
|
||||
<string name="subscribers_count_not_available">عدد المشتركين غير متاح</string>
|
||||
|
@ -449,7 +449,7 @@
|
|||
<string name="paused">متوقف</string>
|
||||
<string name="queued">في قائمة الانتظار</string>
|
||||
<string name="post_processing">قيد المعالجة</string>
|
||||
<string name="enqueue">قائمة الانتظار</string>
|
||||
<string name="enqueue">قائمه انتظار</string>
|
||||
<string name="permission_denied">تم رفضها من قبل النظام</string>
|
||||
<string name="download_failed">فشل التنزيل</string>
|
||||
<string name="download_finished">تم الانتهاء من التحميل</string>
|
||||
|
@ -468,11 +468,9 @@
|
|||
<string name="error_connect_host">لا يمكن الاتصال بالخادم</string>
|
||||
<string name="error_http_no_content">الخادم لايقوم بإرسال البيانات</string>
|
||||
<string name="error_http_unsupported_range">الخادم لا يقبل التنزيل المتعدد، إعادة المحاولة مع @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">عدم استيفاء النطاق المطلوب</string>
|
||||
<string name="error_http_not_found">غير موجود</string>
|
||||
<string name="error_postprocessing_failed">فشلت المعالجة الاولية</string>
|
||||
<string name="clear_finished_download">حذف التنزيلات المنتهية</string>
|
||||
<string name="msg_pending_downloads">"قم بإستكمال %s حيثما يتم التحويل من التنزيلات"</string>
|
||||
<string name="stop">توقف</string>
|
||||
<string name="max_retry_msg">أقصى عدد للمحاولات</string>
|
||||
<string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string>
|
||||
|
@ -522,4 +520,8 @@
|
|||
<string name="delete_playback_states_alert">حذف كل مواقف التشغيل؟</string>
|
||||
<string name="download_choose_new_path">تغيير مجلدات التنزيل إلى حيز التنفيذ</string>
|
||||
<string name="drawer_header_description">تبديل الخدمة ، المحدد حاليًا:</string>
|
||||
<string name="default_kiosk_page_summary">الكشك الافتراضي</string>
|
||||
<string name="no_one_watching">لاتوجد مشاهدة</string>
|
||||
<string name="no_one_listening">لا أحد يستمع</string>
|
||||
<string name="localization_changes_requires_app_restart">ستتغير اللغة بمجرد إعادة تشغيل التطبيق.</string>
|
||||
</resources>
|
|
@ -22,6 +22,7 @@
|
|||
<string name="content_language_title">Llingua predeterminada del conteníu</string>
|
||||
<string name="settings_category_video_audio_title">Videu y audiu</string>
|
||||
<string name="settings_category_appearance_title">Aspeutu</string>
|
||||
<string name="content">Conteníu</string>
|
||||
<string name="show_age_restricted_content_title">Conteníu torgáu pola edá</string>
|
||||
<string name="duration_live">EN DIREUTO</string>
|
||||
<string name="general_error">Fallu</string>
|
||||
|
@ -60,6 +61,7 @@
|
|||
<string name="reCaptchaActivity">reCAPTCHA</string>
|
||||
<string name="reCaptcha_title">Retu de reCAPTCHA</string>
|
||||
<string name="recaptcha_request_toast">Solicitóse\'l retu de reCAPTCHA</string>
|
||||
<string name="controls_background_title">En segundu planu</string>
|
||||
<string name="controls_popup_title">Ventanu</string>
|
||||
<string name="default_popup_resolution_title">Resolución predeterminada del ventanu</string>
|
||||
<string name="show_higher_resolutions_title">Amosar resoluciones más altes</string>
|
||||
|
@ -67,9 +69,13 @@
|
|||
<string name="clear">Llimpiar</string>
|
||||
<string name="use_external_video_player_summary">Quita l\'audiu en DALGUNES resoluciones</string>
|
||||
<string name="player_gesture_controls_summary">Usa xestos pa controlar el brilléu y volume del reproductor</string>
|
||||
<string name="show_search_suggestions_title">Guetar suxerencies</string>
|
||||
<string name="show_search_suggestions_summary">Amuesa suxerencies al guetar</string>
|
||||
<string name="subscribe_button_title">Soscribise</string>
|
||||
<string name="subscription_update_failed">Nun pudo anovase la soscripción</string>
|
||||
<string name="tab_subscriptions">Soscripciones</string>
|
||||
<string name="fragment_whats_new">Qué hai nuevo</string>
|
||||
<string name="enable_search_history_title">Historial de gueta</string>
|
||||
<string name="resume_on_audio_focus_gain_title">Siguir al recuperar el focu</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Sigue cola reproducción dempués de les interrupciones (llamaes telefóniques, por exemplu)</string>
|
||||
<string name="settings_category_player_title">Reproductor</string>
|
||||
|
@ -89,6 +95,7 @@
|
|||
<item quantity="other">%s visualizaciones</item>
|
||||
</plurals>
|
||||
<string name="settings_category_downloads_title">Descarga</string>
|
||||
<string name="settings_file_charset_title">Caráuteres almitíos nos nomes de ficheros</string>
|
||||
<string name="charset_letters_and_digits">Lletres y díxitos</string>
|
||||
<string name="charset_most_special_characters">La mayoría de caráuteres especiales</string>
|
||||
<string name="title_activity_about">Tocante a NewPipe</string>
|
||||
|
@ -109,13 +116,19 @@
|
|||
<string name="title_history_view">Vióse</string>
|
||||
<string name="action_history">Historial</string>
|
||||
<string name="history_empty">L\'historial ta baleru</string>
|
||||
<string name="delete_item_search_history">¿Quies desaniciar esti elementu del historial de gueta\?</string>
|
||||
<string name="play_all">Reproducir too</string>
|
||||
<string name="player_stream_failure">Nun pudo reproducise esti fluxu</string>
|
||||
<string name="player_unrecoverable_failure">Asocedió un fallu irrecuperable del reproductor</string>
|
||||
<string name="main_page_content">Conteníu de la páxina principal</string>
|
||||
<string name="blank_page_summary">Páxina balera</string>
|
||||
<string name="select_a_kiosk">Esbilla d\'un quioscu</string>
|
||||
<string name="kiosk">Quioscu</string>
|
||||
<string name="trending">Tendencies</string>
|
||||
<string name="top_50">Destácase</string>
|
||||
<string name="play_queue_stream_detail">Detalles</string>
|
||||
<string name="new_and_hot">Novedaes</string>
|
||||
<string name="unknown_content">[Desconozse]</string>
|
||||
<string name="start_here_on_background">Reproducir en segundu planu</string>
|
||||
<string name="start_here_on_popup">Reproducir nun ventanu</string>
|
||||
<string name="donation_title">Donación</string>
|
||||
|
@ -132,11 +145,14 @@
|
|||
<string name="invalid_url_toast">URL nun ye válida</string>
|
||||
<string name="override_current_data">Esto va anular la configuración actual.</string>
|
||||
<string name="show_info">Amosar la información</string>
|
||||
<string name="tab_bookmarks">Llistes de reproducción en marcadores</string>
|
||||
<string name="tab_bookmarks">Llistes de reproducción</string>
|
||||
<string name="create">Crear</string>
|
||||
<string name="dismiss">Escartar</string>
|
||||
<string name="delete_all_history_prompt">¿De xuru que quies desaniciar tolos elementos del historial\?</string>
|
||||
<string name="drawer_header_action_paceholder_text">Equí va apaecer dalgo ceo ;D</string>
|
||||
<string name="create_playlist">Llista nueva de repoducción</string>
|
||||
<string name="playlist_name_input">Nome</string>
|
||||
<string name="append_playlist">Amestar a una llista de repoducción</string>
|
||||
<string name="delete_playlist_prompt">¿Desaniciar esta llista de reproducción\?</string>
|
||||
<string name="playlist_delete_failure">Nun pudo desaniciase la llista de reproducción.</string>
|
||||
<string name="caption_none">Ensin sotítulos</string>
|
||||
|
@ -160,7 +176,7 @@
|
|||
<string name="tab_new">Llingüeta nueva</string>
|
||||
<string name="volume_gesture_control_summary">Usa xestos pa controlar el volume del reproductor</string>
|
||||
<string name="brightness_gesture_control_summary">Usa xestos pa controlar el brilléu del reproductor</string>
|
||||
<string name="restore_defaults">Valores predeterminaos</string>
|
||||
<string name="restore_defaults">Reafitar</string>
|
||||
<string name="subscribers_count_not_available">El númberu de soscriptores nun ta disponible</string>
|
||||
<string name="selection">Esbilla</string>
|
||||
<string name="updates_setting_title">Anovamientos</string>
|
||||
|
@ -190,19 +206,41 @@
|
|||
<string name="caption_setting_title">Sotítulos</string>
|
||||
<string name="accept">Aceutar</string>
|
||||
<string name="restore_defaults_confirmation">¿Quies reafitar los valores\?</string>
|
||||
<string name="error_unknown_host"></string>
|
||||
<string name="error_http_unsupported_range">El sirvidor nun aceuta descargues multifilu, volvi probar con @string/msg_threads = 1</string>
|
||||
<string name="no_comments">Nun hai comentarios</string>
|
||||
<string name="settings_category_clear_data_title">Llimpieza de datos</string>
|
||||
<plurals name="videos">
|
||||
<item quantity="one">Vídeos</item>
|
||||
</plurals>
|
||||
<string name="show_comments_title">Amosar comentarios</string>
|
||||
<string name="show_comments_summary">Toca p\'alternar la so des/activación</string>
|
||||
<string name="start_accept_privacy_policy">Pa cumplir cola GDPR (Regulación Xeneral de Proteición de Datos) europea, pidímoste que revises la política de privacidá de NewPipe. Lléila con procuru.
|
||||
<string name="delete_search_history_alert">¿Desaniciar tol historial de gueta\?</string>
|
||||
<string name="start_accept_privacy_policy">Pa cumplir cola GDPR (Regulación Xeneral de Proteición de Datos) europea, pidímoste que revises la política de privacidá de NewPipe. Lléila con procuru
|
||||
\n.Has aceutala unviándonos un informe de fallos.</string>
|
||||
<string name="minimize_on_exit_summary">Aición al cambiar a otra aplicación dende\'l reproductor principal de videu — %s</string>
|
||||
\nHas aceutala unviándonos un informe de fallos.</string>
|
||||
<string name="minimize_on_exit_summary">Aición al cambiar a otra aplicación dende\'l reproductor de vídeos principal — %s</string>
|
||||
<string name="max_retry_desc">El númberu máximu d\'intentos enantes d\'encaboxar la descarga</string>
|
||||
<string name="enable_playback_state_lists_title">Posiciones nes llistes</string>
|
||||
<string name="confirm_prompt">¿De xuru\?</string>
|
||||
<string name="previous_export">Esportación anterior</string>
|
||||
<string name="import_file_title">Importar el ficheru</string>
|
||||
<string name="import_youtube_instructions">Importa les soscripciones de YouTube baxando\'l ficheru d\'esportación:
|
||||
\n
|
||||
\n1.- Vete a esta URL: %1$s
|
||||
\n2.- Anicia sesión cuando se te pida
|
||||
\n3.- Debería aniciar una descarga (que ye\'l ficheru d\'esportación)</string>
|
||||
<string name="import_soundcloud_instructions">Importa un perfil de SoundCloud teclexando la URL o la ID de to:
|
||||
\n1.- Activa\'l mou escritoriu nun restolador web (el sitiu nun ta disponible pa móviles)
|
||||
\n
|
||||
\n3.- Anicia sesión cuando se te pida
|
||||
\n2.- Vete a esta URL: %1$s
|
||||
<string name="import_soundcloud_instructions_hint">LaToID, soundcloud.com/latoid</string>
|
||||
\n4.- Copia la URL del perfil al que se te redirixa.</string>
|
||||
<string name="download_thumbnail_title">Cargar miniatures</string>
|
||||
<string name="download_thumbnail_summary">Desactiva esta opción pa evitar la carga de miniatures, aforrar datos y usu de la memoria. Los cambeos van llimpiar la memoria y la caché d\'imáxenes.</string>
|
||||
<string name="minimize_on_exit_title">Minimizar al cambiar a otra aplicación</string>
|
||||
<string name="minimize_on_exit_background_description">Minimizar al reproductor en segundu planu</string>
|
||||
<string name="minimize_on_exit_popup_description">Minimizar al reproductor en ventanu</string>
|
||||
<string name="unsubscribe">Desoscribise</string>
|
||||
<string name="tab_choose">Escoyeta d\'una llingüeta</string>
|
||||
<string name="enable_playback_resume_title">Siguir cola reproducción</string>
|
||||
<string name="main_page_content_summary">Les llingüetes que s\'amuesen na páxina principal</string>
|
||||
<string name="downloads_storage_ask_title">Entrugar ánde baxar</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,518 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="main_bg_subtitle">点击搜索按钮即可开始使用</string>
|
||||
<string name="view_count_text">%1$s 次观看</string>
|
||||
<string name="upload_date_text">发布于 %1$s</string>
|
||||
<string name="open_in_browser">在浏览器中打开</string>
|
||||
<string name="open_in_popup_mode">在悬浮窗模式下打开</string>
|
||||
<string name="did_you_mean">您是不是要找:%1$s?</string>
|
||||
<string name="no_player_found_toast">找不到串流播放器 (您可以安裝并使用VLC播放)。</string>
|
||||
<string name="controls_download_desc">下载串流文件</string>
|
||||
<string name="install">安装</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="share">分享</string>
|
||||
<string name="download">下载</string>
|
||||
<string name="search">搜索</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="share_dialog_title">分享给...</string>
|
||||
<string name="choose_browser">选择浏览器</string>
|
||||
<string name="download_path_title">视频下载文件夹</string>
|
||||
<string name="download_path_summary">已下载的视频存储在这里</string>
|
||||
<string name="download_path_dialog_title">请选择下载视频的保存位置</string>
|
||||
<string name="download_path_audio_summary">已下载的音频存储在这里</string>
|
||||
<string name="download_path_audio_dialog_title">选择下载音频的储存位置</string>
|
||||
<string name="autoplay_by_calling_app_title">自动播放</string>
|
||||
<string name="play_with_kodi_title">使用Kodi播放</string>
|
||||
<string name="theme_title">主题</string>
|
||||
<string name="light_theme_title">浅色</string>
|
||||
<string name="dark_theme_title">暗黑</string>
|
||||
<string name="black_theme_title">黑色</string>
|
||||
<string name="download_dialog_title">下载</string>
|
||||
<string name="next_video_title">下一个</string>
|
||||
<string name="url_not_supported_toast">不支持的 URL</string>
|
||||
<string name="settings_category_appearance_title">外观</string>
|
||||
<string name="settings_category_other_title">其他</string>
|
||||
<string name="all">全部</string>
|
||||
<string name="channel">频道</string>
|
||||
<string name="yes">是</string>
|
||||
<string name="later">稍后</string>
|
||||
<string name="network_error">网络错误</string>
|
||||
<plurals name="videos">
|
||||
<item quantity="one">视频</item>
|
||||
<item quantity="other"></item>
|
||||
</plurals>
|
||||
<string name="disabled">禁用</string>
|
||||
<string name="controls_background_title">背景</string>
|
||||
<string name="filter">过滤器</string>
|
||||
<string name="refresh">刷新</string>
|
||||
<string name="show_search_suggestions_title">搜索建议</string>
|
||||
<string name="subscribe_button_title">订阅</string>
|
||||
<string name="subscribed_button_title">已订阅</string>
|
||||
<string name="enable_watch_history_title">观看历史</string>
|
||||
<string name="settings_category_player_title">播放器</string>
|
||||
<string name="settings_category_history_title">历史记录与缓存</string>
|
||||
<string name="playlist">播放列表</string>
|
||||
<string name="undo">撤销</string>
|
||||
<string name="play_all">全部播放</string>
|
||||
<string name="always">总是</string>
|
||||
<string name="just_once">仅一次</string>
|
||||
<string name="controls_add_to_playlist_title">添加至</string>
|
||||
<string name="file">文件</string>
|
||||
<string name="download_thumbnail_title">加载缩略图</string>
|
||||
<string name="clear_views_history_title">清除观看记录</string>
|
||||
<string name="minimize_on_exit_none_description">无</string>
|
||||
<string name="minimize_on_exit_background_description">最小化后台播放器</string>
|
||||
<string name="minimize_on_exit_popup_description">最小化悬浮窗播放器</string>
|
||||
<string name="channels">频道</string>
|
||||
<string name="playlists">播放列表</string>
|
||||
<string name="unsubscribe">取消订阅</string>
|
||||
<string name="tab_new">新标签</string>
|
||||
<string name="settings_category_updates_title">更新</string>
|
||||
<string name="file_deleted">文件已删除</string>
|
||||
<string name="subscribers_count_not_available">无法得知订阅人数</string>
|
||||
<string name="updates_setting_description">每推出新版本时,弹出应用升级通知</string>
|
||||
<string name="grid">网格</string>
|
||||
<string name="app_update_notification_content_title">新版 NewPipe 已可升级!</string>
|
||||
<string name="error_http_unsupported_range">服务器不接受 接收 multi-threaded 下载, 以 @string/msg_threads = 1 重试</string>
|
||||
<string name="autoplay_title">自动播放</string>
|
||||
<string name="settings_category_clear_data_title">清除数据</string>
|
||||
<string name="watch_history_deleted">观看记录已删除</string>
|
||||
<string name="detail_likes_img_view_description">喜欢</string>
|
||||
<string name="detail_dislikes_img_view_description">不喜欢</string>
|
||||
<string name="use_tor_title">使用Tor</string>
|
||||
<string name="use_tor_summary">(实验性)通过 Tor 强制下载流量以增强隐私(暂不支持串流视频)。</string>
|
||||
<string name="report_error">报告错误</string>
|
||||
<string name="user_report">用户报告</string>
|
||||
<string name="err_dir_create">无法创建下载目录\"%1$s\"</string>
|
||||
<string name="info_dir_created">已成功创建下载目录「%1$s」</string>
|
||||
<string name="video">视频</string>
|
||||
<string name="audio">音频</string>
|
||||
<string name="retry">重试</string>
|
||||
<string name="storage_permission_denied">存储访问权限已被拒绝</string>
|
||||
<plurals name="views">
|
||||
<item quantity="one">%1$s 次观看</item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="short_thousand">千</string>
|
||||
<string name="short_million">百万</string>
|
||||
<string name="start">开始</string>
|
||||
<string name="pause">暂停</string>
|
||||
<string name="view">播放</string>
|
||||
<string name="delete">删除</string>
|
||||
<string name="checksum">校验</string>
|
||||
<string name="add">新任务</string>
|
||||
<string name="finish">OK</string>
|
||||
<string name="msg_name">文件名</string>
|
||||
<string name="msg_threads">线程数</string>
|
||||
<string name="msg_error">错误</string>
|
||||
<string name="msg_server_unsupported">不支持的服务器</string>
|
||||
<string name="msg_exists">文件已存在</string>
|
||||
<string name="msg_running_detail">点击了解详情</string>
|
||||
<string name="msg_wait">请稍候…</string>
|
||||
<string name="msg_copied">复制至剪贴板</string>
|
||||
<string name="reCaptchaActivity">reCAPTCHA验证码</string>
|
||||
<string name="controls_popup_title">悬浮窗播放</string>
|
||||
<string name="title_activity_about">关于NewPipe</string>
|
||||
<string name="action_settings">设置</string>
|
||||
<string name="action_about">关于</string>
|
||||
<string name="title_licenses">第三方许可</string>
|
||||
<string name="copyright" formatted="true">© %1$s :作者 %2$s (使用 %3$s )</string>
|
||||
<string name="error_unable_to_load_license">无法加载许可证</string>
|
||||
<string name="action_open_website">打开网站</string>
|
||||
<string name="tab_about">关于</string>
|
||||
<string name="tab_contributors">贡献者</string>
|
||||
<string name="tab_licenses">许可证</string>
|
||||
<string name="settings_category_downloads_title">下载</string>
|
||||
<string name="settings_file_charset_title">文件名中允许的字符</string>
|
||||
<string name="settings_file_replacement_character_summary">无效字符将会被替换为此</string>
|
||||
<string name="charset_letters_and_digits">字母和数字</string>
|
||||
<string name="charset_most_special_characters">最特殊字符</string>
|
||||
<string name="search_no_results">没有结果</string>
|
||||
<string name="no_subscribers">没有订阅者</string>
|
||||
<plurals name="subscribers">
|
||||
<item quantity="one">%s个订阅者</item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="no_videos">没有视频</string>
|
||||
<string name="detail_drag_description">拖动以重新排序</string>
|
||||
<string name="create">创建</string>
|
||||
<string name="delete_one">仅删除一个</string>
|
||||
<string name="delete_all">全部删除</string>
|
||||
<string name="dismiss">解除</string>
|
||||
<string name="rename">重 命名</string>
|
||||
<string name="toast_no_player">未安装用于播放此文件的应用程序</string>
|
||||
<string name="one_item_deleted">已删除1个项目。</string>
|
||||
<string name="main_page_content_summary">哪些标签需要在主页上展示</string>
|
||||
<string name="list_view_mode">列表视图模式</string>
|
||||
<string name="missions_header_finished">已完成</string>
|
||||
<string name="missions_header_pending">等待中…</string>
|
||||
<string name="paused">已暂停</string>
|
||||
<string name="queued">排队中</string>
|
||||
<string name="enqueue">已加入队列</string>
|
||||
<string name="permission_denied">操作已被系统拒绝</string>
|
||||
<string name="download_failed">下载失败</string>
|
||||
<string name="download_finished">下载完成</string>
|
||||
<string name="download_finished_more">%s 次下载已完成</string>
|
||||
<string name="no_comments">没有评论</string>
|
||||
<string name="drawer_header_description">切换服务,当前选择:</string>
|
||||
<string name="no_player_found">找不到串流播放器。您想安装 VLC 吗?</string>
|
||||
<string name="screen_rotation">旋转</string>
|
||||
<string name="use_external_video_player_title">使用第三方视频播放器</string>
|
||||
<string name="use_external_audio_player_title">使用第三方视频播放器</string>
|
||||
<string name="download_path_audio_title">音频下载文件夹</string>
|
||||
<string name="autoplay_by_calling_app_summary">从其他应用打开 NewPipe 时就播放视频</string>
|
||||
<string name="default_resolution_title">默认分辨率</string>
|
||||
<string name="kore_not_found">找不到Kore。是否安装?</string>
|
||||
<string name="show_play_with_kodi_title">显示“用Kodi播放”选项</string>
|
||||
<string name="show_play_with_kodi_summary">显示“通过Kodi media center播放视频的选项”</string>
|
||||
<string name="play_audio">音频</string>
|
||||
<string name="default_audio_format_title">默认音频格式</string>
|
||||
<string name="show_next_and_similar_title">显示“下一个”和“类似的”视频</string>
|
||||
<string name="settings_category_video_audio_title">视频和音频</string>
|
||||
<string name="background_player_playing_toast">在后台播放</string>
|
||||
<string name="play_btn_text">播放</string>
|
||||
<string name="content">内容</string>
|
||||
<string name="show_age_restricted_content_title">受年龄限制的内容</string>
|
||||
<string name="video_is_age_restricted">显示受年龄限制的视频。可从设置允许此类内容。</string>
|
||||
<string name="duration_live">直播</string>
|
||||
<string name="downloads">下载</string>
|
||||
<string name="downloads_title">下载</string>
|
||||
<string name="error_report_title">错误报告</string>
|
||||
<string name="general_error">错误</string>
|
||||
<string name="could_not_load_thumbnails">无法加载所有缩略图</string>
|
||||
<string name="youtube_signature_decryption_error">无法解密视频 URL 的签名</string>
|
||||
<string name="parsing_error">无法解析网址</string>
|
||||
<string name="light_parsing_error">无法完全解析网址</string>
|
||||
<string name="content_not_available">内容不可用</string>
|
||||
<string name="could_not_setup_download_menu">无法设置下载菜单</string>
|
||||
<string name="live_streams_not_supported">暂时不支持观看直播</string>
|
||||
<string name="could_not_get_stream">无法获得任何信息流</string>
|
||||
<string name="could_not_load_image">无法加载图像</string>
|
||||
<string name="app_ui_crash">App UI 崩溃</string>
|
||||
<string name="sorry_string">抱歉,这不应该发生的。</string>
|
||||
<string name="error_report_button_text">通过电子邮件报告错误</string>
|
||||
<string name="error_snackbar_message">抱歉,发生了一些错误。</string>
|
||||
<string name="error_snackbar_action">报告</string>
|
||||
<string name="what_device_headline">信息:</string>
|
||||
<string name="what_happened_headline">发生了什么:</string>
|
||||
<string name="info_labels">详情:\\n请求:\\n内容语言:\\n服务:\\nGMT时间:\\n包:\\n版本:\\n操作系统版本:</string>
|
||||
<string name="your_comment">您的注释(请用英文):</string>
|
||||
<string name="error_details_headline">详细信息:</string>
|
||||
<string name="list_thumbnail_view_description">视频预览缩略图</string>
|
||||
<string name="detail_thumbnail_view_description">播放视频,时长:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">视频上传者的头像缩略图</string>
|
||||
<string name="short_billion">字节</string>
|
||||
<string name="msg_url_malform">错误的 URL 或未联网</string>
|
||||
<string name="msg_running">NewPipe下载中</string>
|
||||
<string name="no_available_dir">请稍后在设置中设定下载目录</string>
|
||||
<string name="msg_popup_permission">用悬浮窗模式
|
||||
\n需要此权限</string>
|
||||
<string name="reCaptcha_title">reCAPTCHA验证</string>
|
||||
<string name="recaptcha_request_toast">请求的新的CAPTCHA验证</string>
|
||||
<string name="popup_mode_share_menu_title">NewPipe悬浮窗模式</string>
|
||||
<string name="popup_playing_toast">在悬浮窗中播放</string>
|
||||
<string name="default_popup_resolution_title">默认悬浮窗分辨率</string>
|
||||
<string name="show_higher_resolutions_title">使用更高的分辨率</string>
|
||||
<string name="show_higher_resolutions_summary">仅某些设备支持播放2K / 4K视频</string>
|
||||
<string name="clear">清除</string>
|
||||
<string name="popup_remember_size_pos_title">记住悬浮窗的尺寸与位置</string>
|
||||
<string name="popup_remember_size_pos_summary">记住最后一次使用悬浮窗的大小和位置</string>
|
||||
<string name="settings_category_popup_title">悬浮窗</string>
|
||||
<string name="popup_resizing_indicator_title">调整大小</string>
|
||||
<string name="use_external_video_player_summary">删除“某些”分辨率的音频</string>
|
||||
<string name="player_gesture_controls_title">播放器手势控制</string>
|
||||
<string name="player_gesture_controls_summary">使用手势控制播放器的亮度和音量</string>
|
||||
<string name="show_search_suggestions_summary">显示搜索建议</string>
|
||||
<string name="best_resolution">最佳分辨率</string>
|
||||
<string name="app_description">开源小巧的Android媒体播放器。</string>
|
||||
<string name="view_on_github">在GitHub上查看</string>
|
||||
<string name="app_license_title">NewPipe开源许可证</string>
|
||||
<string name="contribution_encouragement">你是否有想:翻译、设计、清理或重型代码更改 ——我们始终欢迎你来贡献!</string>
|
||||
<string name="read_full_license">阅读许可证</string>
|
||||
<string name="contribution_title">贡献</string>
|
||||
<string name="settings_file_replacement_character_title">替换字符</string>
|
||||
<string name="channel_unsubscribed">取消订阅频道</string>
|
||||
<string name="subscription_change_failed">无法修改订阅</string>
|
||||
<string name="subscription_update_failed">无法更新订阅</string>
|
||||
<string name="tab_main">主页面</string>
|
||||
<string name="tab_subscriptions">订阅</string>
|
||||
<string name="fragment_whats_new">新增功能</string>
|
||||
<string name="resume_on_audio_focus_gain_title">恢复前台焦点</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">中断后继续播放(例如突然来电后)</string>
|
||||
<string name="enable_search_history_title">搜索历史记录</string>
|
||||
<string name="enable_search_history_summary">在本地存储搜索查询记录</string>
|
||||
<string name="enable_watch_history_summary">记录已观看视频</string>
|
||||
<string name="title_activity_history">历史</string>
|
||||
<string name="title_history_search">已搜索</string>
|
||||
<string name="title_history_view">已观看</string>
|
||||
<string name="history_disabled">历史记录功能已关闭</string>
|
||||
<string name="action_history">历史</string>
|
||||
<string name="history_empty">历史记录为空</string>
|
||||
<string name="history_cleared">清除历史记录</string>
|
||||
<string name="notification_channel_name">NewPipe 通知</string>
|
||||
<string name="notification_channel_description">NewPipe 后台播放和悬浮窗播放的通知</string>
|
||||
<string name="default_video_format_title">默认视频格式</string>
|
||||
<string name="settings_category_player_behavior_title">行为</string>
|
||||
<string name="empty_subscription_feed_subtitle">空空如也...</string>
|
||||
<string name="no_views">0次观看</string>
|
||||
<string name="item_deleted">项目已删除</string>
|
||||
<string name="delete_item_search_history">是否要从搜索历史记录中删除此项目?</string>
|
||||
<string name="main_page_content">显示在主页面内容</string>
|
||||
<string name="blank_page_summary">空白页</string>
|
||||
<string name="kiosk_page_summary">『时下流行』页-自定义</string>
|
||||
<string name="subscription_page_summary">订阅页</string>
|
||||
<string name="feed_page_summary">Feed 页面</string>
|
||||
<string name="channel_page_summary">频道页</string>
|
||||
<string name="select_a_channel">选择一个频道</string>
|
||||
<string name="no_channel_subscribed_yet">尚未订阅频道</string>
|
||||
<string name="select_a_kiosk">选择一个时下流行页</string>
|
||||
<string name="kiosk">『时下流行』</string>
|
||||
<string name="trending">趋势</string>
|
||||
<string name="top_50">前50</string>
|
||||
<string name="new_and_hot">最新与热门</string>
|
||||
<string name="show_hold_to_append_title">显示 \"长按添加\" 说明</string>
|
||||
<string name="show_hold_to_append_summary">在视频详情页中,按下背景播放或悬浮窗播放按钮时显示提示</string>
|
||||
<string name="background_player_append">已加入后台播放播放列表</string>
|
||||
<string name="popup_playing_append">已加入悬浮窗播放列表</string>
|
||||
<string name="player_stream_failure">无法播放此串流</string>
|
||||
<string name="player_unrecoverable_failure">发生无法恢复播放器错误</string>
|
||||
<string name="player_recoverable_failure">恢复播放器错误</string>
|
||||
<string name="title_activity_background_player">后台播放</string>
|
||||
<string name="title_activity_popup_player">悬浮窗播放器</string>
|
||||
<string name="play_queue_remove">移除</string>
|
||||
<string name="play_queue_stream_detail">详情</string>
|
||||
<string name="play_queue_audio_settings">音频设置</string>
|
||||
<string name="hold_to_append">长按队列</string>
|
||||
<string name="unknown_content">[未知]</string>
|
||||
<string name="enqueue_on_background">添加到后台部分队列</string>
|
||||
<string name="enqueue_on_popup">添加至新悬浮窗列表</string>
|
||||
<string name="start_here_on_main">开始在此处开始播放</string>
|
||||
<string name="start_here_on_background">开始后台播放</string>
|
||||
<string name="start_here_on_popup">开始在新悬浮窗中播放</string>
|
||||
<string name="donation_title">捐赠</string>
|
||||
<string name="donation_encouragement">NewPipe 是由志愿者花费时间为您带来最佳体验开发的。回馈帮助开发人员在享用一杯咖啡的同时,让 NewPipe 变得更好。</string>
|
||||
<string name="give_back">回馈</string>
|
||||
<string name="website_title">网站</string>
|
||||
<string name="website_encouragement">请访问 NewPipe 网站了解更多信息和讯息。</string>
|
||||
<string name="default_content_country_title">默认国家/地区</string>
|
||||
<string name="toggle_orientation">切换方向</string>
|
||||
<string name="switch_to_background">切换到背景播放</string>
|
||||
<string name="switch_to_popup">切换到悬浮窗播放</string>
|
||||
<string name="switch_to_main">切换到主页面</string>
|
||||
<string name="service_title">服务</string>
|
||||
<string name="drawer_open">打开抽屉</string>
|
||||
<string name="drawer_close">关闭抽屉</string>
|
||||
<string name="external_player_unsupported_link_type">第三方播放器不支持此类型链接</string>
|
||||
<string name="invalid_url_toast">无效 URL</string>
|
||||
<string name="video_streams_empty">未找到视频串流</string>
|
||||
<string name="audio_streams_empty">找不到音频串流</string>
|
||||
<string name="video_player">视频播放器</string>
|
||||
<string name="background_player">后台播放器</string>
|
||||
<string name="popup_player">悬浮窗播放器</string>
|
||||
<string name="preferred_player_fetcher_notification_title">正在获取信息…</string>
|
||||
<string name="preferred_player_fetcher_notification_message">正在加载请求的内容</string>
|
||||
<string name="import_data_title">导入数据库</string>
|
||||
<string name="export_data_title">导出数据库</string>
|
||||
<string name="import_data_summary">覆盖当前历史记录和订阅</string>
|
||||
<string name="export_data_summary">导出历史记录、订阅和播放列表</string>
|
||||
<string name="export_complete_toast">导出成功</string>
|
||||
<string name="import_complete_toast">导入成功</string>
|
||||
<string name="no_valid_zip_file">没有有效的ZIP文件</string>
|
||||
<string name="could_not_import_all_files">警告:无法导入所有文件。</string>
|
||||
<string name="override_current_data">这将覆盖当前设置。</string>
|
||||
<string name="show_info">显示信息</string>
|
||||
<string name="tab_bookmarks">已收藏</string>
|
||||
<string name="delete_stream_history_prompt">确定要从观看历史记录中删除该项吗?</string>
|
||||
<string name="delete_all_history_prompt">是否确实要从历史记录中删除所有项目?</string>
|
||||
<string name="title_last_played">最后播放</string>
|
||||
<string name="title_most_played">播放最多</string>
|
||||
<string name="always_ask_open_action">总是寻问</string>
|
||||
<string name="create_playlist">新建播放列表</string>
|
||||
<string name="delete_playlist">删除</string>
|
||||
<string name="rename_playlist">重 命名</string>
|
||||
<string name="playlist_name_input">名称</string>
|
||||
<string name="append_playlist">添加到播放列表</string>
|
||||
<string name="set_as_playlist_thumbnail">设为播放列表缩略图</string>
|
||||
<string name="bookmark_playlist">收藏播放列表</string>
|
||||
<string name="unbookmark_playlist">删除收藏</string>
|
||||
<string name="delete_playlist_prompt">删除此播放列表?</string>
|
||||
<string name="playlist_creation_success">新建播放列表成功</string>
|
||||
<string name="playlist_add_stream_success">加入播放列表成功</string>
|
||||
<string name="playlist_thumbnail_change_success">播放列表缩略图更改成功。</string>
|
||||
<string name="playlist_delete_failure">无法删除播放列表</string>
|
||||
<string name="caption_none">无字幕</string>
|
||||
<string name="resize_fit">适应屏幕</string>
|
||||
<string name="resize_fill">填充屏幕</string>
|
||||
<string name="resize_zoom">缩放</string>
|
||||
<string name="drawer_header_action_paceholder_text">敬请等待</string>
|
||||
<string name="settings_category_debug_title">调试</string>
|
||||
<string name="caption_auto_generated">自动生成</string>
|
||||
<string name="enable_leak_canary_title">启用LeakCanary</string>
|
||||
<string name="enable_leak_canary_summary">『内存泄漏监视』可能导致应用在『核心转储』时无响应</string>
|
||||
<string name="enable_disposed_exceptions_title">报告『提前结束Android生命周期』错误</string>
|
||||
<string name="enable_disposed_exceptions_summary">强制报告处理后的未送达的Activity或Fragment生命周期之外的Rx异常</string>
|
||||
<string name="use_inexact_seek_title">使用快速不精确搜索</string>
|
||||
<string name="use_inexact_seek_summary">粗略定位播放:允许播放器以略低的精确度为代价换取更快的定位速度</string>
|
||||
<string name="auto_queue_title">自动播放下一个</string>
|
||||
<string name="auto_queue_summary">当播放完非循环列表中的最后一个视频时,自动加入一个相关视频到播放列表</string>
|
||||
<string name="invalid_directory">没有此文件夹</string>
|
||||
<string name="invalid_source">无相似文件/内容源</string>
|
||||
<string name="invalid_file">该文件不存在 或 缺少读写该文件的权限</string>
|
||||
<string name="file_name_empty_error">文件名不能为空</string>
|
||||
<string name="error_occurred_detail">发生错误: %1$s</string>
|
||||
<string name="import_export_title">导入/导出</string>
|
||||
<string name="import_title">导入</string>
|
||||
<string name="import_from">从...导入</string>
|
||||
<string name="export_to">导出到...</string>
|
||||
<string name="import_ongoing">正在导入…</string>
|
||||
<string name="export_ongoing">正在导出…</string>
|
||||
<string name="import_file_title">导入文件</string>
|
||||
<string name="previous_export">以前的导出</string>
|
||||
<string name="subscriptions_import_unsuccessful">无法导入订阅</string>
|
||||
<string name="subscriptions_export_unsuccessful">无法导出订阅</string>
|
||||
<string name="import_youtube_instructions">通过下载导出文件来导入 YouTube 订阅:
|
||||
\n
|
||||
\n1. 转到此网站: %1$s
|
||||
\n2. 登录(如果需要)
|
||||
\n3. 应该立即开始下载(即导出文件)</string>
|
||||
<string name="import_soundcloud_instructions">通过键入网址或你的 ID 导入 SoundCloud 配置文件:
|
||||
\n
|
||||
\n1. 在浏览器中启用\"电脑模式\"(该网站不适用于移动设备)
|
||||
\n2. 转到此 URL: %1$s
|
||||
\n3. 登录(如果需要)
|
||||
\n4. 复制重定向的配置文件下载地址。</string>
|
||||
<string name="import_soundcloud_instructions_hint">你的 ID:soundcloud.com/[你的ID]</string>
|
||||
<string name="import_network_expensive_warning">该操作消耗大量流量,
|
||||
\n你想继续吗?</string>
|
||||
<string name="download_thumbnail_summary">关闭可防止加载缩略图,节已省数据和内存使用。(若现在更改会清除内存和储存中缓存)</string>
|
||||
<string name="thumbnail_cache_wipe_complete_notice">清空图像缓存成功</string>
|
||||
<string name="metadata_cache_wipe_title">清空已缓存元数据</string>
|
||||
<string name="metadata_cache_wipe_summary">清空已缓存的网页数据</string>
|
||||
<string name="metadata_cache_wipe_complete_notice">清空元数据缓存成功</string>
|
||||
<string name="playback_speed_control">播放速度控制</string>
|
||||
<string name="playback_tempo">节奏</string>
|
||||
<string name="playback_pitch">音调</string>
|
||||
<string name="unhook_checkbox">解除关联(可能导致失真)</string>
|
||||
<string name="preferred_open_action_settings_title">首选“打开”操作</string>
|
||||
<string name="preferred_open_action_settings_summary">打开内容时默认操作: = %s</string>
|
||||
<string name="no_streams_available_download">无可下载的串流内容</string>
|
||||
<string name="caption_setting_title">字幕</string>
|
||||
<string name="caption_setting_description">修改播放器字幕比例和背景样式。需要重新启动应用程序才能生效。</string>
|
||||
<string name="clear_views_history_summary">删除串流的播放历史和播放位置</string>
|
||||
<string name="delete_view_history_alert">删除全部观看记录?</string>
|
||||
<string name="clear_search_history_title">清除搜索历史记录</string>
|
||||
<string name="clear_search_history_summary">清除搜索关键词的历史记录</string>
|
||||
<string name="delete_search_history_alert">是否删除全部搜索历史记录?</string>
|
||||
<string name="search_history_deleted">搜索历史记录已删除。</string>
|
||||
<string name="app_license">NewPipe 是版权自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证GPLv3或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。</string>
|
||||
<string name="import_settings">是否要同时导入设置?</string>
|
||||
<string name="privacy_policy_title">NewPipe的隐私政策</string>
|
||||
<string name="privacy_policy_encouragement">NewPipe 项目非常重视您的隐私。因此,未经您的同意,应用程序不会收集任何数据。
|
||||
\nNewPipe 的隐私政策详细解释了在发送崩溃报告时发送和存储的数据。</string>
|
||||
<string name="read_privacy_policy">阅读隐私政策</string>
|
||||
<string name="start_accept_privacy_policy">为了遵守欧洲一般数据保护条例 (GDPR),我们提请您注意 NewPipe 的隐私政策。请仔细阅读。
|
||||
\n您必须接受它才能向我们发送错误报告。</string>
|
||||
<string name="accept">接受</string>
|
||||
<string name="decline">拒绝</string>
|
||||
<string name="limit_data_usage_none_description">无限制</string>
|
||||
<string name="limit_mobile_data_usage_title">使用移动数据时限制分辨率</string>
|
||||
<string name="minimize_on_exit_title">切换应用时最小化</string>
|
||||
<string name="minimize_on_exit_summary">从主播放器切换到其他应用时的操作 - %s</string>
|
||||
<string name="skip_silence_checkbox">静音时快进</string>
|
||||
<string name="playback_step">滑块[比例尺]</string>
|
||||
<string name="playback_reset">重 置</string>
|
||||
<string name="tracks">曲目</string>
|
||||
<string name="users">用户</string>
|
||||
<string name="tab_choose">选择标签</string>
|
||||
<string name="volume_gesture_control_title">音量手势控制</string>
|
||||
<string name="volume_gesture_control_summary">使用手势控制播放器的音量</string>
|
||||
<string name="brightness_gesture_control_title">亮度手势控制</string>
|
||||
<string name="brightness_gesture_control_summary">使用手势控制播放器的亮度</string>
|
||||
<string name="content_language_title">视频默认语言</string>
|
||||
<string name="app_update_notification_channel_name">应用更新通知</string>
|
||||
<string name="app_update_notification_channel_description">NewPipe有新版本的通知</string>
|
||||
<string name="download_to_sdcard_error_title">外置存储不可用</string>
|
||||
<string name="download_to_sdcard_error_message">无法下载到外部 SD 卡。重置下载文件夹位置?</string>
|
||||
<string name="saved_tabs_invalid_json">读取已保存标签时发生错误,因此使用者默认标签</string>
|
||||
<string name="restore_defaults">恢复默认</string>
|
||||
<string name="restore_defaults_confirmation">是否恢复默认值?</string>
|
||||
<string name="selection">选择</string>
|
||||
<string name="updates_setting_title">更新</string>
|
||||
<string name="list">列表</string>
|
||||
<string name="auto">自动</string>
|
||||
<string name="switch_view">切换视图</string>
|
||||
<string name="app_update_notification_content_text">点击下载</string>
|
||||
<string name="post_processing">后期处理</string>
|
||||
<string name="generate_unique_name">生成唯一名称</string>
|
||||
<string name="overwrite">覆盖</string>
|
||||
<string name="download_already_running">正在使用此名称进行下载</string>
|
||||
<string name="show_error">显示错误</string>
|
||||
<string name="label_code">代码</string>
|
||||
<string name="error_path_creation">无法创建目标文件夹</string>
|
||||
<string name="error_file_creation">无法创建文件</string>
|
||||
<string name="error_permission_denied">权限被系统拒绝</string>
|
||||
<string name="error_ssl_exception">安全连接失败</string>
|
||||
<string name="error_unknown_host">找不到服务器</string>
|
||||
<string name="error_connect_host">无法连接到服务器</string>
|
||||
<string name="error_http_no_content">服务器未发送数据</string>
|
||||
<string name="error_http_not_found">找不到 NOT FOUND</string>
|
||||
<string name="error_postprocessing_failed">后期处理失败</string>
|
||||
<string name="clear_finished_download">清除已完成的下载</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="max_retry_msg">最大重试次数</string>
|
||||
<string name="max_retry_desc">取消下载前的最多尝试次数</string>
|
||||
<string name="pause_downloads_on_mobile">在切换到移动流量网络时中断播放</string>
|
||||
<string name="pause_downloads_on_mobile_desc">切换至移动数据时可能有用,尽管一些下载无法被暂停</string>
|
||||
<string name="events">事件</string>
|
||||
<string name="conferences">近期大会</string>
|
||||
<string name="show_comments_title">显示评论</string>
|
||||
<string name="show_comments_summary">禁用,以停止显示评论</string>
|
||||
<plurals name="comments">
|
||||
<item quantity="one">评论</item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="error_unable_to_load_comments">无法加载评论</string>
|
||||
<string name="close">关闭</string>
|
||||
<string name="enable_playback_resume_title">恢复播放</string>
|
||||
<string name="enable_playback_resume_summary">恢复上次播放位置</string>
|
||||
<string name="enable_playback_state_lists_title">列表中的位置</string>
|
||||
<string name="enable_playback_state_lists_summary">在列表中,显示视频最后一次播放时的播放位置</string>
|
||||
<string name="watch_history_states_deleted">已删除播放位置记录。</string>
|
||||
<string name="missing_file">文件被已移动或删除</string>
|
||||
<string name="overwrite_unrelated_warning">该名称的文件已经存在</string>
|
||||
<string name="overwrite_finished_warning">命名冲突,已存在具有此名称文件</string>
|
||||
<string name="overwrite_failed">无法覆盖文件</string>
|
||||
<string name="download_already_pending">有此名称的已暂停下载</string>
|
||||
<string name="error_postprocessing_stopped">处理文件时,NewPipe 已关闭</string>
|
||||
<string name="error_insufficient_storage">设备上没有剩余储存空间</string>
|
||||
<string name="error_progress_lost">进度丢失,文件已被删除</string>
|
||||
<string name="error_timeout">连接超时</string>
|
||||
<string name="confirm_prompt">你确定吗?</string>
|
||||
<string name="enable_queue_limit">最大下载队列</string>
|
||||
<string name="enable_queue_limit_desc">同时只允许一个下载进行</string>
|
||||
<string name="start_downloads">开始下载</string>
|
||||
<string name="pause_downloads">暂停下载</string>
|
||||
<string name="downloads_storage_ask_title">询问下载位置</string>
|
||||
<string name="downloads_storage_ask_summary">系统将询问您将每次下载的保存位置</string>
|
||||
<string name="downloads_storage_ask_summary_kitkat">系统将询问您将每次下载的保存位置。
|
||||
\n(如果要下载到外部 SD 卡,请选择 SAF)</string>
|
||||
<string name="downloads_storage_use_saf_title">使用 SAF</string>
|
||||
<string name="downloads_storage_use_saf_summary">存储访问框架(SAF)允许下载文件到外部SD卡。
|
||||
\n注:一些设备不兼容SAF</string>
|
||||
<string name="clear_playback_states_title">删除播放位置记录</string>
|
||||
<string name="clear_playback_states_summary">删除所有播放位置记录</string>
|
||||
<string name="delete_playback_states_alert">删除所有播放位置记录?</string>
|
||||
<string name="download_choose_new_path">更改下载目录让内容生效</string>
|
||||
<string name="default_kiosk_page_summary">『时下流行』页-默认</string>
|
||||
<string name="no_one_watching">无人在线观看</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s 人在观看</item>
|
||||
<item quantity="other"></item>
|
||||
</plurals>
|
||||
<string name="no_one_listening">没人在听</string>
|
||||
<plurals name="listening">
|
||||
<item quantity="one">%s个听众</item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="localization_changes_requires_app_restart">重新启动应用后,语言将更改。</string>
|
||||
</resources>
|
|
@ -455,11 +455,9 @@
|
|||
<string name="error_connect_host">Немагчыма злучыцца з серверам</string>
|
||||
<string name="error_http_no_content">Не атрымалася атрымаць дадзеныя з сервера</string>
|
||||
<string name="error_http_unsupported_range">Сервер не падтрымлівае шматструменную загрузку, паспрабуйце з @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Запытаны дыяпазон недапушчальны</string>
|
||||
<string name="error_http_not_found">Не знойдзена</string>
|
||||
<string name="error_postprocessing_failed">Пасляапрацоўка не ўдалася</string>
|
||||
<string name="clear_finished_download">Ачысціць завершаныя</string>
|
||||
<string name="msg_pending_downloads">Аднавіць прыпыненыя загрузкі (%s)</string>
|
||||
<string name="stop">Спыніць</string>
|
||||
<string name="max_retry_msg">Максімум спробаў</string>
|
||||
<string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<string name="show_search_suggestions_summary">Показвай предложения за търсене</string>
|
||||
<string name="enable_search_history_title">История на търсенията</string>
|
||||
<string name="enable_search_history_summary">Съхранявай заявките за търсене локално</string>
|
||||
<string name="enable_watch_history_title">История и кеш-памет</string>
|
||||
<string name="enable_watch_history_title">История на гледане</string>
|
||||
<string name="enable_watch_history_summary">Запаметявай кои видеота са гледани</string>
|
||||
<string name="resume_on_audio_focus_gain_title">Възобнови при връщане на фокус</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Продължавай възпроизвеждането след прекъсване (например телефонно обаждане)</string>
|
||||
|
@ -254,7 +254,7 @@
|
|||
<string name="no_player_found_toast">Липсва стрийм плейър (можете да изтеглите VLC, за да пуснете стрийма).</string>
|
||||
<string name="download_thumbnail_summary">Изключете, за да спрете зареждането на всички миниатюри, спестявайки трафик и памет. При промяна на тази настройка, текущата кеш-памет на изображенията ще бъде изтрита.</string>
|
||||
<string name="show_hold_to_append_summary">Показвай подсказка, когато е избран фонов режим или режим в прозорец на страницата с детайли на съответния клип</string>
|
||||
<string name="clear_views_history_summary">Изтрива историята на възпроизвежданите стриймове</string>
|
||||
<string name="clear_views_history_summary">Изтрива историята на възпроизвежданите стриймове и позицията на възпроизвеждането</string>
|
||||
<string name="video_streams_empty">Не са намерени видео стриймове</string>
|
||||
<string name="audio_streams_empty">Не са намерени аудио стриймове</string>
|
||||
<string name="info_labels">Какво:\\nЗаявка:\\nЕзик на съдържанието:\\nУслуга:\\nВреме по GMT:\\nПакет:\\nВерсия:\\nОС версия:</string>
|
||||
|
@ -413,6 +413,11 @@
|
|||
<string name="autoplay_title">Автоматично пускане</string>
|
||||
<plurals name="comments">
|
||||
<item quantity="one">Коментари</item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="tab_new">Нов раздел</string>
|
||||
<string name="tab_choose">Избери раздел</string>
|
||||
<string name="settings_category_updates_title">Промени</string>
|
||||
<string name="enable_playback_resume_title">Продължи възпроизвеждане</string>
|
||||
<string name="settings_category_clear_data_title">Изтрии данни</string>
|
||||
</resources>
|
|
@ -7,8 +7,8 @@
|
|||
<string name="download">Baixa</string>
|
||||
<string name="search">Cerca</string>
|
||||
<string name="settings">Paràmetres</string>
|
||||
<string name="choose_browser">Tria un navegador</string>
|
||||
<string name="subscribe_button_title">Subscriu-t\'hi</string>
|
||||
<string name="choose_browser">Trieu un navegador</string>
|
||||
<string name="subscribe_button_title">Subscripció</string>
|
||||
<string name="subscribed_button_title">Subscrit</string>
|
||||
<string name="show_info">Mostra la informació</string>
|
||||
<string name="tab_subscriptions">Subscripcions</string>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<string name="settings_category_debug_title">Depuració</string>
|
||||
<string name="content">Contingut</string>
|
||||
<string name="show_age_restricted_content_title">Desactiva les restriccions per edat</string>
|
||||
<string name="video_is_age_restricted">Mostra el vídeo restringit per edat. Pots permetre aquesta mena de continguts des dels paràmetres.</string>
|
||||
<string name="video_is_age_restricted">Mostra el vídeo restringit per edat. Podeu permetre aquesta mena de continguts des dels paràmetres.</string>
|
||||
<string name="duration_live">EN DIRECTE</string>
|
||||
<string name="downloads">Baixades</string>
|
||||
<string name="downloads_title">Baixades</string>
|
||||
|
@ -46,7 +46,7 @@
|
|||
<string name="playlist">Llista de reproducció</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="disabled">Desactivat</string>
|
||||
<string name="clear">Esborra</string>
|
||||
<string name="clear">Neteja</string>
|
||||
<string name="best_resolution">Millor resolució</string>
|
||||
<string name="undo">Desfés</string>
|
||||
<string name="always">Sempre</string>
|
||||
|
@ -80,27 +80,27 @@
|
|||
<string name="contribution_title">Col·labora-hi</string>
|
||||
<string name="website_title">Lloc web</string>
|
||||
<string name="app_license_title">Llicència del NewPipe</string>
|
||||
<string name="read_full_license">Llegeix la llicència</string>
|
||||
<string name="read_full_license">Llegiu la llicència</string>
|
||||
<string name="title_activity_history">Historial</string>
|
||||
<string name="history_disabled">L\'historial està desactivat</string>
|
||||
<string name="action_history">Historial</string>
|
||||
<string name="history_empty">L\'historial és buit</string>
|
||||
<string name="history_cleared">S\'ha esborrat l\'historial</string>
|
||||
<string name="item_deleted">S\'ha eliminat l\'element</string>
|
||||
<string name="delete_item_search_history">Vols eliminar aquest element de l\'historial de cerca\?</string>
|
||||
<string name="delete_stream_history_prompt">Vols eliminar aquest element de l\'historial de reproduccions\?</string>
|
||||
<string name="delete_all_history_prompt">Segur que vols eliminar tots els elements de l\'historial\?</string>
|
||||
<string name="delete_item_search_history">Voleu eliminar aquest element de l\'historial de cerca\?</string>
|
||||
<string name="delete_stream_history_prompt">Voleu eliminar aquest element de l\'historial de reproduccions\?</string>
|
||||
<string name="delete_all_history_prompt">Segur que voleu eliminar tots els elements de l\'historial\?</string>
|
||||
<string name="main_page_content">Contingut de la pàgina principal</string>
|
||||
<string name="blank_page_summary">Pàgina en blanc</string>
|
||||
<string name="subscription_page_summary">Pàgina de subscripcions</string>
|
||||
<string name="select_a_channel">Tria un canal</string>
|
||||
<string name="select_a_channel">Trieu un canal</string>
|
||||
<string name="export_complete_toast">S\'ha completat l\'exportació</string>
|
||||
<string name="import_complete_toast">S\'ha completat la importació</string>
|
||||
<string name="play_queue_remove">Elimina</string>
|
||||
<string name="play_queue_stream_detail">Detalls</string>
|
||||
<string name="play_queue_audio_settings">Paràmetres d\'àudio</string>
|
||||
<string name="video_player">Reproductor de vídeo</string>
|
||||
<string name="background_player">Reproductor en segon pla</string>
|
||||
<string name="background_player">Reproductor en rerefons</string>
|
||||
<string name="popup_player">Reproductor emergent</string>
|
||||
<string name="always_ask_open_action">Demana-ho sempre</string>
|
||||
<string name="create_playlist">Crea una llista de reproducció</string>
|
||||
|
@ -117,41 +117,41 @@
|
|||
<string name="playback_default">Per defecte</string>
|
||||
<string name="view_count_text">%1$s reproduccions</string>
|
||||
<string name="upload_date_text">Publicat el %1$s</string>
|
||||
<string name="no_player_found">No s\'ha trobat un reproductor de fluxos. Vols instal·lar el VLC\?</string>
|
||||
<string name="no_player_found_toast">No s\'ha trobat cap reproductor de fluxos (pots instal·lar el VLC per reproduir-lo).</string>
|
||||
<string name="no_player_found">No s\'ha trobat cap reproductor de fluxos. Voleu instal·lar el VLC\?</string>
|
||||
<string name="no_player_found_toast">No s\'ha trobat cap reproductor de fluxos (podeu instal·lar el VLC per reproduir-ho).</string>
|
||||
<string name="open_in_popup_mode">Obre en mode emergent</string>
|
||||
<string name="controls_download_desc">Baixa el fitxer de vídeo</string>
|
||||
<string name="did_you_mean">Volies dir: %1$s\?</string>
|
||||
<string name="did_you_mean">Volíeu dir: %1$s\?</string>
|
||||
<string name="share_dialog_title">Comparteix-ho amb</string>
|
||||
<string name="screen_rotation">rotació</string>
|
||||
<string name="use_external_video_player_title">Reproductor de vídeo extern</string>
|
||||
<string name="popup_mode_share_menu_title">Mode emergent del NewPipe</string>
|
||||
<string name="channel_unsubscribed">Has eliminat la subscripció d\'aquest canal</string>
|
||||
<string name="channel_unsubscribed">Heu eliminat la subscripció a aquest canal</string>
|
||||
<string name="subscription_change_failed">No s\'ha pogut modificar la subscripció</string>
|
||||
<string name="subscription_update_failed">No s\'ha pogut actualitzar la subscripció</string>
|
||||
<string name="tab_main">Principal</string>
|
||||
<string name="controls_background_title">Segon pla</string>
|
||||
<string name="controls_background_title">Rerefons</string>
|
||||
<string name="controls_popup_title">Emergent</string>
|
||||
<string name="controls_add_to_playlist_title">Afegeix a</string>
|
||||
<string name="download_path_summary">Els fitxers de vídeo baixats s\'emmagatzemen aquí</string>
|
||||
<string name="download_path_dialog_title">Tria la carpeta de baixades per als fitxers de vídeo</string>
|
||||
<string name="download_path_summary">Els fitxers de vídeo baixats es desen aquí</string>
|
||||
<string name="download_path_dialog_title">Trieu la carpeta de baixades per als fitxers de vídeo</string>
|
||||
<string name="download_path_audio_summary">Els fitxers d\'àudio baixats es desen aquí</string>
|
||||
<string name="download_path_audio_dialog_title">Tria la carpeta de baixada per als fitxers d\'àudio</string>
|
||||
<string name="autoplay_by_calling_app_summary">Reprodueix un vídeo quan el NewPipe s\'executa des d\'una altra aplicació</string>
|
||||
<string name="download_path_audio_dialog_title">Trieu la carpeta de baixada per als fitxers d\'àudio</string>
|
||||
<string name="autoplay_by_calling_app_summary">Reprodueix un vídeo quan el NewPipe s\'executa des d\'altra aplicació</string>
|
||||
<string name="default_popup_resolution_title">Resolució per defecte del mode emergent</string>
|
||||
<string name="show_higher_resolutions_title">Mostra resolucions superiors</string>
|
||||
<string name="show_higher_resolutions_summary">Només alguns dispositius són compatibles amb la reproducció de vídeos en 2K/4K</string>
|
||||
<string name="play_with_kodi_title">Reprodueix amb Kodi</string>
|
||||
<string name="kore_not_found">No s\'ha trobat l\'aplicació Kodi. Vols instal·lar-la\?</string>
|
||||
<string name="show_play_with_kodi_title">Activa «Reprodueix amb Kodi»</string>
|
||||
<string name="show_higher_resolutions_summary">No tots els dispositius són compatibles amb la reproducció de vídeos en 2K/4K</string>
|
||||
<string name="play_with_kodi_title">Reprodueix amb el Kodi</string>
|
||||
<string name="kore_not_found">No s\'ha trobat l\'aplicació Kodi. Voleu instal·lar-la\?</string>
|
||||
<string name="show_play_with_kodi_title">Mostra «Reprodueix amb el Kodi»</string>
|
||||
<string name="show_play_with_kodi_summary">Mostra una opció per reproduir un vídeo amb el centre multimèdia Kodi</string>
|
||||
<string name="popup_remember_size_pos_title">Reproductor emergent intel·ligent</string>
|
||||
<string name="popup_remember_size_pos_summary">Recorda la darrera mida i posició del reproductor emergent</string>
|
||||
<string name="use_inexact_seek_title">Cerca ràpida poc precisa</string>
|
||||
<string name="use_inexact_seek_summary">La cerca poc precisa permet que el reproductor cerqui una posició més ràpidament amb menys precisió</string>
|
||||
<string name="download_thumbnail_title">Carrega les miniatures</string>
|
||||
<string name="thumbnail_cache_wipe_complete_notice">S\'ha esborrat la memòria cau d\'imatges</string>
|
||||
<string name="metadata_cache_wipe_title">Esborra les metadades de la memòria cau</string>
|
||||
<string name="thumbnail_cache_wipe_complete_notice">S\'ha eliminat la memòria cau d\'imatges</string>
|
||||
<string name="metadata_cache_wipe_title">Elimina les metadades de la memòria cau</string>
|
||||
<string name="metadata_cache_wipe_complete_notice">S\'ha esborrat la memòria cau de metadades</string>
|
||||
<string name="auto_queue_title">Afegeix vídeos relacionats a la cua</string>
|
||||
<string name="player_gesture_controls_title">Control per gestos del reproductor</string>
|
||||
|
@ -165,15 +165,15 @@
|
|||
<string name="default_content_country_title">País per defecte dels continguts</string>
|
||||
<string name="content_language_title">Llengua per defecte dels continguts</string>
|
||||
<string name="settings_category_popup_title">Emergent</string>
|
||||
<string name="background_player_playing_toast">S\'està reproduint en segon pla</string>
|
||||
<string name="background_player_playing_toast">S\'està reproduint en rerefons</string>
|
||||
<string name="popup_playing_toast">S\'està reproduint en mode emergent</string>
|
||||
<string name="background_player_append">Afegit a la cua del reproductor en segon pla</string>
|
||||
<string name="popup_playing_append">Afegit a la cua del reproductor emergent</string>
|
||||
<string name="background_player_append">S\'ha afegit a la cua del reproductor en rerefons</string>
|
||||
<string name="popup_playing_append">S\'ha afegit a la cua del reproductor emergent</string>
|
||||
<string name="play_btn_text">Reprodueix</string>
|
||||
<string name="notification_channel_name">Notificació del NewPipe</string>
|
||||
<string name="notification_channel_description">Notificacions dels reproductors en segon pla o emergents del NewPipe</string>
|
||||
<string name="notification_channel_description">Notificacions dels reproductors en rerefons o emergents del NewPipe</string>
|
||||
<string name="could_not_load_thumbnails">No s\'han pogut carregar totes les miniatures</string>
|
||||
<string name="youtube_signature_decryption_error">No s\'ha pogut desencriptar la signatura de l\'URL del vídeo</string>
|
||||
<string name="youtube_signature_decryption_error">No s\'ha pogut desxifrar la signatura de l\'URL del vídeo</string>
|
||||
<string name="parsing_error">No s\'ha pogut processar el lloc web</string>
|
||||
<string name="light_parsing_error">No s\'ha pogut processar del tot el lloc web</string>
|
||||
<string name="content_not_available">Contingut no disponible</string>
|
||||
|
@ -190,41 +190,41 @@
|
|||
<string name="audio_streams_empty">No s\'ha trobat cap flux d\'àudio</string>
|
||||
<string name="invalid_directory">La carpeta no existeix</string>
|
||||
<string name="invalid_source">El fitxer o la font de contingut no existeix</string>
|
||||
<string name="invalid_file">El fitxer no existeix o no teniu permisos per llegir-lo o escriure-hi</string>
|
||||
<string name="invalid_file">El fitxer no existeix o bé no teniu permisos de lectura/escriptura</string>
|
||||
<string name="file_name_empty_error">El nom del fitxer no pot estar en blanc</string>
|
||||
<string name="error_occurred_detail">S\'ha produït un error: %1$s</string>
|
||||
<string name="error_report_button_text">Informa de l\'error per correu electrònic</string>
|
||||
<string name="error_report_button_text">Informeu de l\'error per correu electrònic</string>
|
||||
<string name="error_snackbar_message">S\'han produït alguns errors.</string>
|
||||
<string name="error_snackbar_action">INFORMA\'N</string>
|
||||
<string name="error_snackbar_action">INFORME</string>
|
||||
<string name="what_device_headline">Informació:</string>
|
||||
<string name="what_happened_headline">Què ha passat:</string>
|
||||
<string name="your_comment">Comentari (en anglès):</string>
|
||||
<string name="error_details_headline">Detalls:</string>
|
||||
<string name="list_thumbnail_view_description">Miniatura de previsualització del vídeo</string>
|
||||
<string name="detail_thumbnail_view_description">Miniatura de previsualització del vídeo</string>
|
||||
<string name="detail_thumbnail_view_description">Reprodueix el vídeo, duració:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Miniatura de l\'avatar del propietari</string>
|
||||
<string name="detail_likes_img_view_description">M\'agrada</string>
|
||||
<string name="detail_dislikes_img_view_description">No m\'agrada</string>
|
||||
<string name="use_tor_title">Fes servir el Tor</string>
|
||||
<string name="use_tor_summary">(En proves) Força el trànsit de baixada a través del Tor per a més privadesa (no compatible encara amb les emissions de vídeo).</string>
|
||||
<string name="report_error">Informa sobre un error</string>
|
||||
<string name="report_error">Notifiqueu un error</string>
|
||||
<string name="user_report">Informe de l\'usuari</string>
|
||||
<string name="search_no_results">Cap resultat</string>
|
||||
<string name="empty_subscription_feed_subtitle">No hi ha res aquí</string>
|
||||
<string name="err_dir_create">No s\'ha pogut crear el directori de baixades «%1$s»</string>
|
||||
<string name="info_dir_created">S\'ha creat el directori de baixades «%1$s»</string>
|
||||
<string name="retry">Torna a intentar-ho</string>
|
||||
<string name="retry">Torna a provar</string>
|
||||
<string name="storage_permission_denied">S\'ha denegat el permís d\'accés a l\'emmagatzematge</string>
|
||||
<string name="no_subscribers">Sense subscriptors</string>
|
||||
<string name="no_views">Sense reproduccions</string>
|
||||
<string name="no_subscribers">Cap subscripció</string>
|
||||
<string name="no_views">Cap reproducció</string>
|
||||
<plurals name="views">
|
||||
<item quantity="one">%s reproducció</item>
|
||||
<item quantity="other">%s reproduccions</item>
|
||||
</plurals>
|
||||
<string name="no_videos">Sense vídeos</string>
|
||||
<string name="no_videos">Cap vídeo</string>
|
||||
<plurals name="videos">
|
||||
<item quantity="one">%s vídeo</item>
|
||||
<item quantity="other">%s vídeos</item>
|
||||
<item quantity="one">Vídeo</item>
|
||||
<item quantity="other">Vídeos</item>
|
||||
</plurals>
|
||||
<string name="pause">Pausa</string>
|
||||
<string name="view">Reprodueix</string>
|
||||
|
@ -236,10 +236,10 @@
|
|||
<string name="dismiss">Tanca</string>
|
||||
<string name="rename">Canvia el nom</string>
|
||||
<string name="msg_threads">Fils</string>
|
||||
<string name="msg_server_unsupported">Servidor incompatible</string>
|
||||
<string name="msg_server_unsupported">Servidor no compatible</string>
|
||||
<string name="msg_exists">El fitxer ja existeix</string>
|
||||
<string name="msg_running">Baixada del NewPipe activa</string>
|
||||
<string name="msg_wait">Espera…</string>
|
||||
<string name="msg_wait">Un moment…</string>
|
||||
<string name="msg_copied">S\'ha copiat al porta-retalls</string>
|
||||
<string name="settings_file_charset_title">Caràcters permesos als noms de fitxer</string>
|
||||
<string name="charset_letters_and_digits">Lletres i dígits</string>
|
||||
|
@ -247,43 +247,43 @@
|
|||
<string name="copyright" formatted="true">© %1$s per %2$s sota %3$s</string>
|
||||
<string name="app_description">Reprodueix transmissions de manera lliure i lleugera a l\'Android.</string>
|
||||
<string name="view_on_github">Visualitza a GitHub</string>
|
||||
<string name="donation_title">Fes una donació</string>
|
||||
<string name="website_encouragement">Per a més informació i notícies, visita el nostre lloc web.</string>
|
||||
<string name="donation_title">Feu una donació</string>
|
||||
<string name="website_encouragement">Per a més informació i notícies, visiteu el nostre web.</string>
|
||||
<string name="title_last_played">Últimes reproduccions</string>
|
||||
<string name="title_most_played">Més reproduïts</string>
|
||||
<string name="kiosk_page_summary">Pàgina d\'un quiosc</string>
|
||||
<string name="kiosk_page_summary">Tendències</string>
|
||||
<string name="feed_page_summary">Pàgina de novetats</string>
|
||||
<string name="channel_page_summary">Pàgina d\'un canal</string>
|
||||
<string name="select_a_kiosk">Tria un quiosc</string>
|
||||
<string name="select_a_kiosk">Trieu un quiosc</string>
|
||||
<string name="no_valid_zip_file">El fitxer no té un format ZIP vàlid</string>
|
||||
<string name="could_not_import_all_files">Avís: No s\'han pogut importar tots els fitxers.</string>
|
||||
<string name="override_current_data">Això sobreescriurà els paràmetres actuals.</string>
|
||||
<string name="kiosk">Quiosc</string>
|
||||
<string name="trending">Tendències</string>
|
||||
<string name="top_50">Els millors 50</string>
|
||||
<string name="title_activity_background_player">Reproductor en segon pla</string>
|
||||
<string name="title_activity_background_player">Reproductor en rerefons</string>
|
||||
<string name="title_activity_popup_player">Reproductor emergent</string>
|
||||
<string name="enqueue_on_background">Afegeix a la cua de reproducció en segon pla</string>
|
||||
<string name="enqueue_on_background">Afegeix a la cua de reproducció en rerefons</string>
|
||||
<string name="enqueue_on_popup">Afegeix a la cua de reproducció emergent</string>
|
||||
<string name="start_here_on_main">Reprodueix aquí</string>
|
||||
<string name="drawer_open">Obre el calaix</string>
|
||||
<string name="drawer_close">Tanca el calaix</string>
|
||||
<string name="preferred_player_fetcher_notification_title">S\'està obtenint la informació…</string>
|
||||
<string name="preferred_player_fetcher_notification_message">S\'està carregant el contingut seleccionat</string>
|
||||
<string name="delete_playlist_prompt">Vols eliminar aquesta llista de reproducció\?</string>
|
||||
<string name="delete_playlist_prompt">Voleu eliminar aquesta llista de reproducció\?</string>
|
||||
<string name="playlist_delete_failure">No s\'ha pogut eliminar la llista de reproducció.</string>
|
||||
<string name="import_export_title">Importació i exportació</string>
|
||||
<string name="playback_speed_control">Controls de la velocitat de reproducció</string>
|
||||
<string name="playback_tempo">Tempo</string>
|
||||
<string name="playback_pitch">To</string>
|
||||
<string name="main_bg_subtitle">Toca el botó de cerca per començar</string>
|
||||
<string name="main_bg_subtitle">Feu un toc al botó de cerca per començar</string>
|
||||
<string name="use_external_video_player_summary">Elimina l\'àudio en algunes resolucions</string>
|
||||
<string name="use_external_audio_player_title">Reproductor d\'àudio extern</string>
|
||||
<string name="download_thumbnail_summary">Desactiva-ho per evitar que es carreguin les miniatures i estalviar dades i memòria. Si canvies aquesta opció, s\'esborrarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge.</string>
|
||||
<string name="download_thumbnail_summary">Desactiveu-ho per no generar miniatures i estalviar dades i memòria. Canviant aquesta opció, s\'eliminarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge.</string>
|
||||
<string name="enable_search_history_summary">Emmagatzema les cerques localment</string>
|
||||
<string name="enable_watch_history_summary">Registra els vídeos visualitzats</string>
|
||||
<string name="enable_watch_history_summary">Crea un historial de vídeos visualitzats</string>
|
||||
<string name="resume_on_audio_focus_gain_title">Reprèn automàticament</string>
|
||||
<string name="url_not_supported_toast">Aquesta URL no és compatible</string>
|
||||
<string name="url_not_supported_toast">Aquest URL no és compatible</string>
|
||||
<string name="error_report_title">Informe d\'error</string>
|
||||
<string name="later">Més tard</string>
|
||||
<string name="filter">Filtra</string>
|
||||
|
@ -291,20 +291,20 @@
|
|||
<string name="popup_resizing_indicator_title">S\'està redimensionant</string>
|
||||
<string name="play_all">Reprodueix-ho tot</string>
|
||||
<string name="toggle_orientation">Canvia l\'orientació</string>
|
||||
<string name="switch_to_background">Canvia al mode en segon pla</string>
|
||||
<string name="switch_to_background">Canvia al mode en rerefons</string>
|
||||
<string name="switch_to_popup">Canvia al mode emergent</string>
|
||||
<string name="switch_to_main">Canvia al mode principal</string>
|
||||
<string name="import_data_summary">Sobreescriu l\'historial i les subscripcions actuals</string>
|
||||
<string name="player_recoverable_failure">S\'està recuperant el reproductor després de l\'error</string>
|
||||
<string name="sorry_string">Ho sentim, això no hauria d\'haver ocorregut.</string>
|
||||
<string name="detail_drag_description">Arrossega per a reordenar la llista</string>
|
||||
<string name="sorry_string">Bé, és lamentable.</string>
|
||||
<string name="detail_drag_description">Arrossegueu per reordenar la llista</string>
|
||||
<string name="short_thousand">mil</string>
|
||||
<string name="short_million">milions</string>
|
||||
<string name="short_billion">mil milions</string>
|
||||
<string name="start">Inicia</string>
|
||||
<string name="add">Nova missió</string>
|
||||
<string name="msg_url_malform">L\'URL té un format incorrecte o no hi ha connexió a internet</string>
|
||||
<string name="msg_running_detail">Toca aquí per a més detalls</string>
|
||||
<string name="msg_url_malform">L\'URL té un format no vàlid o no hi ha connexió a Internet</string>
|
||||
<string name="msg_running_detail">Feu un toc aquí per a més detalls</string>
|
||||
<string name="no_available_dir">Defineix una carpeta de baixades més endavant als paràmetres</string>
|
||||
<string name="msg_popup_permission">Es necessita aquest permís per a obrir el mode emergent</string>
|
||||
<string name="reCaptcha_title">Camp reCAPTCHA</string>
|
||||
|
@ -312,15 +312,15 @@
|
|||
<string name="settings_file_replacement_character_summary">Se substituiran els caràcters no vàlids amb aquest valor</string>
|
||||
<string name="settings_file_replacement_character_title">Caràcter de substitució</string>
|
||||
<string name="charset_most_special_characters">Principals caràcters especials</string>
|
||||
<string name="contribution_encouragement">Ja siguin idees, traduccions, canvis en el disseny, una neteja del codi o canvis importants de programació, la teva ajuda sempre és benvinguda. Com més feina feta hi hagi, millor!</string>
|
||||
<string name="donation_encouragement">El NewPipe està desenvolupat per voluntaris que fan servir el seu temps lliure per a oferir-te la millor experiència possible. Fes una aportació per assegurar que els nostres desenvolupadors puguin millorar encara més el NewPipe mentre fan un cafè.</string>
|
||||
<string name="give_back">Fes la teva aportació</string>
|
||||
<string name="contribution_encouragement">Idees, traduccions, canvis en el disseny, neteja del codi, canvis importants de programació... La vostra ajuda sempre és benvinguda. Com més feina feta hi hagi, millor!</string>
|
||||
<string name="donation_encouragement">El NewPipe està desenvolupat per voluntaris que fan servir el seu temps lliure per oferir-vos la millor experiència possible. Feu una aportació per assegurar que els nostres desenvolupadors puguin millorar encara més el NewPipe mentre fan un cafè.</string>
|
||||
<string name="give_back">Feu la vostra aportació</string>
|
||||
<string name="title_history_search">Cerques</string>
|
||||
<string name="title_history_view">Reproduccions</string>
|
||||
<string name="no_channel_subscribed_yet">Encara no t\'has subscrit a cap canal</string>
|
||||
<string name="no_channel_subscribed_yet">Encara no us heu subscrit a cap canal</string>
|
||||
<string name="new_and_hot">Novetats</string>
|
||||
<string name="hold_to_append">Mantén premut per afegir a la cua</string>
|
||||
<string name="start_here_on_background">Comença a reproduir en segon pla</string>
|
||||
<string name="hold_to_append">Manteniu premut per afegir a la cua</string>
|
||||
<string name="start_here_on_background">Comença a reproduir en rerefons</string>
|
||||
<string name="start_here_on_popup">Comença a reproduir en mode emergent</string>
|
||||
<string name="set_as_playlist_thumbnail">Defineix com a miniatura de la llista de reproducció</string>
|
||||
<string name="bookmark_playlist">Afegeix la llista de reproducció a les adreces d\'interès</string>
|
||||
|
@ -346,45 +346,45 @@
|
|||
<string name="drawer_header_action_paceholder_text">Aviat hi haurà novetats aquí ;D</string>
|
||||
<string name="preferred_open_action_settings_title">Acció d\'obertura preferida</string>
|
||||
<string name="preferred_open_action_settings_summary">Acció per defecte en obrir continguts — %s</string>
|
||||
<string name="enable_leak_canary_summary">"La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria "</string>
|
||||
<string name="enable_leak_canary_summary">La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria</string>
|
||||
<string name="enable_disposed_exceptions_title">Informa d\'errors fora del cicle de vida</string>
|
||||
<string name="enable_disposed_exceptions_summary">Força l\'informe d\'excepcions Rx que no es puguin transmetre que tinguin lloc fora del cicle de vida d\'un fragment o activitat després de disposar-los</string>
|
||||
<string name="import_youtube_instructions">Importa les teves subscripcions de YouTube mitjançant el fitxer d\'exportació:
|
||||
<string name="import_youtube_instructions">Importeu les vostres subscripcions de YouTube mitjançant el fitxer d\'exportació:
|
||||
\n
|
||||
\n1. Vés a aquesta URL: %1$s
|
||||
\n2. Inicia la sessió quan se\'t demani
|
||||
\n1. Aneu a : %1$s
|
||||
\n2. Inicieu la sessió quan si us demani
|
||||
\n3. S\'hauria d\'iniciar una baixada (el fitxer d\'exportació)</string>
|
||||
<string name="import_soundcloud_instructions">Importa un perfil de SoundCloud mitjançant l\'URL o l\'identificador del teu perfil:
|
||||
<string name="import_soundcloud_instructions">Importeu un perfil del SoundCloud mitjançant l\'URL o l\'identificador del vostre perfil:
|
||||
\n
|
||||
\n1. Activa el «Mode d\'ordinador» en un navegador (el lloc web no està disponible per a dispositius mòbils)
|
||||
\n2. Vés a aquesta URL: %1$s
|
||||
\n3. Inicia la sessió al teu compte quan se\'t demani
|
||||
\n4. Copia l\'URL de la pàgina on se\'t redireccioni</string>
|
||||
\n1. Activeu el «Mode d\'ordinador» en un navegador (el lloc web no està disponible per a dispositius mòbils)
|
||||
\n2. Aneu a: %1$s
|
||||
\n3. Inicieu la sessió al vostre compte quan si us demani
|
||||
\n4. Copieu l\'URL on si us ha redirigit.</string>
|
||||
<string name="import_soundcloud_instructions_hint">identificador, soundcloud.com/identificador</string>
|
||||
<string name="import_network_expensive_warning">Tingues en compte que això pot comportar un ús intensiu de la xarxa.
|
||||
<string name="import_network_expensive_warning">Tingueu en compte que això pot comportar un ús intensiu de la xarxa.
|
||||
\n
|
||||
\nVols continuar\?</string>
|
||||
\nVoleu continuar\?</string>
|
||||
<string name="no_streams_available_download">No hi ha vídeos que es puguin baixar</string>
|
||||
<string name="caption_setting_title">Subtítols</string>
|
||||
<string name="caption_setting_description">Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis.</string>
|
||||
<string name="toast_no_player">No s\'ha trobat cap aplicació que pugui reproduir aquest fitxer</string>
|
||||
<string name="clear_views_history_title">Esborra l\'historial de reproduccions</string>
|
||||
<string name="clear_views_history_summary">Esborra l\'historial dels vídeos reproduïts i les posicions de reproducció</string>
|
||||
<string name="delete_view_history_alert">Vols esborrar tot l\'historial de reproduccions\?</string>
|
||||
<string name="watch_history_deleted">S\'ha esborrat l\'historial de reproduccions.</string>
|
||||
<string name="clear_search_history_title">Esborra l\'historial de cerca</string>
|
||||
<string name="clear_search_history_summary">Esborra l\'historial de paraules cercades</string>
|
||||
<string name="delete_search_history_alert">Vols esborrar tot l\'historial de cerca\?</string>
|
||||
<string name="search_history_deleted">S\'ha esborrat l\'historial de cerca.</string>
|
||||
<string name="clear_views_history_title">Neteja l\'historial de reproduccions</string>
|
||||
<string name="clear_views_history_summary">Neteja l\'historial dels vídeos reproduïts i les posicions de reproducció</string>
|
||||
<string name="delete_view_history_alert">Voleu suprimir tot l\'historial de reproduccions\?</string>
|
||||
<string name="watch_history_deleted">S\'ha netejat l\'historial de reproduccions.</string>
|
||||
<string name="clear_search_history_title">Neteja l\'historial de cerca</string>
|
||||
<string name="clear_search_history_summary">Neteja l\'historial de paraules cercades</string>
|
||||
<string name="delete_search_history_alert">Voleu suprimir tot l\'historial de cerca\?</string>
|
||||
<string name="search_history_deleted">S\'ha netejat l\'historial de cerca.</string>
|
||||
<string name="one_item_deleted">S\'ha esborrat 1 element.</string>
|
||||
<string name="app_license">NewPipe és programari lliure sota llicència copyleft: pots fer-lo servir, estudiar-lo, compartir-lo i millorar-lo al teu gust. En concret, pots redistribuir-lo i/o modificar-lo d\'acord amb els termes de la llicència GNU GPL publicada per la Free Software Foundation, ja sigui la versió 3 o (segons vulguis) qualsevol altra versió posterior.</string>
|
||||
<string name="import_settings">Vols importar també els paràmetres\?</string>
|
||||
<string name="privacy_policy_title">Política de privacitat del NewPipe</string>
|
||||
<string name="privacy_policy_encouragement">El projecte NewPipe es pren molt seriosament la teva privacitat. Per aquesta raó, l\'aplicació no emmagatzema cap mena de dades sense el teu consentiment.
|
||||
\nLa política de privacitat del NewPipe descriu detalladament quines dades s\'envien i s\'emmagatzemen quan envies un informe d\'error.</string>
|
||||
<string name="read_privacy_policy">Llegeix la política de privacitat</string>
|
||||
<string name="start_accept_privacy_policy">Per tal de complir amb el Reglament General de Protecció de Dades europeu (GDPR), et demanem que posis atenció a la política de privacitat del NewPipe. Llegeix-la detingudament.
|
||||
\nSi vols enviar-nos un informe d\'error, l\'hauràs d\'acceptar.</string>
|
||||
<string name="app_license">El NewPipe és programari lliure sota llicència copyleft: el podeu fer servir, estudiar, compartir i millorar com vulgueu. Concretament, el podeu redistribuir i/o modificar d\'acord amb els termes de la llicència GNU GPL publicada per la Free Software Foundation, versió 3 o qualsevol altra versió posterior.</string>
|
||||
<string name="import_settings">Voleu importar també els paràmetres\?</string>
|
||||
<string name="privacy_policy_title">Política de privadesa del NewPipe</string>
|
||||
<string name="privacy_policy_encouragement">El projecte NewPipe es pren molt seriosament la vostra privadesa. Per aquesta raó, l\'aplicació no emmagatzema cap dada sense el vostre consentiment.
|
||||
\nLa política de privadesa del NewPipe descriu detalladament quines dades s\'envien i s\'emmagatzemen quan envieu un informe d\'error.</string>
|
||||
<string name="read_privacy_policy">Llegiu la política de privadesa</string>
|
||||
<string name="start_accept_privacy_policy">Per complir amb el Reglament General de Protecció de Dades europeu (GDPR), us demanem que pareu atenció a la política de privadesa del NewPipe. Llegiu-la detingudament.
|
||||
\nSi voleu informar d\'un error, l\'haureu d\'acceptar.</string>
|
||||
<string name="accept">Accepta</string>
|
||||
<string name="decline">Rebutja</string>
|
||||
<string name="limit_data_usage_none_description">Sense restriccions</string>
|
||||
|
@ -396,13 +396,13 @@
|
|||
<string name="minimize_on_exit_popup_description">Minimitza al reproductor emergent</string>
|
||||
<string name="skip_silence_checkbox">Avança ràpid durant el silenci</string>
|
||||
<string name="playback_step">Pas</string>
|
||||
<string name="playback_reset">Reinicialitza</string>
|
||||
<string name="playback_reset">Reinicia</string>
|
||||
<string name="channels">Canals</string>
|
||||
<string name="playlists">Llistes de reproducció</string>
|
||||
<string name="tracks">Pistes</string>
|
||||
<string name="users">Usuaris</string>
|
||||
<string name="tab_new">Pestanya nova</string>
|
||||
<string name="tab_choose">Tria una pestanya</string>
|
||||
<string name="tab_choose">Trieu una pestanya</string>
|
||||
<string name="volume_gesture_control_title">Control de volum per gestos</string>
|
||||
<string name="volume_gesture_control_summary">Fes servir gestos per controlar el volum del reproductor</string>
|
||||
<string name="brightness_gesture_control_title">Control de brillantor per gestos</string>
|
||||
|
@ -410,15 +410,15 @@
|
|||
<string name="settings_category_updates_title">Actualitzacions</string>
|
||||
<string name="file_deleted">S\'ha eliminat el fitxer</string>
|
||||
<string name="download_to_sdcard_error_title">L\'emmagatzematge extern no està disponible</string>
|
||||
<string name="restore_defaults">Reinicialitza els valors per defecte</string>
|
||||
<string name="restore_defaults_confirmation">Vols reinicialitzar els valors per defecte\?</string>
|
||||
<string name="restore_defaults">Reinicia als valors per defecte</string>
|
||||
<string name="restore_defaults_confirmation">Voleu reiniciar als valors per defecte\?</string>
|
||||
<string name="selection">Selecció</string>
|
||||
<string name="updates_setting_title">Actualitzacions</string>
|
||||
<string name="list">Llista</string>
|
||||
<string name="grid">Quadrícula</string>
|
||||
<string name="auto">Automàtic</string>
|
||||
<string name="switch_view">Canvia la vista</string>
|
||||
<string name="app_update_notification_content_title">Està disponible una nova actualització del NewPipe!</string>
|
||||
<string name="app_update_notification_content_title">Està disponible una actualització del NewPipe!</string>
|
||||
<string name="missions_header_pending">Pendent</string>
|
||||
<string name="paused">en pausa</string>
|
||||
<string name="queued">a la cua</string>
|
||||
|
@ -456,14 +456,14 @@
|
|||
<string name="overwrite">Sobreescriu</string>
|
||||
<string name="error_http_not_found">No s\'ha trobat</string>
|
||||
<string name="show_comments_title">Mostra els comentaris</string>
|
||||
<string name="show_comments_summary">Desactiva-ho per deixar de mostrar els comentaris</string>
|
||||
<string name="show_comments_summary">Desactiveu-ho per no mostrar els comentaris</string>
|
||||
<string name="autoplay_title">Reproducció automàtica</string>
|
||||
<string name="no_comments">No hi ha comentaris</string>
|
||||
<string name="no_comments">Cap comentari</string>
|
||||
<string name="error_unable_to_load_comments">No s\'han pogut carregar els comentaris</string>
|
||||
<string name="close">Tanca</string>
|
||||
<string name="saved_tabs_invalid_json">S\'estan utilitzant les pestanyes per defecte, s\'ha produït un error en llegir les pestanyes desades</string>
|
||||
<string name="updates_setting_description">Mostra una notificació per demanar l\'actualització de l\'aplicació si hi ha una nova versió disponible</string>
|
||||
<string name="app_update_notification_content_text">Toca per baixar</string>
|
||||
<string name="saved_tabs_invalid_json">S\'ha produït un error en llegir les pestanyes desades; s\'estan utilitzant les pestanyes per defecte</string>
|
||||
<string name="updates_setting_description">Mostra una notificació per demanar l\'actualització de l\'aplicació si hi ha una versió nova disponible</string>
|
||||
<string name="app_update_notification_content_text">Toqueu per baixar</string>
|
||||
<string name="error_http_no_content">El servidor no està enviant dades</string>
|
||||
<plurals name="comments">
|
||||
<item quantity="one">Comentaris</item>
|
||||
|
@ -480,13 +480,40 @@
|
|||
<string name="enable_queue_limit">Limita la cua de baixades</string>
|
||||
<string name="start_downloads">Inicia les baixades</string>
|
||||
<string name="pause_downloads">Pausa les baixades</string>
|
||||
<string name="downloads_storage_ask_summary">Se us demanarà la ubicació de cada baixada</string>
|
||||
<string name="downloads_storage_ask_summary">Si us demanarà la ubicació de cada baixada</string>
|
||||
<string name="enable_playback_state_lists_title">Posicions a les llistes</string>
|
||||
<string name="enable_playback_state_lists_summary">Mostra els indicadors de posició de reproducció a les llistes</string>
|
||||
<string name="settings_category_clear_data_title">Neteja les dades</string>
|
||||
<string name="permission_denied">El sistema ha denegat l\'acció</string>
|
||||
<string name="msg_pending_downloads">Reprèn les teves %s baixades pendents des de Baixades</string>
|
||||
<string name="error_postprocessing_stopped">S\'ha tancat el NewPipe mentre es treballava en el fitxer</string>
|
||||
<string name="downloads_storage_ask_title">Pregunta on baixar</string>
|
||||
<string name="downloads_storage_ask_title">Demana on baixar</string>
|
||||
<string name="download_choose_new_path">Canvia les carpetes de baixada perquè tingui efecte</string>
|
||||
<string name="download_to_sdcard_error_message">No es pot desar a la targeta externa. Voleu restablir la carpeta de baixades\?</string>
|
||||
<string name="error_permission_denied">Permís denegat pel sistema</string>
|
||||
<string name="error_http_unsupported_range">El servidor no accepta baixades simultànies. Proveu amb @string/msg_threads = 1</string>
|
||||
<string name="enable_playback_resume_summary">Restaura la darrera posició de la reproducció</string>
|
||||
<string name="watch_history_states_deleted">S\'ha suprimit les posicions de reproducció.</string>
|
||||
<string name="missing_file">El fitxer s\'ha mogut o suprimit</string>
|
||||
<string name="enable_queue_limit_desc">Només una baixada alhora</string>
|
||||
<string name="downloads_storage_ask_summary_kitkat">Si us demanarà la ubicació de cada baixada.
|
||||
\nTrieu SAF si voleu desar el contingut en una memòria externa</string>
|
||||
<string name="downloads_storage_use_saf_title">Utilitza SAF</string>
|
||||
<string name="downloads_storage_use_saf_summary">El SAF (Storage Access Framework; estructura d\'accés a l\'emmagatzematge) us permet realitzar baixades a una memòria externa com una targeta SD.
|
||||
\nNota: No és compatible en tots els dispositius</string>
|
||||
<string name="clear_playback_states_title">Esborra les posicions de reproducció</string>
|
||||
<string name="clear_playback_states_summary">Esborra totes les posicions de reproducció</string>
|
||||
<string name="delete_playback_states_alert">Voleu suprimir tots els punts de reproducció\?</string>
|
||||
<string name="drawer_header_description">In/Habilita el servei; selecció actual:</string>
|
||||
<string name="no_one_watching">Cap visualització</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s visualització</item>
|
||||
<item quantity="other">%s visualitzacions</item>
|
||||
</plurals>
|
||||
<string name="no_one_listening">Cap reproducció</string>
|
||||
<plurals name="listening">
|
||||
<item quantity="one">%s escoltant</item>
|
||||
<item quantity="other">%s escoltant</item>
|
||||
</plurals>
|
||||
<string name="localization_changes_requires_app_restart">Es canviarà la llengua en reiniciar l\'aplicació.</string>
|
||||
<string name="default_kiosk_page_summary">Tendències</string>
|
||||
</resources>
|
|
@ -460,8 +460,6 @@
|
|||
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
|
||||
<string name="error_path_creation">无法创建目标文件夹</string>
|
||||
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string>
|
||||
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>
|
||||
<string name="pause_downloads_on_mobile_desc">切换至移动数据时有用,尽管一些下载无法被暂停</string>
|
||||
<string name="show_comments_title">显示评论</string>
|
||||
<string name="show_comments_summary">禁用停止显示评论</string>
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<string name="parsing_error">Nebylo možné analyzovat stránku</string>
|
||||
<string name="content_not_available">Obsah není k dispozici</string>
|
||||
<string name="list_thumbnail_view_description">Náhled videa</string>
|
||||
<string name="detail_thumbnail_view_description">Náhled videa</string>
|
||||
<string name="detail_thumbnail_view_description">Přehrát video, délka:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Náhled avataru uploadera</string>
|
||||
<string name="detail_likes_img_view_description">To se mi líbí</string>
|
||||
<string name="detail_dislikes_img_view_description">To se mi nelíbí</string>
|
||||
|
@ -314,7 +314,6 @@ otevření ve vyskakovacím okně</string>
|
|||
<string name="resize_fit">Přizpůsobit</string>
|
||||
<string name="resize_fill">Vyplnit</string>
|
||||
<string name="resize_zoom">Zvětšit</string>
|
||||
<string name="toggle_leak_canary">Sledovat únik paměti</string>
|
||||
<string name="settings_category_debug_title">Ladění</string>
|
||||
<string name="caption_auto_generated">"Automaticky generováno "</string>
|
||||
<string name="enable_leak_canary_title">Povolit službu LeakCanary</string>
|
||||
|
@ -463,11 +462,9 @@ otevření ve vyskakovacím okně</string>
|
|||
<string name="error_connect_host">Nelze se připojit k serveru</string>
|
||||
<string name="error_http_no_content">Server neposílá data</string>
|
||||
<string name="error_http_unsupported_range">Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Požadovaný rozsah nelze splnit</string>
|
||||
<string name="error_http_not_found">Nenalezeno</string>
|
||||
<string name="error_postprocessing_failed">Post-processing selhal</string>
|
||||
<string name="clear_finished_download">Vyčistit dokončená stahování</string>
|
||||
<string name="msg_pending_downloads">Pokračovat ve stahování %s souborů, čekajících na stažení</string>
|
||||
<string name="stop">Zastavit</string>
|
||||
<string name="max_retry_msg">Maximální počet pokusů o opakování</string>
|
||||
<string name="max_retry_desc">Maximální počet pokusů před zrušením stahování</string>
|
||||
|
@ -514,4 +511,19 @@ otevření ve vyskakovacím okně</string>
|
|||
<string name="clear_playback_states_summary">Smazat všechny pozice playbacku</string>
|
||||
<string name="delete_playback_states_alert">Smazat všechny pozice playbacku\?</string>
|
||||
<string name="download_choose_new_path">Změnit adresář pro stažené soubory</string>
|
||||
<string name="drawer_header_description">Přepnout službu, právě vybráno:</string>
|
||||
<string name="no_one_watching">Nikdo nesleduje</string>
|
||||
<plurals name="watching">
|
||||
<item quantity="one">%s sleduje</item>
|
||||
<item quantity="few">%s sledují</item>
|
||||
<item quantity="other">%s sleduje</item>
|
||||
</plurals>
|
||||
<string name="no_one_listening">Nikdo neposlouchá</string>
|
||||
<plurals name="listening">
|
||||
<item quantity="one">%s posluchač</item>
|
||||
<item quantity="few">%s posluchači</item>
|
||||
<item quantity="other">%s posluchačů</item>
|
||||
</plurals>
|
||||
<string name="localization_changes_requires_app_restart">Ke změně jazyka dojde po restartu aplikace.</string>
|
||||
<string name="default_kiosk_page_summary">Výchozí kiosek</string>
|
||||
</resources>
|
|
@ -380,7 +380,6 @@
|
|||
<string name="error_connect_host">Kan ikke forbinde til serveren</string>
|
||||
<string name="error_http_no_content">Serveren sender ikke data</string>
|
||||
<string name="error_http_unsupported_range">Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">Det anmodede interval er ikke gyldigt</string>
|
||||
<string name="error_http_not_found">Ikke fundet</string>
|
||||
<string name="error_postprocessing_failed">Efterbehandling fejlede</string>
|
||||
<string name="stop">Stop</string>
|
||||
|
@ -448,7 +447,6 @@
|
|||
<string name="paused">sat på pause</string>
|
||||
<string name="queued">sat i kø</string>
|
||||
<string name="clear_finished_download">Ryd færdige downloads</string>
|
||||
<string name="msg_pending_downloads">Fortsæt dine %s ventende overførsler fra Downloads</string>
|
||||
<string name="max_retry_msg">Maksimalt antal genforsøg</string>
|
||||
<string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</string>
|
||||
<string name="pause_downloads_on_mobile">Sæt på pause ved skift til mobildata</string>
|
||||
|
|