diff --git a/app/build.gradle b/app/build.gradle index 13aae361908..19e05789ef2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.teamnewpipe:NewPipeExtractor:06f2144e4daa10' + implementation 'com.github.timfbfbfbfb:NewPipeExtractor:5a51a7f8fc0a62e1374d1ddd08a250e6c05bd840' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 1be6e096a22..e355348ed65 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -436,7 +436,7 @@ private void openDownloadDialog() { sortedVideoStreams); FragmentManager fm = getSupportFragmentManager(); - DownloadDialog downloadDialog = DownloadDialog.newInstance(result); + DownloadDialog downloadDialog = DownloadDialog.newInstance(result, null); downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setAudioStreams(result.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); 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 bf446ca1f55..fa257cfedd2 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 PlaylistRemoteEntity(final PlaylistInfo info) { @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 1d536ea1a73..d2033c50651 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -23,9 +23,11 @@ import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.CheckBox; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; @@ -46,6 +48,7 @@ import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.Localization; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.NewPipeSettings; @@ -76,7 +79,8 @@ import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.MissionState; -public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { +public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, + AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230; @@ -107,21 +111,27 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private RadioGroup radioStreamsGroup; private TextView threadsCountTextView; private SeekBar threadsSeekBar; + private CheckBox smartDownloadCheckbox; + private DownloadSetting downloadSetting; + + @Nullable + private PlaylistFragment.PlaylistDownloadCallback playlistDownloadCallback; private SharedPreferences prefs; - public static DownloadDialog newInstance(StreamInfo info) { + public static DownloadDialog newInstance(StreamInfo info, @Nullable PlaylistFragment.PlaylistDownloadCallback callback) { DownloadDialog dialog = new DownloadDialog(); dialog.setInfo(info); + dialog.setPlaylistCallback(callback); return dialog; } - public static DownloadDialog newInstance(Context context, StreamInfo info) { + public static DownloadDialog newInstance(Context context, StreamInfo info, @Nullable PlaylistFragment.PlaylistDownloadCallback callback) { final ArrayList streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), info.getVideoOnlyStreams(), false)); final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList); - final DownloadDialog instance = newInstance(info); + final DownloadDialog instance = newInstance(info, callback); instance.setVideoStreams(streamsList); instance.setSelectedVideoStream(selectedStreamIndex); instance.setAudioStreams(info.getAudioStreams()); @@ -162,6 +172,10 @@ public void setSelectedVideoStream(int selectedVideoIndex) { this.selectedVideoIndex = selectedVideoIndex; } + public void setPlaylistCallback(PlaylistFragment.PlaylistDownloadCallback callback) { + this.playlistDownloadCallback = callback; + } + public void setSelectedAudioStream(int selectedAudioIndex) { this.selectedAudioIndex = selectedAudioIndex; } @@ -258,6 +272,10 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat radioStreamsGroup = view.findViewById(R.id.video_audio_group); radioStreamsGroup.setOnCheckedChangeListener(this); + smartDownloadCheckbox = view.findViewById(R.id.dialog_download_checkbox_intelligent); + if (playlistDownloadCallback != null) + smartDownloadCheckbox.setVisibility(View.VISIBLE); + initToolbar(view.findViewById(R.id.toolbar)); setupDownloadOptions(); @@ -364,12 +382,29 @@ private void initToolbar(Toolbar toolbar) { okButton = toolbar.findViewById(R.id.okay); okButton.setEnabled(false);// disable until the download service connection is done + if (playlistDownloadCallback != null) { + toolbar.setTitle(getResources().getString(R.string.download) + " (" + + getResources().getString(R.string.playlist) + ")"); + MenuItem menuItem = toolbar.getMenu().findItem(R.id.skip); + menuItem.setVisible(true); + } else { + toolbar.setTitle(R.string.download_dialog_title); + } + toolbar.setOnMenuItemClickListener(item -> { + if (item.getItemId() == R.id.okay) { prepareSelectedDownload(); - return true; + + } else if (item.getItemId() == R.id.skip) { + if (playlistDownloadCallback != null) { + + playlistDownloadCallback.accept(null); + } } - return false; + + getDialog().dismiss(); + return true; }); } @@ -780,12 +815,17 @@ private void continueSelectedDownload(@NonNull StoredFileHelper storage) { String[] psArgs = null; String secondaryStreamUrl = null; long nearLength = 0; + String videoResolution = null; + int audioBitrate = -1; + Locale subtitleLocale = null; // more download logic: select muxer, subtitle converter, etc. switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: kind = 'a'; - selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); + AudioStream audioStream = audioStreamsAdapter.getItem(selectedAudioIndex); + audioBitrate = audioStream.getAverageBitrate(); + selectedStream = audioStream; if (selectedStream.getFormat() == MediaFormat.M4A) { psName = Postprocessing.ALGORITHM_M4A_NO_DASH; @@ -793,7 +833,9 @@ private void continueSelectedDownload(@NonNull StoredFileHelper storage) { break; case R.id.video_button: kind = 'v'; - selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); + VideoStream videoStream = videoStreamsAdapter.getItem(selectedVideoIndex); + videoResolution = videoStream.getResolution(); + selectedStream = videoStream; SecondaryStreamHelper secondaryStream = videoStreamsAdapter .getAllSecondary() @@ -820,7 +862,9 @@ private void continueSelectedDownload(@NonNull StoredFileHelper storage) { case R.id.subtitle_button: threads = 1;// use unique thread for subtitles due small file size kind = 's'; - selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); + SubtitlesStream subtitlesStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); + subtitleLocale = subtitlesStream.getLocale(); + selectedStream = subtitlesStream; if (selectedStream.getFormat() == MediaFormat.TTML) { psName = Postprocessing.ALGORITHM_TTML_CONVERTER; @@ -840,8 +884,17 @@ private void continueSelectedDownload(@NonNull StoredFileHelper storage) { } else { urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; } + this.downloadSetting = new DownloadSetting(storage, threads, urls, currentInfo.getUrl(), kind, psName, psArgs, + nearLength, videoResolution, audioBitrate, subtitleLocale); + DownloadManagerService.startMission(context, downloadSetting); - DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); + if (playlistDownloadCallback != null) { + if (smartDownloadCheckbox.isChecked() && downloadSetting != null) { + playlistDownloadCallback.accept(downloadSetting); + } else { + playlistDownloadCallback.accept(null); + } + } dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadSetting.java b/app/src/main/java/org/schabi/newpipe/download/DownloadSetting.java new file mode 100644 index 00000000000..26ff1b32829 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadSetting.java @@ -0,0 +1,80 @@ +package org.schabi.newpipe.download; + +import java.util.Locale; + +import us.shandian.giga.io.StoredFileHelper; + +public class DownloadSetting { + + private StoredFileHelper storedFileHelper; + private int threadCount; + private String[] urls; + private char kind; + private String psName; + private String[] psArgs; + private Long nearLength; + private String source; + private String videoResolution; + private int audioBitRate; + private Locale subtitleLocale; + + public DownloadSetting(StoredFileHelper storedFileHelper, int threadCount, String[] urls, + String source, char kind, String psName, String[] psArgs, Long nearLength, + String videoResolution, int audioBitRate, Locale subtitleLocale) { + this.storedFileHelper = storedFileHelper; + this.threadCount = threadCount; + this.urls = urls; + this.kind = kind; + this.psName = psName; + this.psArgs = psArgs; + this.nearLength = nearLength; + this.source = source; + this.videoResolution = videoResolution; + this.audioBitRate = audioBitRate; + this.subtitleLocale = subtitleLocale; + } + + public StoredFileHelper getStoredFileHelper() { + return storedFileHelper; + } + + public int getThreadCount() { + return threadCount; + } + + public String[] getUrls() { + return urls; + } + + public char getKind() { + return this.kind; + } + + public String getPsName() { + return this.psName; + } + + public String[] getPsArgs() { + return this.psArgs; + } + + public Long getNearLength() { + return this.nearLength; + } + + public String getSource() { + return this.source; + } + + public String getVideoResolution() { + return this.videoResolution; + } + + public int getAudioBitRate() { + return this.audioBitRate; + } + + public Locale getSubtitleLocale() { + return this.subtitleLocale; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 37d8851ea42..4245d21b2d0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1168,7 +1168,7 @@ public void handleResult(@NonNull StreamInfo info) { public void openDownloadDialog() { try { - DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); + DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo, null); downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); 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 32b83bb2252..13b085f78a1 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 @@ -3,9 +3,6 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -17,11 +14,16 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.download.DownloadSetting; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; @@ -39,6 +41,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.util.ThemeHelper; @@ -79,6 +82,7 @@ public class PlaylistFragment extends BaseListInfoFragment { private View headerPlayAllButton; private View headerPopupButton; private View headerBackgroundButton; + private View headerDownloadAllButton; private MenuItem playlistBookmarkButton; @@ -123,7 +127,7 @@ protected View getListHeader() { headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button); - + headerDownloadAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_download_all_button); return headerRootLayout; } @@ -259,7 +263,8 @@ public void handleResult(@NonNull final PlaylistInfo result) { 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 + //headerUploaderLayout.setVisibility(View.VISIBLE); headerUploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { headerUploaderLayout.setOnClickListener(v -> { @@ -273,6 +278,9 @@ public void handleResult(@NonNull final PlaylistInfo result) { } }); } + } else { // Else : hide the uploader section + //headerUploaderLayout.setVisibility(View.INVISIBLE); + headerUploaderName.setText(R.string.playlist_no_uploader); } playlistCtrl.setVisibility(View.VISIBLE); @@ -304,6 +312,12 @@ public void handleResult(@NonNull final PlaylistInfo result) { return true; }); + headerDownloadAllButton.setOnClickListener(view -> { + if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { + NavigationHelper.downloadPlaylist(this, getPlayQueue()); + } + }); + headerBackgroundButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; @@ -316,8 +330,8 @@ private PlayQueue getPlayQueue() { private PlayQueue getPlayQueue(final int index) { final List infoItems = new ArrayList<>(); - for(InfoItem i : infoListAdapter.getItemsList()) { - if(i instanceof StreamInfoItem) { + for (InfoItem i : infoListAdapter.getItemsList()) { + if (i instanceof StreamInfoItem) { infoItems.add((StreamInfoItem) i); } } @@ -330,6 +344,16 @@ private PlayQueue getPlayQueue(final int index) { ); } + public interface PlaylistDownloadCallback { + /** + * Callback for next item in playlist queue to invoke download dialog for next item + * + * @param downloadSetting if smart download checkbox was checked, in which case, + * we should skip presenting the dialog for each video + */ + void accept(DownloadSetting downloadSetting); + } + @Override public void handleNextItems(ListExtractor.InfoItemsPage result) { super.handleNextItems(result); @@ -345,7 +369,7 @@ public void handleNextItems(ListExtractor.InfoItemsPage result) { //////////////////////////////////////////////////////////////////////////*/ @Override - protected boolean onError(Throwable exception) { + public boolean onError(Throwable exception) { if (super.onError(exception)) return true; int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java index 04406c3da34..86de5b42626 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java @@ -5,6 +5,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; +import androidx.recyclerview.widget.RecyclerView; + import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -12,30 +14,34 @@ import android.view.View; import android.view.ViewGroup; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.subscription.SubscriptionService; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.ListUtils; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.Flowable; -import io.reactivex.MaybeObserver; -import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; public class FeedFragment extends BaseListFragment, Void> { @@ -43,17 +49,23 @@ public class FeedFragment extends BaseListFragment, Voi private static final int MIN_ITEMS_INITIAL_LOAD = 8; private int FEED_LOAD_COUNT = MIN_ITEMS_INITIAL_LOAD; - private int subscriptionPoolSize; + private AtomicInteger numLoadedChunks = new AtomicInteger(1); + private AtomicInteger numChannels = new AtomicInteger(0); + private AtomicInteger numLoadedChannels = new AtomicInteger(0); + private AtomicBoolean hasStartedLoading = new AtomicBoolean(false); private SubscriptionService subscriptionService; - private AtomicBoolean allItemsLoaded = new AtomicBoolean(false); - private HashSet itemsLoaded = new HashSet<>(); - private final AtomicInteger requestLoadedAtomic = new AtomicInteger(); - private CompositeDisposable compositeDisposable = new CompositeDisposable(); private Disposable subscriptionObserver; - private Subscription feedSubscriber; + + private Set itemIds = new HashSet<>(); + private Map isoTimeStrLookup = new HashMap<>(); + private List listItems = new ArrayList<>(); + private Set loadedSubscriptionEntities = new HashSet<>(); + + private AtomicBoolean shouldSkipUpdate = new AtomicBoolean(false); + private AtomicBoolean isScrolling = new AtomicBoolean(false); /*////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle @@ -85,7 +97,8 @@ public void onPause() { @Override public void onResume() { super.onResume(); - if (wasLoading.get()) doInitialLoadLogic(); + // Start fetching remaining channels, if any + startLoading(false); } @Override @@ -96,7 +109,6 @@ public void onDestroy() { subscriptionService = null; compositeDisposable = null; subscriptionObserver = null; - feedSubscriber = null; } @Override @@ -133,6 +145,22 @@ public void reloadContent() { super.reloadContent(); } + @Override + protected void initListeners() { + super.initListeners(); + itemsList.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + delayHandler.postDelayed(() -> isScrolling.set(false), 200); + } else { + isScrolling.set(true); + } + } + }); + } + /*////////////////////////////////////////////////////////////////////////// // StateSaving //////////////////////////////////////////////////////////////////////////*/ @@ -140,16 +168,20 @@ public void reloadContent() { @Override public void writeTo(Queue objectsToSave) { super.writeTo(objectsToSave); - objectsToSave.add(allItemsLoaded); - objectsToSave.add(itemsLoaded); + objectsToSave.add(loadedSubscriptionEntities); + objectsToSave.add(itemIds); + objectsToSave.add(isoTimeStrLookup); + objectsToSave.add(listItems); } @Override @SuppressWarnings("unchecked") public void readFrom(@NonNull Queue savedObjects) throws Exception { super.readFrom(savedObjects); - allItemsLoaded = (AtomicBoolean) savedObjects.poll(); - itemsLoaded = (HashSet) savedObjects.poll(); + loadedSubscriptionEntities = (Set) savedObjects.poll(); + itemIds = (Set) savedObjects.poll(); + isoTimeStrLookup = (Map) savedObjects.poll(); + listItems = (List) savedObjects.poll(); } /*////////////////////////////////////////////////////////////////////////// @@ -159,220 +191,171 @@ public void readFrom(@NonNull Queue savedObjects) throws Exception { @Override public void startLoading(boolean forceLoad) { if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]"); - if (subscriptionObserver != null) subscriptionObserver.dispose(); - - if (allItemsLoaded.get()) { - if (infoListAdapter.getItemsList().size() == 0) { - showEmptyState(); - } else { - showListFooter(false); - hideLoading(); - } - isLoading.set(false); - return; - } + if (!hasStartedLoading.get() || forceLoad) { + if (subscriptionObserver != null) subscriptionObserver.dispose(); - isLoading.set(true); - showLoading(); - showListFooter(true); - subscriptionObserver = subscriptionService.getSubscription() + setLoadingState(true); + subscriptionObserver = subscriptionService.getSubscription() .onErrorReturnItem(Collections.emptyList()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleResult, this::onError); + .observeOn(Schedulers.io()) + .subscribe(this::handleResult, this::handleError); + hasStartedLoading.set(true); + } } @Override - public void handleResult(@androidx.annotation.NonNull List result) { - super.handleResult(result); - + public void handleResult(@NonNull List result) { if (result.isEmpty()) { - infoListAdapter.clearStreamItemList(); - showEmptyState(); + delayHandler.post(() -> { + setLoadingState(false); + infoListAdapter.clearStreamItemList(); + showEmptyState(); + }); return; } - subscriptionPoolSize = result.size(); - Flowable.fromIterable(result) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionObserver()); - } + List filteredResult = new ArrayList<>(); - /** - * Responsible for reacting to user pulling request and starting a request for new feed stream. - *

- * On initialization, it automatically requests the amount of feed needed to display - * a minimum amount required (FEED_LOAD_SIZE). - *

- * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo - * containing the feed streams. - **/ - private Subscriber getSubscriptionObserver() { - return new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - if (feedSubscriber != null) feedSubscriber.cancel(); - feedSubscriber = s; - - int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size(); - if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT; + for (SubscriptionEntity subscriptionEntity: result) { + if (loadedSubscriptionEntities.contains(subscriptionEntity.getUrl())) continue; + filteredResult.add(subscriptionEntity); + } - boolean hasToLoad = requestSize > 0; - if (hasToLoad) { - requestLoadedAtomic.set(infoListAdapter.getItemsList().size()); - requestFeed(requestSize); - } - isLoading.set(hasToLoad); - } + numChannels.set(filteredResult.size()); + compositeDisposable.add( + Flowable.fromIterable(filteredResult) + .observeOn(Schedulers.io()) + .subscribe(this::handleReceiveSubscriptionEntity, this::handleError)); - @Override - public void onNext(SubscriptionEntity subscriptionEntity) { - if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) { - subscriptionService.getChannelInfo(subscriptionEntity) - .observeOn(AndroidSchedulers.mainThread()) - .onErrorComplete( - (@io.reactivex.annotations.NonNull Throwable throwable) -> - FeedFragment.super.onError(throwable)) - .subscribe( - getChannelInfoObserver(subscriptionEntity.getServiceId(), - subscriptionEntity.getUrl())); - } else { - requestFeed(1); - } - } - - @Override - public void onError(Throwable exception) { - FeedFragment.this.onError(exception); - } + // Start item list UI update scheduler + delayHandler.postDelayed(this::updateItemsList, 3200); + } - @Override - public void onComplete() { - if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called"); - } - }; + private void handleReceiveSubscriptionEntity(SubscriptionEntity subscriptionEntity) { + compositeDisposable.add( + subscriptionService.getChannelInfo(subscriptionEntity) + .observeOn(Schedulers.io()) + .onErrorComplete((@NonNull Throwable throwable) -> FeedFragment.super.onError( + throwable)) + .subscribe(this.getReceiveChannelInfoHandler(subscriptionEntity.getUrl()), + getFetchChannelInfoErrorHandler(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl()))); } - /** - * On each request, a subscription item from the updated table is transformed - * into a ChannelInfo, containing the latest streams from the channel. - *

