From 73f46d37628149ff3d9a65b1d29b2ac0ad7ca8ff Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Mon, 4 Sep 2017 10:23:56 -0700 Subject: [PATCH] -Modified play queues and items to use extraction helper. -Fixed play queue item removal. -Rebase changes. --- .../newpipe/database/stream/StreamDAO.java | 2 +- .../newpipe/database/stream/StreamEntity.java | 27 +- .../list/playlist/PlaylistFragment.java | 25 + .../fragments/playlist/PlaylistFragment.java | 545 ------------------ .../fragments/subscription/FeedFragment.java | 496 ---------------- .../subscription/SubscriptionEngine.java | 170 ------ .../info_list/PlaylistInfoItemHolder.java | 49 -- .../org/schabi/newpipe/player/BasePlayer.java | 8 +- .../newpipe/player/MainVideoPlayer.java | 4 +- .../newpipe/player/MediaSourceManager.java | 86 ++- .../newpipe/player/PlaybackManager.java | 212 ------- .../schabi/newpipe/player/VideoPlayer.java | 73 ++- .../newpipe/playlist/ExternalPlayQueue.java | 93 +-- .../schabi/newpipe/playlist/PlayQueue.java | 15 +- .../newpipe/playlist/PlayQueueAdapter.java | 3 +- .../newpipe/playlist/PlayQueueItem.java | 66 +-- .../playlist/PlayQueueItemBuilder.java | 10 +- .../newpipe/playlist/PlayQueueItemHolder.java | 2 +- app/src/main/res/layout/playlist_header.xml | 37 +- app/src/main/res/values/strings.xml | 1 + 20 files changed, 212 insertions(+), 1712 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java delete mode 100644 app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java index 31e156cc8..3f1dc275d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamDAO.java @@ -16,7 +16,7 @@ import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_TABLE; public interface StreamDAO extends BasicDAO { @Override @Query("SELECT * FROM " + STREAM_TABLE) - Flowable> findAll(); + Flowable> getAll(); @Override @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId") diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java index 20eddb38a..d4ce96104 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamEntity.java @@ -6,8 +6,8 @@ import android.arch.persistence.room.Ignore; import android.arch.persistence.room.Index; import android.arch.persistence.room.PrimaryKey; -import org.schabi.newpipe.extractor.AbstractStreamInfo; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; @@ -63,21 +63,20 @@ public class StreamEntity { private long uploadDate; @ColumnInfo(name = STREAM_DURATION) - private int duration; + private long duration; @Ignore public StreamInfoItem toStreamInfoItem() { StreamInfoItem item = new StreamInfoItem(); - item.stream_type = AbstractStreamInfo.StreamType.valueOf( this.getType() ); + item.stream_type = StreamType.valueOf( this.getType() ); item.service_id = this.getServiceId(); - item.id = this.getId(); - item.webpage_url = this.getUrl(); - item.title = this.getTitle(); + item.url = this.getUrl(); + item.name = this.getTitle(); item.thumbnail_url = this.getThumbnailUrl(); item.view_count = this.getViewCount(); - item.uploader = this.getUploader(); + item.uploader_name = this.getUploader(); // TODO: temporary until upload date parsing is fleshed out item.upload_date = "Unknown"; @@ -97,12 +96,11 @@ public class StreamEntity { this.type = item.stream_type.name(); this.serviceId = item.service_id; - this.id = item.id; - this.url = item.webpage_url; - this.title = item.title; + this.url = item.url; + this.title = item.name; this.thumbnailUrl = item.thumbnail_url; this.viewCount = item.view_count; - this.uploader = item.uploader; + this.uploader = item.uploader_name; // TODO: temporary until upload date parsing is fleshed out this.uploadDate = new Date().getTime(); @@ -113,8 +111,7 @@ public class StreamEntity { public boolean is(final StreamInfoItem item) { return this.type.equals( item.stream_type.name() ) && this.serviceId == item.service_id && - this.id.equals( item.id ) && - this.url.equals( item.webpage_url ); + this.url.equals( item.url ); } public long getUid() { @@ -197,7 +194,7 @@ public class StreamEntity { this.uploadDate = uploadDate; } - public int getDuration() { + public long getDuration() { return duration; } 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 eeb95f9b1..97779eadc 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 @@ -1,5 +1,6 @@ package org.schabi.newpipe.fragments.list.playlist; +import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -10,6 +11,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; @@ -18,11 +20,15 @@ import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; +import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import java.util.List; + import io.reactivex.Single; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -40,6 +46,8 @@ public class PlaylistFragment extends BaseListInfoFragment { private ImageView headerUploaderAvatar; private TextView headerStreamCount; + private Button headerPlayAllButton; + public static PlaylistFragment getInstance(int serviceId, String url, String name) { PlaylistFragment instance = new PlaylistFragment(); instance.setInitialData(serviceId, url, name); @@ -66,6 +74,7 @@ public class PlaylistFragment extends BaseListInfoFragment { headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name); headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view); headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count); + headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_play_all_button); return headerRootLayout; } @@ -137,6 +146,22 @@ public class PlaylistFragment extends BaseListInfoFragment { if (!result.errors.isEmpty()) { showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.service_id), result.url, 0); } + + headerPlayAllButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + play(); + } + }); + } + + private void play() { + Intent mIntent = new Intent(activity, MainVideoPlayer.class) + .putExtra("serviceId", serviceId) + .putExtra("index", 0) + .putExtra("streams", infoListAdapter.getItemsList()) + .putExtra("nextPageUrl", currentInfo.next_streams_url); + startActivity(mIntent); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java deleted file mode 100644 index 5d257e92b..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistFragment.java +++ /dev/null @@ -1,545 +0,0 @@ -package org.schabi.newpipe.fragments.playlist; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; -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.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import org.schabi.newpipe.ImageErrorLoadingListener; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.playlist.PlayListExtractor; -import org.schabi.newpipe.extractor.playlist.PlayListInfo; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.fragments.BaseFragment; -import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.player.BasePlayer; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.VideoPlayer; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.Utils; - -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.concurrent.Callable; - -import io.reactivex.Observable; -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class PlaylistFragment extends BaseFragment { - private final String TAG = "PlaylistFragment@" + Integer.toHexString(hashCode()); - - private static final String INFO_LIST_KEY = "info_list_key"; - private static final String PLAYLIST_INFO_KEY = "playlist_info_key"; - private static final String PAGE_NUMBER_KEY = "page_number_key"; - - private InfoListAdapter infoListAdapter; - - private PlayListInfo currentPlaylistInfo; - private int serviceId = -1; - private String playlistTitle = ""; - private String playlistUrl = ""; - private int pageNumber = 0; - private boolean hasNextPage = true; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - private RecyclerView playlistStreams; - - private View headerRootLayout; - private ImageView headerBannerView; - private ImageView headerAvatarView; - private TextView headerTitleView; - private Button headerPlayAllButton; - - /*////////////////////////////////////////////////////////////////////////*/ - // Reactors - //////////////////////////////////////////////////////////////////////////*/ - private Disposable loadingReactor; - - /*////////////////////////////////////////////////////////////////////////*/ - - public PlaylistFragment() { - } - - public static Fragment getInstance(int serviceId, String playlistUrl, String title) { - PlaylistFragment instance = new PlaylistFragment(); - instance.setPlaylist(serviceId, playlistUrl, title); - return instance; - } - - public void play(Context context, Class targetClazz) { - Intent mIntent = new Intent(context, targetClazz) - .putExtra("url", playlistUrl) - .putExtra("nextPage", 1) - .putExtra("index", 0) - .putExtra("stream", currentPlaylistInfo); - startActivity(mIntent); - } - - /*////////////////////////////////////////////////////////////////////////// - // Fragment's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - if (savedInstanceState != null) { - playlistUrl = savedInstanceState.getString(Constants.KEY_URL); - playlistTitle = savedInstanceState.getString(Constants.KEY_TITLE); - serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1); - - pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0); - Serializable serializable = savedInstanceState.getSerializable(PLAYLIST_INFO_KEY); - if (serializable instanceof PlayListInfo) currentPlaylistInfo = (PlayListInfo) serializable; - } - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); - return inflater.inflate(R.layout.fragment_channel, container, false); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - if (currentPlaylistInfo == null) loadPage(0); - else handlePlayListInfo(currentPlaylistInfo, false, false); - } - - @Override - public void onDestroyView() { - if (DEBUG) Log.d(TAG, "onDestroyView() called"); - headerAvatarView.setImageBitmap(null); - headerBannerView.setImageBitmap(null); - playlistStreams.removeAllViews(); - - playlistStreams = null; - headerRootLayout = null; - headerBannerView = null; - headerAvatarView = null; - headerTitleView = null; - - super.onDestroyView(); - } - - @Override - public void onResume() { - if (DEBUG) Log.d(TAG, "onResume() called"); - super.onResume(); - if (wasLoading.getAndSet(false)) { - loadPage(pageNumber); - } - } - - @Override - public void onStop() { - if (DEBUG) Log.d(TAG, "onStop() called"); - - if (loadingReactor != null) loadingReactor.dispose(); - loadingReactor = null; - - super.onStop(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]"); - super.onSaveInstanceState(outState); - outState.putString(Constants.KEY_URL, playlistUrl); - outState.putString(Constants.KEY_TITLE, playlistTitle); - outState.putInt(Constants.KEY_SERVICE_ID, serviceId); - - outState.putSerializable(INFO_LIST_KEY, infoListAdapter.getItemsList()); - outState.putSerializable(PLAYLIST_INFO_KEY, currentPlaylistInfo); - outState.putInt(PAGE_NUMBER_KEY, pageNumber); - } - - /*////////////////////////////////////////////////////////////////////////// - // Menu - //////////////////////////////////////////////////////////////////////////*/ - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_channel, menu); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(true); - supportActionBar.setDisplayHomeAsUpEnabled(true); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); - super.onOptionsItemSelected(item); - switch (item.getItemId()) { - case R.id.menu_item_openInBrowser: { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(playlistUrl)); - startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); - return true; - } - case R.id.menu_item_share: { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, playlistUrl); - intent.setType("text/plain"); - startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); - return true; - } - default: - return super.onOptionsItemSelected(item); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Init's - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - - playlistStreams = rootView.findViewById(R.id.channel_streams_view); - - playlistStreams.setLayoutManager(new LinearLayoutManager(activity)); - if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(activity); - if (savedInstanceState != null) { - //noinspection unchecked - ArrayList serializable = (ArrayList) savedInstanceState.getSerializable(INFO_LIST_KEY); - infoListAdapter.addInfoItemList(serializable); - } - } - - playlistStreams.setAdapter(infoListAdapter); - headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, playlistStreams, false); - infoListAdapter.setHeader(headerRootLayout); - infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, playlistStreams, false)); - - headerBannerView = headerRootLayout.findViewById(R.id.playlist_banner_image); - headerAvatarView = headerRootLayout.findViewById(R.id.playlist_avatar_view); - headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); - - headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_play_all_button); - headerPlayAllButton.setVisibility(View.VISIBLE); - } - - protected void initListeners() { - super.initListeners(); - - infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]"); - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); - } - }); - - playlistStreams.clearOnScrollListeners(); - playlistStreams.addOnScrollListener(new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(RecyclerView recyclerView) { - loadMore(true); - } - }); - - headerPlayAllButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - play(activity, MainVideoPlayer.class); - } - }); - } - - - @Override - protected void reloadContent() { - if (DEBUG) Log.d(TAG, "reloadContent() called"); - currentPlaylistInfo = null; - infoListAdapter.clearStreamItemList(); - loadPage(0); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playlist Loader - //////////////////////////////////////////////////////////////////////////*/ - - private StreamingService getService(final int serviceId) throws ExtractionException { - return NewPipe.getService(serviceId); - } - - private void loadAll() { - final Callable task = new Callable() { - @Override - public PlayListInfo call() throws Exception { - int pageCount = 0; - - final PlayListExtractor extractor = getService(serviceId) - .getPlayListExtractorInstance(playlistUrl, 0); - - final PlayListInfo info = PlayListInfo.getInfo(extractor); - - boolean hasNext = info.hasNextPage; - while(hasNext) { - pageCount++; - - final PlayListExtractor moreExtractor = getService(serviceId) - .getPlayListExtractorInstance(playlistUrl, pageCount); - - final PlayListInfo moreInfo = PlayListInfo.getInfo(moreExtractor); - - info.related_streams.addAll(moreInfo.related_streams); - hasNext = moreInfo.hasNextPage; - } - return info; - } - }; - - Observable.fromCallable(task) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - if (loadingReactor == null || loadingReactor.isDisposed()) { - loadingReactor = d; - isLoading.set(true); - } else { - d.dispose(); - } - } - - @Override - public void onNext(@NonNull PlayListInfo playListInfo) { - if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + playListInfo + "]"); - if (playListInfo == null || isRemoving() || !isVisible()) return; - - handlePlayListInfo(playListInfo, false, true); - isLoading.set(false); - pageNumber++; - } - - @Override - public void onError(@NonNull Throwable e) { - onRxError(e, "Observer failure"); - } - - @Override - public void onComplete() { - if (loadingReactor != null) { - loadingReactor.dispose(); - loadingReactor = null; - } - } - }); - } - - private void loadMore(final boolean onlyVideos) { - final Callable task = new Callable() { - @Override - public PlayListInfo call() throws Exception { - final PlayListExtractor extractor = getService(serviceId) - .getPlayListExtractorInstance(playlistUrl, pageNumber); - - return PlayListInfo.getInfo(extractor); - } - }; - - - Observable.fromCallable(task) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - if (loadingReactor == null || loadingReactor.isDisposed()) { - loadingReactor = d; - isLoading.set(true); - } else { - d.dispose(); - } - } - - @Override - public void onNext(@NonNull PlayListInfo playListInfo) { - if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + playListInfo + "]"); - if (playListInfo == null || isRemoving() || !isVisible()) return; - - handlePlayListInfo(playListInfo, onlyVideos, true); - isLoading.set(false); - pageNumber++; - } - - @Override - public void onError(@NonNull Throwable e) { - onRxError(e, "Observer failure"); - } - - @Override - public void onComplete() { - if (loadingReactor != null) { - loadingReactor.dispose(); - loadingReactor = null; - } - } - }); - } - - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void loadPage(int page) { - if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]"); - isLoading.set(true); - pageNumber = page; - infoListAdapter.showFooter(false); - - animateView(loadingProgressBar, true, 200); - animateView(errorPanel, false, 200); - - imageLoader.cancelDisplayTask(headerBannerView); - imageLoader.cancelDisplayTask(headerAvatarView); - - headerTitleView.setText(playlistTitle != null ? playlistTitle : ""); - headerBannerView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner)); - headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(playlistTitle != null ? playlistTitle : ""); - - loadMore(true); - } - - private void setPlaylist(int serviceId, String playlistUrl, String title) { - this.serviceId = serviceId; - this.playlistUrl = playlistUrl; - this.playlistTitle = title; - } - - private void handlePlayListInfo(PlayListInfo info, boolean onlyVideos, boolean addVideos) { - if (currentPlaylistInfo == null) { - currentPlaylistInfo = info; - } else if (currentPlaylistInfo != info) { - currentPlaylistInfo.related_streams.addAll(info.related_streams); - } - - animateView(errorPanel, false, 300); - animateView(playlistStreams, true, 200); - animateView(loadingProgressBar, false, 200); - - if (!onlyVideos) { - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().invalidateOptionsMenu(); - - headerRootLayout.setVisibility(View.VISIBLE); - //animateView(loadingProgressBar, false, 200, null); - - if (!TextUtils.isEmpty(info.playList_name)) { - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.playList_name); - headerTitleView.setText(info.playList_name); - playlistTitle = info.playList_name; - } else playlistTitle = ""; - - if (!TextUtils.isEmpty(info.banner_url)) { - imageLoader.displayImage(info.banner_url, headerBannerView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id)); - } - - if (!TextUtils.isEmpty(info.avatar_url)) { - headerAvatarView.setVisibility(View.VISIBLE); - imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id)); - } - - infoListAdapter.showFooter(true); - } - - hasNextPage = info.hasNextPage; - if (!hasNextPage) infoListAdapter.showFooter(false); - - if (addVideos) { - infoListAdapter.addInfoItemList(info.related_streams); - } - } - - @Override - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); - - animateView(playlistStreams, false, 200); - currentPlaylistInfo = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Error Handlers - //////////////////////////////////////////////////////////////////////////*/ - - private void onRxError(final Throwable exception, final String tag) { - if (exception instanceof IOException) { - onRecoverableError(R.string.network_error); - } else { - onUnrecoverableError(exception, tag); - } - } - - private void onRecoverableError(int messageId) { - if (!this.isAdded()) return; - - if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); - setErrorMessage(getString(messageId), true); - } - - private void onUnrecoverableError(Throwable exception, final String tag) { - if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); - ErrorActivity.reportError( - getContext(), - exception, - MainActivity.class, - null, - ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_PLAYLIST, "Feed", tag, R.string.general_error) - ); - - activity.finish(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java deleted file mode 100644 index df92449d2..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/FeedFragment.java +++ /dev/null @@ -1,496 +0,0 @@ -package org.schabi.newpipe.fragments.subscription; - -import android.os.Bundle; -import android.os.Parcelable; -import android.support.annotation.Nullable; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.jakewharton.rxbinding2.view.RxView; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.fragments.BaseFragment; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.NavigationHelper; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import io.reactivex.Flowable; -import io.reactivex.MaybeObserver; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; - -import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -public class FeedFragment extends BaseFragment { - private static final String VIEW_STATE_KEY = "view_state_key"; - private static final String INFO_ITEMS_KEY = "info_items_key"; - - private static final int FEED_LOAD_SIZE = 4; - private static final int LOAD_ITEM_DEBOUNCE_INTERVAL = 500; - - private final String TAG = "FeedFragment@" + Integer.toHexString(hashCode()); - - private View inflatedView; - private View emptyPanel; - private View loadItemFooter; - - private InfoListAdapter infoListAdapter; - private RecyclerView resultRecyclerView; - - private Parcelable viewState; - private AtomicBoolean retainFeedItems; - - private SubscriptionEngine subscriptionEngine; - - private Disposable loadItemObserver; - private Disposable subscriptionObserver; - private Subscription feedSubscriber; - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - /////////////////////////////////////////////////////////////////////////// - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - subscriptionEngine = SubscriptionEngine.getInstance(getContext()); - - retainFeedItems = new AtomicBoolean(false); - - if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(getActivity()); - } - - if (savedInstanceState != null) { - // Get recycler view state - viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY); - - // Deserialize and get recycler adapter list - final Object[] serializedInfoItems = (Object[]) savedInstanceState.getSerializable(INFO_ITEMS_KEY); - if (serializedInfoItems != null) { - final InfoItem[] infoItems = Arrays.copyOf( - serializedInfoItems, - serializedInfoItems.length, - InfoItem[].class - ); - final List feedInfos = Arrays.asList(infoItems); - infoListAdapter.addInfoItemList( feedInfos ); - } - - // Already displayed feed items survive configuration changes - retainFeedItems.set(true); - } - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - if (inflatedView == null) { - inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false); - } - return inflatedView; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - if (resultRecyclerView != null) { - outState.putParcelable( - VIEW_STATE_KEY, - resultRecyclerView.getLayoutManager().onSaveInstanceState() - ); - } - - if (infoListAdapter != null) { - outState.putSerializable(INFO_ITEMS_KEY, infoListAdapter.getItemsList().toArray()); - } - } - - @Override - public void onDestroyView() { - // Do not monitor for updates when user is not viewing the feed fragment. - // This is a waste of bandwidth. - if (loadItemObserver != null) loadItemObserver.dispose(); - if (subscriptionObserver != null) subscriptionObserver.dispose(); - if (feedSubscriber != null) feedSubscriber.cancel(); - - loadItemObserver = null; - subscriptionObserver = null; - feedSubscriber = null; - - loadItemFooter = null; - - // Retain the already displayed items for backstack pops - retainFeedItems.set(true); - - super.onDestroyView(); - } - - @Override - public void onDestroy() { - subscriptionEngine = null; - - super.onDestroy(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Views - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); - super.onCreateOptionsMenu(menu, inflater); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayShowTitleEnabled(true); - supportActionBar.setDisplayHomeAsUpEnabled(true); - } - } - - private RecyclerView.OnScrollListener getOnScrollListener() { - return new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - viewState = recyclerView.getLayoutManager().onSaveInstanceState(); - } - } - }; - } - - @Override - protected void initViews(View rootView, Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - - if (infoListAdapter == null) return; - - animateView(errorPanel, false, 200); - animateView(loadingProgressBar, true, 200); - - emptyPanel = rootView.findViewById(R.id.empty_panel); - - resultRecyclerView = rootView.findViewById(R.id.result_list_view); - resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); - - loadItemFooter = activity.getLayoutInflater().inflate(R.layout.load_item_footer, resultRecyclerView, false); - infoListAdapter.setFooter(loadItemFooter); - infoListAdapter.showFooter(false); - infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(int serviceId, String url, String title) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); - } - }); - - resultRecyclerView.setAdapter(infoListAdapter); - resultRecyclerView.addOnScrollListener(getOnScrollListener()); - - if (viewState != null) { - resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); - viewState = null; - } - - if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(R.string.fragment_whats_new); - - populateFeed(); - } - - private void resetFragment() { - if (subscriptionObserver != null) subscriptionObserver.dispose(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); - } - - @Override - protected void reloadContent() { - resetFragment(); - populateFeed(); - } - - @Override - protected void setErrorMessage(String message, boolean showRetryButton) { - super.setErrorMessage(message, showRetryButton); - - resetFragment(); - } - - /** - * Changes the state of the load item footer. - * - * If the current state of the feed is loaded, this displays the load item button and - * starts its reactor. - * - * Otherwise, show a spinner in place of the loader button. */ - private void setLoader(final boolean isLoaded) { - if (loadItemFooter == null) return; - - if (loadItemObserver != null) loadItemObserver.dispose(); - - if (isLoaded) { - loadItemObserver = getLoadItemObserver(loadItemFooter); - } - - loadItemFooter.findViewById(R.id.paginate_progress_bar).setVisibility(isLoaded ? View.GONE : View.VISIBLE); - loadItemFooter.findViewById(R.id.load_more_text).setVisibility(isLoaded ? View.VISIBLE : View.GONE); - } - - /////////////////////////////////////////////////////////////////////////// - // Feeds Loader - /////////////////////////////////////////////////////////////////////////// - - /** - * Responsible for reacting to subscription database updates and displaying feeds. - * - * Upon each update, the feed info list is cleared unless the fragment is - * recently recovered from a configuration change or backstack. - * - * All existing and pending feed requests are dropped. - * - * The newly received list of subscriptions is then transformed into a - * flowable, reacting to pulling requests. - * - * Pulled requests are transformed first into ChannelInfo, then Stream Info items and - * displayed on the feed fragment. - **/ - private void populateFeed() { - final Consumer> consumer = new Consumer>() { - @Override - public void accept(@NonNull List subscriptionEntities) throws Exception { - animateView(loadingProgressBar, false, 200); - - if (subscriptionEntities.isEmpty()) { - infoListAdapter.clearStreamItemList(); - emptyPanel.setVisibility(View.VISIBLE); - } else { - emptyPanel.setVisibility(View.INVISIBLE); - } - - // show progress bar on receiving a non-empty updated list of subscriptions - if (!retainFeedItems.get() && !subscriptionEntities.isEmpty()) { - infoListAdapter.clearStreamItemList(); - animateView(loadingProgressBar, true, 200); - } - - retainFeedItems.set(false); - Flowable.fromIterable(subscriptionEntities) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getSubscriptionObserver()); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(@NonNull Throwable exception) throws Exception { - onRxError(exception, "Subscription Database Reactor"); - } - }; - - if (subscriptionObserver != null) subscriptionObserver.dispose(); - subscriptionObserver = subscriptionEngine.getSubscription() - .onErrorReturnItem(Collections.emptyList()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(consumer, onError); - } - - /** - * 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; - - final int requestSize = FEED_LOAD_SIZE - infoListAdapter.getItemsList().size(); - if (requestSize > 0) { - requestFeed(requestSize); - } else { - setLoader(true); - } - - animateView(loadingProgressBar, false, 200); - // Footer spinner persists until subscription list is exhausted. - infoListAdapter.showFooter(true); - } - - @Override - public void onNext(SubscriptionEntity subscriptionEntity) { - setLoader(false); - - subscriptionEngine.getChannelInfo(subscriptionEntity) - .observeOn(AndroidSchedulers.mainThread()) - .onErrorComplete() - .subscribe(getChannelInfoObserver()); - } - - @Override - public void onError(Throwable exception) { - onRxError(exception, "Feed Pull Reactor"); - } - - @Override - public void onComplete() { - infoListAdapter.showFooter(false); - } - }; - } - - /** - * 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 dispose 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. - **/ - private MaybeObserver getChannelInfoObserver() { - return new MaybeObserver() { - Disposable observer; - @Override - public void onSubscribe(Disposable d) { - observer = d; - } - - // Called only when response is non-empty - @Override - public void onSuccess(ChannelInfo channelInfo) { - emptyPanel.setVisibility(View.INVISIBLE); - - if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) return; - - final InfoItem item = channelInfo.related_streams.get(0); - // Keep requesting new items if the current one already exists - if (!doesItemExist(infoListAdapter.getItemsList(), item)) { - infoListAdapter.addInfoItem(item); - } else { - requestFeed(1); - } - onDone(); - } - - @Override - public void onError(Throwable exception) { - onRxError(exception, "Feed Display Reactor"); - onDone(); - } - - // Called only when response is empty - @Override - public void onComplete() { - onDone(); - } - - private void onDone() { - setLoader(true); - - observer.dispose(); - observer = null; - } - }; - } - - private boolean doesItemExist(final List items, final InfoItem item) { - for (final InfoItem existingItem: items) { - if (existingItem.infoType() == item.infoType() && - existingItem.getTitle().equals(item.getTitle()) && - existingItem.getLink().equals(item.getLink())) return true; - } - return false; - } - - private void requestFeed(final int count) { - if (feedSubscriber == null) return; - - feedSubscriber.request(count); - } - - private Disposable getLoadItemObserver(@NonNull final View itemLoader) { - final Consumer onNext = new Consumer() { - @Override - public void accept(Object o) throws Exception { - requestFeed(FEED_LOAD_SIZE); - } - }; - - final Consumer onError = new Consumer() { - @Override - public void accept(Throwable throwable) throws Exception { - onRxError(throwable, "Load Button Reactor"); - } - }; - - return RxView.clicks(itemLoader) - .debounce(LOAD_ITEM_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) - .subscribe(onNext, onError); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - private void onRxError(final Throwable exception, final String tag) { - if (exception instanceof IOException) { - onRecoverableError(R.string.network_error); - } else { - onUnrecoverableError(exception, tag); - } - } - - private void onRecoverableError(int messageId) { - if (!this.isAdded()) return; - - if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); - setErrorMessage(getString(messageId), true); - } - - private void onUnrecoverableError(Throwable exception, final String tag) { - if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); - ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "Feed", tag, R.string.general_error)); - - activity.finish(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java deleted file mode 100644 index 25285db41..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionEngine.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.schabi.newpipe.fragments.subscription; - -import android.content.Context; - -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.subscription.SubscriptionDAO; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.channel.ChannelExtractor; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; - -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Completable; -import io.reactivex.CompletableSource; -import io.reactivex.Flowable; -import io.reactivex.Maybe; -import io.reactivex.Scheduler; -import io.reactivex.annotations.NonNull; -import io.reactivex.functions.Function; -import io.reactivex.schedulers.Schedulers; - -/** Subscription Service singleton: - * Provides a basis for channel Subscriptions. - * Provides access to subscription table in database as well as - * up-to-date observations on the subscribed channels - * */ -public class SubscriptionEngine { - - private static SubscriptionEngine sInstance; - private static final Object LOCK = new Object(); - - public static SubscriptionEngine getInstance(Context context) { - if (sInstance == null) { - synchronized (LOCK) { - if (sInstance == null) { - sInstance = new SubscriptionEngine(context); - } - } - } - return sInstance; - } - - protected final String TAG = "SubscriptionEngine@" + Integer.toHexString(hashCode()); - private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500; - private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4; - - private AppDatabase db; - private Flowable> subscription; - - private Scheduler subscriptionScheduler; - - private SubscriptionEngine(Context context) { - db = NewPipeDatabase.getInstance( context ); - subscription = getSubscriptionInfos(); - - final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE); - subscriptionScheduler = Schedulers.from(subscriptionExecutor); - } - - /** Part of subscription observation pipeline - * @see SubscriptionEngine#getSubscription() - */ - private Flowable> getSubscriptionInfos() { - return subscriptionTable().findAll() - // Wait for a period of infrequent updates and return the latest update - .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) - .share() // Share allows multiple subscribers on the same observable - .replay(1) // Replay synchronizes subscribers to the last emitted result - .autoConnect(); - } - - /** - * Provides an observer to the latest update to the subscription table. - * - * This observer may be subscribed multiple times, where each subscriber obtains - * the latest synchronized changes available, effectively share the same data - * across all subscribers. - * - * This observer has a debounce cooldown, meaning if multiple updates are observed - * in the cooldown interval, only the latest changes are emitted to the subscribers. - * This reduces the amount of observations caused by frequent updates to the database. - * */ - @android.support.annotation.NonNull - public Flowable> getSubscription() { - return subscription; - } - - public Maybe getChannelInfo(final SubscriptionEntity subscriptionEntity) { - final StreamingService service = getService(subscriptionEntity.getServiceId()); - if (service == null) return Maybe.empty(); - - final String url = subscriptionEntity.getUrl(); - final Callable callable = new Callable() { - @Override - public ChannelInfo call() throws Exception { - final ChannelExtractor extractor = service.getChannelExtractorInstance(url, 0); - return ChannelInfo.getInfo(extractor); - } - }; - - return Maybe.fromCallable(callable).subscribeOn(subscriptionScheduler); - } - - private StreamingService getService(final int serviceId) { - try { - return NewPipe.getService(serviceId); - } catch (ExtractionException e) { - return null; - } - } - - /** Returns the database access interface for subscription table. */ - public SubscriptionDAO subscriptionTable() { - return db.subscriptionDAO(); - } - - public Completable updateChannelInfo(final int serviceId, - final String channelUrl, - final ChannelInfo info) { - final Function, CompletableSource> update = new Function, CompletableSource>() { - @Override - public CompletableSource apply(@NonNull List subscriptionEntities) throws Exception { - if (subscriptionEntities.size() == 1) { - SubscriptionEntity subscription = subscriptionEntities.get(0); - - // Subscriber count changes very often, making this check almost unnecessary. - // Consider removing it later. - if (isSubscriptionUpToDate(channelUrl, info, subscription)) { - subscription.setData(info.channel_name, info.avatar_url, "", info.subscriberCount); - - return update(subscription); - } - } - - return Completable.complete(); - } - }; - - return subscriptionTable().findAll(serviceId, channelUrl) - .firstOrError() - .flatMapCompletable(update); - } - - private Completable update(final SubscriptionEntity updatedSubscription) { - return Completable.fromRunnable(new Runnable() { - @Override - public void run() { - subscriptionTable().update(updatedSubscription); - } - }); - } - - private boolean isSubscriptionUpToDate(final String channelUrl, - final ChannelInfo info, - final SubscriptionEntity entity) { - return channelUrl.equals( entity.getUrl() ) && - info.service_id == entity.getServiceId() && - info.channel_name.equals( entity.getTitle() ) && - info.avatar_url.equals( entity.getThumbnailUrl() ) && - info.subscriberCount == entity.getSubscriberCount(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java deleted file mode 100644 index ffd3b09e8..000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/PlaylistInfoItemHolder.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.schabi.newpipe.info_list; - -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; - -/** - * Created by Christian Schabesberger on 12.02.17. - * - * Copyright (C) Christian Schabesberger 2016 - * ChannelInfoItemHolder .java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class PlaylistInfoItemHolder extends InfoItemHolder { - public final ImageView itemThumbnailView; - public final TextView itemPlaylistTitleView; - public final TextView itemAdditionalDetailView; - - public final View itemRoot; - - PlaylistInfoItemHolder(View v) { - super(v); - itemRoot = v.findViewById(R.id.itemRoot); - itemThumbnailView = v.findViewById(R.id.itemThumbnailView); - itemPlaylistTitleView = v.findViewById(R.id.itemPlaylistTitleView); - itemAdditionalDetailView = v.findViewById(R.id.itemAdditionalDetails); - } - - @Override - public InfoItem.InfoType infoType() { - return InfoItem.InfoType.PLAYLIST; - } -} 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 49f29afb0..c845bd825 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -70,7 +70,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.Downloader; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.playlist.PlayQueue; import java.io.File; @@ -594,10 +594,9 @@ public abstract class BasePlayer implements Player.EventListener, public void sync(final int windowIndex, final long windowPos, final StreamInfo info) { Log.d(TAG, "Syncing..."); - videoUrl = info.webpage_url; + videoUrl = info.url; videoThumbnailUrl = info.thumbnail_url; - videoTitle = info.title; - channelName = info.uploader; + videoTitle = info.name; if (simpleExoPlayer.getCurrentWindowIndex() != windowIndex) { Log.w(TAG, "Rewinding to correct window"); @@ -615,7 +614,6 @@ public abstract class BasePlayer implements Player.EventListener, simpleExoPlayer.prepare(playbackManager.getMediaSource()); simpleExoPlayer.seekToDefaultPosition(); simpleExoPlayer.setPlayWhenReady(true); - changeState(STATE_PLAYING); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index ed49e66a2..663f532b2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -40,7 +40,7 @@ import android.widget.TextView; import android.widget.Toast; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -232,7 +232,7 @@ public class MainVideoPlayer extends Activity { public void sync(final int windowIndex, final long windowPos, final StreamInfo info) { super.sync(windowIndex, windowPos, info); titleTextView.setText(getVideoTitle()); - channelTextView.setText(getChannelName()); + channelTextView.setText(getUploaderName()); playPauseButton.setImageResource(R.drawable.ic_pause_white); } diff --git a/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java index d30484802..37fab1ef0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/MediaSourceManager.java @@ -7,7 +7,7 @@ import com.google.android.exoplayer2.source.MediaSource; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.events.PlayQueueMessage; @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; import io.reactivex.MaybeObserver; +import io.reactivex.SingleObserver; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; import io.reactivex.disposables.CompositeDisposable; @@ -31,14 +32,14 @@ class MediaSourceManager { // Effectively loads WINDOW_SIZE * 2 streams private static final int WINDOW_SIZE = 3; - private final DynamicConcatenatingMediaSource sources; + private final PlaybackListener playbackListener; + private final PlayQueue playQueue; + + private DynamicConcatenatingMediaSource sources; // sourceToQueueIndex maps media source index to play queue index // Invariant 1: this list is sorted in ascending order // Invariant 2: this list contains no duplicates - private final List sourceToQueueIndex; - - private final PlaybackListener playbackListener; - private final PlayQueue playQueue; + private List sourceToQueueIndex; private Subscription playQueueReactor; private Subscription loadingReactor; @@ -83,13 +84,13 @@ class MediaSourceManager { MediaSourceManager(@NonNull final MediaSourceManager.PlaybackListener listener, @NonNull final PlayQueue playQueue) { - this.sources = new DynamicConcatenatingMediaSource(); - this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList()); - this.playbackListener = listener; this.playQueue = playQueue; - disposables = new CompositeDisposable(); + this.disposables = new CompositeDisposable(); + + this.sources = new DynamicConcatenatingMediaSource(); + this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList()); playQueue.getBroadcastReceiver() .observeOn(AndroidSchedulers.mainThread()) @@ -113,34 +114,25 @@ class MediaSourceManager { } /* - * Called when the player has seamlessly transitioned to another stream. - * Currently only expecting transitioning to the next stream and updates - * the play queue that a transition has occurred. + * Called when the player has transitioned to another stream. * */ void refresh(final int newSourceIndex) { - if (newSourceIndex == getCurrentSourceIndex()) return; - - if (newSourceIndex == getCurrentSourceIndex() + 1) { - playQueue.incrementIndex(); - } else { - //something went wrong - Log.e(TAG, "Refresh media failed, reloading."); + if (sourceToQueueIndex.indexOf(newSourceIndex) != -1) { + playQueue.setIndex(sourceToQueueIndex.indexOf(newSourceIndex)); } - - sync(); } void report(final Exception error) { // ignore error checking for now, just remove the current index - if (error != null && !isBlocked) { - doBlock(); + if (error != null) { + tryBlock(); } final int index = playQueue.getIndex(); - remove(index); playQueue.remove(index); - tryUnblock(); - sync(); + + resetSources(); + init(); } void dispose() { @@ -192,8 +184,8 @@ class MediaSourceManager { break; } - if (!isPlayQueueReady() && !isBlocked) { - doBlock(); + if (!isPlayQueueReady()) { + tryBlock(); playQueue.fetch(); } if (playQueueReactor != null) playQueueReactor.request(1); @@ -221,9 +213,11 @@ class MediaSourceManager { return getCurrentSourceIndex() != -1; } - private void doBlock() { - playbackListener.block(); - isBlocked = true; + private void tryBlock() { + if (!isBlocked) { + playbackListener.block(); + isBlocked = true; + } } private void tryUnblock() { @@ -241,8 +235,8 @@ class MediaSourceManager { private void onSelect() { if (isCurrentIndexLoaded()) { sync(); - } else if (!isBlocked) { - doBlock(); + } else { + tryBlock(); } load(); @@ -274,7 +268,7 @@ class MediaSourceManager { private void init() { final PlayQueueItem init = playQueue.getCurrent(); - init.getStream().subscribe(new MaybeObserver() { + init.getStream().subscribe(new SingleObserver() { @Override public void onSubscribe(@NonNull Disposable d) { if (disposables != null) { @@ -303,17 +297,11 @@ class MediaSourceManager { playQueue.remove(playQueue.indexOf(init)); init(); } - - @Override - public void onComplete() { - playQueue.remove(playQueue.indexOf(init)); - init(); - } }); } private void load(final PlayQueueItem item) { - item.getStream().subscribe(new MaybeObserver() { + item.getStream().subscribe(new SingleObserver() { @Override public void onSubscribe(@NonNull Disposable d) { if (disposables != null) { @@ -335,15 +323,17 @@ class MediaSourceManager { playQueue.remove(playQueue.indexOf(item)); load(); } - - @Override - public void onComplete() { - playQueue.remove(playQueue.indexOf(item)); - load(); - } }); } + private void resetSources() { + if (this.disposables != null) this.disposables.clear(); + if (this.sources != null) this.sources.releaseSource(); + if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear(); + + this.sources = new DynamicConcatenatingMediaSource(); + } + /*////////////////////////////////////////////////////////////////////////// // Media Source List Manipulation //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java b/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java deleted file mode 100644 index 76938bcb4..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PlaybackManager.java +++ /dev/null @@ -1,212 +0,0 @@ -package org.schabi.newpipe.player; - -import android.util.Log; - -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.playlist.PlayQueue; -import org.schabi.newpipe.playlist.PlayQueueItem; -import org.schabi.newpipe.playlist.events.PlayQueueMessage; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import io.reactivex.Maybe; -import io.reactivex.annotations.NonNull; - -public class PlaybackManager { - private final String TAG = "PlaybackManager@" + Integer.toHexString(hashCode()); - - private static final int WINDOW_SIZE = 3; - - private DynamicConcatenatingMediaSource mediaSource; - private List syncInfos; - - private int sourceIndex; - - private PlaybackListener listener; - private PlayQueue playQueue; - - private Subscription playQueueReactor; - - public boolean prepared = false; - - interface PlaybackListener { - void block(); - void unblock(); - - void resync(); - void sync(final StreamInfo info); - MediaSource sourceOf(final StreamInfo info); - } - - public PlaybackManager(@NonNull final PlaybackListener listener, - @NonNull final PlayQueue playQueue) { - this.mediaSource = new DynamicConcatenatingMediaSource(); - this.syncInfos = Collections.synchronizedList(new ArrayList()); - this.sourceIndex = 0; - - this.listener = listener; - this.playQueue = playQueue; - - playQueue.getBroadcastReceiver().subscribe(getReactor()); - } - - @NonNull - public DynamicConcatenatingMediaSource getMediaSource() { - return mediaSource; - } - - private void reload() { - listener.block(); - mediaSource = new DynamicConcatenatingMediaSource(); - syncInfos.clear(); - load(); - } - - public void changeSource(final MediaSource newSource) { - this.mediaSource.removeMediaSource(0); - this.mediaSource.addMediaSource(0, newSource); - } - - public void refreshMedia(final int newMediaIndex) { - if (newMediaIndex == sourceIndex) return; - - if (newMediaIndex == sourceIndex + 1) { - playQueue.incrementIndex(); - mediaSource.removeMediaSource(0); - syncInfos.remove(0); - } else { - //something went wrong - Log.e(TAG, "Refresh media failed, reloading."); - reload(); - } - } - - private Subscription loaderReactor; - - private void load() { - if (mediaSource.getSize() < WINDOW_SIZE) load(mediaSource.getSize()); - } - - private void load(final int from) { - // Fetch queue items - //todo fix out of bound - final int index = playQueue.getIndex(); - - List> maybes = new ArrayList<>(); - for (int i = from; i < WINDOW_SIZE; i++) { - final PlayQueueItem item = playQueue.get(index + i); - - maybes.add(item.getStream()); - } - - // Stop loading and clear pending media sources - if (loaderReactor != null) loaderReactor.cancel(); - clear(from); - - // Start sequential loading of media sources - Maybe.concat(maybes).subscribe(getSubscriber()); - } - - private Subscriber getSubscriber() { - return new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - if (loaderReactor != null) loaderReactor.cancel(); - loaderReactor = s; - s.request(1); - } - - @Override - public void onNext(StreamInfo streamInfo) { - mediaSource.addMediaSource(listener.sourceOf(streamInfo)); - syncInfos.add(streamInfo); - tryUnblock(); - loaderReactor.request(1); - } - - @Override - public void onError(Throwable t) { - playQueue.remove(playQueue.getIndex()); - } - - @Override - public void onComplete() { - if (loaderReactor != null) loaderReactor.cancel(); - loaderReactor = null; - } - }; - } - - private void tryUnblock() { - if (mediaSource.getSize() > 0) listener.unblock(); - } - - private void clear(int from) { - while (mediaSource.getSize() > from) { - mediaSource.removeMediaSource(from); - syncInfos.remove(from); - } - } - - private Subscriber getReactor() { - return new Subscriber() { - @Override - public void onSubscribe(@NonNull Subscription d) { - if (playQueueReactor != null) playQueueReactor.cancel(); - playQueueReactor = d; - playQueueReactor.request(1); - } - - @Override - public void onNext(@NonNull PlayQueueMessage event) { - if (playQueue.getStreams().size() - playQueue.getIndex() < WINDOW_SIZE && !playQueue.isComplete()) { - listener.block(); - playQueue.fetch(); - } - - switch (event.type()) { - case SELECT: - case INIT: - reload(); - break; - case APPEND: - load(); - break; - case REMOVE: - case SWAP: - load(1); - break; - case NEXT: - default: - break; - } - - tryUnblock(); - if (!syncInfos.isEmpty()) listener.sync(syncInfos.get(0)); - if (playQueueReactor != null) playQueueReactor.request(1); - } - - @Override - public void onError(@NonNull Throwable e) { - - } - - @Override - public void onComplete() { - dispose(); - } - }; - } - - public void dispose() { - if (playQueueReactor != null) playQueueReactor.cancel(); - playQueueReactor = null; - } -} 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 ac22e742f..028f85a43 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -53,14 +53,15 @@ import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.playlist.PlayListInfo; import org.schabi.newpipe.playlist.ExternalPlayQueue; import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.Utils; +import org.schabi.newpipe.util.ListHelper; import java.io.Serializable; import java.util.ArrayList; @@ -201,42 +202,50 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. simpleExoPlayer.setVideoListener(this); } -// @SuppressWarnings("unchecked") -// public void handleIntent2(Intent intent) { -// super.handleIntent(intent); -// if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); -// if (intent == null) return; -// -// selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1); -// -// Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST); -// -// if (serializable instanceof ArrayList) videoStreamsList = (ArrayList) serializable; -// if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List) serializable); -// -// Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM); -// if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream; -// -// startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true); -// play(true); -// } + @SuppressWarnings("unchecked") + public void handleSingleStreamIntent(Intent intent) { + super.handleIntent(intent); + if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + if (intent == null) return; + + selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1); + + Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST); + + if (serializable instanceof ArrayList) videoStreamsList = (ArrayList) serializable; + if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List) serializable); + + Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM); + if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream; + + startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true); + play(true); + } @SuppressWarnings("unchecked") public void handleIntent(Intent intent) { if (intent == null) return; + handleExternalPlaylistIntent(intent); + } + + @SuppressWarnings("unchecked") + public void handleExternalPlaylistIntent(Intent intent) { selectedIndexStream = 0; - String url = intent.getStringExtra("url"); - int nextPage = intent.getIntExtra("nextPage", 0); - int index = intent.getIntExtra("index", 0); + final int serviceId = intent.getIntExtra("serviceId", -1); + final int index = intent.getIntExtra("index", 0); + final Serializable serializable = intent.getSerializableExtra("streams"); + final String nextPageUrl = intent.getStringExtra("nextPageUrl"); - PlayListInfo info; - Serializable serializable = intent.getSerializableExtra("stream"); - if (serializable instanceof PlayListInfo) info = (PlayListInfo) serializable; - else return; + List info = new ArrayList<>(); + if (serializable instanceof List) { + for (final Object o : (List) serializable) { + if (o instanceof InfoItem) info.add((StreamInfoItem) o); + } + } - playQueue = new ExternalPlayQueue(url, info, nextPage, index); + playQueue = new ExternalPlayQueue(serviceId, nextPageUrl, info, index); playbackManager = new MediaSourceManager(this, playQueue); } @@ -263,13 +272,13 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer. @Override public MediaSource sourceOf(final StreamInfo info) { - final List videos = Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); - final VideoStream video = videos.get(Utils.getDefaultResolution(context, videos)); + final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); + final VideoStream video = videos.get(ListHelper.getDefaultResolutionIndex(context, videos)); final MediaSource mediaSource = super.buildMediaSource(video.url, MediaFormat.getSuffixById(video.format)); if (!video.isVideoOnly) return mediaSource; - final AudioStream audio = Utils.getHighestQualityAudio(info.audio_streams); + final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams); final Uri audioUri = Uri.parse(audio.url); return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null)); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java index ab63ae98e..63bfe7ab1 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java @@ -1,22 +1,20 @@ package org.schabi.newpipe.playlist; +import android.util.Log; + import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.playlist.PlayListExtractor; -import org.schabi.newpipe.extractor.playlist.PlayListInfo; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.util.ExtractorHelper; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicInteger; -import io.reactivex.Maybe; +import io.reactivex.SingleObserver; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; public class ExternalPlayQueue extends PlayQueue { @@ -24,24 +22,21 @@ public class ExternalPlayQueue extends PlayQueue { private boolean isComplete; - private StreamingService service; + private int serviceId; private String playlistUrl; - private AtomicInteger pageNumber; private Disposable fetchReactor; - public ExternalPlayQueue(final String playlistUrl, - final PlayListInfo info, - final int currentPage, + public ExternalPlayQueue(final int serviceId, + final String nextPageUrl, + final List streams, final int index) { - super(index, extractPlaylistItems(info)); + super(index, extractPlaylistItems(streams)); - this.service = getService(info.service_id); + this.playlistUrl = nextPageUrl; + this.serviceId = serviceId; - this.isComplete = !info.hasNextPage; - this.pageNumber = new AtomicInteger(currentPage + 1); - - this.playlistUrl = playlistUrl; + this.isComplete = nextPageUrl == null || nextPageUrl.isEmpty(); } @Override @@ -57,31 +52,39 @@ public class ExternalPlayQueue extends PlayQueue { @Override public void fetch() { - if (isComplete) return; - if (fetchReactor != null && !fetchReactor.isDisposed()) return; - - final Callable task = new Callable() { - @Override - public PlayListInfo call() throws Exception { - PlayListExtractor extractor = service.getPlayListExtractorInstance(playlistUrl, pageNumber.get()); - return PlayListInfo.getInfo(extractor); - } - }; - - final Consumer onSuccess = new Consumer() { - @Override - public void accept(PlayListInfo playListInfo) throws Exception { - if (!playListInfo.hasNextPage) isComplete = true; - - append(extractPlaylistItems(playListInfo)); - pageNumber.incrementAndGet(); - } - }; - - fetchReactor = Maybe.fromCallable(task) + ExtractorHelper.getPlaylistInfo(this.serviceId, this.playlistUrl, false) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess); + .retry(2) + .subscribe(getPlaylistObserver()); + } + + private SingleObserver getPlaylistObserver() { + return new SingleObserver() { + @Override + public void onSubscribe(@NonNull Disposable d) { + if (isComplete || (fetchReactor != null && !fetchReactor.isDisposed())) { + d.dispose(); + } else { + fetchReactor = d; + } + } + + @Override + public void onSuccess(@NonNull PlaylistInfo playlistInfo) { + if (!playlistInfo.has_more_streams) isComplete = true; + playlistUrl = playlistInfo.next_streams_url; + + append(extractPlaylistItems(playlistInfo.related_streams)); + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e(TAG, "Error fetching more playlist, marking playlist as complete.", e); + isComplete = true; + append(Collections.emptyList()); + } + }; } @Override @@ -90,9 +93,9 @@ public class ExternalPlayQueue extends PlayQueue { if (fetchReactor != null) fetchReactor.dispose(); } - private static List extractPlaylistItems(final PlayListInfo info) { + private static List extractPlaylistItems(final List infos) { List result = new ArrayList<>(); - for (final InfoItem stream : info.related_streams) { + for (final InfoItem stream : infos) { if (stream instanceof StreamInfoItem) { result.add(new PlayQueueItem((StreamInfoItem) stream)); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index 5c8b75ef3..a1c759150 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -108,11 +108,6 @@ public abstract class PlayQueue { broadcast(new SelectEvent(index)); } - public void incrementIndex() { - final int index = queueIndex.incrementAndGet(); - broadcast(new NextEvent(index)); - } - protected void append(final PlayQueueItem item) { streams.add(item); broadcast(new AppendEvent(1)); @@ -127,6 +122,8 @@ public abstract class PlayQueue { if (index >= streams.size()) return; streams.remove(index); + queueIndex.set(Math.max(0, queueIndex.get() - 1)); + broadcast(new RemoveEvent(index)); } @@ -151,14 +148,6 @@ public abstract class PlayQueue { } } - protected StreamingService getService(final int serviceId) { - try { - return NewPipe.getService(serviceId); - } catch (ExtractionException e) { - return null; - } - } - private Subscriber getSelfReporter() { return new Subscriber() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index 7352dcb06..f81cc7fc3 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -7,7 +7,6 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.R; -import org.schabi.newpipe.info_list.StreamInfoItemHolder; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import java.util.List; @@ -163,7 +162,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter stream; + private Single stream; PlayQueueItem(final StreamInfoItem streamInfoItem) { - this.title = streamInfoItem.getTitle(); - this.url = streamInfoItem.getLink(); + this.title = streamInfoItem.name; + this.url = streamInfoItem.url; this.serviceId = streamInfoItem.service_id; this.duration = streamInfoItem.duration; - this.isDone = false; this.stream = getInfo(); } @@ -53,37 +45,22 @@ public class PlayQueueItem { return serviceId; } - public int getDuration() { + public long getDuration() { return duration; } - public boolean isDone() { - return isDone; - } - @Nullable public Throwable getError() { return error; } @NonNull - public Maybe getStream() { + public Single getStream() { return stream; } @NonNull - private Maybe getInfo() { - final StreamingService service = getService(serviceId); - if (service == null) return Maybe.empty(); - - final Callable task = new Callable() { - @Override - public StreamInfo call() throws Exception { - final StreamExtractor extractor = service.getExtractorInstance(url); - return StreamInfo.getVideoInfo(extractor); - } - }; - + private Single getInfo() { final Consumer onError = new Consumer() { @Override public void accept(Throwable throwable) throws Exception { @@ -91,27 +68,10 @@ public class PlayQueueItem { } }; - final Action onComplete = new Action() { - @Override - public void run() throws Exception { - isDone = true; - } - }; - - return Maybe.fromCallable(task) + return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnError(onError) - .doOnComplete(onComplete) .retry(3) - .cache(); - } - - private StreamingService getService(final int serviceId) { - try { - return NewPipe.getService(serviceId); - } catch (ExtractionException e) { - return null; - } + .doOnError(onError); } } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java index 2d7a084fb..86ff67898 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java @@ -57,17 +57,17 @@ public class PlayQueueItemBuilder { } - public static String getDurationString(int duration) { + public static String getDurationString(long duration) { if(duration < 0) { duration = 0; } String output; - int days = duration / (24 * 60 * 60); /* greater than a day */ + long days = duration / (24 * 60 * 60); /* greater than a day */ duration %= (24 * 60 * 60); - int hours = duration / (60 * 60); /* greater than an hour */ + long hours = duration / (60 * 60); /* greater than an hour */ duration %= (60 * 60); - int minutes = duration / 60; - int seconds = duration % 60; + long minutes = duration / 60; + long seconds = duration % 60; //handle days if (days > 0) { diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java index c25210ee2..d6bb9665a 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemHolder.java @@ -7,7 +7,7 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.InfoItemHolder; +import org.schabi.newpipe.info_list.holder.InfoItemHolder; /** * Created by Christian Schabesberger on 01.08.16. diff --git a/app/src/main/res/layout/playlist_header.xml b/app/src/main/res/layout/playlist_header.xml index 7de6f79a9..5c054fcca 100644 --- a/app/src/main/res/layout/playlist_header.xml +++ b/app/src/main/res/layout/playlist_header.xml @@ -9,6 +9,19 @@ android:background="?attr/contrast_background_color" android:paddingBottom="6dp"> +