Working history DB implementation

This commit is contained in:
Kelvin 2023-11-20 21:27:27 +01:00
parent f52b731615
commit b65fc594dc
12 changed files with 392 additions and 150 deletions

View file

@ -0,0 +1,31 @@
package com.futo.platformplayer.api.media.structures
class AdhocPager<T>: IPager<T> {
private var _page = 0;
private val _nextPage: (Int) -> List<T>;
private var _currentResults: List<T> = listOf();
private var _hasMore = true;
constructor(nextPage: (Int) -> List<T>, initialResults: List<T>? = null){
_nextPage = nextPage;
if(initialResults != null)
_currentResults = initialResults;
else
nextPage();
}
override fun hasMorePages(): Boolean {
return _hasMore;
}
override fun nextPage() {
val newResults = _nextPage(++_page);
if(newResults.isEmpty())
_hasMore = false;
_currentResults = newResults;
}
override fun getResults(): List<T> {
return _currentResults;
}
}

View file

@ -75,6 +75,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
import com.futo.platformplayer.views.casting.CastView
@ -125,6 +126,7 @@ class VideoDetailView : ConstraintLayout {
var video: IPlatformVideoDetails? = null
private set;
private var _playbackTracker: IPlaybackTracker? = null;
private var _historyIndex: DBHistory.Index? = null;
val currentUrl get() = video?.url ?: _searchVideo?.url ?: _url;
@ -772,6 +774,15 @@ class VideoDetailView : ConstraintLayout {
}
}
}
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
val current = _historyIndex;
if(current == null || current.url != video.url) {
val index = StatePlaylists.instance.getHistoryByVideo(video, true);
_historyIndex = index;
return@withContext index;
}
return@withContext current;
}
//Lifecycle
@ -1248,24 +1259,30 @@ class VideoDetailView : ConstraintLayout {
updateQueueState();
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, false, (toResume.toFloat() / 1000.0f).toLong());
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
fragment.lifecycleScope.launch(Dispatchers.IO) {
val historyItem = getHistoryIndex(videoDetail);
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
delay(8000);
withContext(Dispatchers.Main) {
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
delay(8000);
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
}
}
} else {
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
}
}
} else {
_layoutResume.visibility = View.GONE;
_textResume.text = "";
}
@ -1568,7 +1585,7 @@ class VideoDetailView : ConstraintLayout {
if(localVideoSources?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video",
*localVideoSources.stream()
*localVideoSources
.map {
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, "${it.width}x${it.height}", it,
{ handleSelectVideoTrack(it) });
@ -1576,7 +1593,7 @@ class VideoDetailView : ConstraintLayout {
else null,
if(localAudioSource?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
*localAudioSource.stream()
*localAudioSource
.map {
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
{ handleSelectAudioTrack(it) });
@ -1592,7 +1609,7 @@ class VideoDetailView : ConstraintLayout {
else null,
if(liveStreamVideoFormats?.isEmpty() == false)
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
*liveStreamVideoFormats.stream()
*liveStreamVideoFormats
.map {
SlideUpMenuItem(this.context, R.drawable.ic_movie, it?.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it,
{ _player.selectVideoTrack(it.height) });
@ -1600,7 +1617,7 @@ class VideoDetailView : ConstraintLayout {
else null,
if(liveStreamAudioFormats?.isEmpty() == false)
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
*liveStreamAudioFormats.stream()
*liveStreamAudioFormats
.map {
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it?.label ?: it.containerMimeType} ${it.bitrate}", "", it,
{ _player.selectAudioTrack(it.bitrate) });
@ -1609,7 +1626,7 @@ class VideoDetailView : ConstraintLayout {
if(bestVideoSources.isNotEmpty())
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
*bestVideoSources.stream()
*bestVideoSources
.map {
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", it,
{ handleSelectVideoTrack(it) });
@ -1617,7 +1634,7 @@ class VideoDetailView : ConstraintLayout {
else null,
if(bestAudioSources.isNotEmpty())
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
*bestAudioSources.stream()
*bestAudioSources
.map {
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
{ handleSelectAudioTrack(it) });
@ -2049,7 +2066,10 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return;
val currentTime = System.currentTimeMillis();
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
StatePlaylists.instance.updateHistoryPosition(v, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
fragment.lifecycleScope.launch(Dispatchers.IO) {
val history = getHistoryIndex(v);
StatePlaylists.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
}
_lastPositionSaveTime = currentTime;
}

View file

@ -59,20 +59,6 @@ import kotlin.system.measureTimeMillis
class StateApp {
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
/*
private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
fun getExternalRootDirectory(): File? {
if(!externalRootDirectory.exists()) {
val result = externalRootDirectory.mkdirs();
if(!result)
return null;
return externalRootDirectory;
}
else
return externalRootDirectory;
}*/
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
val generalUri = Settings.instance.storage.getStorageGeneralUri();
if(isValidStorageUri(context, generalUri))
@ -539,8 +525,14 @@ class StateApp {
StateAnnouncement.instance.registerDidYouKnow();
Logger.i(TAG, "MainApp Started: Finished");
StatePlaylists.instance.toMigrateCheck();
if(true) {
StatePlaylists.instance._historyDBStore.deleteAll();
if(StatePlaylists.instance.shouldMigrateLegacyHistory())
StatePlaylists.instance.migrateLegacyHistory();
if(false) {
Logger.i(TAG, "TEST:--------(200)---------");
testHistoryDB(200);
Logger.i(TAG, "TEST:--------(1000)---------");

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
@ -58,6 +59,7 @@ class StatePlaylists {
val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex)
.withUnique({ it.url }, historyIndex)
.load();
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
@ -69,6 +71,137 @@ class StatePlaylists {
return listOf(playlistStore, _watchlistStore, _historyStore);
}
fun shouldMigrateLegacyHistory(): Boolean {
return _historyDBStore.count() == 0 && _historyStore.count() > 0;
}
fun migrateLegacyHistory() {
Logger.i(TAG, "Migrating legacy history");
_historyDBStore.deleteAll();
val allHistory = _historyStore.getItems();
Logger.i(TAG, "Migrating legacy history (${allHistory.size}) items");
for(item in allHistory) {
_historyDBStore.insert(item);
}
}
fun getHistoryPosition(url: String): Long {
return historyIndex[url]?.position ?: 0;
}
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
val pos = if(position < 0) 0 else position;
if(index.obj == null) throw IllegalStateException("Can only update history with a deserialized db item");
val historyVideo = index.obj!!;
val positionBefore = historyVideo.position;
if (updateExisting) {
var shouldUpdate = false;
if (positionBefore < 30) {
shouldUpdate = true;
} else {
if (position > 30) {
shouldUpdate = true;
}
}
if (shouldUpdate) {
//A unrecovered item
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
historyVideo.video = SerializedPlatformVideo.fromVideo(liveObj);
historyVideo.position = pos;
historyVideo.date = OffsetDateTime.now();
_historyDBStore.update(index.id!!, historyVideo);
onHistoricVideoChanged.emit(liveObj, pos);
}
return positionBefore;
}
return positionBefore;
}
/*
fun updateHistoryPosition(video: IPlatformVideo, updateExisting: Boolean, position: Long = -1L): Long {
val pos = if(position < 0) 0 else position;
val historyVideo = _historyStore.findItem { it.video.url == video.url };
if (historyVideo != null) {
val positionBefore = historyVideo.position;
if (updateExisting) {
var shouldUpdate = false;
if (positionBefore < 30) {
shouldUpdate = true;
} else {
if (position > 30) {
shouldUpdate = true;
}
}
if (shouldUpdate) {
//A unrecovered item
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
historyVideo.video = SerializedPlatformVideo.fromVideo(video);
historyVideo.position = pos;
historyVideo.date = OffsetDateTime.now();
_historyStore.saveAsync(historyVideo);
onHistoricVideoChanged.emit(video, pos);
}
}
return positionBefore;
} else {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), pos, OffsetDateTime.now());
_historyStore.saveAsync(newHistItem);
return 0;
}
}
*/
fun getHistory() : List<HistoryVideo> {
return _historyDBStore.getAllObjects();
//return _historyStore.getItems().sortedByDescending { it.date };
}
fun getHistoryPager(): IPager<HistoryVideo> {
return _historyDBStore.getObjectPager();
}
fun getHistoryIndexByUrl(url: String): DBHistory.Index? {
return historyIndex[url];
}
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false): DBHistory.Index {
val existing = historyIndex[video.url];
if(existing != null)
return _historyDBStore.get(existing.id!!);
else {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, OffsetDateTime.now());
val id = _historyDBStore.insert(newHistItem);
return _historyDBStore.get(id);
}
}
fun removeHistory(url: String) {
val hist = getHistoryIndexByUrl(url);
if(hist != null)
_historyDBStore.delete(hist.id!!);
/*
val hist = _historyStore.findItem { it.video.url == url };
if(hist != null)
_historyStore.delete(hist);*/
}
fun removeHistoryRange(minutesToDelete: Long) {
val now = OffsetDateTime.now().toEpochSecond();
val toDelete = _historyDBStore.getAllIndexes().filter { minutesToDelete == -1L || (now - it.date) < minutesToDelete * 60 };
for(item in toDelete)
_historyDBStore.delete(item);
/*
val now = OffsetDateTime.now();
val toDelete = _historyStore.findItems { minutesToDelete == -1L || ChronoUnit.MINUTES.between(it.date, now) < minutesToDelete };
for(item in toDelete)
_historyStore.delete(item);*/
}
fun getWatchLater() : List<SerializedPlatformVideo> {
synchronized(_watchlistStore) {
return _watchlistStore.getItems();
@ -109,6 +242,7 @@ class StatePlaylists {
return playlistStore.findItem { it.id == id };
}
fun didPlay(playlistId: String) {
val playlist = getPlaylist(playlistId);
if(playlist != null) {
@ -117,66 +251,6 @@ class StatePlaylists {
}
}
fun getHistoryPosition(url: String): Long {
val histVideo = _historyStore.findItem { it.video.url == url };
if(histVideo != null)
return histVideo.position;
return 0;
}
fun updateHistoryPosition(video: IPlatformVideo, updateExisting: Boolean, position: Long = -1L): Long {
val pos = if(position < 0) 0 else position;
val historyVideo = _historyStore.findItem { it.video.url == video.url };
if (historyVideo != null) {
val positionBefore = historyVideo.position;
if (updateExisting) {
var shouldUpdate = false;
if (positionBefore < 30) {
shouldUpdate = true;
} else {
if (position > 30) {
shouldUpdate = true;
}
}
if (shouldUpdate) {
//A unrecovered item
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
historyVideo.video = SerializedPlatformVideo.fromVideo(video);
historyVideo.position = pos;
historyVideo.date = OffsetDateTime.now();
_historyStore.saveAsync(historyVideo);
onHistoricVideoChanged.emit(video, pos);
}
}
return positionBefore;
} else {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), pos, OffsetDateTime.now());
_historyStore.saveAsync(newHistItem);
return 0;
}
}
fun getHistory() : List<HistoryVideo> {
return _historyStore.getItems().sortedByDescending { it.date };
}
fun removeHistory(url: String) {
val hist = _historyStore.findItem { it.video.url == url };
if(hist != null)
_historyStore.delete(hist);
}
fun removeHistoryRange(minutesToDelete: Long) {
val now = OffsetDateTime.now();
val toDelete = _historyStore.findItems { minutesToDelete == -1L || ChronoUnit.MINUTES.between(it.date, now) < minutesToDelete };
for(item in toDelete)
_historyStore.delete(item);
}
suspend fun createPlaylistFromChannel(channelUrl: String, onPage: (Int) -> Unit): Playlist {
val channel = StatePlatform.instance.getChannel(channelUrl).await();
return createPlaylistFromChannel(channel, onPage);

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.stores.db
import androidx.room.ColumnInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ColumnIndex(val name: String = ColumnInfo.INHERIT_FIELD_NAME)

View file

@ -0,0 +1,5 @@
package com.futo.platformplayer.stores.db
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class ColumnOrdered(val priority: Int, val descending: Boolean = false);

View file

@ -3,14 +3,13 @@ package com.futo.platformplayer.stores.db
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.types.DBHistory
import kotlin.reflect.KClass
abstract class ManagedDBDescriptor<T, I: ManagedDBIndex<T>, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
abstract fun dbClass(): Class<D>;
abstract val table_name: String;
abstract fun dbClass(): KClass<D>;
abstract fun create(obj: T): I;
open val ordered: String? = null;
open fun sqlIndexOnly(tableName: String): SimpleSQLiteQuery? = null;
open fun sqlPage(tableName: String, page: Int, length: Int): SimpleSQLiteQuery? = null;
abstract fun indexClass(): KClass<I>;
}

View file

@ -5,10 +5,13 @@ import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.futo.platformplayer.api.media.Serializer
interface ManagedDBIndex<T> {
var id: Long?
var serialized: ByteArray?
open class ManagedDBIndex<T> {
@ColumnIndex
@PrimaryKey(true)
open var id: Long? = null;
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var serialized: ByteArray? = null;
@get:Ignore
var obj: T?;
@Ignore
var obj: T? = null;
}

View file

@ -1,15 +1,26 @@
package com.futo.platformplayer.stores.db
import androidx.room.ColumnInfo
import androidx.room.Room
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.assume
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
private val _class: KType;
@ -22,16 +33,25 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
private var _dbDescriptor: ManagedDBDescriptor<T, I, D, DA>;
private val _columnInfo: List<ColumnMetadata>;
private val _sqlGet: (Long)-> SimpleSQLiteQuery;
private val _sqlGetAll: (LongArray)-> SimpleSQLiteQuery;
private val _sqlAll: SimpleSQLiteQuery;
private val _sqlCount: SimpleSQLiteQuery;
private val _sqlDeleteAll: SimpleSQLiteQuery;
private val _sqlDeleteById: (Long) -> SimpleSQLiteQuery;
private var _sqlIndexed: SimpleSQLiteQuery? = null;
private var _sqlPage: ((Int, Int) -> SimpleSQLiteQuery)? = null;
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
val name: String;
private val _indexes: ArrayList<Pair<(I)->Any, ConcurrentMap<Any, I>>> = arrayListOf();
private val _indexCollection = ConcurrentHashMap<Long, I>();
private var _withUnique: Pair<(I)->Any, ConcurrentMap<Any, I>>? = null;
constructor(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
_dbDescriptor = descriptor;
@ -43,23 +63,52 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
};
_serializer = serializer;
_class = clazz;
_columnInfo = _dbDescriptor.indexClass().memberProperties
.filter { it.hasAnnotation<ColumnIndex>() && it.name != "serialized" }
.map { ColumnMetadata(it.javaField!!, it.findAnnotation<ColumnIndex>()!!, it.findAnnotation<ColumnOrdered>()) };
_sqlAll = SimpleSQLiteQuery("SELECT * FROM $_name" + if(descriptor.ordered.isNullOrEmpty()) "" else " ${descriptor.ordered}");
_sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${_name}");
_sqlIndexed = descriptor.sqlIndexOnly(_name);
val indexColumnNames = _columnInfo.map { it.name };
val orderedColumns = _columnInfo.filter { it.ordered != null }.sortedBy { it.ordered!!.priority };
val orderSQL = if(orderedColumns.size > 0)
" ORDER BY " + orderedColumns.map { "${it.name} ${if(it.ordered!!.descending) "DESC" else "ASC"}" }.joinToString(", ");
else "";
_sqlGet = { SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} WHERE id = ?", arrayOf(it)) };
_sqlGetAll = { SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} WHERE id IN (?)", arrayOf(it)) };
_sqlAll = SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} ${orderSQL}");
_sqlCount = SimpleSQLiteQuery("SELECT COUNT(id) FROM ${_dbDescriptor.table_name}");
_sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${_dbDescriptor.table_name}");
_sqlDeleteById = { id -> SimpleSQLiteQuery("DELETE FROM ${_dbDescriptor.table_name} WHERE id = :id", arrayOf(id)) };
_sqlIndexed = SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${_dbDescriptor.table_name}");
if(orderedColumns.size > 0) {
_sqlPage = { page, length ->
SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} ${orderSQL} LIMIT ? OFFSET ?", arrayOf(length, page * length));
}
}
}
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>): ManagedDBStore<I, T, D, DA> {
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>, withUnique: Boolean = false): ManagedDBStore<I, T, D, DA> {
if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
_indexes.add(Pair(keySelector, indexContainer));
if(withUnique)
withUnique(keySelector, indexContainer);
return this;
}
fun withUnique(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>): ManagedDBStore<I, T, D, DA> {
if(_withUnique != null)
throw IllegalStateException("Only 1 unique property is allowed");
_withUnique = Pair(keySelector, indexContainer);
return this;
}
fun load(): ManagedDBStore<I, T, D, DA> {
_db = Room.databaseBuilder(StateApp.instance.context, _dbDescriptor.dbClass(), _name)
_db = Room.databaseBuilder(StateApp.instance.context, _dbDescriptor.dbClass().java, _name)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
@ -73,18 +122,41 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
return this;
}
fun insert(obj: T) {
fun getUnique(obj: I): I? {
if(_withUnique == null)
throw IllegalStateException("Unique is not configured for ${name}");
val key = _withUnique!!.first.invoke(obj);
return _withUnique!!.second[key];
}
fun isUnique(obj: I): Boolean {
if(_withUnique == null)
throw IllegalStateException("Unique is not configured for ${name}");
val key = _withUnique!!.first.invoke(obj);
return !_withUnique!!.second.containsKey(key);
}
fun count(): Int {
return dbDaoBase.action(_sqlCount);
}
fun insert(obj: T): Long {
val newIndex = _dbDescriptor.create(obj);
val unique = getUnique(newIndex);
if(unique != null)
return unique.id!!;
newIndex.serialized = serialize(obj);
newIndex.id = dbDaoBase.insert(newIndex);
newIndex.serialized = null;
if(!_indexes.isEmpty()) {
for (index in _indexes) {
val key = index.first(newIndex);
index.second.put(key, newIndex);
}
}
return newIndex.id!!;
}
fun update(id: Long, obj: T) {
val newIndex = _dbDescriptor.create(obj);
@ -109,45 +181,72 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
fun getAllObjects(): List<T> = convertObjects(getAll());
fun getAll(): List<I> {
return dbDaoBase.getMultiple(_sqlAll);
return deserializeIndexes(dbDaoBase.getMultiple(_sqlAll));
}
fun getObject(id: Long) = convertObject(get(id));
fun getObject(id: Long) = get(id).obj!!;
fun get(id: Long): I {
return dbDaoBase.get(SimpleSQLiteQuery("SELECT * FROM $_name WHERE id = ?", arrayOf(id)));
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
}
fun getAllObjects(vararg id: Long): List<T> = convertObjects(getAll(*id));
fun getAllObjects(vararg id: Long): List<T> = getAll(*id).map { it.obj!! };
fun getAll(vararg id: Long): List<I> {
return dbDaoBase.getMultiple(SimpleSQLiteQuery("SELECT * FROM $_name WHERE id IN (?)", arrayOf(id)));
return deserializeIndexes(dbDaoBase.getMultiple(_sqlGetAll(id)));
}
fun getPageObjects(page: Int, length: Int): List<T> = convertObjects(getPage(page, length));
fun getObjectPage(page: Int, length: Int): List<T> = convertObjects(getPage(page, length));
fun getObjectPager(pageLength: Int = 20): IPager<T> {
return AdhocPager({
getObjectPage(it - 1, pageLength);
});
}
fun getPage(page: Int, length: Int): List<I> {
val query = _dbDescriptor.sqlPage(_name, page, length) ?: throw IllegalStateException("Paged db not setup for ${_name}");
if(_sqlPage == null)
throw IllegalStateException("DB Store [${name}] does not have ordered fields to provide pages");
val query = _sqlPage!!(page, length) ?: throw IllegalStateException("Paged db not setup for ${_name}");
return dbDaoBase.getMultiple(query);
}
fun getPager(pageLength: Int = 20): IPager<I> {
return AdhocPager({
getPage(it - 1, pageLength);
});
}
fun delete(item: I) {
dbDaoBase.delete(item);
for(index in _indexes)
index.second.remove(index.first(item));
}
fun delete(id: Long) {
dbDaoBase.action(_sqlDeleteById(id));
for(index in _indexes)
index.second.values.removeIf { it.id == id }
}
fun deleteAll() {
dbDaoBase.action(_sqlDeleteAll);
_indexCollection.clear();
for(index in _indexes)
index.second.clear();
}
fun convertObject(index: ManagedDBIndex<T>): T? {
return index.serialized?.let {
_serializer.deserialize(_class, it);
};
fun convertObject(index: I): T? {
return index.obj ?: deserializeIndex(index).obj;
}
fun convertObjects(indexes: List<ManagedDBIndex<T>>): List<T> {
return indexes.mapNotNull { convertObject(it) };
fun convertObjects(indexes: List<I>): List<T> {
return indexes.mapNotNull { it.obj ?: convertObject(it) };
}
fun deserializeIndex(index: I): I {
if(index.serialized == null) throw IllegalStateException("Cannot deserialize index-only items from [${name}]");
val obj = _serializer.deserialize(_class, index.serialized!!);
index.obj = obj;
return index;
}
fun deserializeIndexes(indexes: List<I>): List<I> {
for(index in indexes)
deserializeIndex(index);
return indexes;
}
fun serialize(obj: T): ByteArray {
@ -158,4 +257,12 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
inline fun <reified T, I: ManagedDBIndex<T>, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> create(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, serializer: KSerializer<T>? = null)
= ManagedDBStore(name, descriptor, kotlin.reflect.typeOf<T>(), JsonStoreSerializer.create(serializer));
}
class ColumnMetadata(
val field: Field,
val info: ColumnIndex,
val ordered: ColumnOrdered?
) {
val name get() = if(info.name == ColumnInfo.INHERIT_FIELD_NAME) field.name else info.name;
}
}

View file

@ -15,11 +15,6 @@ class DBChannelCache {
class Index: ManagedDBIndex<SerializedPlatformContent> {
@PrimaryKey(true)
override var id: Long? = null;
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
override var serialized: ByteArray? = null;
@Ignore
override var obj: SerializedPlatformContent? = null;
var feedType: String? = null;
var channelUrl: String? = null;

View file

@ -10,11 +10,14 @@ import androidx.room.Query
import androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.ColumnIndex
import com.futo.platformplayer.stores.db.ColumnOrdered
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
import com.futo.platformplayer.stores.db.ManagedDBDatabase
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
import com.futo.platformplayer.stores.db.ManagedDBIndex
import com.futo.platformplayer.stores.db.ManagedDBStore
import kotlin.reflect.KClass
import kotlin.reflect.KType
class DBHistory {
@ -22,6 +25,7 @@ class DBHistory {
const val TABLE_NAME = "history";
}
//These classes solely exist for bounding generics for type erasure
@Dao
interface DBDAO: ManagedDBDAOBase<HistoryVideo, Index> {}
@Database(entities = [Index::class], version = 2)
@ -30,26 +34,25 @@ class DBHistory {
}
class Descriptor: ManagedDBDescriptor<HistoryVideo, Index, DB, DBDAO>() {
override val table_name: String = TABLE_NAME;
override fun create(obj: HistoryVideo): Index = Index(obj);
override fun dbClass(): Class<DB> = DB::class.java;
//Optional
override fun sqlIndexOnly(tableName: String): SimpleSQLiteQuery = SimpleSQLiteQuery("SELECT id, url, position, date FROM $TABLE_NAME");
override fun sqlPage(tableName: String, page: Int, length: Int): SimpleSQLiteQuery = SimpleSQLiteQuery("SELECT * FROM $TABLE_NAME ORDER BY date DESC, id DESC LIMIT ? OFFSET ?", arrayOf(length, page * length));
override fun dbClass(): KClass<DB> = DB::class;
override fun indexClass(): KClass<Index> = Index::class;
}
@Entity(TABLE_NAME)
class Index: ManagedDBIndex<HistoryVideo> {
@PrimaryKey(true)
@ColumnOrdered(1)
@ColumnIndex
override var id: Long? = null;
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
override var serialized: ByteArray? = null;
@Ignore
override var obj: HistoryVideo? = null;
@ColumnIndex
var url: String;
@ColumnIndex
var position: Long;
@ColumnIndex
@ColumnOrdered(0, true)
var date: Long;
constructor() {

View file

@ -6,7 +6,11 @@ import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
private lateinit var _filteredVideos: MutableList<HistoryVideo>;
@ -18,16 +22,18 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
updateFilteredVideos();
StatePlaylists.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
if (index == -1) {
return@subscribe;
}
StateApp.instance.scope.launch(Dispatchers.Main) {
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
if (index == -1) {
return@launch;
}
_filteredVideos[index].position = position;
if (index < _filteredVideos.size - 2) {
notifyItemRangeChanged(index, 2);
} else {
notifyItemChanged(index);
_filteredVideos[index].position = position;
if (index < _filteredVideos.size - 2) {
notifyItemRangeChanged(index, 2);
} else {
notifyItemChanged(index);
}
}
};
}