Merge pull request #2862 from TeamNewPipe/release_0.18.0

Release 0.18.0
This commit is contained in:
Tobias Groza 2019-12-21 19:47:54 +01:00 committed by GitHub
commit 0d7d610127
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 6271 additions and 2132 deletions

View File

@ -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:

View File

@ -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}"

View File

@ -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

View File

@ -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()));
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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() {

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -1067,7 +1067,13 @@ public class VideoDetailFragment
uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
if (info.getViewCount() >= 0) {
videoCountView.setText(Localization.localizeViewCount(activity, info.getViewCount()));
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);

View File

@ -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)

View File

@ -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:
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
if (currentInfo != null) {
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
}
break;
default:
return super.onOptionsItemSelected(item);
@ -218,7 +223,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
updateSubscribeButton(!subscriptionEntities.isEmpty())
updateSubscribeButton(!subscriptionEntities.isEmpty())
, onError));
}
@ -359,9 +364,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
headerRootLayout.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerSubscribersTextView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
@ -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;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception,
UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
url,
errorId);
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;
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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);
@ -444,4 +446,4 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
playlistBookmarkButton.setTitle(titleRes);
}
}
}

View File

@ -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();

View File

@ -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) {
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
if (viewsAndDate.isEmpty()) {
viewsAndDate = infoItem.getUploadDate();
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 += "" + infoItem.getUploadDate();
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
}
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
if (!TextUtils.isEmpty(uploadDate)) {
if (viewsAndDate.isEmpty()) {
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();
}
}
}

View File

@ -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()));
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
// 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);

View File

@ -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() {

View File

@ -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);

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}
}
};
}
}

View File

@ -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()));
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>
}

View File

@ -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,10 +384,16 @@ public class Mp4FromDashWriter {
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
}
if (is64) {
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
} else {
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
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;
}
}

View File

@ -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;
}
}

View File

@ -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>
}

View File

@ -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);

View File

@ -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;

View File

@ -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:

View File

@ -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();
}
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
return df.format(datum);
public static String formatDate(Date date) {
return DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(date);
}
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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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 rangeStart range start
* @param rangeEnd range end
* @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 (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;
}
}
}
if (err instanceof IOException) {
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
if (err.getMessage().contains("Permission denied")) {
code = ERROR_PERMISSION_DENIED;
err = null;
} else if (err.getMessage().contains(INSUFFICIENT_STORAGE)) {
} else if (err.getMessage().contains("ENOSPC")) {
code = ERROR_INSUFFICIENT_STORAGE;
err = null;
} else if (!storage.canWrite()) {
code = ERROR_FILE_CREATION;
err = null;
}
}
@ -342,44 +358,42 @@ 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++;
unknownLength = false;
if (!doPostprocessing()) return;
enqueued = false;
running = false;
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
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;
enqueued = false;
running = false;
deleteThisFromFile();
notify(DownloadManagerService.MESSAGE_FINISHED);
}
private void notifyPostProcessing(int state) {
@ -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,29 +510,14 @@ 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;
}
// wait for all threads are suspended before save the state
if (threads != null) runAsync(-1, this::selfPause);
init = null;
pauseThreads();
}
private void selfPause() {
try {
for (Thread thread : threads) {
if (thread.isAlive()) {
thread.interrupt();
thread.join(5000);
}
}
} catch (Exception e) {
// nothing to do
} finally {
writeThisToFile();
}
private void pauseThreads() {
running = false;
joinForThreads(-1);
writeThisToFile();
}
/**
@ -523,9 +525,10 @@ public class DownloadMission extends Mission {
*/
@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 {

View File

@ -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();
}
}

View File

@ -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?
}
f.close();
if (DEBUG) {
Log.d(TAG, "thread " + mId + " exited from main download loop");

View File

@ -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
@ -33,7 +36,11 @@ public class DownloadRunnableFallback extends Thread {
private void dispose() {
try {
if (mIs != null) mIs.close();
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;
}
}

View File

@ -2,17 +2,17 @@ package us.shandian.giga.get;
import androidx.annotation.NonNull;
public class FinishedMission extends Mission {
public class FinishedMission extends Mission {
public FinishedMission() {
}
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;
}
}

View File

@ -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];
}
};
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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");
}
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;
}
return dir;
}
/**
@ -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);

View File

@ -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,9 +216,11 @@ public class DownloadManagerService extends Service {
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
);
}
return START_NOT_STICKY;
}
}
return START_NOT_STICKY;
return START_STICKY;
}
@Override
@ -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) {
observer.handleMessage(msg);
}
}
for (Callback observer : mEchoObservers)
observer.handleMessage(msg);
return true;
}
@ -364,18 +370,20 @@ public class DownloadManagerService extends Service {
/**
* Start a new download mission
*
* @param context the activity context
* @param urls the list 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.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @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 context the activity context
* @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.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @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() {

View File

@ -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;
h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")"));
h.resetSpeedMeasure();
return;
}
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;
}
if (h.lastTimestamp < 0) {
h.size.setText(sizeStr);
h.lastTimestamp = now;
h.lastDone = done;
return;
}
h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")"));
} else if (deltaDone > 0) {
h.lastTimeStamp = now;
h.lastDone = mission.done;
}
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,33 +410,28 @@ 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;
}
updateProgress(h);
return true;
}
return false;
updateProgress(h);
return true;
}
private void showError(@NonNull DownloadMission mission) {
@ -430,7 +440,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
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);
return;
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);
}
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 recoverMission(DownloadMission mission) {
for (ViewHolderItem h : mPendingDownloadsItems) {
if (mission != h.item.mission) continue;
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);
mDeleter.pause();
mHandler.removeCallbacks(rUpdater);
}
private void setAutoRefresh(boolean enabled) {
if (enabled && !mUpdaterRunning) {
mUpdaterRunning = true;
updater();
} else if (!enabled && mUpdaterRunning) {
mUpdaterRunning = false;
mHandler.removeCallbacks(rUpdater);
}
public void recoverMission(DownloadMission mission) {
ViewHolderItem h = getViewHolder(mission);
if (h == null) return;
mission.errObject = null;
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
h.status.setText(UNDEFINED_PROGRESS);
h.size.setText(Utility.formatBytes(mission.getLength()));
h.progress.setMarquee(true);
mDownloadManager.resumeMission(mission);
}
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;
}
mHandler.postDelayed(rUpdater, 1000);
}
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 {

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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" />

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">你的 IDsoundcloud.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>

View File

@ -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>

View File

@ -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>

View File

@ -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"></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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More