- * Currently, the feed uses the first into from the list of streams. - *

- * If chosen feed already displayed, then we request another feed from another - * subscription, until the subscription table runs out of new items. - *

- * This Observer is self-contained and will close itself when complete. However, this - * does not obey the fragment lifecycle and may continue running in the background - * until it is complete. This is done due to RxJava2 no longer propagate errors once - * an observer is unsubscribed while the thread process is still running. - *

- * To solve the above issue, we can either set a global RxJava Error Handler, or - * manage exceptions case by case. This should be done if the current implementation is - * too costly when dealing with larger subscription sets. - * - * @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded. - */ - private MaybeObserver getChannelInfoObserver(final int serviceId, final String url) { - return new MaybeObserver() { - private Disposable observer; + private Consumer getReceiveChannelInfoHandler(String url) { + return channelInfo -> { + addIsoTimeStrsToLookup(channelInfo.getPublishIsoTimeStrLookup()); - @Override - public void onSubscribe(Disposable d) { - observer = d; - compositeDisposable.add(d); - isLoading.set(true); - } + List relatedItems = channelInfo.getRelatedItems(); - // Called only when response is non-empty - @Override - public void onSuccess(final ChannelInfo channelInfo) { - if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) { - onDone(); - return; - } + for (StreamInfoItem item : relatedItems) { + String itemId = item.getId(); + if (itemId == null || itemIds.contains(itemId)) continue; - final InfoItem item = channelInfo.getRelatedItems().get(0); - // Keep requesting new items if the current one already exists - boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item); - if (!itemExists) { - infoListAdapter.addInfoItem(item); - //updateSubscription(channelInfo); - } else { - requestFeed(1); - } - onDone(); - } + String isoTimeStr = isoTimeStrLookup.get(itemId); + if (isoTimeStr == null) continue; - @Override - public void onError(Throwable exception) { - showSnackBarError(exception, - UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(serviceId), - url, 0); - requestFeed(1); - onDone(); + insertItem(itemId, isoTimeStr, item); } - // Called only when response is empty - @Override - public void onComplete() { - onDone(); - } + numLoadedChannels.incrementAndGet(); + loadedSubscriptionEntities.add(url); + }; + } - private void onDone() { - if (observer.isDisposed()) { - return; - } + private synchronized void addIsoTimeStrsToLookup(Map lookup) { + isoTimeStrLookup.putAll(lookup); + } - itemsLoaded.add(serviceId + url); - compositeDisposable.remove(observer); + private synchronized void insertItem(String itemId, String isoTimeStr, StreamInfoItem item) { + itemIds.add(itemId); - int loaded = requestLoadedAtomic.incrementAndGet(); - if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) { - requestLoadedAtomic.set(0); - isLoading.set(false); - } + int insertPosition = ListUtils.binarySearchUpperBound( + listItems, + isoTimeStr, + x -> isoTimeStrLookup.get(x.getId()), + (a, b) -> b.compareTo(a)); + listItems.add(insertPosition, item); + } - if (itemsLoaded.size() == subscriptionPoolSize) { - if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded"); - allItemsLoaded.set(true); - showListFooter(false); - isLoading.set(false); - hideLoading(); - if (infoListAdapter.getItemsList().size() == 0) { - showEmptyState(); - } - } - } - }; + private Consumer getFetchChannelInfoErrorHandler(int serviceId, String url) { + return ex -> showSnackBarError(ex, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0); + } + + private void handleError(Throwable ex) { + delayHandler.post(() -> this.onError(ex)); } @Override protected void loadMoreItems() { - isLoading.set(true); - delayHandler.removeCallbacksAndMessages(null); - // Add a little of a delay when requesting more items because the cache is so fast, - // that the view seems stuck to the user when he scroll to the bottom - delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300); + if (!isLoading.get()) { + setLoadingState(true); + shouldSkipUpdate.set(true); + numLoadedChunks.incrementAndGet(); + } } @Override protected boolean hasMoreItems() { - return !allItemsLoaded.get(); + return listItems.size() > getNumVisibleItems(); } - private final Handler delayHandler = new Handler(); + private void updateItemsList() { + List viewItemsList = infoListAdapter.getItemsList(); + int viewItemSize = viewItemsList.size(); + int numVisibleItems = getNumVisibleItems(); + + if (!shouldSkipUpdate.getAndSet(false) && !isScrolling.get()) { + int minDirtyIndex = Integer.MAX_VALUE; + int maxDirtyIndex = Integer.MIN_VALUE; + boolean isDirty = false; - private void requestFeed(final int count) { - if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]"); - if (feedSubscriber == null) return; + for (int i = 0; i < numVisibleItems; i++) { + InfoItem infoItem = listItems.get(i); - isLoading.set(true); - delayHandler.removeCallbacksAndMessages(null); - feedSubscriber.request(count); + if (i < viewItemSize) { + // Just do shallow comparison, since this is cheaper + if (infoItem != viewItemsList.get(i)) { + viewItemsList.set(i, infoItem); + isDirty = true; + } + } else { + viewItemsList.add(i, infoItem); + isDirty = true; + } + + // Update minDirtyIndex and maxDirtyIndex only if the item list has been modified + if (isDirty) { + if (i < minDirtyIndex) { + minDirtyIndex = i; + } + if (i > maxDirtyIndex) { + maxDirtyIndex = i; + } + } + } + + // Notify the infoListAdapter the range of changes only if the item list has been modified + if (minDirtyIndex < maxDirtyIndex) { + infoListAdapter.notifyItemRangeChanged(minDirtyIndex, maxDirtyIndex - minDirtyIndex + 1); + } + + setLoadingState(false); + } + + // Schedule next item list UI update + if (isScrolling.get()) { + delayHandler.postDelayed(this::updateItemsList, 200); + } else if (numLoadedChannels.get() < numChannels.get()) { + // Schedule next task with longer delay if the background thread is still fetching recent videos + delayHandler.postDelayed(this::updateItemsList, 1000); + } else if (hasMoreItems()) { + delayHandler.postDelayed(this::updateItemsList, 300); + } } + private final Handler delayHandler = new Handler(); + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -383,28 +366,13 @@ private void resetFragment() { if (compositeDisposable != null) compositeDisposable.clear(); if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); - delayHandler.removeCallbacksAndMessages(null); - requestLoadedAtomic.set(0); - allItemsLoaded.set(false); showListFooter(false); - itemsLoaded.clear(); } private void disposeEverything() { if (subscriptionObserver != null) subscriptionObserver.dispose(); if (compositeDisposable != null) compositeDisposable.clear(); - if (feedSubscriber != null) feedSubscriber.cancel(); - delayHandler.removeCallbacksAndMessages(null); - } - - private boolean doesItemExist(final List items, final InfoItem item) { - for (final InfoItem existingItem : items) { - if (existingItem.getInfoType() == item.getInfoType() && - existingItem.getServiceId() == item.getServiceId() && - existingItem.getName().equals(item.getName()) && - existingItem.getUrl().equals(item.getUrl())) return true; - } - return false; + hasStartedLoading.set(false); } private int howManyItemsToLoad() { @@ -417,6 +385,20 @@ private int howManyItemsToLoad() { return Math.max(MIN_ITEMS_INITIAL_LOAD, items); } + private int getNumVisibleItems() { + return Math.min(listItems.size(), numLoadedChunks.get() * FEED_LOAD_COUNT); + } + + private void setLoadingState(boolean isLoading) { + this.isLoading.set(isLoading); + if (isLoading) { + showLoading(); + } else { + hideLoading(); + } + showListFooter(isLoading); + } + /*////////////////////////////////////////////////////////////////////////// // Fragment Error Handling //////////////////////////////////////////////////////////////////////////*/ 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 f9542850edd..b30a6230dfa 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 @@ -28,8 +28,13 @@ public void updateFromItem(final LocalItem localItem, HistoryRecordManager histo itemTitleView.setText(item.getName()); itemStreamCountView.setText(String.valueOf(item.getStreamCount())); + // Here is where the uploader name is set in the bookmarked playlists library itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), NewPipe.getNameOfService(item.getServiceId()))); + if (item.getUploader() == null) { + 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/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index ab07ded2273..10f92782b87 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -226,13 +226,20 @@ private void setupNotification(RemoteViews remoteViews) { PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationFForward, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); - } else { + // We dont need a restart track button (skip track backwards) + remoteViews.setViewVisibility(R.id.notificationRestartTrack, View.GONE); + } else { // But if we only have one song + // Use time skipping for fastforward/rewind remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_rewind); remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_fastforward); remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationFForward, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); + // Add a restart track button (skip track backwards) + remoteViews.setViewVisibility(R.id.notificationRestartTrack, View.VISIBLE); + remoteViews.setOnClickPendingIntent(R.id.notificationRestartTrack, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); } setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode()); diff --git a/app/src/main/java/org/schabi/newpipe/util/Function.java b/app/src/main/java/org/schabi/newpipe/util/Function.java new file mode 100644 index 00000000000..b6b918c2d47 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/Function.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.util; + +public interface Function { + O apply(I input); +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ListUtils.java b/app/src/main/java/org/schabi/newpipe/util/ListUtils.java new file mode 100644 index 00000000000..3b9f2da2889 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ListUtils.java @@ -0,0 +1,36 @@ +package org.schabi.newpipe.util; + +import java.util.Comparator; +import java.util.List; + +public class ListUtils { + /** + * Find upper bound with binary search, the computational complexity is O(log N) + * @param ls List of elements + * @param target The target we are searching for + * @param getter A function for retrieving the desired attribute from the elements in the list, + * the return value will be used for comparisons. The getter could be an identity + * function, x -> x. + * @param cmp The comparator for comparing elements in the list + * @param The type of the elements in the list, should be the same as the input type of + * the getter + * @param

The type of the target, should be the same as the output type of the getter + * @return The index of the upper bound + */ + public static int binarySearchUpperBound(List ls, P target, Function getter, Comparator

cmp) { + int left = 0; + int right = ls.size(); + + while (left < right) { + int mid = (left + right) / 2; + + if (cmp.compare(target, getter.apply(ls.get(mid))) > 0) { + left = mid + 1; + } else { + right = mid; + } + } + + return left; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index e2b03c8e830..b7802a2afd1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -15,6 +15,7 @@ import androidx.fragment.app.FragmentTransaction; import androidx.appcompat.app.AlertDialog; import android.util.Log; +import android.util.SparseArray; import android.widget.Toast; import com.nostra13.universalimageloader.core.ImageLoader; @@ -24,12 +25,16 @@ import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.download.DownloadActivity; +import org.schabi.newpipe.download.DownloadDialog; +import org.schabi.newpipe.download.DownloadSetting; +import org.schabi.newpipe.extractor.MediaFormat; 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.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; @@ -52,9 +57,21 @@ import org.schabi.newpipe.player.PopupVideoPlayerActivity; import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import java.io.IOException; import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.functions.Predicate; +import us.shandian.giga.io.StoredDirectoryHelper; +import us.shandian.giga.io.StoredFileHelper; +import us.shandian.giga.postprocessing.Postprocessing; +import us.shandian.giga.service.DownloadManagerService; @SuppressWarnings({"unused", "WeakerAccess"}) public class NavigationHelper { @@ -138,6 +155,231 @@ public static void playOnBackgroundPlayer(final Context context, final PlayQueue startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback)); } + public static void downloadPlaylist(final PlaylistFragment context, final PlayQueue queue) { + List events = queue.getStreams(); + Iterator eventsIterator = events.listIterator(); + if (eventsIterator.hasNext()) { + startDownloadPlaylist(context, eventsIterator, null); + } + } + + private static void startDownloadPlaylist(final PlaylistFragment activity, + final Iterator itemIterator, + DownloadSetting downloadSetting) { + if (downloadSetting != null) { + Completable.create(emitter -> { + while (itemIterator.hasNext()) { + PlayQueueItem queueItem = itemIterator.next(); + queueItem.getStream().filter(streamInfo -> + streamInfo != null).subscribe(streamInfo -> startDownloadFromDownloadSetting(activity, + downloadSetting, streamInfo), activity::onError); + } + emitter.onComplete(); + }).subscribe(); + } else { + try { + if (itemIterator.hasNext()) { + PlayQueueItem item = itemIterator.next(); + item.getStream().subscribe(streamInfo -> startDownloadFromStreamInfo(activity, streamInfo, itemIterator), + activity::onError); + } + } catch (Exception e) { + Toast.makeText(activity.getActivity(), + R.string.could_not_setup_download_menu, + Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + } + } + + /** + * Starts downloading video without invoking the {@link DownloadDialog} + * + * @param activity + * @param downloadSetting + * @param streamInfo + */ + private static void startDownloadFromDownloadSetting(PlaylistFragment activity, DownloadSetting downloadSetting, StreamInfo streamInfo) { + + DownloadSetting downloadSettingNew = refactorDownloadSetting(activity, downloadSetting, streamInfo); + + if (downloadSettingNew == null) return; + + DownloadManagerService.startMission(activity.getContext(), downloadSettingNew); + } + + /** + * Refactors the download setting for the new stream info + * + * @param activity + * @param downloadSetting + * @param streamInfo + * @return + */ + private static DownloadSetting refactorDownloadSetting(PlaylistFragment activity, DownloadSetting downloadSetting, + StreamInfo streamInfo) { + Stream selectedStream; + String[] urls; + String psName = null; + String[] psArgs = null; + String secondaryStreamUrl = null; + long nearLength = 0; + String fileName = streamInfo.getName().concat("."); + String mime; + + + List sortedVideoStream = ListHelper.getSortedStreamVideosList(activity.getActivity(), + streamInfo.getVideoStreams(), streamInfo.getVideoOnlyStreams(), false); + + StreamItemAdapter.StreamSizeWrapper wrappedVideoStreams = new StreamSizeWrapper<>(sortedVideoStream, activity.getContext()); + StreamItemAdapter.StreamSizeWrapper wrappedAudioStreams = new StreamSizeWrapper<>(streamInfo.getAudioStreams(), activity.getContext()); + StreamItemAdapter.StreamSizeWrapper wrappedSubtitleStreams = new StreamSizeWrapper<>(streamInfo.getSubtitles(), activity.getContext()); + + SparseArray> secondaryAudioStreams = new SparseArray<>(4); + for (int i = 0; i < sortedVideoStream.size(); i++) { + if (!sortedVideoStream.get(i).isVideoOnly()) continue; + AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), sortedVideoStream.get(i)); + + if (audioStream != null) { + secondaryAudioStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); + } + } + + StreamItemAdapter audioStreamsAdapter = new StreamItemAdapter<>(activity.getActivity(), + wrappedAudioStreams); + StreamItemAdapter videoStreamsAdapter = new StreamItemAdapter<>(activity.getActivity(), + wrappedVideoStreams, secondaryAudioStreams); + StreamItemAdapter subtitleStreamsAdapter = new StreamItemAdapter<>(activity.getActivity(), + wrappedSubtitleStreams); + + switch (downloadSetting.getKind()) { + case 'a': + AudioStream audioStream = null; + for (AudioStream currentStream : audioStreamsAdapter.getAll()) { + if (currentStream.getAverageBitrate() == downloadSetting.getAudioBitRate()) { + audioStream = currentStream; + break; + } + } + if (audioStream == null) { + audioStream = audioStreamsAdapter.getItem(0); + } + if (audioStream.getFormat() == MediaFormat.M4A) { + psName = Postprocessing.ALGORITHM_M4A_NO_DASH; + } + fileName += audioStream.getFormat().getSuffix(); + mime = audioStream.getFormat().getMimeType(); + selectedStream = audioStream; + break; + case 'v': + VideoStream videoStream = null; + for (VideoStream currentStream : sortedVideoStream) { + if (currentStream.getResolution().equals(downloadSetting.getVideoResolution())) { + videoStream = currentStream; + break; + } + } + if (videoStream == null) { + videoStream = sortedVideoStream.get(0); + } + SecondaryStreamHelper secondaryStream = videoStreamsAdapter + .getAllSecondary() + .get(wrappedVideoStreams.getStreamsList().indexOf(videoStream)); + if (secondaryStream != null) { + secondaryStreamUrl = secondaryStream.getStream().getUrl(); + + if (videoStream.getFormat() == MediaFormat.MPEG_4) + psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER; + else + psName = Postprocessing.ALGORITHM_WEBM_MUXER; + + psArgs = null; + long videoSize = wrappedVideoStreams.getSizeInBytes(videoStream); + + // 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; + } + } + fileName += videoStream.getFormat().getSuffix(); + mime = videoStream.getFormat().getMimeType(); + selectedStream = videoStream; + break; + case 's': + SubtitlesStream subtitlesStream = null; + for (SubtitlesStream currentStream : subtitleStreamsAdapter.getAll()) { + if (currentStream.getLocale().equals(downloadSetting.getSubtitleLocale())) { + subtitlesStream = currentStream; + break; + } + } + if (subtitlesStream == null) { + subtitlesStream = subtitleStreamsAdapter.getItem(0); + } + if (subtitlesStream.getFormat() == MediaFormat.TTML) { + psName = Postprocessing.ALGORITHM_TTML_CONVERTER; + psArgs = new String[]{ + subtitlesStream.getFormat().getSuffix(), + "false",// ignore empty frames + "false",// detect youtube duplicate lines + }; + } + fileName += subtitlesStream.getFormat() == MediaFormat.TTML ? MediaFormat.SRT.suffix : + subtitlesStream.getFormat().suffix; + mime = subtitlesStream.getFormat().getMimeType(); + selectedStream = subtitlesStream; + break; + default: + return null; + } + + if (secondaryStreamUrl == null) { + urls = new String[]{selectedStream.getUrl()}; + } else { + urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; + } + StoredFileHelper storedFileHelper = downloadSetting.getStoredFileHelper(); + try { + StoredDirectoryHelper storedDirectoryHelper = new StoredDirectoryHelper(activity.getContext(), + storedFileHelper.getParentUri(), storedFileHelper.getTag()); + StoredFileHelper storedFileHelperNew = storedDirectoryHelper.createFile(fileName, mime); + if(storedFileHelperNew == null) { + return null; + } + DownloadSetting downloadSettingNew = new DownloadSetting(storedFileHelperNew, downloadSetting.getThreadCount(), + urls, streamInfo.getUrl(), downloadSetting.getKind(), psName, psArgs, + nearLength, downloadSetting.getVideoResolution(), downloadSetting.getAudioBitRate(), + downloadSetting.getSubtitleLocale()); + return downloadSettingNew; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Invokes the {@link DownloadDialog} for each play queue item + * + * @param activity + * @param streamInfo + * @param itemIterator + */ + private static void startDownloadFromStreamInfo(PlaylistFragment activity, StreamInfo streamInfo, Iterator itemIterator) { + DownloadDialog downloadDialog = DownloadDialog.newInstance(streamInfo, smartDownload -> { + if (itemIterator.hasNext()) + startDownloadPlaylist(activity, itemIterator, smartDownload); + }); + List sortedVideoStream = ListHelper.getSortedStreamVideosList(activity.getActivity(), + streamInfo.getVideoStreams(), streamInfo.getVideoOnlyStreams(), false); + downloadDialog.setVideoStreams(sortedVideoStream); + downloadDialog.setAudioStreams(streamInfo.getAudioStreams()); + downloadDialog.setSelectedVideoStream(ListHelper.getDefaultResolutionIndex(activity.getActivity(), streamInfo.getVideoStreams())); + downloadDialog.setSubtitleStreams(streamInfo.getSubtitles()); + + downloadDialog.show(activity.getActivity().getSupportFragmentManager(), "downloadDialog"); + } + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { enqueueOnPopupPlayer(context, queue, false, resumePlayback); } @@ -499,7 +741,7 @@ public static Intent getIntentByLink(Context context, StreamingService service, case STREAM: rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); + .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); break; } @@ -532,6 +774,7 @@ private static void installApp(Context context, String packageName) { /** * Start an activity to install Kore + * * @param context the context */ public static void installKore(Context context) { @@ -540,13 +783,12 @@ public static void installKore(Context context) { /** * Start Kore app to show a video on Kodi - * * For a list of supported urls see the * - * Kore source code + * Kore source code * . * - * @param context the context to use + * @param context the context to use * @param videoURL the url to the video */ public static void playWithKore(Context context, Uri videoURL) { 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 57cd15e8c5b..815ad9b65d4 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -3,6 +3,8 @@ import android.os.Handler; import android.util.Log; +import androidx.annotation.Nullable; + import org.schabi.newpipe.Downloader; import java.io.File; @@ -15,7 +17,6 @@ import java.net.URL; import java.net.UnknownHostException; -import javax.annotation.Nullable; import javax.net.ssl.SSLException; import us.shandian.giga.io.StoredFileHelper; 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 461787b624b..42d0222ab74 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -35,6 +35,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; +import org.schabi.newpipe.download.DownloadSetting; import org.schabi.newpipe.player.helper.LockManager; import java.io.File; @@ -364,31 +365,23 @@ public void updateForegroundState(boolean state) { /** * 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 downloadSetting downloadSetting */ - 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, DownloadSetting downloadSetting) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); - intent.putExtra(EXTRA_URLS, urls); - intent.putExtra(EXTRA_KIND, kind); - intent.putExtra(EXTRA_THREADS, threads); - intent.putExtra(EXTRA_SOURCE, source); - intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); - intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); - intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); - - intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); - intent.putExtra(EXTRA_PATH, storage.getUri()); - intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag()); + intent.putExtra(EXTRA_URLS, downloadSetting.getUrls()); + intent.putExtra(EXTRA_KIND, downloadSetting.getKind()); + intent.putExtra(EXTRA_THREADS, downloadSetting.getThreadCount()); + intent.putExtra(EXTRA_SOURCE, downloadSetting.getSource()); + intent.putExtra(EXTRA_POSTPROCESSING_NAME, downloadSetting.getPsName()); + intent.putExtra(EXTRA_POSTPROCESSING_ARGS, downloadSetting.getPsArgs()); + intent.putExtra(EXTRA_NEAR_LENGTH, downloadSetting.getNearLength()); + + intent.putExtra(EXTRA_PARENT_PATH, downloadSetting.getStoredFileHelper().getParentUri()); + intent.putExtra(EXTRA_PATH, downloadSetting.getStoredFileHelper().getUri()); + intent.putExtra(EXTRA_STORAGE_TAG, downloadSetting.getStoredFileHelper().getTag()); context.startService(intent); } diff --git a/app/src/main/res/layout/download_dialog.xml b/app/src/main/res/layout/download_dialog.xml index 985ce03f5e3..6acf2bd238b 100644 --- a/app/src/main/res/layout/download_dialog.xml +++ b/app/src/main/res/layout/download_dialog.xml @@ -61,11 +61,24 @@ android:text="@string/caption_setting_title"/> + + - + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/dialog_url.xml b/app/src/main/res/menu/dialog_url.xml index 919ddf3687c..4fbbfdb8f9d 100644 --- a/app/src/main/res/menu/dialog_url.xml +++ b/app/src/main/res/menu/dialog_url.xml @@ -6,5 +6,11 @@ android:id="@+id/okay" android:title="@string/finish" app:showAsAction="always"/> - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb7adfe758e..70bd9e0a8d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -428,6 +428,7 @@ Playlisted Playlist thumbnail changed. Could not delete playlist. + Auto-Generated (no uploader found) No Captions Fit