-Added schema for local playlist and stream statistics.

-Added normalized schema for stream history.
-Added managers for specialized database access for stream and local playlist.
This commit is contained in:
John Zhen Mo 2018-01-15 12:30:52 -08:00
parent 960fd9be38
commit f71242a036
21 changed files with 913 additions and 23 deletions

View file

@ -28,4 +28,10 @@ public final class NewPipeDatabase {
return databaseInstance; return databaseInstance;
} }
@NonNull
public static AppDatabase getInstance(Context context) {
if (databaseInstance == null) init(context);
return databaseInstance;
}
} }

View file

@ -4,16 +4,31 @@ import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.TypeConverters; import android.arch.persistence.room.TypeConverters;
import org.schabi.newpipe.database.history.Converters;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO; import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry; import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
@TypeConverters({Converters.class}) @TypeConverters({Converters.class})
@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false) @Database(
entities = {
SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class,
StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class,
PlaylistStreamEntity.class
},
version = 1,
exportSchema = false
)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db"; public static final String DATABASE_NAME = "newpipe.db";
@ -23,4 +38,12 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract WatchHistoryDAO watchHistoryDAO(); public abstract WatchHistoryDAO watchHistoryDAO();
public abstract SearchHistoryDAO searchHistoryDAO(); public abstract SearchHistoryDAO searchHistoryDAO();
public abstract StreamDAO streamDAO();
public abstract StreamHistoryDAO streamHistoryDAO();
public abstract PlaylistDAO playlistDAO();
public abstract PlaylistStreamDAO playlistStreamDAO();
} }

View file

@ -23,9 +23,6 @@ public interface BasicDAO<Entity> {
@Insert(onConflict = OnConflictStrategy.FAIL) @Insert(onConflict = OnConflictStrategy.FAIL)
List<Long> insertAll(final Collection<Entity> entities); List<Long> insertAll(final Collection<Entity> entities);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long upsert(final Entity entity);
/* Searches */ /* Searches */
Flowable<List<Entity>> getAll(); Flowable<List<Entity>> getAll();

View file

@ -1,7 +1,9 @@
package org.schabi.newpipe.database.history; package org.schabi.newpipe.database;
import android.arch.persistence.room.TypeConverter; import android.arch.persistence.room.TypeConverter;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date; import java.util.Date;
public class Converters { public class Converters {
@ -25,4 +27,14 @@ public class Converters {
public static Long dateToTimestamp(Date date) { public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime(); return date == null ? null : date.getTime();
} }
@TypeConverter
public static StreamType streamTypeOf(String value) {
return StreamType.valueOf(value);
}
@TypeConverter
public static String stringOf(StreamType streamType) {
return streamType.name();
}
} }

View file

@ -0,0 +1,36 @@
package org.schabi.newpipe.database.playlist;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
public class PlaylistMetadataEntry {
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
@ColumnInfo(name = PLAYLIST_ID)
final public long uid;
@ColumnInfo(name = PLAYLIST_NAME)
final public String name;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
final public String thumbnailUrl;
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
final public long streamCount;
public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) {
this.uid = uid;
this.name = name;
this.thumbnailUrl = thumbnailUrl;
this.streamCount = streamCount;
}
public LocalPlaylistInfoItem toStoredPlaylistInfoItem() {
LocalPlaylistInfoItem storedPlaylistInfoItem = new LocalPlaylistInfoItem(uid, name);
storedPlaylistInfoItem.setThumbnailUrl(thumbnailUrl);
storedPlaylistInfoItem.setStreamCount(streamCount);
return storedPlaylistInfoItem;
}
}

View file

@ -0,0 +1,35 @@
package org.schabi.newpipe.database.playlist.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
@Dao
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
@Override
@Query("SELECT * FROM " + PLAYLIST_TABLE)
public abstract Flowable<List<PlaylistEntity>> getAll();
@Override
@Query("DELETE FROM " + PLAYLIST_TABLE)
public abstract int deleteAll();
@Override
public Flowable<List<PlaylistEntity>> listByService(int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
}

View file

@ -0,0 +1,69 @@
package org.schabi.newpipe.database.playlist.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*;
import static org.schabi.newpipe.database.stream.model.StreamEntity.*;
@Dao
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
@Override
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
@Override
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
public abstract int deleteAll();
@Override
public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE +
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract void deleteBatch(final long playlistId);
@Query("SELECT MAX(" + JOIN_INDEX + ")" +
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
@Transaction
@Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " +
STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " +
STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL +
" FROM " + STREAM_TABLE + " INNER JOIN " +
// get ids of streams of the given playlist
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
" FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE "
+ JOIN_PLAYLIST_ID + " = :playlistId)" +
// then merge with the stream metadata
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
" ORDER BY " + JOIN_INDEX + " ASC")
public abstract Flowable<List<StreamEntity>> getOrderedStreamsOf(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " +
PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT +
" FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID +
" GROUP BY " + JOIN_PLAYLIST_ID)
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
}

View file

@ -0,0 +1,59 @@
package org.schabi.newpipe.database.playlist.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import java.util.Date;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
@Entity(tableName = PLAYLIST_TABLE,
indices = {@Index(value = {PLAYLIST_NAME})})
public class PlaylistEntity {
final public static String PLAYLIST_TABLE = "playlists";
final public static String PLAYLIST_ID = "uid";
final public static String PLAYLIST_NAME = "name";
final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = PLAYLIST_ID)
private long uid = 0;
@ColumnInfo(name = PLAYLIST_NAME)
private String name;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
private String thumbnailUrl;
public PlaylistEntity(String name, String thumbnailUrl) {
this.name = name;
this.thumbnailUrl = thumbnailUrl;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
}

View file

@ -0,0 +1,77 @@
package org.schabi.newpipe.database.playlist.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import static android.arch.persistence.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE,
primaryKeys = {JOIN_PLAYLIST_ID, JOIN_STREAM_ID, JOIN_INDEX},
indices = {
@Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true),
@Index(value = {JOIN_STREAM_ID})
},
foreignKeys = {
@ForeignKey(entity = PlaylistEntity.class,
parentColumns = PlaylistEntity.PLAYLIST_ID,
childColumns = JOIN_PLAYLIST_ID,
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
@ForeignKey(entity = StreamEntity.class,
parentColumns = StreamEntity.STREAM_ID,
childColumns = JOIN_STREAM_ID,
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
})
public class PlaylistStreamEntity {
final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
final public static String JOIN_PLAYLIST_ID = "playlist_id";
final public static String JOIN_STREAM_ID = "stream_id";
final public static String JOIN_INDEX = "join_index";
@ColumnInfo(name = JOIN_PLAYLIST_ID)
private long playlistUid;
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@ColumnInfo(name = JOIN_INDEX)
private int index;
public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) {
this.playlistUid = playlistUid;
this.streamUid = streamUid;
this.index = index;
}
public long getPlaylistUid() {
return playlistUid;
}
public long getStreamUid() {
return streamUid;
}
public int getIndex() {
return index;
}
public void setPlaylistUid(long playlistUid) {
this.playlistUid = playlistUid;
}
public void setStreamUid(long streamUid) {
this.streamUid = streamUid;
}
public void setIndex(int index) {
this.index = index;
}
}

View file

@ -0,0 +1,54 @@
package org.schabi.newpipe.database.stream;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date;
public class StreamStatisticsEntry {
final public static String STREAM_LATEST_DATE = "latestAccess";
final public static String STREAM_WATCH_COUNT = "watchCount";
@ColumnInfo(name = StreamEntity.STREAM_ID)
final public long uid;
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
final public int serviceId;
@ColumnInfo(name = StreamEntity.STREAM_URL)
final public String url;
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
final public String title;
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
final public StreamType streamType;
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
final public long duration;
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
final public String uploader;
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
final public String thumbnailUrl;
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
final public long streamId;
@ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE)
final public Date latestAccessDate;
@ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT)
final public long watchCount;
public StreamStatisticsEntry(long uid, int serviceId, String url, String title,
StreamType streamType, long duration, String uploader,
String thumbnailUrl, long streamId, Date latestAccessDate,
long watchCount) {
this.uid = uid;
this.serviceId = serviceId;
this.url = url;
this.title = title;
this.streamType = streamType;
this.duration = duration;
this.uploader = uploader;
this.thumbnailUrl = thumbnailUrl;
this.streamId = streamId;
this.latestAccessDate = latestAccessDate;
this.watchCount = watchCount;
}
}

View file

@ -0,0 +1,57 @@
package org.schabi.newpipe.database.stream.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Flowable;
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;
@Dao
public abstract class StreamDAO implements BasicDAO<StreamEntity> {
@Override
@Query("SELECT * FROM " + STREAM_TABLE)
public abstract Flowable<List<StreamEntity>> getAll();
@Override
@Query("DELETE FROM " + STREAM_TABLE)
public abstract int deleteAll();
@Override
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<StreamEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " +
STREAM_URL + " LIKE :url AND " +
STREAM_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<StreamEntity>> getStream(long serviceId, String url);
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " +
STREAM_URL + " LIKE :url AND " +
STREAM_SERVICE_ID + " = :serviceId")
abstract List<StreamEntity> getStreamInternal(long serviceId, String url);
@Transaction
public long upsert(StreamEntity stream) {
final List<StreamEntity> streams = getStreamInternal(stream.getServiceId(), stream.getUrl());
final long uid;
if (streams.isEmpty()) {
uid = insert(stream);
} else {
uid = streams.get(0).getUid();
stream.setUid(uid);
update(stream);
}
return uid;
}
}

View file

@ -0,0 +1,54 @@
package org.schabi.newpipe.database.stream.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
@Dao
public abstract class StreamHistoryDAO implements BasicDAO<StreamHistoryEntity> {
@Override
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE)
public abstract Flowable<List<StreamHistoryEntity>> getAll();
@Override
@Query("DELETE FROM " + STREAM_HISTORY_TABLE)
public abstract int deleteAll();
@Override
public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteHistory(final long streamId);
@Query("SELECT * FROM " + STREAM_TABLE +
// Select the latest entry and watch count for each stream id on history table
" INNER JOIN " +
"(SELECT " + JOIN_STREAM_ID + ", " +
" MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " +
" COUNT(*) AS " + STREAM_WATCH_COUNT +
" FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" +
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
}

View file

@ -0,0 +1,154 @@
package org.schabi.newpipe.database.stream.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
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.util.Constants;
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 {
final public static String STREAM_TABLE = "streams";
final public static String STREAM_ID = "uid";
final public static String STREAM_SERVICE_ID = "service_id";
final public static String STREAM_URL = "url";
final public static String STREAM_TITLE = "title";
final public static String STREAM_TYPE = "streamType";
final public static String STREAM_UPLOADER = "uploader";
final public static String STREAM_DURATION = "duration";
final public static String STREAM_THUMBNAIL_URL = "thumbnail_url";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = STREAM_ID)
private long uid = 0;
@ColumnInfo(name = STREAM_SERVICE_ID)
private int serviceId = Constants.NO_SERVICE_ID;
@ColumnInfo(name = STREAM_URL)
private String url;
@ColumnInfo(name = STREAM_TITLE)
private String title;
@ColumnInfo(name = STREAM_TYPE)
private StreamType streamType;
@ColumnInfo(name = STREAM_DURATION)
private Long duration;
@ColumnInfo(name = STREAM_UPLOADER)
private String uploader;
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
private String thumbnailUrl;
public StreamEntity(final int serviceId, final String title, final String url,
final StreamType streamType, final String thumbnailUrl, final String uploader,
final long duration) {
this.serviceId = serviceId;
this.title = title;
this.url = url;
this.streamType = streamType;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.duration = duration;
}
@Ignore
public StreamEntity(final StreamInfoItem item) {
this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url,
item.uploader_name, item.duration);
}
@Ignore
public StreamEntity(final StreamInfo info) {
this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url,
info.uploader_name, info.duration);
}
@Ignore
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
StreamInfoItem item = new StreamInfoItem(
getServiceId(), getUrl(), getTitle(), getStreamType());
item.setThumbnailUrl(getThumbnailUrl());
item.setUploaderName(getUploader());
item.setDuration(getDuration());
return item;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public int getServiceId() {
return serviceId;
}
public void setServiceId(int serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public StreamType getStreamType() {
return streamType;
}
public void setStreamType(StreamType type) {
this.streamType = type;
}
public Long getDuration() {
return duration;
}
public void setDuration(Long duration) {
this.duration = duration;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
}

View file

@ -0,0 +1,58 @@
package org.schabi.newpipe.database.stream.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import android.arch.persistence.room.Index;
import android.support.annotation.NonNull;
import java.util.Date;
import static android.arch.persistence.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
@Entity(tableName = STREAM_HISTORY_TABLE,
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
// No need to index for timestamp as they will almost always be unique
indices = {@Index(value = {JOIN_STREAM_ID})},
foreignKeys = {
@ForeignKey(entity = StreamEntity.class,
parentColumns = StreamEntity.STREAM_ID,
childColumns = JOIN_STREAM_ID,
onDelete = CASCADE, onUpdate = CASCADE)
})
public class StreamHistoryEntity {
final public static String STREAM_HISTORY_TABLE = "stream_history";
final public static String JOIN_STREAM_ID = "stream_id";
final public static String STREAM_ACCESS_DATE = "access_date";
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@NonNull
@ColumnInfo(name = STREAM_ACCESS_DATE)
private Date accessDate;
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) {
this.streamUid = streamUid;
this.accessDate = accessDate;
}
public long getStreamUid() {
return streamUid;
}
public void setStreamUid(long streamUid) {
this.streamUid = streamUid;
}
public Date getAccessDate() {
return accessDate;
}
public void setAccessDate(@NonNull Date accessDate) {
this.accessDate = accessDate;
}
}

