mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Working history DB implementation
This commit is contained in:
parent
f52b731615
commit
b65fc594dc
12 changed files with 392 additions and 150 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)---------");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
|
@ -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);
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue