diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 7111abcf7..4da1c63f2 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -28,4 +28,10 @@ public final class NewPipeDatabase { return databaseInstance; } + + @NonNull + public static AppDatabase getInstance(Context context) { + if (databaseInstance == null) init(context); + return databaseInstance; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 21868e3c2..e09687ce4 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -4,16 +4,31 @@ import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; 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.WatchHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; 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.SubscriptionEntity; @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 static final String DATABASE_NAME = "newpipe.db"; @@ -23,4 +38,12 @@ public abstract class AppDatabase extends RoomDatabase { public abstract WatchHistoryDAO watchHistoryDAO(); public abstract SearchHistoryDAO searchHistoryDAO(); + + public abstract StreamDAO streamDAO(); + + public abstract StreamHistoryDAO streamHistoryDAO(); + + public abstract PlaylistDAO playlistDAO(); + + public abstract PlaylistStreamDAO playlistStreamDAO(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index 03a94508b..425c122ca 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -23,9 +23,6 @@ public interface BasicDAO { @Insert(onConflict = OnConflictStrategy.FAIL) List insertAll(final Collection entities); - @Insert(onConflict = OnConflictStrategy.REPLACE) - long upsert(final Entity entity); - /* Searches */ Flowable> getAll(); diff --git a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java similarity index 63% rename from app/src/main/java/org/schabi/newpipe/database/history/Converters.java rename to app/src/main/java/org/schabi/newpipe/database/Converters.java index 093c741f1..d48fbfaf1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/Converters.java +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.java @@ -1,7 +1,9 @@ -package org.schabi.newpipe.database.history; +package org.schabi.newpipe.database; import android.arch.persistence.room.TypeConverter; +import org.schabi.newpipe.extractor.stream.StreamType; + import java.util.Date; public class Converters { @@ -25,4 +27,14 @@ public class Converters { public static Long dateToTimestamp(Date date) { 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(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java new file mode 100644 index 000000000..53ae3d48a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java new file mode 100644 index 000000000..b337769bc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -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 { + @Override + @Query("SELECT * FROM " + PLAYLIST_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + PLAYLIST_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") + public abstract Flowable> getPlaylist(final long playlistId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java new file mode 100644 index 000000000..b9f325aa2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -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 { + @Override + @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> 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 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> 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> getPlaylistMetadata(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java new file mode 100644 index 000000000..a3ec1b5f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java new file mode 100644 index 000000000..3d71f7e70 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java new file mode 100644 index 000000000..5893394c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java new file mode 100644 index 000000000..f7807ef42 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.java @@ -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 { + @Override + @Query("SELECT * FROM " + STREAM_TABLE) + public abstract Flowable> 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> listByService(int serviceId); + + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " LIKE :url AND " + + STREAM_SERVICE_ID + " = :serviceId") + public abstract Flowable> getStream(long serviceId, String url); + + @Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + + STREAM_URL + " LIKE :url AND " + + STREAM_SERVICE_ID + " = :serviceId") + abstract List getStreamInternal(long serviceId, String url); + + @Transaction + public long upsert(StreamEntity stream) { + final List 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java new file mode 100644 index 000000000..19c7b9e90 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -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 { + @Override + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_HISTORY_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> 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> getStatistics(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java new file mode 100644 index 000000000..27d0aa7e1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java new file mode 100644 index 000000000..d937a29ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamHistoryEntity.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index e71088ac9..60eb0c3d3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -50,8 +50,7 @@ public class SubscriptionEntity { return uid; } - /* Keep this package-private since UID should always be auto generated by Room impl */ - void setUid(long uid) { + public void setUid(long uid) { this.uid = uid; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java new file mode 100644 index 000000000..db32a392e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java @@ -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> createPlaylist(final String name, final List 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 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 appendToPlaylist(final long playlistId, final StreamEntity stream) { + final Maybe streamIdFuture = Maybe.fromCallable(() -> streamTable.upsert(stream)); + final Maybe 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 streamIds) { + List 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> getPlaylists() { + return playlistStreamTable.getPlaylistMetadata().firstElement(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java new file mode 100644 index 000000000..bd5bd36a2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java @@ -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 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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java new file mode 100644 index 000000000..63f61cc43 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java new file mode 100644 index 000000000..ef82826ba --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 55a73d484..ad2200bfc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -61,8 +61,10 @@ import com.google.android.exoplayer2.util.Util; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; 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.CacheFactory; import org.schabi.newpipe.player.helper.LoadController; @@ -77,9 +79,9 @@ import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Predicate; +import io.reactivex.schedulers.Schedulers; 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 Disposable progressUpdateReactor; + protected CompositeDisposable databaseUpdateReactor; + + protected StreamRecordManager recordManager; //////////////////////////////////////////////////////////////////////////*/ @@ -172,6 +177,12 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public void initPlayer() { 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 AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); final LoadControl loadControl = new LoadController(context); @@ -193,18 +204,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen private Disposable getProgressReactor() { return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .filter(new Predicate() { - @Override - public boolean test(Long aLong) throws Exception { - return isProgressLoopRunning(); - } - }) - .subscribe(new Consumer() { - @Override - public void accept(Long aLong) throws Exception { - triggerProgressUpdate(); - } - }); + .filter(ignored -> isProgressLoopRunning()) + .subscribe(ignored -> triggerProgressUpdate()); } public void handleIntent(Intent intent) { @@ -281,6 +282,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen if (playQueue != null) playQueue.dispose(); if (playbackManager != null) playbackManager.dispose(); if (audioReactor != null) audioReactor.abandonAudioFocus(); + if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); } public void destroy() { @@ -291,6 +293,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen trackSelector = null; simpleExoPlayer = null; + recordManager = null; } public MediaSource buildMediaSource(String url, String overrideExtension) { @@ -668,10 +671,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen "], queue index=[" + playQueue.getIndex() + "]"); } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) { 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); } + databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); + recordManager.removeRecord(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java index a6aec96e2..4238424d9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -12,4 +12,5 @@ public class Constants { public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change"; public static final int NO_SERVICE_ID = -1; + public static final String NO_URL = ""; }