View file

@ -50,8 +50,7 @@ public class SubscriptionEntity {
return uid; return uid;
} }
/* Keep this package-private since UID should always be auto generated by Room impl */ public void setUid(long uid) {
void setUid(long uid) {
this.uid = uid; this.uid = uid;
} }

View file

@ -0,0 +1,78 @@
package org.schabi.newpipe.fragments.playlist;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Maybe;
public class LocalPlaylistManager {
private final AppDatabase database;
private final StreamDAO streamTable;
private final PlaylistDAO playlistTable;
private final PlaylistStreamDAO playlistStreamTable;
public LocalPlaylistManager(final AppDatabase db) {
database = db;
streamTable = db.streamDAO();
playlistTable = db.playlistDAO();
playlistStreamTable = db.playlistStreamDAO();
}
public Maybe<List<Long>> createPlaylist(final String name, final List<StreamEntity> streams) {
// Disallow creation of empty playlists until user is able to select thumbnail
if (streams.isEmpty()) return Maybe.empty();
final StreamEntity defaultStream = streams.get(0);
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);
}));
}
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();
return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) ->
playlistStreamTable.insert(new PlaylistStreamEntity(playlistId,
streamId, currentMaxJoinIndex + 1))
);
}
public Completable updateJoin(final long playlistId, final List<Long> streamIds) {
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streamIds.size());
for (int i = 0; i < streamIds.size(); i++) {
joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i));
}
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
playlistStreamTable.deleteBatch(playlistId);
playlistStreamTable.insertAll(joinEntities);
}));
}
public Maybe<List<PlaylistMetadataEntry>> getPlaylists() {
return playlistStreamTable.getPlaylistMetadata().firstElement();
}
}

View file

@ -0,0 +1,47 @@
package org.schabi.newpipe.fragments.playlist;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.Date;
import java.util.List;
import io.reactivex.MaybeObserver;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class StreamRecordManager {
private final AppDatabase database;
private final StreamDAO streamTable;
private final StreamHistoryDAO historyTable;
public StreamRecordManager(final AppDatabase db) {
database = db;
streamTable = db.streamDAO();
historyTable = db.streamHistoryDAO();
}
public int onChanged(final StreamInfoItem infoItem) {
// Only existing streams are updated
return streamTable.update(new StreamEntity(infoItem));
}
public Single<Long> onViewed(final StreamInfo info) {
return Single.fromCallable(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
return historyTable.insert(new StreamHistoryEntity(streamId, new Date()));
})).subscribeOn(Schedulers.io());
}
public int removeHistory(final long streamId) {
return historyTable.deleteHistory(streamId);
}
}

View file

