-Added bulk playlist creation and append.

-Added UI to create playlist from service player activity.
-Added state saving to playlist dialogs.
-Removed access to history activity on service player activity.
-Made StreamEntity serializable.
This commit is contained in:
John Zhen Mo 2018-01-21 19:32:49 -08:00
parent 168ac91ab8
commit 776dbc34f7
12 changed files with 182 additions and 101 deletions

View File

@ -19,7 +19,7 @@ public final class NewPipeDatabase {
public static void init(Context context) {
databaseInstance = Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_11_12)
.fallbackToDestructiveMigration()
.build();

View File

@ -15,7 +15,7 @@ public class Migrations {
/*
* Unfortunately these queries must be hardcoded due to the possibility of
* schema and names changing at a later date, thus invalidating the older migration
* scripts if names are not hardcoded.
* scripts if they are not hardcoded.
* */
// Not much we can do about this, since room doesn't create tables before migration.

View File

@ -9,15 +9,18 @@ import android.arch.persistence.room.PrimaryKey;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.util.Constants;
import java.io.Serializable;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
@Entity(tableName = STREAM_TABLE,
indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)})
public class StreamEntity {
public class StreamEntity implements Serializable {
final public static String STREAM_TABLE = "streams";
final public static String STREAM_ID = "uid";
@ -78,6 +81,12 @@ public class StreamEntity {
info.uploader_name, info.duration);
}
@Ignore
public StreamEntity(final PlayQueueItem item) {
this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(),
item.getThumbnailUrl(), item.getUploader(), item.getDuration());
}
@Ignore
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
StreamInfoItem item = new StreamInfoItem(

View File

@ -331,7 +331,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
break;
case R.id.detail_controls_playlist_append:
if (getFragmentManager() != null && currentInfo != null) {
PlaylistAppendDialog.newInstance(currentInfo).show(getFragmentManager(), TAG);
PlaylistAppendDialog.fromStreamInfo(currentInfo)
.show(getFragmentManager(), TAG);
}
break;
case R.id.detail_uploader_root_layout:

View File

@ -33,34 +33,38 @@ public class LocalPlaylistManager {
}
public Maybe<List<Long>> createPlaylist(final String name, final List<StreamEntity> streams) {
// Disallow creation of empty playlists until user is able to select thumbnail
// Disallow creation of empty playlists
if (streams.isEmpty()) return Maybe.empty();
final StreamEntity defaultStream = streams.get(0);
final PlaylistEntity newPlaylist = new PlaylistEntity(name, defaultStream.getThumbnailUrl());
final PlaylistEntity newPlaylist =
new PlaylistEntity(name, defaultStream.getThumbnailUrl());
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long playlistId = playlistTable.insert(newPlaylist);
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streams.size());
for (int index = 0; index < streams.size(); index++) {
// Upsert streams and get their ids
final long streamId = streamTable.upsert(streams.get(index));
joinEntities.add(new PlaylistStreamEntity(playlistId, streamId, index));
}
return playlistStreamTable.insertAll(joinEntities);
})).subscribeOn(Schedulers.io());
return Maybe.fromCallable(() -> database.runInTransaction(() ->
upsertStreams(playlistTable.insert(newPlaylist), streams, 0))
).subscribeOn(Schedulers.io());
}
public Maybe<Long> appendToPlaylist(final long playlistId, final StreamEntity stream) {
final Maybe<Long> streamIdFuture = Maybe.fromCallable(() -> streamTable.upsert(stream));
final Maybe<Integer> joinIndexFuture =
playlistStreamTable.getMaximumIndexOf(playlistId).firstElement();
public Maybe<List<Long>> appendToPlaylist(final long playlistId,
final List<StreamEntity> streams) {
return playlistStreamTable.getMaximumIndexOf(playlistId)
.firstElement()
.map(maxJoinIndex -> database.runInTransaction(() ->
upsertStreams(playlistId, streams, maxJoinIndex + 1))
).subscribeOn(Schedulers.io());
}
return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) ->
playlistStreamTable.insert(new PlaylistStreamEntity(playlistId,
streamId, currentMaxJoinIndex + 1))
).subscribeOn(Schedulers.io());
private List<Long> upsertStreams(final long playlistId,
final List<StreamEntity> streams,
final int indexOffset) {
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streams.size());
for (int index = 0; index < streams.size(); index++) {
// Upsert streams and get their ids
final long streamId = streamTable.upsert(streams.get(index));
joinEntities.add(new PlaylistStreamEntity(playlistId, streamId,
index + indexOffset));
}
return playlistStreamTable.insertAll(joinEntities);
}
public Completable updateJoin(final long playlistId, final List<Long> streamIds) {

View File

@ -4,7 +4,6 @@ import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
@ -19,34 +18,48 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
public class PlaylistAppendDialog extends DialogFragment {
public final class PlaylistAppendDialog extends PlaylistDialog {
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
private static final String INFO_KEY = "info_key";
private StreamInfo streamInfo;
private View newPlaylistButton;
private RecyclerView playlistRecyclerView;
private InfoListAdapter playlistAdapter;
public static PlaylistAppendDialog newInstance(final StreamInfo info) {
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
dialog.setInfo(info);
dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
return dialog;
}
private void setInfo(StreamInfo info) {
this.streamInfo = info;
public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) {
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
List<StreamEntity> entities = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
entities.add(new StreamEntity(item));
}
dialog.setInfo(entities);
return dialog;
}
public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) {
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
List<StreamEntity> entities = new ArrayList<>(items.size());
for (final PlayQueueItem item : items) {
entities.add(new StreamEntity(item));
}
dialog.setInfo(entities);
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
@ -60,14 +73,9 @@ public class PlaylistAppendDialog extends DialogFragment {
playlistAdapter.useMiniItemVariants(true);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
Serializable serial = savedInstanceState.getSerializable(INFO_KEY);
if (serial instanceof StreamInfo) streamInfo = (StreamInfo) serial;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@ -79,7 +87,7 @@ public class PlaylistAppendDialog extends DialogFragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
newPlaylistButton = view.findViewById(R.id.newPlaylist);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
playlistRecyclerView = view.findViewById(R.id.playlist_list);
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
playlistRecyclerView.setAdapter(playlistAdapter);
@ -92,12 +100,14 @@ public class PlaylistAppendDialog extends DialogFragment {
playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
if (!(selectedItem instanceof LocalPlaylistInfoItem)) return;
if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null)
return;
final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId();
final Toast successToast =
Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT);
playlistManager.appendToPlaylist(playlistId, new StreamEntity(streamInfo))
playlistManager.appendToPlaylist(playlistId, getStreams())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show());
@ -127,20 +137,14 @@ public class PlaylistAppendDialog extends DialogFragment {
});
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, streamInfo);
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
public void openCreatePlaylistDialog() {
if (streamInfo == null || getFragmentManager() == null) return;
if (getStreams() == null || getFragmentManager() == null) return;
PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG);
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG);
getDialog().dismiss();
}
}

View File

@ -5,63 +5,35 @@ import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.Collections;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
public class PlaylistCreationDialog extends DialogFragment {
public final class PlaylistCreationDialog extends PlaylistDialog {
private static final String TAG = PlaylistCreationDialog.class.getCanonicalName();
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String INFO_KEY = "info_key";
private StreamInfo streamInfo;
public static PlaylistCreationDialog newInstance(final StreamInfo info) {
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
PlaylistCreationDialog dialog = new PlaylistCreationDialog();
dialog.setInfo(info);
dialog.setInfo(streams);
return dialog;
}
private void setInfo(final StreamInfo info) {
this.streamInfo = info;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
// Dialog
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (streamInfo != null) {
outState.putSerializable(INFO_KEY, streamInfo);
}
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null && streamInfo == null) {
final Object infoCandidate = savedInstanceState.getSerializable(INFO_KEY);
if (infoCandidate != null && infoCandidate instanceof StreamInfo) {
streamInfo = (StreamInfo) infoCandidate;
}
}
if (streamInfo == null) return super.onCreateDialog(savedInstanceState);
if (getStreams() == null) return super.onCreateDialog(savedInstanceState);
View dialogView = View.inflate(getContext(),
R.layout.dialog_create_playlist, null);
@ -76,13 +48,11 @@ public class PlaylistCreationDialog extends DialogFragment {
final String name = nameInput.getText().toString();
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
final List<StreamEntity> streams =
Collections.singletonList(new StreamEntity(streamInfo));
final Toast successToast = Toast.makeText(getActivity(),
"Playlist " + name + " successfully created",
"Playlist successfully created",
Toast.LENGTH_SHORT);
playlistManager.createPlaylist(name, streams)
playlistManager.createPlaylist(name, getStreams())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> successToast.show());
});

