> headers = request.headers();
@@ -153,4 +173,47 @@ public class DownloaderImpl extends Downloader {
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(_,_)
+ *
+ * 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 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();
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index c24d77d03..90d299c7f 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -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 instances = PeertubeHelper.getInstanceList(this);
+ List 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 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) {
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java
index bf446ca1f..fa257cfed 100644
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java
@@ -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() {
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 59bffa933..4fbf4ab5f 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -38,6 +38,7 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
@@ -68,6 +69,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;
@@ -367,6 +369,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
+ if (getActivity() instanceof RouterActivity) {
+ getActivity().finish();
+ }
return true;
}
return false;
@@ -762,12 +767,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.
@@ -778,18 +784,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 secondaryStream = videoStreamsAdapter
+ SecondaryStreamHelper 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;
@@ -801,8 +809,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;
@@ -824,13 +832,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();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
index 720e0f216..88a4c9c63 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java
@@ -30,6 +30,7 @@ import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
+import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList;
import java.util.List;
@@ -37,7 +38,7 @@ import java.util.List;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager;
private SelectedTabsPagerAdapter pagerAdapter;
- private TabLayout tabLayout;
+ private ScrollableTabLayout tabLayout;
private List tabsList = new ArrayList<>();
private TabsManager tabsManager;
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
index 95aef4764..c20ff0fc2 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
@@ -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 {
@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 {
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 {
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 {
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 {
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe((List subscriptionEntities) ->
- updateSubscribeButton(!subscriptionEntities.isEmpty())
+ updateSubscribeButton(!subscriptionEntities.isEmpty())
, onError));
}
@@ -359,9 +364,9 @@ public class ChannelFragment extends BaseListInfoFragment {
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 {
private PlayQueue getPlayQueue(final int index) {
final List 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 {
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;
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
index 32b83bb22..6941741af 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
@@ -259,7 +259,7 @@ public class PlaylistFragment extends BaseListInfoFragment {
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 {
}
});
}
+ } 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 {
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
playlistBookmarkButton.setTitle(titleRes);
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java
index f9542850e..8bb16c318 100644
--- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java
@@ -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);
diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
index 9e72838ad..c60cdac3f 100644
--- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
@@ -325,6 +325,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
+ headerPopupButton.setOnLongClickListener(view -> {
+ NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
+ return true;
+ });
+
+ headerBackgroundButton.setOnLongClickListener(view -> {
+ NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
+ return true;
+ });
+
hideLoading();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
index 521daf184..f36e352a6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java
@@ -102,6 +102,9 @@ public final class BackgroundPlayer extends Service {
private boolean shouldUpdateOnProgress;
+ private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
+ private int timesNotificationUpdated;
+
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -188,6 +191,7 @@ public final class BackgroundPlayer extends Service {
private void resetNotification() {
notBuilder = createNotification();
+ timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
@@ -295,6 +299,7 @@ public final class BackgroundPlayer extends Service {
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
+ timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -398,9 +403,9 @@ public final class BackgroundPlayer extends Service {
updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress) return;
- resetNotification();
+ if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {resetNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/)
- updateNotificationThumbnail();
+ updateNotificationThumbnail();}
if (bigNotRemoteView != null) {
if (cachedDuration != duration) {
cachedDuration = duration;
diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index a07afcea9..6452a9850 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -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
@@ -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() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
index 360475ba2..0734139e1 100644
--- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java
@@ -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);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
index 64022e39c..9134d6144 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
@@ -50,15 +50,8 @@ public class MediaSessionManager {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- public void setLockScreenArt(
- NotificationCompat.Builder builder,
- @Nullable Bitmap thumbnailBitmap
- ) {
- if (thumbnailBitmap == null) {
- return;
- }
-
- if (!mediaSession.isActive()) {
+ public void setLockScreenArt(NotificationCompat.Builder builder, @Nullable Bitmap thumbnailBitmap) {
+ if (thumbnailBitmap == null || !mediaSession.isActive()) {
return;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index 85c852f57..e4cef8c5c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -319,6 +319,7 @@ public class MediaSourceManager {
private Observable getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
.filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
new file mode 100644
index 000000000..a0c16af75
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
@@ -0,0 +1,427 @@
+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 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);
+
+ initViews(rootView);
+ }
+
+ private void initViews(@NonNull View rootView) {
+ TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV);
+ instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, getString(R.string.peertube_instance_list_url)));
+
+ 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 {
+ 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);
+ }
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
index c52ebf3aa..0cfd856e1 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
@@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
*/
public class Mp4DashReader {
- //
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;
- //
+
private final DataReader stream;
@@ -293,7 +292,8 @@ public class Mp4DashReader {
return null;
}
- //
+
+
private long readUint() throws IOException {
return stream.readInt() & 0xffffffffL;
}
@@ -392,9 +392,7 @@ public class Mp4DashReader {
return readBox();
}
- //
- //
private Moof parse_moof(Box ref, int trackId) throws IOException {
Moof obj = new Moof();
@@ -795,9 +793,8 @@ public class Mp4DashReader {
return readFullBox(b);
}
- //
- //
+
class Box {
int type;
@@ -1013,5 +1010,5 @@ public class Mp4DashReader {
public TrunEntry info;
public byte[] data;
}
-//
+
}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java
index 03aab447c..818f6148e 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java
@@ -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();
}
- //
+ 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
}
}
- //
+
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 {
}
}
- //
+
private int writeEntry64(int offset, long value) throws IOException {
outBackup();
@@ -447,16 +469,16 @@ public class Mp4FromDashWriter {
lastWriteOffset = -1;
}
}
- //
- //
+
+
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();
}
- //
- //
+
+
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();
}
- //
+
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;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java
new file mode 100644
index 000000000..16bffea9a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java
@@ -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;
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
index 0c635ebe3..42875c364 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
@@ -15,7 +15,6 @@ import java.util.NoSuchElementException;
*/
public class WebMReader {
- //
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;
-//
+ 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;
}
- //
+
+
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);
}
-//
- //
+
+
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;
}
-//
- //
+
+
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 {
}
}
-//
+
}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
index e5881fd0b..8525fabd2 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
@@ -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);
diff --git a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java
index a0e7de4ac..7ad71eb5c 100644
--- a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java
@@ -22,11 +22,11 @@ public class BitmapUtils {
float newYScale;
if (yScale > xScale) {
- newXScale = (1.0f / yScale) * xScale;
+ newXScale = xScale / yScale;
newYScale = 1.0f;
} else {
newXScale = 1.0f;
- newYScale = (1.0f / xScale) * yScale;
+ newYScale = yScale / xScale;
}
float scaledWidth = newXScale * sourceWidth;
diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
index a04e1145f..18c95e394 100644
--- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
+++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
@@ -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:
diff --git a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java
new file mode 100644
index 000000000..0d695e275
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java
@@ -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 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 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();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
index d2ebcd9f8..ab58bc917 100644
--- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java
@@ -52,10 +52,12 @@ public class SecondaryStreamHelper {
}
}
+ 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;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
index 27e2f8422..8929cc654 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
@@ -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());
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java
new file mode 100644
index 000000000..d8b6f78f5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java b/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java
new file mode 100644
index 000000000..48327220a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java
@@ -0,0 +1,128 @@
+package org.schabi.newpipe.views;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayout.Tab;
+
+/**
+ * A TabLayout that is scrollable when tabs exceed its width.
+ * Hides when there are less than 2 tabs.
+ */
+public class ScrollableTabLayout extends TabLayout {
+ private static final String TAG = ScrollableTabLayout.class.getSimpleName();
+
+ private int layoutWidth = 0;
+ private int prevVisibility = View.GONE;
+
+ public ScrollableTabLayout(Context context) {
+ super(context);
+ }
+
+ public ScrollableTabLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ScrollableTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ remeasureTabs();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ layoutWidth = w;
+ }
+
+ @Override
+ public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
+ super.addTab(tab, position, setSelected);
+
+ hasMultipleTabs();
+
+ // Adding a tab won't decrease total tabs' width so tabMode won't have to change to FIXED
+ if (getTabMode() != MODE_SCROLLABLE) {
+ remeasureTabs();
+ }
+ }
+
+ @Override
+ public void removeTabAt(int position) {
+ super.removeTabAt(position);
+
+ hasMultipleTabs();
+
+ // Removing a tab won't increase total tabs' width so tabMode won't have to change to SCROLLABLE
+ if (getTabMode() != MODE_FIXED) {
+ remeasureTabs();
+ }
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ // Recheck content width in case some tabs have been added or removed while ScrollableTabLayout was invisible
+ // We don't have to check if it was GONE because then requestLayout() will be called
+ if (changedView == this) {
+ if (prevVisibility == View.INVISIBLE) {
+ remeasureTabs();
+ }
+ prevVisibility = visibility;
+ }
+ }
+
+ private void setMode(int mode) {
+ if (mode == getTabMode()) return;
+
+ setTabMode(mode);
+ }
+
+ /**
+ * Make ScrollableTabLayout not visible if there are less than two tabs
+ */
+ private void hasMultipleTabs() {
+ if (getTabCount() > 1) {
+ setVisibility(View.VISIBLE);
+ } else {
+ setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Calculate minimal width required by tabs and set tabMode accordingly
+ */
+ private void remeasureTabs() {
+ if (prevVisibility != View.VISIBLE) return;
+ if (layoutWidth == 0) return;
+
+ final int count = getTabCount();
+ int contentWidth = 0;
+ for (int i = 0; i < count; i++) {
+ View child = getTabAt(i).view;
+ if (child.getVisibility() == View.VISIBLE) {
+ // Use tab's minimum requested width should actual content be too small
+ contentWidth += Math.max(child.getMinimumWidth(), child.getMeasuredWidth());
+ }
+ }
+
+ if (contentWidth > layoutWidth) {
+ setMode(TabLayout.MODE_SCROLLABLE);
+ } else {
+ setMode(TabLayout.MODE_FIXED);
+ }
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
index 247faeb6d..618200f27 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
@@ -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();
}
}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java
index d78f8e32b..c0f85b321 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java
@@ -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.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", 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 {
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java
new file mode 100644
index 000000000..14ac392a0
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java
@@ -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 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();
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
index f5b9b06d4..4aa6e912e 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java
@@ -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");
diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
index 7fb1f0c77..9cb40cb32 100644
--- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
+++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java
@@ -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;
}
}
diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java
index b468f3c76..6bc5423b8 100644
--- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java
+++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java
@@ -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;
-
}
+
}
diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
new file mode 100644
index 000000000..e52f35cc6
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java
@@ -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 CREATOR = new Parcelable.Creator() {
+ @Override
+ public MissionRecoveryInfo createFromParcel(Parcel source) {
+ return new MissionRecoveryInfo(source);
+ }
+
+ @Override
+ public MissionRecoveryInfo[] newArray(int size) {
+ return new MissionRecoveryInfo[size];
+ }
+ };
+}
diff --git a/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java
index 16a90fcee..98015e37e 100644
--- a/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java
+++ b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java
@@ -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;
}
diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java
index e2afb9202..102580570 100644
--- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java
+++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java
@@ -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
}
diff --git a/app/src/main/java/us/shandian/giga/io/ProgressReport.java b/app/src/main/java/us/shandian/giga/io/ProgressReport.java
new file mode 100644
index 000000000..14ae9ded9
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/io/ProgressReport.java
@@ -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);
+}
\ No newline at end of file
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java
new file mode 100644
index 000000000..04958c495
--- /dev/null
+++ b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
index 22cc325d5..773ff92d1 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
@@ -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();
}
}
diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java
index 3d34411b9..e8bc468e9 100644
--- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java
@@ -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);
diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
index 461787b62..3da0e75b8 100755
--- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
@@ -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() {
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index 6d1169031..8420e343b 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 implements Handler.Callb
mIterator.end();
for (ViewHolderItem item : mPendingDownloadsItems) {
- item.lastTimeStamp = -1;
+ item.resetSpeedMeasure();
}
notifyDataSetChanged();
@@ -654,6 +679,7 @@ public class MissionAdapter extends Adapter 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 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 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 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 implements Handler.Callb
return popup;
}
+
+ private void resetSpeedMeasure() {
+ estimatedTimeArrival = UNDEFINED_ETA;
+ lastTimestamp = -1;
+ lastSpeedIdx = -1;
+ }
}
class ViewHolderHeader extends RecyclerView.ViewHolder {
diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java
index 81b4e33e8..a0828c23d 100644
--- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java
+++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java
@@ -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;
}
diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java
index a0ff24aaa..3f638d418 100644
--- a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java
+++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java
@@ -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();
}
diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
index 26da47b1f..921eaff5c 100644
--- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
+++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java
@@ -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);
}
diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java
index 21fdd72ad..46207777a 100644
--- a/app/src/main/java/us/shandian/giga/util/Utility.java
+++ b/app/src/main/java/us/shandian/giga/util/Utility.java
@@ -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);
+ }
}
diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png
new file mode 100755
index 000000000..a9e2993eb
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png
new file mode 100755
index 000000000..a9af000b4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png
new file mode 100755
index 000000000..13813ff82
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png
new file mode 100755
index 000000000..9054e0042
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png
new file mode 100755
index 000000000..1eba63792
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png
new file mode 100755
index 000000000..23d8145f5
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png
new file mode 100755
index 000000000..adc36b227
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png
new file mode 100755
index 000000000..c19bfe964
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png differ
diff --git a/app/src/main/res/drawable-nodpi/place_holder_peertube.png b/app/src/main/res/drawable-nodpi/place_holder_peertube.png
new file mode 100644
index 000000000..68850054d
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/place_holder_peertube.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png
new file mode 100755
index 000000000..e20865ab0
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png
new file mode 100755
index 000000000..2d3474832
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png
new file mode 100755
index 000000000..54e815980
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png
new file mode 100755
index 000000000..3141a790d
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png
new file mode 100755
index 000000000..bcbeb199c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png
new file mode 100755
index 000000000..6b27fb23c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png
new file mode 100755
index 000000000..92fc748ec
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png
new file mode 100755
index 000000000..5b0aa6ae2
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png
new file mode 100755
index 000000000..e208010a2
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png
new file mode 100755
index 000000000..b04fd7a88
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png
new file mode 100755
index 000000000..152259fab
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png
new file mode 100755
index 000000000..1aac3b986
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png differ
diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml
index 22e81883d..9ed9f833a 100644
--- a/app/src/main/res/layout-v21/drawer_header.xml
+++ b/app/src/main/res/layout-v21/drawer_header.xml
@@ -47,15 +47,22 @@
+ android:textStyle="italic"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+ android:textStyle="italic"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:singleLine="true" />
+ app:headerLayout="@layout/drawer_header"
+ android:theme="@style/NavViewTextStyle"/>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
index 85614342d..1a2455691 100644
--- a/app/src/main/res/layout/fragment_main.xml
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -6,12 +6,13 @@
android:layout_height="match_parent">
-
+
diff --git a/app/src/main/res/layout/instance_spinner_layout.xml b/app/src/main/res/layout/instance_spinner_layout.xml
new file mode 100644
index 000000000..63e910d96
--- /dev/null
+++ b/app/src/main/res/layout/instance_spinner_layout.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/layout/item_instance.xml b/app/src/main/res/layout/item_instance.xml
new file mode 100644
index 000000000..b0e4e25bd
--- /dev/null
+++ b/app/src/main/res/layout/item_instance.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 7156d08ba..3ad4c2f39 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -20,7 +20,7 @@
يتم تخزين ملفات الفيديو التي تم تنزيلها هنا
مجلد تحميل الفيديو
"لا يمكن إنشاء مجلد للتنزيلات في '%1$s'"
- إنشاء دليل التنزيل \'%1$s\'
+ دليل التنزيل الذي تم إنشاؤه \'%1$s\'
تثبيت
تطبيق Kore غير موجود. هل تريد تثبيته ؟
مضيء
@@ -42,7 +42,7 @@
مشاركة
مشاركة بواسطة
عرض مقاطع الفيديو \"التالية\" و \"المشابهة\"
- عرض خيار تشغيل الفيديو عبر مركز وسائط كودي
+ عرض خيارات تشغيل الفيديو من خلال مركز كودي ميديا
عرض خيار التشغيل بواسطة كودي
السمة
تم النشر يوم %1$s
@@ -69,8 +69,8 @@
مراقبة السجل
التاريخ و ذاكرة التخزين المؤقت
محتوى
- التحميلات
- التحميلات
+ الملفات المحملة
+ الملفات المحملة
الجميع
القناة
الفيديو
@@ -113,11 +113,11 @@
محتوى مقيد بحسب العمر
"إظهار الفيديو المقيد بحسب العمر. يمكن السماح باستخدام هذه المواد من \"الإعدادات\"."
بث مباشر
- تقرير خطأ
+ تقرير عن مشكلة
قائمة التشغيل
نعم
لاحقاً
- تعطيل
+ متوقف
فلتر
تحديث
تنظيف
@@ -200,7 +200,7 @@
فتح الموقع
المساهمون
التراخيص
- مجاني خفيف الوزن بث حي على أندرويد.
+ تطبيق مجاني خفيف الوزن وبث حي على نظام أندرويد.
ساهم
إذا كانت لديك أفكار؛ أو ترجمة، أو تغييرات تخص التصميم، أو تنظيف و تحسين الشفرة البرمجية ، أو تعديلات عميقة عليها، فتذكر أنّ مساعدتك دائما موضع ترحيب. وكلما أتممنا شيئا كلما كان ذلك أفضل !
عرض على GitHub
@@ -234,7 +234,7 @@
التفاصيل
الإعدادات الصوتية
تشغيل هنا
- تشغيل في وضع نافذة منبثقة
+ بدأ التشغيل في نافذة منبثقة جديدة
تحدي الكابتشا
ضغط مطول للإدراج الى قائمة الانتظار
@@ -246,12 +246,12 @@
- %s شاهدو
- - فيديو%s video
- - %s videosفيديوهات
- - %s videosفيديوهات
- - %s videosفيديوهات
- - %s videosفيديوهات
- - %s videosفيديوهات
+ - فيديوهات
+ - فيديوهات
+ - فيديوهات
+ - فيديوهات
+ - فيديوهات
+ - فيديوهات
طلب اختبار الكابتشا مطلوب
© %1$sبواسطة%2$sتحت%3$s
@@ -425,10 +425,10 @@
تتبيه تحديث التطبيق
إيماءة التحكم بالصوت
الأحداث
- إخطارات NewPipe جديدة الإصدار
- ذاكرة التخزين الخارجية غير متوفرة
+ إخطارات لنسخة NewPipe الجديدة
+ وحدة التخزين الخارجية غير متوفرة
"التنزيل على بطاقة SD الخارجية غير ممكن. إعادة تعيين موقع مجلد التحميل؟"
- استخدام خطأ علامات التبويب الافتراضية, أثناء قراءة علامات التبويب المحفوظة
+ باستخدام علامات التبويب الافتراضية ، خطأ أثناء قراءة علامات التبويب المحفوظة
استعادة الضبط الافتراضي
هل تريد استعادة الإعدادات الافتراضية؟
عدد المشتركين غير متاح
@@ -449,7 +449,7 @@
متوقف
في قائمة الانتظار
قيد المعالجة
- قائمة الانتظار
+ قائمه انتظار
تم رفضها من قبل النظام
فشل التنزيل
تم الانتهاء من التحميل
@@ -468,11 +468,9 @@
لا يمكن الاتصال بالخادم
الخادم لايقوم بإرسال البيانات
الخادم لا يقبل التنزيل المتعدد، إعادة المحاولة مع @string/msg_threads = 1
- عدم استيفاء النطاق المطلوب
غير موجود
فشلت المعالجة الاولية
حذف التنزيلات المنتهية
- "قم بإستكمال %s حيثما يتم التحويل من التنزيلات"
توقف
أقصى عدد للمحاولات
الحد الأقصى لعدد محاولات قبل إلغاء التحميل
@@ -522,5 +520,39 @@
حذف كل مواقف التشغيل؟
تغيير مجلدات التنزيل إلى حيز التنفيذ
تبديل الخدمة ، المحدد حاليًا:
- الكشك الافتراضي
+ الكشك الافتراضي
+ لاتوجد مشاهدة
+ لا أحد يستمع
+ ستتغير اللغة بمجرد إعادة تشغيل التطبيق.
+
+ - مشاهد
+ - مشاهدة
+ - مشاهدة
+ - مشاهدات
+ - مشاهدات
+ - مشاهدات
+
+
+ - مستمع
+ - مستمع
+ - مستمعين
+ - مستمعين
+ - مستمعين
+ - مستمعين
+
+ تسريع إلى الأمام/-ترجيع وقت البحث
+ نموذج بيرتوب
+ تعيين حالات بيرتوب المفضلة لديك
+ إضافة نموذج
+ أدخل رابط نموذج
+ فشل في التحقق من النموذج
+ فقط عناوين https المدعومة
+ نموذج موجود بالفعل
+ محلي
+ أضيف مؤخرا
+ الأكثر إعجابا
+ تم إنشاؤه-تلقائيًا (لم يتم العثور على برنامج تحميل)
+ استرد
+ لا يمكن استرداد هذا التنزيل
+ اختيار نموذج
\ No newline at end of file
diff --git a/app/src/main/res/values-b+zh+HANS+CN/strings.xml b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
index d97835b80..8714c6aca 100644
--- a/app/src/main/res/values-b+zh+HANS+CN/strings.xml
+++ b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
@@ -38,8 +38,8 @@
稍后
网络错误
- - %s 个视频
-
+ - 视频
+
禁用
背景
@@ -62,7 +62,7 @@
清除观看记录
无
最小化后台播放器
- 最小化小窗口播放器
+ 最小化悬浮窗播放器
频道
播放列表
取消订阅
@@ -91,7 +91,7 @@
存储访问权限已被拒绝
- %1$s 次观看
-
+
千
百万
@@ -111,7 +111,7 @@
请稍候…
复制至剪贴板
reCAPTCHA验证码
- 弹出
+ 悬浮窗播放
关于NewPipe
设置
关于
@@ -124,14 +124,14 @@
许可证
下载
文件名中允许的字符
- 无效字符将会替换为此字符
+ 无效字符将会被替换为此
字母和数字
最特殊字符
没有结果
没有订阅者
- %s个订阅者
-
+
没有视频
拖动以重新排序
@@ -143,7 +143,7 @@
未安装用于播放此文件的应用程序
已删除1个项目。
哪些标签需要在主页上展示
- 列表观看模式
+ 列表视图模式
已完成
等待中…
已暂停
@@ -155,4 +155,364 @@
%s 次下载已完成
没有评论
切换服务,当前选择:
+ 找不到串流播放器。您想安装 VLC 吗?
+ 旋转
+ 使用第三方视频播放器
+ 使用第三方视频播放器
+ 音频下载文件夹
+ 从其他应用打开 NewPipe 时就播放视频
+ 默认分辨率
+ 找不到Kore。是否安装?
+ 显示“用Kodi播放”选项
+ 显示“通过Kodi media center播放视频的选项”
+ 音频
+ 默认音频格式
+ 显示“下一个”和“类似的”视频
+ 视频和音频
+ 在后台播放
+ 播放
+ 内容
+ 受年龄限制的内容
+ 显示受年龄限制的视频。可从设置允许此类内容。
+ 直播
+ 下载
+ 下载
+ 错误报告
+ 错误
+ 无法加载所有缩略图
+ 无法解密视频 URL 的签名
+ 无法解析网址
+ 无法完全解析网址
+ 内容不可用
+ 无法设置下载菜单
+ 暂时不支持观看直播
+ 无法获得任何信息流
+ 无法加载图像
+ App UI 崩溃
+ 抱歉,这不应该发生的。
+ 通过电子邮件报告错误
+ 抱歉,发生了一些错误。
+ 报告
+ 信息:
+ 发生了什么:
+ 详情:\\n请求:\\n内容语言:\\n服务:\\nGMT时间:\\n包:\\n版本:\\n操作系统版本:
+ 您的注释(请用英文):
+ 详细信息:
+ 视频预览缩略图
+ 播放视频,时长:
+ 视频上传者的头像缩略图
+ 字节
+ 错误的 URL 或未联网
+ NewPipe下载中
+ 请稍后在设置中设定下载目录
+ 用悬浮窗模式
+\n需要此权限
+ reCAPTCHA验证
+ 请求的新的CAPTCHA验证
+ NewPipe悬浮窗模式
+ 在悬浮窗中播放
+ 默认悬浮窗分辨率
+ 使用更高的分辨率
+ 仅某些设备支持播放2K / 4K视频
+ 清除
+ 记住悬浮窗的尺寸与位置
+ 记住最后一次使用悬浮窗的大小和位置
+ 悬浮窗
+ 调整大小
+ 删除“某些”分辨率的音频
+ 播放器手势控制
+ 使用手势控制播放器的亮度和音量
+ 显示搜索建议
+ 最佳分辨率
+ 开源小巧的Android媒体播放器。
+ 在GitHub上查看
+ NewPipe开源许可证
+ 你是否有想:翻译、设计、清理或重型代码更改 ——我们始终欢迎你来贡献!
+ 阅读许可证
+ 贡献
+ 替换字符
+ 取消订阅频道
+ 无法修改订阅
+ 无法更新订阅
+ 主页面
+ 订阅
+ 新增功能
+ 恢复前台焦点
+ 中断后继续播放(例如突然来电后)
+ 搜索历史记录
+ 在本地存储搜索查询记录
+ 记录已观看视频
+ 历史
+ 已搜索
+ 已观看
+ 历史记录功能已关闭
+ 历史
+ 历史记录为空
+ 清除历史记录
+ NewPipe 通知
+ NewPipe 后台播放和悬浮窗播放的通知
+ 默认视频格式
+ 行为
+ 空空如也...
+ 0次观看
+ 项目已删除
+ 是否要从搜索历史记录中删除此项目?
+ 显示在主页面内容
+ 空白页
+ 『时下流行』页-自定义
+ 订阅页
+ Feed 页面
+ 频道页
+ 选择一个频道
+ 尚未订阅频道
+ 选择一个时下流行页
+ 『时下流行』
+ 趋势
+ 前50
+ 最新与热门
+ 显示 \"长按添加\" 说明
+ 在视频详情页中,按下背景播放或悬浮窗播放按钮时显示提示
+ 已加入后台播放播放列表
+ 已加入悬浮窗播放列表
+ 无法播放此串流
+ 发生无法恢复播放器错误
+ 恢复播放器错误
+ 后台播放
+ 悬浮窗播放器
+ 移除
+ 详情
+ 音频设置
+ 长按队列
+ [未知]
+ 添加到后台部分队列
+ 添加至新悬浮窗列表
+ 开始在此处开始播放
+ 开始后台播放
+ 开始在新悬浮窗中播放
+ 捐赠
+ NewPipe 是由志愿者花费时间为您带来最佳体验开发的。回馈帮助开发人员在享用一杯咖啡的同时,让 NewPipe 变得更好。
+ 回馈
+ 网站
+ 请访问 NewPipe 网站了解更多信息和讯息。
+ 默认国家/地区
+ 切换方向
+ 切换到背景播放
+ 切换到悬浮窗播放
+ 切换到主页面
+ 服务
+ 打开抽屉
+ 关闭抽屉
+ 第三方播放器不支持此类型链接
+ 无效 URL
+ 未找到视频串流
+ 找不到音频串流
+ 视频播放器
+ 后台播放器
+ 悬浮窗播放器
+ 正在获取信息…
+ 正在加载请求的内容
+ 导入数据库
+ 导出数据库
+ 覆盖当前历史记录和订阅
+ 导出历史记录、订阅和播放列表
+ 导出成功
+ 导入成功
+ 没有有效的ZIP文件
+ 警告:无法导入所有文件。
+ 这将覆盖当前设置。
+ 显示信息
+ 已收藏
+ 确定要从观看历史记录中删除该项吗?
+ 是否确实要从历史记录中删除所有项目?
+ 最后播放
+ 播放最多
+ 总是寻问
+ 新建播放列表
+ 删除
+ 重 命名
+ 名称
+ 添加到播放列表
+ 设为播放列表缩略图
+ 收藏播放列表
+ 删除收藏
+ 删除此播放列表?
+ 新建播放列表成功
+ 加入播放列表成功
+ 播放列表缩略图更改成功。
+ 无法删除播放列表
+ 无字幕
+ 适应屏幕
+ 填充屏幕
+ 缩放
+ 敬请等待
+ 调试
+ 自动生成
+ 启用LeakCanary
+ 『内存泄漏监视』可能导致应用在『核心转储』时无响应
+ 报告『提前结束Android生命周期』错误
+ 强制报告处理后的未送达的Activity或Fragment生命周期之外的Rx异常
+ 使用快速不精确搜索
+ 粗略定位播放:允许播放器以略低的精确度为代价换取更快的定位速度
+ 自动播放下一个
+ 当播放完非循环列表中的最后一个视频时,自动加入一个相关视频到播放列表
+ 没有此文件夹
+ 无相似文件/内容源
+ 该文件不存在 或 缺少读写该文件的权限
+ 文件名不能为空
+ 发生错误: %1$s
+ 导入/导出
+ 导入
+ 从...导入
+ 导出到...
+ 正在导入…
+ 正在导出…
+ 导入文件
+ 以前的导出
+ 无法导入订阅
+ 无法导出订阅
+ 通过下载导出文件来导入 YouTube 订阅:
+\n
+\n1. 转到此网站: %1$s
+\n2. 登录(如果需要)
+\n3. 应该立即开始下载(即导出文件)
+ 通过键入网址或你的 ID 导入 SoundCloud 配置文件:
+\n
+\n1. 在浏览器中启用\"电脑模式\"(该网站不适用于移动设备)
+\n2. 转到此 URL: %1$s
+\n3. 登录(如果需要)
+\n4. 复制重定向的配置文件下载地址。
+ 你的 ID:soundcloud.com/[你的ID]
+ 该操作消耗大量流量,
+\n你想继续吗?
+ 关闭可防止加载缩略图,节已省数据和内存使用。(若现在更改会清除内存和储存中缓存)
+ 清空图像缓存成功
+ 清空已缓存元数据
+ 清空已缓存的网页数据
+ 清空元数据缓存成功
+ 播放速度控制
+ 节奏
+ 音调
+ 解除关联(可能导致失真)
+ 首选“打开”操作
+ 打开内容时默认操作: = %s
+ 无可下载的串流内容
+ 字幕
+ 修改播放器字幕比例和背景样式。需要重新启动应用程序才能生效。
+ 删除串流的播放历史和播放位置
+ 删除全部观看记录?
+ 清除搜索历史记录
+ 清除搜索关键词的历史记录
+ 是否删除全部搜索历史记录?
+ 搜索历史记录已删除。
+ NewPipe 是版权自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证GPLv3或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。
+ 是否要同时导入设置?
+ NewPipe的隐私政策
+ NewPipe 项目非常重视您的隐私。因此,未经您的同意,应用程序不会收集任何数据。
+\nNewPipe 的隐私政策详细解释了在发送崩溃报告时发送和存储的数据。
+ 阅读隐私政策
+ 为了遵守欧洲一般数据保护条例 (GDPR),我们提请您注意 NewPipe 的隐私政策。请仔细阅读。
+\n您必须接受它才能向我们发送错误报告。
+ 接受
+ 拒绝
+ 无限制
+ 使用移动数据时限制分辨率
+ 切换应用时最小化
+ 从主播放器切换到其他应用时的操作 - %s
+ 静音时快进
+ 滑块[比例尺]
+ 重 置
+ 曲目
+ 用户
+ 选择标签
+ 音量手势控制
+ 使用手势控制播放器的音量
+ 亮度手势控制
+ 使用手势控制播放器的亮度
+ 视频默认语言
+ 应用更新通知
+ NewPipe有新版本的通知
+ 外置存储不可用
+ 无法下载到外部 SD 卡。重置下载文件夹位置?
+ 读取已保存标签时发生错误,因此使用者默认标签
+ 恢复默认
+ 是否恢复默认值?
+ 选择
+ 更新
+ 列表
+ 自动
+ 切换视图
+ 点击下载
+ 后期处理
+ 生成唯一名称
+ 覆盖
+ 正在使用此名称进行下载
+ 显示错误
+ 代码
+ 无法创建目标文件夹
+ 无法创建文件
+ 权限被系统拒绝
+ 安全连接失败
+ 找不到服务器
+ 无法连接到服务器
+ 服务器未发送数据
+ 找不到 NOT FOUND
+ 后期处理失败
+ 清除已完成的下载
+ 停止
+ 最大重试次数
+ 取消下载前的最多尝试次数
+ 在切换到移动流量网络时中断播放
+ 切换至移动数据时可能有用,尽管一些下载无法被暂停
+ 事件
+ 近期大会
+ 显示评论
+ 禁用,以停止显示评论
+
+ - 评论
+
+
+ 无法加载评论
+ 关闭
+ 恢复播放
+ 恢复上次播放位置
+ 列表中的位置
+ 在列表中,显示视频最后一次播放时的播放位置
+ 已删除播放位置记录。
+ 文件被已移动或删除
+ 该名称的文件已经存在
+ 命名冲突,已存在具有此名称文件
+ 无法覆盖文件
+ 有此名称的已暂停下载
+ 处理文件时,NewPipe 已关闭
+ 设备上没有剩余储存空间
+ 进度丢失,文件已被删除
+ 连接超时
+ 你确定吗?
+ 最大下载队列
+ 同时只允许一个下载进行
+ 开始下载
+ 暂停下载
+ 询问下载位置
+ 系统将询问您将每次下载的保存位置
+ 系统将询问您将每次下载的保存位置。
+\n(如果要下载到外部 SD 卡,请选择 SAF)
+ 使用 SAF
+ 存储访问框架(SAF)允许下载文件到外部SD卡。
+\n注:一些设备不兼容SAF
+ 删除播放位置记录
+ 删除所有播放位置记录
+ 删除所有播放位置记录?
+ 更改下载目录让内容生效
+ 『时下流行』页-默认
+ 无人在线观看
+
+ - %s 人在观看
+
+
+ 没人在听
+
+ - %s个听众
+
+
+ 重新启动应用后,语言将更改。
\ No newline at end of file
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 93307cbcf..1cf3abd7e 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -455,11 +455,9 @@
Немагчыма злучыцца з серверам
Не атрымалася атрымаць дадзеныя з сервера
Сервер не падтрымлівае шматструменную загрузку, паспрабуйце з @string/msg_threads = 1
- Запытаны дыяпазон недапушчальны
Не знойдзена
Пасляапрацоўка не ўдалася
Ачысціць завершаныя
- Аднавіць прыпыненыя загрузкі (%s)
Спыніць
Максімум спробаў
Колькасць спробаў перад адменай загрузкі
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index ee48181c8..bcc816577 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -7,8 +7,8 @@
Baixa
Cerca
Paràmetres
- Tria un navegador
- Subscriu-t\'hi
+ Trieu un navegador
+ Subscripció
Subscrit
Mostra la informació
Subscripcions
@@ -37,7 +37,7 @@
Depuració
Contingut
Desactiva les restriccions per edat
- Mostra el vídeo restringit per edat. Pots permetre aquesta mena de continguts des dels paràmetres.
+ Mostra el vídeo restringit per edat. Podeu permetre aquesta mena de continguts des dels paràmetres.
EN DIRECTE
Baixades
Baixades
@@ -46,7 +46,7 @@
Llista de reproducció
Sí
Desactivat
- Esborra
+ Neteja
Millor resolució
Desfés
Sempre
@@ -80,27 +80,27 @@
Col·labora-hi
Lloc web
Llicència del NewPipe
- Llegeix la llicència
+ Llegiu la llicència
Historial
L\'historial està desactivat
Historial
L\'historial és buit
S\'ha esborrat l\'historial
S\'ha eliminat l\'element
- Vols eliminar aquest element de l\'historial de cerca\?
- Vols eliminar aquest element de l\'historial de reproduccions\?
- Segur que vols eliminar tots els elements de l\'historial\?
+ Voleu eliminar aquest element de l\'historial de cerca\?
+ Voleu eliminar aquest element de l\'historial de reproduccions\?
+ Segur que voleu eliminar tots els elements de l\'historial\?
Contingut de la pàgina principal
Pàgina en blanc
Pàgina de subscripcions
- Tria un canal
+ Trieu un canal
S\'ha completat l\'exportació
S\'ha completat la importació
Elimina
Detalls
Paràmetres d\'àudio
Reproductor de vídeo
- Reproductor en segon pla
+ Reproductor en rerefons
Reproductor emergent
Demana-ho sempre
Crea una llista de reproducció
@@ -117,41 +117,41 @@
Per defecte
%1$s reproduccions
Publicat el %1$s
- No s\'ha trobat un reproductor de fluxos. Vols instal·lar el VLC\?
- No s\'ha trobat cap reproductor de fluxos (pots instal·lar el VLC per reproduir-lo).
+ No s\'ha trobat cap reproductor de fluxos. Voleu instal·lar el VLC\?
+ No s\'ha trobat cap reproductor de fluxos (podeu instal·lar el VLC per reproduir-ho).
Obre en mode emergent
Baixa el fitxer de vídeo
- Volies dir: %1$s\?
+ Volíeu dir: %1$s\?
Comparteix-ho amb
rotació
Reproductor de vídeo extern
Mode emergent del NewPipe
- Has eliminat la subscripció d\'aquest canal
+ Heu eliminat la subscripció a aquest canal
No s\'ha pogut modificar la subscripció
No s\'ha pogut actualitzar la subscripció
Principal
- Segon pla
+ Rerefons
Emergent
Afegeix a
- Els fitxers de vídeo baixats s\'emmagatzemen aquí
- Tria la carpeta de baixades per als fitxers de vídeo
+ Els fitxers de vídeo baixats es desen aquí
+ Trieu la carpeta de baixades per als fitxers de vídeo
Els fitxers d\'àudio baixats es desen aquí
- Tria la carpeta de baixada per als fitxers d\'àudio
- Reprodueix un vídeo quan el NewPipe s\'executa des d\'una altra aplicació
+ Trieu la carpeta de baixada per als fitxers d\'àudio
+ Reprodueix un vídeo quan el NewPipe s\'executa des d\'altra aplicació
Resolució per defecte del mode emergent
Mostra resolucions superiors
- Només alguns dispositius són compatibles amb la reproducció de vídeos en 2K/4K
- Reprodueix amb Kodi
- No s\'ha trobat l\'aplicació Kodi. Vols instal·lar-la\?
- Activa «Reprodueix amb Kodi»
+ No tots els dispositius són compatibles amb la reproducció de vídeos en 2K/4K
+ Reprodueix amb el Kodi
+ No s\'ha trobat l\'aplicació Kodi. Voleu instal·lar-la\?
+ Mostra «Reprodueix amb el Kodi»
Mostra una opció per reproduir un vídeo amb el centre multimèdia Kodi
Reproductor emergent intel·ligent
Recorda la darrera mida i posició del reproductor emergent
Cerca ràpida poc precisa
La cerca poc precisa permet que el reproductor cerqui una posició més ràpidament amb menys precisió
Carrega les miniatures
- S\'ha esborrat la memòria cau d\'imatges
- Esborra les metadades de la memòria cau
+ S\'ha eliminat la memòria cau d\'imatges
+ Elimina les metadades de la memòria cau
S\'ha esborrat la memòria cau de metadades
Afegeix vídeos relacionats a la cua
Control per gestos del reproductor
@@ -165,15 +165,15 @@
País per defecte dels continguts
Llengua per defecte dels continguts
Emergent
- S\'està reproduint en segon pla
+ S\'està reproduint en rerefons
S\'està reproduint en mode emergent
- Afegit a la cua del reproductor en segon pla
- Afegit a la cua del reproductor emergent
+ S\'ha afegit a la cua del reproductor en rerefons
+ S\'ha afegit a la cua del reproductor emergent
Reprodueix
Notificació del NewPipe
- Notificacions dels reproductors en segon pla o emergents del NewPipe
+ Notificacions dels reproductors en rerefons o emergents del NewPipe
No s\'han pogut carregar totes les miniatures
- No s\'ha pogut desencriptar la signatura de l\'URL del vídeo
+ No s\'ha pogut desxifrar la signatura de l\'URL del vídeo
No s\'ha pogut processar el lloc web
No s\'ha pogut processar del tot el lloc web
Contingut no disponible
@@ -190,41 +190,41 @@
No s\'ha trobat cap flux d\'àudio
La carpeta no existeix
El fitxer o la font de contingut no existeix
- El fitxer no existeix o no teniu permisos per llegir-lo o escriure-hi
+ El fitxer no existeix o bé no teniu permisos de lectura/escriptura
El nom del fitxer no pot estar en blanc
S\'ha produït un error: %1$s
- Informa de l\'error per correu electrònic
+ Informeu de l\'error per correu electrònic
S\'han produït alguns errors.
- INFORMA\'N
+ INFORME
Informació:
Què ha passat:
Comentari (en anglès):
Detalls:
Miniatura de previsualització del vídeo
- Miniatura de previsualització del vídeo
+ Reprodueix el vídeo, duració:
Miniatura de l\'avatar del propietari
M\'agrada
No m\'agrada
Fes servir el Tor
(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).
- Informa sobre un error
+ Notifiqueu un error
Informe de l\'usuari
Cap resultat
No hi ha res aquí
No s\'ha pogut crear el directori de baixades «%1$s»
S\'ha creat el directori de baixades «%1$s»
- Torna a intentar-ho
+ Torna a provar
S\'ha denegat el permís d\'accés a l\'emmagatzematge
- Sense subscriptors
- Sense reproduccions
+ Cap subscripció
+ Cap reproducció
- %s reproducció
- %s reproduccions
- Sense vídeos
+ Cap vídeo
- - %s vídeo
- - %s vídeos
+ - Vídeo
+ - Vídeos
Pausa
Reprodueix
@@ -236,10 +236,10 @@
Tanca
Canvia el nom
Fils
- Servidor incompatible
+ Servidor no compatible
El fitxer ja existeix
Baixada del NewPipe activa
- Espera…
+ Un moment…
S\'ha copiat al porta-retalls
Caràcters permesos als noms de fitxer
Lletres i dígits
@@ -247,43 +247,43 @@
© %1$s per %2$s sota %3$s
Reprodueix transmissions de manera lliure i lleugera a l\'Android.
Visualitza a GitHub
- Fes una donació
- Per a més informació i notícies, visita el nostre lloc web.
+ Feu una donació
+ Per a més informació i notícies, visiteu el nostre web.
Últimes reproduccions
Més reproduïts
- Pàgina d\'un quiosc
+ Tendències
Pàgina de novetats
Pàgina d\'un canal
- Tria un quiosc
+ Trieu un quiosc
El fitxer no té un format ZIP vàlid
Avís: No s\'han pogut importar tots els fitxers.
Això sobreescriurà els paràmetres actuals.
Quiosc
Tendències
Els millors 50
- Reproductor en segon pla
+ Reproductor en rerefons
Reproductor emergent
- Afegeix a la cua de reproducció en segon pla
+ Afegeix a la cua de reproducció en rerefons
Afegeix a la cua de reproducció emergent
Reprodueix aquí
Obre el calaix
Tanca el calaix
S\'està obtenint la informació…
S\'està carregant el contingut seleccionat
- Vols eliminar aquesta llista de reproducció\?
+ Voleu eliminar aquesta llista de reproducció\?
No s\'ha pogut eliminar la llista de reproducció.
Importació i exportació
Controls de la velocitat de reproducció
Tempo
To
- Toca el botó de cerca per començar
+ Feu un toc al botó de cerca per començar
Elimina l\'àudio en algunes resolucions
Reproductor d\'àudio extern
- 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.
+ 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.
Emmagatzema les cerques localment
- Registra els vídeos visualitzats
+ Crea un historial de vídeos visualitzats
Reprèn automàticament
- Aquesta URL no és compatible
+ Aquest URL no és compatible
Informe d\'error
Més tard
Filtra
@@ -291,20 +291,20 @@
S\'està redimensionant
Reprodueix-ho tot
Canvia l\'orientació
- Canvia al mode en segon pla
+ Canvia al mode en rerefons
Canvia al mode emergent
Canvia al mode principal
Sobreescriu l\'historial i les subscripcions actuals
S\'està recuperant el reproductor després de l\'error
- Ho sentim, això no hauria d\'haver ocorregut.
- Arrossega per a reordenar la llista
+ Bé, és lamentable.
+ Arrossegueu per reordenar la llista
mil
milions
mil milions
Inicia
Nova missió
- L\'URL té un format incorrecte o no hi ha connexió a internet
- Toca aquí per a més detalls
+ L\'URL té un format no vàlid o no hi ha connexió a Internet
+ Feu un toc aquí per a més detalls
Defineix una carpeta de baixades més endavant als paràmetres
Es necessita aquest permís per a obrir el mode emergent
Camp reCAPTCHA
@@ -312,15 +312,15 @@
Se substituiran els caràcters no vàlids amb aquest valor
Caràcter de substitució
Principals caràcters especials
- 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!
- 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è.
- Fes la teva aportació
+ 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!
+ 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è.
+ Feu la vostra aportació
Cerques
Reproduccions
- Encara no t\'has subscrit a cap canal
+ Encara no us heu subscrit a cap canal
Novetats
- Mantén premut per afegir a la cua
- Comença a reproduir en segon pla
+ Manteniu premut per afegir a la cua
+ Comença a reproduir en rerefons
Comença a reproduir en mode emergent
Defineix com a miniatura de la llista de reproducció
Afegeix la llista de reproducció a les adreces d\'interès
@@ -346,45 +346,45 @@
Aviat hi haurà novetats aquí ;D
Acció d\'obertura preferida
Acció per defecte en obrir continguts — %s
- "La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria "
+ La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria
Informa d\'errors fora del cicle de vida
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
- Importa les teves subscripcions de YouTube mitjançant el fitxer d\'exportació:
+ 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ó)
- Importa un perfil de SoundCloud mitjançant l\'URL o l\'identificador del teu perfil:
+ 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
+\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.
identificador, soundcloud.com/identificador
- Tingues en compte que això pot comportar un ús intensiu de la xarxa.
+ Tingueu en compte que això pot comportar un ús intensiu de la xarxa.
\n
-\nVols continuar\?
+\nVoleu continuar\?
No hi ha vídeos que es puguin baixar
Subtítols
Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis.
No s\'ha trobat cap aplicació que pugui reproduir aquest fitxer
- Esborra l\'historial de reproduccions
- Esborra l\'historial dels vídeos reproduïts i les posicions de reproducció
- Vols esborrar tot l\'historial de reproduccions\?
- S\'ha esborrat l\'historial de reproduccions.
- Esborra l\'historial de cerca
- Esborra l\'historial de paraules cercades
- Vols esborrar tot l\'historial de cerca\?
- S\'ha esborrat l\'historial de cerca.
+ Neteja l\'historial de reproduccions
+ Neteja l\'historial dels vídeos reproduïts i les posicions de reproducció
+ Voleu suprimir tot l\'historial de reproduccions\?
+ S\'ha netejat l\'historial de reproduccions.
+ Neteja l\'historial de cerca
+ Neteja l\'historial de paraules cercades
+ Voleu suprimir tot l\'historial de cerca\?
+ S\'ha netejat l\'historial de cerca.
S\'ha esborrat 1 element.
- 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.
- Vols importar també els paràmetres\?
- Política de privacitat del NewPipe
- 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.
- Llegeix la política de privacitat
- 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.
+ 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.
+ Voleu importar també els paràmetres\?
+ Política de privadesa del NewPipe
+ 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.
+ Llegiu la política de privadesa
+ 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.
Accepta
Rebutja
Sense restriccions
@@ -396,13 +396,13 @@
Minimitza al reproductor emergent
Avança ràpid durant el silenci
Pas
- Reinicialitza
+ Reinicia
Canals
Llistes de reproducció
Pistes
Usuaris
Pestanya nova
- Tria una pestanya
+ Trieu una pestanya
Control de volum per gestos
Fes servir gestos per controlar el volum del reproductor
Control de brillantor per gestos
@@ -410,15 +410,15 @@
Actualitzacions
S\'ha eliminat el fitxer
L\'emmagatzematge extern no està disponible
- Reinicialitza els valors per defecte
- Vols reinicialitzar els valors per defecte\?
+ Reinicia als valors per defecte
+ Voleu reiniciar als valors per defecte\?
Selecció
Actualitzacions
Llista
Quadrícula
Automàtic
Canvia la vista
- Està disponible una nova actualització del NewPipe!
+ Està disponible una actualització del NewPipe!
Pendent
en pausa
a la cua
@@ -456,14 +456,14 @@
Sobreescriu
No s\'ha trobat
Mostra els comentaris
- Desactiva-ho per deixar de mostrar els comentaris
+ Desactiveu-ho per no mostrar els comentaris
Reproducció automàtica
- No hi ha comentaris
+ Cap comentari
No s\'han pogut carregar els comentaris
Tanca
- S\'estan utilitzant les pestanyes per defecte, s\'ha produït un error en llegir les pestanyes desades
- Mostra una notificació per demanar l\'actualització de l\'aplicació si hi ha una nova versió disponible
- Toca per baixar
+ S\'ha produït un error en llegir les pestanyes desades; s\'estan utilitzant les pestanyes per defecte
+ Mostra una notificació per demanar l\'actualització de l\'aplicació si hi ha una versió nova disponible
+ Toqueu per baixar
El servidor no està enviant dades
- Comentaris
@@ -480,13 +480,40 @@
Limita la cua de baixades
Inicia les baixades
Pausa les baixades
- Se us demanarà la ubicació de cada baixada
+ Si us demanarà la ubicació de cada baixada
Posicions a les llistes
Mostra els indicadors de posició de reproducció a les llistes
Neteja les dades
El sistema ha denegat l\'acció
- Reprèn les teves %s baixades pendents des de Baixades
S\'ha tancat el NewPipe mentre es treballava en el fitxer
- Pregunta on baixar
+ Demana on baixar
Canvia les carpetes de baixada perquè tingui efecte
+ No es pot desar a la targeta externa. Voleu restablir la carpeta de baixades\?
+ Permís denegat pel sistema
+ El servidor no accepta baixades simultànies. Proveu amb @string/msg_threads = 1
+ Restaura la darrera posició de la reproducció
+ S\'ha suprimit les posicions de reproducció.
+ El fitxer s\'ha mogut o suprimit
+ Només una baixada alhora
+ Si us demanarà la ubicació de cada baixada.
+\nTrieu SAF si voleu desar el contingut en una memòria externa
+ Utilitza SAF
+ 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
+ Esborra les posicions de reproducció
+ Esborra totes les posicions de reproducció
+ Voleu suprimir tots els punts de reproducció\?
+ In/Habilita el servei; selecció actual:
+ Cap visualització
+
+ - %s visualització
+ - %s visualitzacions
+
+ Cap reproducció
+
+ - %s escoltant
+ - %s escoltant
+
+ Es canviarà la llengua en reiniciar l\'aplicació.
+ Tendències
\ No newline at end of file
diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml
index 49801a190..3ff479bfd 100644
--- a/app/src/main/res/values-cmn/strings.xml
+++ b/app/src/main/res/values-cmn/strings.xml
@@ -460,8 +460,6 @@
NewPipe 更新可用!
无法创建目标文件夹
服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试
- 请求范围无法满足
- 继续进行%s个待下载转移
切换至移动数据时有用,尽管一些下载无法被暂停
显示评论
禁用停止显示评论
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index d539923fe..66c952a26 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -314,7 +314,6 @@ otevření ve vyskakovacím okně
Přizpůsobit
Vyplnit
Zvětšit
- Sledovat únik paměti
Ladění
"Automaticky generováno "
Povolit službu LeakCanary
@@ -463,11 +462,9 @@ otevření ve vyskakovacím okně
Nelze se připojit k serveru
Server neposílá data
Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1
- Požadovaný rozsah nelze splnit
Nenalezeno
Post-processing selhal
Vyčistit dokončená stahování
- Pokračovat ve stahování %s souborů, čekajících na stažení
Zastavit
Maximální počet pokusů o opakování
Maximální počet pokusů před zrušením stahování
@@ -515,4 +512,18 @@ otevření ve vyskakovacím okně
Smazat všechny pozice playbacku\?
Změnit adresář pro stažené soubory
Přepnout službu, právě vybráno:
+ Nikdo nesleduje
+
+ - %s sleduje
+ - %s sledují
+ - %s sleduje
+
+ Nikdo neposlouchá
+
+ - %s posluchač
+ - %s posluchači
+ - %s posluchačů
+
+ Ke změně jazyka dojde po restartu aplikace.
+ Výchozí kiosek
\ No newline at end of file
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 42ffd474b..5e44aab61 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -380,7 +380,6 @@
Kan ikke forbinde til serveren
Serveren sender ikke data
Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1
- Det anmodede interval er ikke gyldigt
Ikke fundet
Efterbehandling fejlede
Stop
@@ -448,7 +447,6 @@
sat på pause
sat i kø
Ryd færdige downloads
- Fortsæt dine %s ventende overførsler fra Downloads
Maksimalt antal genforsøg
Maksimalt antal forsøg før downloaden opgives
Sæt på pause ved skift til mobildata
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 2d6b5b6d2..d72b3de29 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -202,8 +202,8 @@
Keine Videos
- - %s Video
- - %s Videos
+ - Video
+ - Videos
Die meisten Sonderzeichen
Element gelöscht
@@ -454,11 +454,9 @@
Kann nicht mit dem Server verbinden
Der Server sendet keine Daten
Der Server erlaubt kein mehrfädiges Herunterladen – wiederhole mit @string/msg_threads = 1
- Gewünschter Bereich ist nicht verfügbar
Nicht gefunden
Nachbearbeitung fehlgeschlagen
Um fertige Downloads bereinigen
- Setze deine %s ausstehenden Übertragungen von Downloads fort
Anhalten
Maximale Wiederholungen
Maximalanzahl der Versuche, bevor der Download abgebrochen wird
@@ -507,5 +505,16 @@
Alle Wiedergabepositionen löschen\?
Ändere die Downloadordner, damit sie wirksam werden
Dienst umschalten, aktuell ausgewählt:
- Quiosque Predefinido
+ Quiosque Predefinido
+ Niemand schaut zu
+
+ - %s Zuschauer
+ - %s Zuschauer
+
+ Niemand hört zu
+
+ - %s Zuhörer
+ - %s Zuhörer
+
+ Die Sprache ändert sich, sobald die App neu gestartet wird.
\ No newline at end of file
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 4f3499cfd..142813119 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -16,12 +16,12 @@
περιστροφή
Χρήση εξωτερικής εφαρμογής αναπαραγωγής βίντεο
Χρήση εξωτερικής συσκευής αναπαραγωγής ήχου
- Διαδρομή λήψης βίντεο
- Διαδρομή για αποθήκευση των βίντεο
- Εισάγετε διαδρομή για λήψη των βίντεο
+ Φάκελος λήψης βίντεο
+ Τα ληφθέντα αρχεία βίντεο αποθηκεύονται εδώ
+ Επιλέξτε το φάκελο λήψης για αρχεία βίντεο
Διαδρομή λήψης αρχείων ήχου
- Αυτή είναι η διαδρομή για την αποθήκευση αρχείων ήχου
- Εισάγετε διαδρομή για λήψη αρχείων ήχου
+ Τα ληφθέντα αρχεία ήχου αποθηκεύονται εδώ
+ Επιλέξτε το φάκελο λήψης για αρχεία ήχου
Προεπιλεγμένη ανάλυση
Αναπαραγωγή με το Kodi
Η εφαρμογή Kore δεν βρέθηκε. Εγκατάσταση της;
@@ -40,11 +40,11 @@
Βίντεο & Ήχος
Εμφάνιση
Άλλα
- Αναπαραγωγή στο υπόβαθρο
+ Αναπαραγωγή στο παρασκήνιο
Αναπαραγωγή
Σφάλμα δικτύου
Μικρογραφία προεπισκόπισης βίντεο
- Μικρογραφία προεπισκόπησης βίντεο
+ Αναπαραγωγή βίντεο, διάρκεια:
Μικρογραφία εικόνας προφίλ του χρήστη
Like
Dislike
@@ -313,7 +313,7 @@
Αφαίρεση
Λεπτομέρειες
Ρυθμίσεις ήχου
- Πιέστε παρατεταμένα για να προστεθεί στην ουρά
+ Πιέστε για να προστεθεί στην ουρά
"Προσθήκη στη λίστα αναπαραγωγής του παρασκήνιου"
Προσθήκη στη λίστα αναπαραγωγής νέου Αναδυόμενου παραθύρου
Εκκίνηση Αναπαραγωγής εδώ
@@ -456,11 +456,9 @@
Αδυναμία σύνδεσης με τον εξυπηρετητή
Ο εξυπηρετητής δεν μπορεί να στείλει τα δεδομένα
Ο εξυπηρετητής δέν υποστηρίζει πολυνηματικές λήψεις, ξαναπροσπαθήστε με @string/msg_threads = 1
- Το ζητούμενο εύρος δεν μπορεί να εξυπηρετηθεί
Δεν βρέθηκε
Μετεπεξεργασία απέτυχε
Εκκαθάριση ολοκληρωμένων λήψεων
- Συνέχιση των %s εκκρεμών σας λήψεων
Διακοπή
Μέγιστες επαναπροσπάθειες
Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης
@@ -505,4 +503,17 @@
Διαγράφει όλες τις θέσεις αναπαραγωγής
Να διαγραφούν όλες οι θέσεις αναπαραγωγής;
Αλλαγή των φακέλων λήψης για να τεθούν σε ισχύ
+ Εναλλαγή υπηρεσιών, επιλεγμένη αυτήν τη στιγμή:
+ Κανείς δεν παρακολουθεί
+
+ - %s παρακολουθεί
+ - %s παρακολουθούν
+
+ Κανείς δεν ακούει
+
+ - %s ακροατής
+ - %s ακροατές
+
+ Η γλώσσα θα αλλάξει μόλις θα επανεκκινηθεί η εφαρμογή.
+ Προεπιλεγμένο περίπτερο
\ No newline at end of file
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index d5cc38a1a..c14195276 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -328,7 +328,7 @@
k
M
- B
+ Mrd
Pri NewPipe
Agordoj
Pri
@@ -425,9 +425,9 @@
Ne povis importi abonojn
Ne povis eksporti abonojn
via salutnomo, soundcloud.com/salutnomo
- "Memoru, ke ĉi tiu operacio povas esti multekosta en la reto.
+ Memoru, ke ĉi tiu operacio povas esti multekosta en la reto.
\n
-\nĈu vi volas daŭrigi\?"
+\nĈu vi volas daŭrigi\?
Kontroloj de rapideco de ludo
Tempoindiko
Ludkampo
@@ -491,11 +491,9 @@
Ne povas konektiĝi al la servilo
La servilo ne sendas datumojn
La servilo ne akceptas plurfadenajn elŝutojn, reprovu kun @string/msg_threads = 1
- Petita kampo ne estas havebla
Ne trovita
Postprocesado malsukcesis
Forviŝi la finitajn elŝutojn
- Daŭrigi viajn %s pritraktajn delokigojn el Elŝutoj
Utila dum la ŝanĝo al poŝdatumoj, kvankam kelkaj elŝutoj ne povas esti interrompitaj
Neniu komento
Dosiero kun ĉi tiu nomo jam ekzistas
@@ -506,5 +504,16 @@
Progreso perdita, ĉar la dosiero estis forviŝita
Eltempiĝo de Konekto
Ŝangi la servon, nuntempe elektita:
- Defaŭlta Kiosko
+ Defaŭlta Kiosko
+ Neniu spektas
+
+ - %s spektanta
+ - %s spektanta
+
+ Neniu aŭskultas
+
+ - %s aŭskultanto
+ - %s aŭskultantoj
+
+ La lingvo ŝanĝos kiam la apo restartos.
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 3aa0bac66..5691c2ab6 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -351,8 +351,8 @@
\n3. Inicie sesión cuando se le pida
\n4. Copie la URL del perfil a la que fue redireccionado.