@ -0,0 +1,30 @@
package org.schabi.newpipe.info_list.stored;
import org.schabi.newpipe.extractor.InfoItem;
import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID;
import static org.schabi.newpipe.util.Constants.NO_URL;
public class LocalPlaylistInfoItem extends InfoItem {
private final long playlistId;
private long streamCount;
public LocalPlaylistInfoItem(final long playlistId, final String name) {
super(InfoType.PLAYLIST, NO_SERVICE_ID, NO_URL, name);
this.playlistId = playlistId;
this.streamCount = streamCount;
}
public long getPlaylistId() {
return playlistId;
}
public long getStreamCount() {
return streamCount;
}
public void setStreamCount(long streamCount) {
this.streamCount = streamCount;
}
}

View file

@ -0,0 +1,38 @@
package org.schabi.newpipe.info_list.stored;
import org.schabi.newpipe.extractor.InfoItem;
import java.util.Date;
public class StreamStatisticsInfoItem extends InfoItem {
private final long streamId;
private Date latestAccessDate;
private long watchCount;
public StreamStatisticsInfoItem(final long streamId, final int serviceId,
final String url, final String name) {
super(InfoType.STREAM, serviceId, url, name);
this.streamId = streamId;
}
public long getStreamId() {
return streamId;
}
public Date getLatestAccessDate() {
return latestAccessDate;
}
public void setLatestAccessDate(Date latestAccessDate) {
this.latestAccessDate = latestAccessDate;
}
public long getWatchCount() {
return watchCount;
}
public void setWatchCount(long watchCount) {
this.watchCount = watchCount;
}
}

View file

@ -61,8 +61,10 @@ import com.google.android.exoplayer2.util.Util;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.playlist.StreamRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.CacheFactory;
import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.LoadController;
@ -77,9 +79,9 @@ import java.util.concurrent.TimeUnit;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers;
import io.reactivex.functions.Predicate;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
@ -147,6 +149,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
protected DefaultExtractorsFactory extractorsFactory; protected DefaultExtractorsFactory extractorsFactory;
protected Disposable progressUpdateReactor; protected Disposable progressUpdateReactor;
protected CompositeDisposable databaseUpdateReactor;
protected StreamRecordManager recordManager;
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -172,6 +177,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
public void initPlayer() { public void initPlayer() {
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
if (recordManager == null) {
recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context));
}
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
databaseUpdateReactor = new CompositeDisposable();
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
final LoadControl loadControl = new LoadController(context); final LoadControl loadControl = new LoadController(context);
@ -193,18 +204,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
private Disposable getProgressReactor() { private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.filter(new Predicate<Long>() { .filter(ignored -> isProgressLoopRunning())
@Override .subscribe(ignored -> triggerProgressUpdate());
public boolean test(Long aLong) throws Exception {
return isProgressLoopRunning();
}
})
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
triggerProgressUpdate();
}
});
} }
public void handleIntent(Intent intent) { public void handleIntent(Intent intent) {
@ -281,6 +282,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
if (playQueue != null) playQueue.dispose(); if (playQueue != null) playQueue.dispose();
if (playbackManager != null) playbackManager.dispose(); if (playbackManager != null) playbackManager.dispose();
if (audioReactor != null) audioReactor.abandonAudioFocus(); if (audioReactor != null) audioReactor.abandonAudioFocus();
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
} }
public void destroy() { public void destroy() {
@ -291,6 +293,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
trackSelector = null; trackSelector = null;
simpleExoPlayer = null; simpleExoPlayer = null;
recordManager = null;
} }
public MediaSource buildMediaSource(String url, String overrideExtension) { public MediaSource buildMediaSource(String url, String overrideExtension) {
@ -668,10 +671,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
"], queue index=[" + playQueue.getIndex() + "]"); "], queue index=[" + playQueue.getIndex() + "]");
} else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) {
final long startPos = info != null ? info.start_position : 0; final long startPos = info != null ? info.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
" at: " + getTimeString((int)startPos));
simpleExoPlayer.seekTo(currentSourceIndex, startPos); simpleExoPlayer.seekTo(currentSourceIndex, startPos);
} }
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe());
recordManager.removeRecord();
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
} }

View file

@ -12,4 +12,5 @@ public class Constants {
public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change";
public static final int NO_SERVICE_ID = -1; public static final int NO_SERVICE_ID = -1;
public static final String NO_URL = "";
} }