mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 21:10:24 +02:00
Working history DB implementation
This commit is contained in:
parent
f52b731615
commit
b65fc594dc
@ -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.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)---------");
|
||||||
|
@ -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);
|
||||||
|
@ -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 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;
|
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user