View File

@ -0,0 +1,73 @@
package org.schabi.newpipe.fragments.local;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Queue;
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
private List<StreamEntity> streamEntities;
private StateSaver.SavedState savedState;
protected void setInfo(final List<StreamEntity> entities) {
this.streamEntities = entities;
}
protected List<StreamEntity> getStreams() {
return streamEntities;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
savedState = StateSaver.tryToRestore(savedInstanceState, this);
}
@Override
public void onDestroy() {
super.onDestroy();
StateSaver.onDestroy(savedState);
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public String generateSuffix() {
final int size = streamEntities == null ? 0 : streamEntities.size();
return "." + size + ".list";
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
objectsToSave.add(streamEntities);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
streamEntities = (List<StreamEntity>) savedObjects.poll();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (getActivity() != null) {
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),
savedState, outState, this);
}
}
}

View File

@ -675,6 +675,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
}
// TODO: update exoplayer to 2.6.x in order to register view count on repeated streams
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe());
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
}

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
@ -149,8 +150,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
case android.R.id.home:
finish();
return true;
case R.id.action_history:
NavigationHelper.openHistory(this);
case R.id.action_append_playlist:
appendToPlaylist();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
@ -185,6 +186,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
null
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
private void appendToPlaylist() {
if (this.player != null && this.player.getPlayQueue() != null) {
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
.show(getSupportFragmentManager(), getTag());
}
}
////////////////////////////////////////////////////////////////////////////
// Service Connection
////////////////////////////////////////////////////////////////////////////

View File

@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable;
@ -23,6 +24,7 @@ public class PlayQueueItem implements Serializable {
final private long duration;
final private String thumbnailUrl;
final private String uploader;
final private StreamType streamType;
private long recoveryPosition;
private Throwable error;
@ -30,22 +32,26 @@ public class PlayQueueItem implements Serializable {
private transient Single<StreamInfo> stream;
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.duration, info.thumbnail_url, info.uploader_name);
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
this.stream = Single.just(info);
}
PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.getName(), item.getUrl(), item.getServiceId(), item.duration, item.thumbnail_url, item.uploader_name);
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
}
private PlayQueueItem(final String name, final String url, final int serviceId,
final long duration, final String thumbnailUrl, final String uploader) {
final long duration, final String thumbnailUrl, final String uploader,
final StreamType streamType) {
this.title = name;
this.url = url;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET;
}
@ -78,6 +84,10 @@ public class PlayQueueItem implements Serializable {
return uploader;
}
public StreamType getStreamType() {
return streamType;
}
public long getRecoveryPosition() {
return recoveryPosition;
}

View File

@ -1,11 +1,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.schabi.newpipe.history.HistoryActivity">
tools:context=".player.BackgroundPlayerActivity">
<item android:id="@+id/action_history"
<item android:id="@+id/action_append_playlist"
android:orderInCategory="981"
android:title="@string/action_history"
android:title="@string/append_playlist"
app:showAsAction="never"/>
<item android:id="@+id/action_settings"