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.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage 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.MonetizationView
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
import com.futo.platformplayer.views.casting.CastView import com.futo.platformplayer.views.casting.CastView
@ -125,6 +126,7 @@ class VideoDetailView : ConstraintLayout {
var video: IPlatformVideoDetails? = null var video: IPlatformVideoDetails? = null
private set; private set;
private var _playbackTracker: IPlaybackTracker? = null; private var _playbackTracker: IPlaybackTracker? = null;
private var _historyIndex: DBHistory.Index? = null;
val currentUrl get() = video?.url ?: _searchVideo?.url ?: _url; 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 //Lifecycle
@ -1248,24 +1259,30 @@ class VideoDetailView : ConstraintLayout {
updateQueueState(); updateQueueState();
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, false, (toResume.toFloat() / 1000.0f).toLong()); fragment.lifecycleScope.launch(Dispatchers.IO) {
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"); val historyItem = getHistoryIndex(videoDetail);
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) { withContext(Dispatchers.Main) {
try { _historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
delay(8000); 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; _layoutResume.visibility = View.GONE;
_textResume.text = ""; _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) if(localVideoSources?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video", SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video",
*localVideoSources.stream() *localVideoSources
.map { .map {
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, "${it.width}x${it.height}", it, SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, "${it.width}x${it.height}", it,
{ handleSelectVideoTrack(it) }); { handleSelectVideoTrack(it) });
@ -1576,7 +1593,7 @@ class VideoDetailView : ConstraintLayout {
else null, else null,
if(localAudioSource?.isNotEmpty() == true) if(localAudioSource?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio", SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
*localAudioSource.stream() *localAudioSource
.map { .map {
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it, SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
{ handleSelectAudioTrack(it) }); { handleSelectAudioTrack(it) });
@ -1592,7 +1609,7 @@ class VideoDetailView : ConstraintLayout {
else null, else null,
if(liveStreamVideoFormats?.isEmpty() == false) if(liveStreamVideoFormats?.isEmpty() == false)
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video", SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
*liveStreamVideoFormats.stream() *liveStreamVideoFormats
.map { .map {
SlideUpMenuItem(this.context, R.drawable.ic_movie, it?.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it, SlideUpMenuItem(this.context, R.drawable.ic_movie, it?.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it,
{ _player.selectVideoTrack(it.height) }); { _player.selectVideoTrack(it.height) });
@ -1600,7 +1617,7 @@ class VideoDetailView : ConstraintLayout {
else null, else null,
if(liveStreamAudioFormats?.isEmpty() == false) if(liveStreamAudioFormats?.isEmpty() == false)
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio", SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
*liveStreamAudioFormats.stream() *liveStreamAudioFormats
.map { .map {
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it?.label ?: it.containerMimeType} ${it.bitrate}", "", it, SlideUpMenuItem(this.context, R.drawable.ic_music, "${it?.label ?: it.containerMimeType} ${it.bitrate}", "", it,
{ _player.selectAudioTrack(it.bitrate) }); { _player.selectAudioTrack(it.bitrate) });
@ -1609,7 +1626,7 @@ class VideoDetailView : ConstraintLayout {
if(bestVideoSources.isNotEmpty()) if(bestVideoSources.isNotEmpty())
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video", SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
*bestVideoSources.stream() *bestVideoSources
.map { .map {
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", it, 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) }); { handleSelectVideoTrack(it) });
@ -1617,7 +1634,7 @@ class VideoDetailView : ConstraintLayout {
else null, else null,
if(bestAudioSources.isNotEmpty()) if(bestAudioSources.isNotEmpty())
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio", SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
*bestAudioSources.stream() *bestAudioSources
.map { .map {
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it, SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
{ handleSelectAudioTrack(it) }); { handleSelectAudioTrack(it) });
@ -2049,7 +2066,10 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return; val v = video ?: return;
val currentTime = System.currentTimeMillis(); val currentTime = System.currentTimeMillis();
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) { 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; _lastPositionSaveTime = currentTime;
} }

View File

@ -59,20 +59,6 @@ import kotlin.system.measureTimeMillis
class StateApp { class StateApp {
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active 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? { fun getExternalGeneralDirectory(context: Context): DocumentFile? {
val generalUri = Settings.instance.storage.getStorageGeneralUri(); val generalUri = Settings.instance.storage.getStorageGeneralUri();
if(isValidStorageUri(context, generalUri)) if(isValidStorageUri(context, generalUri))
@ -539,8 +525,14 @@ class StateApp {
StateAnnouncement.instance.registerDidYouKnow(); StateAnnouncement.instance.registerDidYouKnow();
Logger.i(TAG, "MainApp Started: Finished"); 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)---------"); Logger.i(TAG, "TEST:--------(200)---------");
testHistoryDB(200); testHistoryDB(200);
Logger.i(TAG, "TEST:--------(1000)---------"); 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.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo 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.Event0
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
@ -58,6 +59,7 @@ class StatePlaylists {
val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap(); val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor()) val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex) .withIndex({ it.url }, historyIndex)
.withUnique({ it.url }, historyIndex)
.load(); .load();
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares"); val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
@ -69,6 +71,137 @@ class StatePlaylists {
return listOf(playlistStore, _watchlistStore, _historyStore); 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> { fun getWatchLater() : List<SerializedPlatformVideo> {
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
return _watchlistStore.getItems(); return _watchlistStore.getItems();
@ -109,6 +242,7 @@ class StatePlaylists {
return playlistStore.findItem { it.id == id }; return playlistStore.findItem { it.id == id };
} }
fun didPlay(playlistId: String) { fun didPlay(playlistId: String) {
val playlist = getPlaylist(playlistId); val playlist = getPlaylist(playlistId);
if(playlist != null) { 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 { suspend fun createPlaylistFromChannel(channelUrl: String, onPage: (Int) -> Unit): Playlist {
val channel = StatePlatform.instance.getChannel(channelUrl).await(); val channel = StatePlatform.instance.getChannel(channelUrl).await();
return createPlaylistFromChannel(channel, onPage); 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 androidx.sqlite.db.SimpleSQLiteQuery
import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.db.types.DBHistory 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 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; abstract fun create(obj: T): I;
open val ordered: String? = null; abstract fun indexClass(): KClass<I>;
open fun sqlIndexOnly(tableName: String): SimpleSQLiteQuery? = null;
open fun sqlPage(tableName: String, page: Int, length: Int): SimpleSQLiteQuery? = null;
} }

View File

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

View File

@ -1,15 +1,26 @@
package com.futo.platformplayer.stores.db package com.futo.platformplayer.stores.db
import androidx.room.ColumnInfo
import androidx.room.Room import androidx.room.Room
import androidx.sqlite.db.SimpleSQLiteQuery 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.assume
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp 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.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap import java.util.concurrent.ConcurrentMap
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType 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>> { class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA: ManagedDBDAOBase<T, I>> {
private val _class: KType; 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 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 _sqlAll: SimpleSQLiteQuery;
private val _sqlCount: SimpleSQLiteQuery;
private val _sqlDeleteAll: SimpleSQLiteQuery; private val _sqlDeleteAll: SimpleSQLiteQuery;
private val _sqlDeleteById: (Long) -> SimpleSQLiteQuery;
private var _sqlIndexed: SimpleSQLiteQuery? = null; private var _sqlIndexed: SimpleSQLiteQuery? = null;
private var _sqlPage: ((Int, Int) -> SimpleSQLiteQuery)? = null;
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName; val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
val name: String; val name: String;
private val _indexes: ArrayList<Pair<(I)->Any, ConcurrentMap<Any, I>>> = arrayListOf(); 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) { constructor(name: String, descriptor: ManagedDBDescriptor<T, I, D, DA>, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
_dbDescriptor = descriptor; _dbDescriptor = descriptor;
@ -43,23 +63,52 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
}; };
_serializer = serializer; _serializer = serializer;
_class = clazz; _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}"); val indexColumnNames = _columnInfo.map { it.name };
_sqlDeleteAll = SimpleSQLiteQuery("DELETE FROM ${_name}");
_sqlIndexed = descriptor.sqlIndexOnly(_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) if(_sqlIndexed == null)
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented"); throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
_indexes.add(Pair(keySelector, indexContainer)); _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; return this;
} }
fun load(): ManagedDBStore<I, T, D, DA> { 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() .fallbackToDestructiveMigration()
.allowMainThreadQueries() .allowMainThreadQueries()
.build() .build()
@ -73,18 +122,41 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
return this; 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 newIndex = _dbDescriptor.create(obj);
val unique = getUnique(newIndex);
if(unique != null)
return unique.id!!;
newIndex.serialized = serialize(obj); newIndex.serialized = serialize(obj);
newIndex.id = dbDaoBase.insert(newIndex); newIndex.id = dbDaoBase.insert(newIndex);
newIndex.serialized = null; newIndex.serialized = null;
if(!_indexes.isEmpty()) { if(!_indexes.isEmpty()) {
for (index in _indexes) { for (index in _indexes) {
val key = index.first(newIndex); val key = index.first(newIndex);
index.second.put(key, newIndex); index.second.put(key, newIndex);
} }
} }
return newIndex.id!!;
} }
fun update(id: Long, obj: T) { fun update(id: Long, obj: T) {
val newIndex = _dbDescriptor.create(obj); 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 getAllObjects(): List<T> = convertObjects(getAll());
fun getAll(): List<I> { 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 { 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> { 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> { 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); return dbDaoBase.getMultiple(query);
} }
fun getPager(pageLength: Int = 20): IPager<I> {
return AdhocPager({
getPage(it - 1, pageLength);
});
}
fun delete(item: I) { fun delete(item: I) {
dbDaoBase.delete(item); dbDaoBase.delete(item);
for(index in _indexes) for(index in _indexes)
index.second.remove(index.first(item)); 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() { fun deleteAll() {
dbDaoBase.action(_sqlDeleteAll); dbDaoBase.action(_sqlDeleteAll);
_indexCollection.clear();
for(index in _indexes) for(index in _indexes)
index.second.clear(); index.second.clear();
} }
fun convertObject(index: I): T? {
fun convertObject(index: ManagedDBIndex<T>): T? { return index.obj ?: deserializeIndex(index).obj;
return index.serialized?.let {
_serializer.deserialize(_class, it);
};
} }
fun convertObjects(indexes: List<ManagedDBIndex<T>>): List<T> { fun convertObjects(indexes: List<I>): List<T> {
return indexes.mapNotNull { convertObject(it) }; 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 { 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) 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)); = 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> { class Index: ManagedDBIndex<SerializedPlatformContent> {
@PrimaryKey(true) @PrimaryKey(true)
override var id: Long? = null; 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 feedType: String? = null;
var channelUrl: String? = null; var channelUrl: String? = null;

View File

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

View File

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