-Modified play queues and items to use extraction helper.
-Fixed play queue item removal. -Rebase changes.
This commit is contained in:
parent
1ceda017c7
commit
73f46d3762
|
@ -16,7 +16,7 @@ import static org.schabi.newpipe.database.stream.StreamEntity.STREAM_TABLE;
|
||||||
public interface StreamDAO extends BasicDAO<StreamEntity> {
|
public interface StreamDAO extends BasicDAO<StreamEntity> {
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE)
|
@Query("SELECT * FROM " + STREAM_TABLE)
|
||||||
Flowable<List<StreamEntity>> findAll();
|
Flowable<List<StreamEntity>> getAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
|
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
|
||||||
|
|
|
@ -6,8 +6,8 @@ import android.arch.persistence.room.Ignore;
|
||||||
import android.arch.persistence.room.Index;
|
import android.arch.persistence.room.Index;
|
||||||
import android.arch.persistence.room.PrimaryKey;
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.AbstractStreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
@ -63,21 +63,20 @@ public class StreamEntity {
|
||||||
private long uploadDate;
|
private long uploadDate;
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_DURATION)
|
@ColumnInfo(name = STREAM_DURATION)
|
||||||
private int duration;
|
private long duration;
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public StreamInfoItem toStreamInfoItem() {
|
public StreamInfoItem toStreamInfoItem() {
|
||||||
StreamInfoItem item = new StreamInfoItem();
|
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.service_id = this.getServiceId();
|
||||||
item.id = this.getId();
|
item.url = this.getUrl();
|
||||||
item.webpage_url = this.getUrl();
|
item.name = this.getTitle();
|
||||||
item.title = this.getTitle();
|
|
||||||
item.thumbnail_url = this.getThumbnailUrl();
|
item.thumbnail_url = this.getThumbnailUrl();
|
||||||
item.view_count = this.getViewCount();
|
item.view_count = this.getViewCount();
|
||||||
item.uploader = this.getUploader();
|
item.uploader_name = this.getUploader();
|
||||||
|
|
||||||
// TODO: temporary until upload date parsing is fleshed out
|
// TODO: temporary until upload date parsing is fleshed out
|
||||||
item.upload_date = "Unknown";
|
item.upload_date = "Unknown";
|
||||||
|
@ -97,12 +96,11 @@ public class StreamEntity {
|
||||||
this.type = item.stream_type.name();
|
this.type = item.stream_type.name();
|
||||||
|
|
||||||
this.serviceId = item.service_id;
|
this.serviceId = item.service_id;
|
||||||
this.id = item.id;
|
this.url = item.url;
|
||||||
this.url = item.webpage_url;
|
this.title = item.name;
|
||||||
this.title = item.title;
|
|
||||||
this.thumbnailUrl = item.thumbnail_url;
|
this.thumbnailUrl = item.thumbnail_url;
|
||||||
this.viewCount = item.view_count;
|
this.viewCount = item.view_count;
|
||||||
this.uploader = item.uploader;
|
this.uploader = item.uploader_name;
|
||||||
|
|
||||||
// TODO: temporary until upload date parsing is fleshed out
|
// TODO: temporary until upload date parsing is fleshed out
|
||||||
this.uploadDate = new Date().getTime();
|
this.uploadDate = new Date().getTime();
|
||||||
|
@ -113,8 +111,7 @@ public class StreamEntity {
|
||||||
public boolean is(final StreamInfoItem item) {
|
public boolean is(final StreamInfoItem item) {
|
||||||
return this.type.equals( item.stream_type.name() ) &&
|
return this.type.equals( item.stream_type.name() ) &&
|
||||||
this.serviceId == item.service_id &&
|
this.serviceId == item.service_id &&
|
||||||
this.id.equals( item.id ) &&
|
this.url.equals( item.url );
|
||||||
this.url.equals( item.webpage_url );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
|
@ -197,7 +194,7 @@ public class StreamEntity {
|
||||||
this.uploadDate = uploadDate;
|
this.uploadDate = uploadDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDuration() {
|
public long getDuration() {
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.fragments.list.playlist;
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
@ -10,6 +11,7 @@ import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
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.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
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.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.player.MainVideoPlayer;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
@ -40,6 +46,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
private ImageView headerUploaderAvatar;
|
private ImageView headerUploaderAvatar;
|
||||||
private TextView headerStreamCount;
|
private TextView headerStreamCount;
|
||||||
|
|
||||||
|
private Button headerPlayAllButton;
|
||||||
|
|
||||||
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
||||||
PlaylistFragment instance = new PlaylistFragment();
|
PlaylistFragment instance = new PlaylistFragment();
|
||||||
instance.setInitialData(serviceId, url, name);
|
instance.setInitialData(serviceId, url, name);
|
||||||
|
@ -66,6 +74,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
||||||
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
|
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
|
||||||
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||||
|
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_play_all_button);
|
||||||
|
|
||||||
return headerRootLayout;
|
return headerRootLayout;
|
||||||
}
|
}
|
||||||
|
@ -137,6 +146,22 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
if (!result.errors.isEmpty()) {
|
if (!result.errors.isEmpty()) {
|
||||||
showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.service_id), result.url, 0);
|
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
|
@Override
|
||||||
|
|
|
@ -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<InfoItem> serializable = (ArrayList<InfoItem>) 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<PlayListInfo> task = new Callable<PlayListInfo>() {
|
|
||||||
@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<PlayListInfo>() {
|
|
||||||
@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<PlayListInfo> task = new Callable<PlayListInfo>() {
|
|
||||||
@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<PlayListInfo>() {
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<InfoItem> 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<List<SubscriptionEntity>> consumer = new Consumer<List<SubscriptionEntity>>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull List<SubscriptionEntity> 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<Throwable> onError = new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull Throwable exception) throws Exception {
|
|
||||||
onRxError(exception, "Subscription Database Reactor");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
|
||||||
subscriptionObserver = subscriptionEngine.getSubscription()
|
|
||||||
.onErrorReturnItem(Collections.<SubscriptionEntity>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<SubscriptionEntity> getSubscriptionObserver() {
|
|
||||||
return new Subscriber<SubscriptionEntity>() {
|
|
||||||
@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<ChannelInfo> getChannelInfoObserver() {
|
|
||||||
return new MaybeObserver<ChannelInfo>() {
|
|
||||||
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<InfoItem> 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<Object> onNext = new Consumer<Object>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Object o) throws Exception {
|
|
||||||
requestFeed(FEED_LOAD_SIZE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<List<SubscriptionEntity>> 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<List<SubscriptionEntity>> 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<List<SubscriptionEntity>> getSubscription() {
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Maybe<ChannelInfo> getChannelInfo(final SubscriptionEntity subscriptionEntity) {
|
|
||||||
final StreamingService service = getService(subscriptionEntity.getServiceId());
|
|
||||||
if (service == null) return Maybe.empty();
|
|
||||||
|
|
||||||
final String url = subscriptionEntity.getUrl();
|
|
||||||
final Callable<ChannelInfo> callable = new Callable<ChannelInfo>() {
|
|
||||||
@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<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() {
|
|
||||||
@Override
|
|
||||||
public CompletableSource apply(@NonNull List<SubscriptionEntity> 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <chris.schabesberger@mailbox.org>
|
|
||||||
* 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -70,7 +70,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene
|
||||||
|
|
||||||
import org.schabi.newpipe.Downloader;
|
import org.schabi.newpipe.Downloader;
|
||||||
import org.schabi.newpipe.R;
|
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 org.schabi.newpipe.playlist.PlayQueue;
|
||||||
|
|
||||||
import java.io.File;
|
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) {
|
public void sync(final int windowIndex, final long windowPos, final StreamInfo info) {
|
||||||
Log.d(TAG, "Syncing...");
|
Log.d(TAG, "Syncing...");
|
||||||
|
|
||||||
videoUrl = info.webpage_url;
|
videoUrl = info.url;
|
||||||
videoThumbnailUrl = info.thumbnail_url;
|
videoThumbnailUrl = info.thumbnail_url;
|
||||||
videoTitle = info.title;
|
videoTitle = info.name;
|
||||||
channelName = info.uploader;
|
|
||||||
|
|
||||||
if (simpleExoPlayer.getCurrentWindowIndex() != windowIndex) {
|
if (simpleExoPlayer.getCurrentWindowIndex() != windowIndex) {
|
||||||
Log.w(TAG, "Rewinding to correct window");
|
Log.w(TAG, "Rewinding to correct window");
|
||||||
|
@ -615,7 +614,6 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
simpleExoPlayer.prepare(playbackManager.getMediaSource());
|
simpleExoPlayer.prepare(playbackManager.getMediaSource());
|
||||||
simpleExoPlayer.seekToDefaultPosition();
|
simpleExoPlayer.seekToDefaultPosition();
|
||||||
simpleExoPlayer.setPlayWhenReady(true);
|
simpleExoPlayer.setPlayWhenReady(true);
|
||||||
changeState(STATE_PLAYING);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -40,7 +40,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
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.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
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) {
|
public void sync(final int windowIndex, final long windowPos, final StreamInfo info) {
|
||||||
super.sync(windowIndex, windowPos, info);
|
super.sync(windowIndex, windowPos, info);
|
||||||
titleTextView.setText(getVideoTitle());
|
titleTextView.setText(getVideoTitle());
|
||||||
channelTextView.setText(getChannelName());
|
channelTextView.setText(getUploaderName());
|
||||||
|
|
||||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
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.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
||||||
|
@ -19,6 +19,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.MaybeObserver;
|
import io.reactivex.MaybeObserver;
|
||||||
|
import io.reactivex.SingleObserver;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.annotations.NonNull;
|
import io.reactivex.annotations.NonNull;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
@ -31,14 +32,14 @@ class MediaSourceManager {
|
||||||
// Effectively loads WINDOW_SIZE * 2 streams
|
// Effectively loads WINDOW_SIZE * 2 streams
|
||||||
private static final int WINDOW_SIZE = 3;
|
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
|
// sourceToQueueIndex maps media source index to play queue index
|
||||||
// Invariant 1: this list is sorted in ascending order
|
// Invariant 1: this list is sorted in ascending order
|
||||||
// Invariant 2: this list contains no duplicates
|
// Invariant 2: this list contains no duplicates
|
||||||
private final List<Integer> sourceToQueueIndex;
|
private List<Integer> sourceToQueueIndex;
|
||||||
|
|
||||||
private final PlaybackListener playbackListener;
|
|
||||||
private final PlayQueue playQueue;
|
|
||||||
|
|
||||||
private Subscription playQueueReactor;
|
private Subscription playQueueReactor;
|
||||||
private Subscription loadingReactor;
|
private Subscription loadingReactor;
|
||||||
|
@ -83,13 +84,13 @@ class MediaSourceManager {
|
||||||
|
|
||||||
MediaSourceManager(@NonNull final MediaSourceManager.PlaybackListener listener,
|
MediaSourceManager(@NonNull final MediaSourceManager.PlaybackListener listener,
|
||||||
@NonNull final PlayQueue playQueue) {
|
@NonNull final PlayQueue playQueue) {
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
|
||||||
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
|
|
||||||
|
|
||||||
this.playbackListener = listener;
|
this.playbackListener = listener;
|
||||||
this.playQueue = playQueue;
|
this.playQueue = playQueue;
|
||||||
|
|
||||||
disposables = new CompositeDisposable();
|
this.disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
|
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
|
||||||
|
|
||||||
playQueue.getBroadcastReceiver()
|
playQueue.getBroadcastReceiver()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -113,34 +114,25 @@ class MediaSourceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Called when the player has seamlessly transitioned to another stream.
|
* Called when the player has transitioned to another stream.
|
||||||
* Currently only expecting transitioning to the next stream and updates
|
|
||||||
* the play queue that a transition has occurred.
|
|
||||||
* */
|
* */
|
||||||
void refresh(final int newSourceIndex) {
|
void refresh(final int newSourceIndex) {
|
||||||
if (newSourceIndex == getCurrentSourceIndex()) return;
|
if (sourceToQueueIndex.indexOf(newSourceIndex) != -1) {
|
||||||
|
playQueue.setIndex(sourceToQueueIndex.indexOf(newSourceIndex));
|
||||||
if (newSourceIndex == getCurrentSourceIndex() + 1) {
|
|
||||||
playQueue.incrementIndex();
|
|
||||||
} else {
|
|
||||||
//something went wrong
|
|
||||||
Log.e(TAG, "Refresh media failed, reloading.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void report(final Exception error) {
|
void report(final Exception error) {
|
||||||
// ignore error checking for now, just remove the current index
|
// ignore error checking for now, just remove the current index
|
||||||
if (error != null && !isBlocked) {
|
if (error != null) {
|
||||||
doBlock();
|
tryBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
final int index = playQueue.getIndex();
|
final int index = playQueue.getIndex();
|
||||||
remove(index);
|
|
||||||
playQueue.remove(index);
|
playQueue.remove(index);
|
||||||
tryUnblock();
|
|
||||||
sync();
|
resetSources();
|
||||||
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -192,8 +184,8 @@ class MediaSourceManager {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPlayQueueReady() && !isBlocked) {
|
if (!isPlayQueueReady()) {
|
||||||
doBlock();
|
tryBlock();
|
||||||
playQueue.fetch();
|
playQueue.fetch();
|
||||||
}
|
}
|
||||||
if (playQueueReactor != null) playQueueReactor.request(1);
|
if (playQueueReactor != null) playQueueReactor.request(1);
|
||||||
|
@ -221,10 +213,12 @@ class MediaSourceManager {
|
||||||
return getCurrentSourceIndex() != -1;
|
return getCurrentSourceIndex() != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doBlock() {
|
private void tryBlock() {
|
||||||
|
if (!isBlocked) {
|
||||||
playbackListener.block();
|
playbackListener.block();
|
||||||
isBlocked = true;
|
isBlocked = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void tryUnblock() {
|
private void tryUnblock() {
|
||||||
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
|
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
|
||||||
|
@ -241,8 +235,8 @@ class MediaSourceManager {
|
||||||
private void onSelect() {
|
private void onSelect() {
|
||||||
if (isCurrentIndexLoaded()) {
|
if (isCurrentIndexLoaded()) {
|
||||||
sync();
|
sync();
|
||||||
} else if (!isBlocked) {
|
} else {
|
||||||
doBlock();
|
tryBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
@ -274,7 +268,7 @@ class MediaSourceManager {
|
||||||
private void init() {
|
private void init() {
|
||||||
final PlayQueueItem init = playQueue.getCurrent();
|
final PlayQueueItem init = playQueue.getCurrent();
|
||||||
|
|
||||||
init.getStream().subscribe(new MaybeObserver<StreamInfo>() {
|
init.getStream().subscribe(new SingleObserver<StreamInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull Disposable d) {
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
|
@ -303,17 +297,11 @@ class MediaSourceManager {
|
||||||
playQueue.remove(playQueue.indexOf(init));
|
playQueue.remove(playQueue.indexOf(init));
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
playQueue.remove(playQueue.indexOf(init));
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void load(final PlayQueueItem item) {
|
private void load(final PlayQueueItem item) {
|
||||||
item.getStream().subscribe(new MaybeObserver<StreamInfo>() {
|
item.getStream().subscribe(new SingleObserver<StreamInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull Disposable d) {
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
|
@ -335,15 +323,17 @@ class MediaSourceManager {
|
||||||
playQueue.remove(playQueue.indexOf(item));
|
playQueue.remove(playQueue.indexOf(item));
|
||||||
load();
|
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
|
// Media Source List Manipulation
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -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<StreamInfo> 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<StreamInfo>());
|
|
||||||
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<Maybe<StreamInfo>> 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<StreamInfo> getSubscriber() {
|
|
||||||
return new Subscriber<StreamInfo>() {
|
|
||||||
@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<PlayQueueMessage> getReactor() {
|
|
||||||
return new Subscriber<PlayQueueMessage>() {
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -53,14 +53,15 @@ import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
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.VideoStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlayListInfo;
|
|
||||||
import org.schabi.newpipe.playlist.ExternalPlayQueue;
|
import org.schabi.newpipe.playlist.ExternalPlayQueue;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.Utils;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -201,42 +202,50 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||||
simpleExoPlayer.setVideoListener(this);
|
simpleExoPlayer.setVideoListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
// public void handleIntent2(Intent intent) {
|
public void handleSingleStreamIntent(Intent intent) {
|
||||||
// super.handleIntent(intent);
|
super.handleIntent(intent);
|
||||||
// if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||||
// if (intent == null) return;
|
if (intent == null) return;
|
||||||
//
|
|
||||||
// selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1);
|
selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1);
|
||||||
//
|
|
||||||
// Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST);
|
Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST);
|
||||||
//
|
|
||||||
// if (serializable instanceof ArrayList) videoStreamsList = (ArrayList<VideoStream>) serializable;
|
if (serializable instanceof ArrayList) videoStreamsList = (ArrayList<VideoStream>) serializable;
|
||||||
// if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List<VideoStream>) serializable);
|
if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List<VideoStream>) serializable);
|
||||||
//
|
|
||||||
// Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM);
|
Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM);
|
||||||
// if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream;
|
if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream;
|
||||||
//
|
|
||||||
// startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true);
|
startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true);
|
||||||
// play(true);
|
play(true);
|
||||||
// }
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void handleIntent(Intent intent) {
|
public void handleIntent(Intent intent) {
|
||||||
if (intent == null) return;
|
if (intent == null) return;
|
||||||
|
|
||||||
|
handleExternalPlaylistIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void handleExternalPlaylistIntent(Intent intent) {
|
||||||
selectedIndexStream = 0;
|
selectedIndexStream = 0;
|
||||||
|
|
||||||
String url = intent.getStringExtra("url");
|
final int serviceId = intent.getIntExtra("serviceId", -1);
|
||||||
int nextPage = intent.getIntExtra("nextPage", 0);
|
final int index = intent.getIntExtra("index", 0);
|
||||||
int index = intent.getIntExtra("index", 0);
|
final Serializable serializable = intent.getSerializableExtra("streams");
|
||||||
|
final String nextPageUrl = intent.getStringExtra("nextPageUrl");
|
||||||
|
|
||||||
PlayListInfo info;
|
List<InfoItem> info = new ArrayList<>();
|
||||||
Serializable serializable = intent.getSerializableExtra("stream");
|
if (serializable instanceof List) {
|
||||||
if (serializable instanceof PlayListInfo) info = (PlayListInfo) serializable;
|
for (final Object o : (List) serializable) {
|
||||||
else return;
|
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);
|
playbackManager = new MediaSourceManager(this, playQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,13 +272,13 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaSource sourceOf(final StreamInfo info) {
|
public MediaSource sourceOf(final StreamInfo info) {
|
||||||
final List<VideoStream> videos = Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
|
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
|
||||||
final VideoStream video = videos.get(Utils.getDefaultResolution(context, videos));
|
final VideoStream video = videos.get(ListHelper.getDefaultResolutionIndex(context, videos));
|
||||||
|
|
||||||
final MediaSource mediaSource = super.buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
|
final MediaSource mediaSource = super.buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
|
||||||
if (!video.isVideoOnly) return mediaSource;
|
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);
|
final Uri audioUri = Uri.parse(audio.url);
|
||||||
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
|
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
package org.schabi.newpipe.playlist;
|
package org.schabi.newpipe.playlist;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlayListExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlayListInfo;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.annotations.NonNull;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
public class ExternalPlayQueue extends PlayQueue {
|
public class ExternalPlayQueue extends PlayQueue {
|
||||||
|
@ -24,24 +22,21 @@ public class ExternalPlayQueue extends PlayQueue {
|
||||||
|
|
||||||
private boolean isComplete;
|
private boolean isComplete;
|
||||||
|
|
||||||
private StreamingService service;
|
private int serviceId;
|
||||||
private String playlistUrl;
|
private String playlistUrl;
|
||||||
|
|
||||||
private AtomicInteger pageNumber;
|
|
||||||
private Disposable fetchReactor;
|
private Disposable fetchReactor;
|
||||||
|
|
||||||
public ExternalPlayQueue(final String playlistUrl,
|
public ExternalPlayQueue(final int serviceId,
|
||||||
final PlayListInfo info,
|
final String nextPageUrl,
|
||||||
final int currentPage,
|
final List<InfoItem> streams,
|
||||||
final int index) {
|
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.isComplete = nextPageUrl == null || nextPageUrl.isEmpty();
|
||||||
this.pageNumber = new AtomicInteger(currentPage + 1);
|
|
||||||
|
|
||||||
this.playlistUrl = playlistUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -57,31 +52,39 @@ public class ExternalPlayQueue extends PlayQueue {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fetch() {
|
public void fetch() {
|
||||||
if (isComplete) return;
|
ExtractorHelper.getPlaylistInfo(this.serviceId, this.playlistUrl, false)
|
||||||
if (fetchReactor != null && !fetchReactor.isDisposed()) return;
|
|
||||||
|
|
||||||
final Callable<PlayListInfo> task = new Callable<PlayListInfo>() {
|
|
||||||
@Override
|
|
||||||
public PlayListInfo call() throws Exception {
|
|
||||||
PlayListExtractor extractor = service.getPlayListExtractorInstance(playlistUrl, pageNumber.get());
|
|
||||||
return PlayListInfo.getInfo(extractor);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<PlayListInfo> onSuccess = new Consumer<PlayListInfo>() {
|
|
||||||
@Override
|
|
||||||
public void accept(PlayListInfo playListInfo) throws Exception {
|
|
||||||
if (!playListInfo.hasNextPage) isComplete = true;
|
|
||||||
|
|
||||||
append(extractPlaylistItems(playListInfo));
|
|
||||||
pageNumber.incrementAndGet();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchReactor = Maybe.fromCallable(task)
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(onSuccess);
|
.retry(2)
|
||||||
|
.subscribe(getPlaylistObserver());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SingleObserver<PlaylistInfo> getPlaylistObserver() {
|
||||||
|
return new SingleObserver<PlaylistInfo>() {
|
||||||
|
@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.<PlayQueueItem>emptyList());
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -90,9 +93,9 @@ public class ExternalPlayQueue extends PlayQueue {
|
||||||
if (fetchReactor != null) fetchReactor.dispose();
|
if (fetchReactor != null) fetchReactor.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<PlayQueueItem> extractPlaylistItems(final PlayListInfo info) {
|
private static List<PlayQueueItem> extractPlaylistItems(final List<InfoItem> infos) {
|
||||||
List<PlayQueueItem> result = new ArrayList<>();
|
List<PlayQueueItem> result = new ArrayList<>();
|
||||||
for (final InfoItem stream : info.related_streams) {
|
for (final InfoItem stream : infos) {
|
||||||
if (stream instanceof StreamInfoItem) {
|
if (stream instanceof StreamInfoItem) {
|
||||||
result.add(new PlayQueueItem((StreamInfoItem) stream));
|
result.add(new PlayQueueItem((StreamInfoItem) stream));
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,11 +108,6 @@ public abstract class PlayQueue {
|
||||||
broadcast(new SelectEvent(index));
|
broadcast(new SelectEvent(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void incrementIndex() {
|
|
||||||
final int index = queueIndex.incrementAndGet();
|
|
||||||
broadcast(new NextEvent(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void append(final PlayQueueItem item) {
|
protected void append(final PlayQueueItem item) {
|
||||||
streams.add(item);
|
streams.add(item);
|
||||||
broadcast(new AppendEvent(1));
|
broadcast(new AppendEvent(1));
|
||||||
|
@ -127,6 +122,8 @@ public abstract class PlayQueue {
|
||||||
if (index >= streams.size()) return;
|
if (index >= streams.size()) return;
|
||||||
|
|
||||||
streams.remove(index);
|
streams.remove(index);
|
||||||
|
queueIndex.set(Math.max(0, queueIndex.get() - 1));
|
||||||
|
|
||||||
broadcast(new RemoveEvent(index));
|
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<PlayQueueMessage> getSelfReporter() {
|
private Subscriber<PlayQueueMessage> getSelfReporter() {
|
||||||
return new Subscriber<PlayQueueMessage>() {
|
return new Subscriber<PlayQueueMessage>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.info_list.StreamInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -163,7 +162,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
case 1:
|
case 1:
|
||||||
return new HFHolder(footer);
|
return new HFHolder(footer);
|
||||||
case 2:
|
case 2:
|
||||||
return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext())
|
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.play_queue_item, parent, false));
|
.inflate(R.layout.play_queue_item, parent, false));
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Trollolo");
|
||||||
|
|
|
@ -3,18 +3,12 @@ package org.schabi.newpipe.playlist;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
|
|
||||||
|
|
||||||
import java.util.concurrent.Callable;
|
import io.reactivex.Single;
|
||||||
|
|
||||||
import io.reactivex.Maybe;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.functions.Action;
|
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
@ -23,19 +17,17 @@ public class PlayQueueItem {
|
||||||
final private String title;
|
final private String title;
|
||||||
final private String url;
|
final private String url;
|
||||||
final private int serviceId;
|
final private int serviceId;
|
||||||
final private int duration;
|
final private long duration;
|
||||||
|
|
||||||
private boolean isDone;
|
|
||||||
private Throwable error;
|
private Throwable error;
|
||||||
private Maybe<StreamInfo> stream;
|
private Single<StreamInfo> stream;
|
||||||
|
|
||||||
PlayQueueItem(final StreamInfoItem streamInfoItem) {
|
PlayQueueItem(final StreamInfoItem streamInfoItem) {
|
||||||
this.title = streamInfoItem.getTitle();
|
this.title = streamInfoItem.name;
|
||||||
this.url = streamInfoItem.getLink();
|
this.url = streamInfoItem.url;
|
||||||
this.serviceId = streamInfoItem.service_id;
|
this.serviceId = streamInfoItem.service_id;
|
||||||
this.duration = streamInfoItem.duration;
|
this.duration = streamInfoItem.duration;
|
||||||
|
|
||||||
this.isDone = false;
|
|
||||||
this.stream = getInfo();
|
this.stream = getInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,37 +45,22 @@ public class PlayQueueItem {
|
||||||
return serviceId;
|
return serviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDuration() {
|
public long getDuration() {
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDone() {
|
|
||||||
return isDone;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Throwable getError() {
|
public Throwable getError() {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public Maybe<StreamInfo> getStream() {
|
public Single<StreamInfo> getStream() {
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Maybe<StreamInfo> getInfo() {
|
private Single<StreamInfo> getInfo() {
|
||||||
final StreamingService service = getService(serviceId);
|
|
||||||
if (service == null) return Maybe.empty();
|
|
||||||
|
|
||||||
final Callable<StreamInfo> task = new Callable<StreamInfo>() {
|
|
||||||
@Override
|
|
||||||
public StreamInfo call() throws Exception {
|
|
||||||
final StreamExtractor extractor = service.getExtractorInstance(url);
|
|
||||||
return StreamInfo.getVideoInfo(extractor);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(Throwable throwable) throws Exception {
|
public void accept(Throwable throwable) throws Exception {
|
||||||
|
@ -91,27 +68,10 @@ public class PlayQueueItem {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
final Action onComplete = new Action() {
|
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
|
||||||
@Override
|
|
||||||
public void run() throws Exception {
|
|
||||||
isDone = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Maybe.fromCallable(task)
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnError(onError)
|
|
||||||
.doOnComplete(onComplete)
|
|
||||||
.retry(3)
|
.retry(3)
|
||||||
.cache();
|
.doOnError(onError);
|
||||||
}
|
|
||||||
|
|
||||||
private StreamingService getService(final int serviceId) {
|
|
||||||
try {
|
|
||||||
return NewPipe.getService(serviceId);
|
|
||||||
} catch (ExtractionException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,17 +57,17 @@ public class PlayQueueItemBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static String getDurationString(int duration) {
|
public static String getDurationString(long duration) {
|
||||||
if(duration < 0) {
|
if(duration < 0) {
|
||||||
duration = 0;
|
duration = 0;
|
||||||
}
|
}
|
||||||
String output;
|
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);
|
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);
|
duration %= (60 * 60);
|
||||||
int minutes = duration / 60;
|
long minutes = duration / 60;
|
||||||
int seconds = duration % 60;
|
long seconds = duration % 60;
|
||||||
|
|
||||||
//handle days
|
//handle days
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
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.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
|
|
|
@ -9,6 +9,19 @@
|
||||||
android:background="?attr/contrast_background_color"
|
android:background="?attr/contrast_background_color"
|
||||||
android:paddingBottom="6dp">
|
android:paddingBottom="6dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/playlist_play_all_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_gravity="center_vertical|right"
|
||||||
|
android:layout_marginRight="2dp"
|
||||||
|
android:text="@string/play_all"
|
||||||
|
android:textSize="@dimen/channel_rss_title_size"
|
||||||
|
android:theme="@style/RedButton"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playlist_title_view"
|
android:id="@+id/playlist_title_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -16,6 +29,8 @@
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_toLeftOf="@+id/playlist_play_all_button"
|
||||||
|
android:layout_toStartOf="@+id/playlist_play_all_button"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
@ -24,7 +39,7 @@
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/uploader_layout"
|
android:id="@+id/uploader_layout"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/playlist_detail_uploader_layout_height"
|
android:layout_height="@dimen/playlist_detail_uploader_layout_height"
|
||||||
android:layout_below="@+id/playlist_title_view"
|
android:layout_below="@+id/playlist_title_view"
|
||||||
android:layout_marginLeft="4dp"
|
android:layout_marginLeft="4dp"
|
||||||
|
@ -68,9 +83,10 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignBottom="@+id/uploader_layout"
|
android:layout_alignBottom="@+id/uploader_layout"
|
||||||
android:layout_alignEnd="@+id/playlist_title_view"
|
android:layout_alignEnd="@+id/playlist_play_all_button"
|
||||||
android:layout_alignRight="@+id/playlist_title_view"
|
android:layout_alignRight="@+id/playlist_play_all_button"
|
||||||
android:layout_alignTop="@+id/uploader_layout"
|
android:layout_alignTop="@+id/uploader_layout"
|
||||||
|
android:layout_marginRight="6dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:gravity="right|center_vertical"
|
android:gravity="right|center_vertical"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
@ -78,19 +94,4 @@
|
||||||
tools:ignore="RtlHardcoded"
|
tools:ignore="RtlHardcoded"
|
||||||
tools:text="234 videos"/>
|
tools:text="234 videos"/>
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/playlist_play_all_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_below="@+id/playlist_banner_image"
|
|
||||||
android:layout_gravity="center_vertical|right"
|
|
||||||
android:layout_marginRight="2dp"
|
|
||||||
android:text="Play All"
|
|
||||||
android:textSize="@dimen/channel_rss_title_size"
|
|
||||||
android:theme="@style/RedButton"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:visibility="visible"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
|
@ -112,6 +112,7 @@
|
||||||
<string name="popup_resizing_indicator_title">Resizing</string>
|
<string name="popup_resizing_indicator_title">Resizing</string>
|
||||||
<string name="best_resolution">Best resolution</string>
|
<string name="best_resolution">Best resolution</string>
|
||||||
<string name="undo">Undo</string>
|
<string name="undo">Undo</string>
|
||||||
|
<string name="play_all">Play All</string>
|
||||||
|
|
||||||
<string name="notification_channel_id" translatable="false">newpipe</string>
|
<string name="notification_channel_id" translatable="false">newpipe</string>
|
||||||
<string name="notification_channel_name">NewPipe Notification</string>
|
<string name="notification_channel_name">NewPipe Notification</string>
|
||||||
|
|
Loading…
Reference in New Issue