DBStore improvements, query like support, more unittest, refactor into StateHistory, history indexes

This commit is contained in:
Kelvin 2023-12-01 16:09:41 +01:00
commit 585cf090d6
14 changed files with 396 additions and 276 deletions

View file

@ -203,17 +203,12 @@ class ManagedDBStoreTests {
} }
@Test @Test
fun queryPager() { fun queryPager() {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
val testStr = UUID.randomUUID().toString(); val testStr = UUID.randomUUID().toString();
testQuery(100, { i, testObject ->
val testResults = createSequence(store, 100, { i, testObject ->
if(i % 2 == 0) if(i % 2 == 0)
testObject.someStr = testStr; testObject.someStr = testStr;
}); }) {
val pager = store.queryPager(DBTOs.TestIndex::someString, testStr, 10); val pager = it.queryPager(DBTOs.TestIndex::someString, testStr, 10);
val items = pager.getResults().toMutableList(); val items = pager.getResults().toMutableList();
while(pager.hasMorePages()) { while(pager.hasMorePages()) {
@ -225,13 +220,88 @@ class ManagedDBStoreTests {
val k = i * 2; val k = i * 2;
Assert.assertEquals(k, items[i].someNum); Assert.assertEquals(k, items[i].someNum);
} }
}
store.shutdown();
} }
@Test
fun queryLike() {
val testStr = UUID.randomUUID().toString();
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStrLike;
}) {
val results = it.queryLike(DBTOs.TestIndex::someString, "%Testing%");
Assert.assertEquals(50, results.size);
}
}
@Test
fun queryLikePager() {
val testStr = UUID.randomUUID().toString();
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
testQuery(100, { i, testObject ->
if(i % 2 == 0)
testObject.someStr = testStrLike;
}) {
val pager = it.queryLikePager(DBTOs.TestIndex::someString, "%Testing%", 10);
val items = pager.getResults().toMutableList();
while(pager.hasMorePages()) {
pager.nextPage();
items.addAll(pager.getResults());
}
Assert.assertEquals(50, items.size);
for(i in 0 until 50) {
val k = i * 2;
Assert.assertEquals(k, items[i].someNum);
}
}
}
@Test
fun queryGreater() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.queryGreater(DBTOs.TestIndex::someNum, 51);
Assert.assertEquals(48, results.size);
}
}
@Test
fun querySmaller() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.querySmaller(DBTOs.TestIndex::someNum, 30);
Assert.assertEquals(30, results.size);
}
}
@Test
fun queryBetween() {
testQuery(100, { i, testObject ->
testObject.someNum = i;
}) {
val results = it.queryBetween(DBTOs.TestIndex::someNum, 30, 65);
Assert.assertEquals(34, results.size);
}
}
private fun testQuery(items: Int, modifier: (Int, DBTOs.TestObject)->Unit, testing: (ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>)->Unit) {
val store = ManagedDBStore.create("test", Descriptor())
.load(context, true);
store.deleteAll();
createSequence(store, items, modifier);
try {
testing(store);
}
finally {
store.shutdown();
}
}
private fun createSequence(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List<DBTOs.TestIndex> { private fun createSequence(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List<DBTOs.TestIndex> {

View file

@ -106,7 +106,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
} }
val posBefore = _results.size; val posBefore = _results.size;
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }; val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(toAdd); _results.addAll(toAdd);
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); }; _adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
}.exception<Throwable> { }.exception<Throwable> {

View file

@ -12,6 +12,7 @@ import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.others.TagsView import com.futo.platformplayer.views.others.TagsView
@ -58,7 +59,7 @@ class HistoryFragment : MainFragment() {
tagsView.onClick.subscribe { timeMinutesToErase -> tagsView.onClick.subscribe { timeMinutesToErase ->
UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), { UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), {
StatePlaylists.instance.removeHistoryRange(timeMinutesToErase.second as Long); StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed)); UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed));
adapter.updateFilteredVideos(); adapter.updateFilteredVideos();
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();

View file

@ -778,7 +778,7 @@ class VideoDetailView : ConstraintLayout {
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){ suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
val current = _historyIndex; val current = _historyIndex;
if(current == null || current.url != video.url) { if(current == null || current.url != video.url) {
val index = StatePlaylists.instance.getHistoryByVideo(video, true); val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
_historyIndex = index; _historyIndex = index;
return@withContext index; return@withContext index;
} }
@ -1290,7 +1290,7 @@ class VideoDetailView : ConstraintLayout {
val historyItem = getHistoryIndex(videoDetail); val historyItem = getHistoryIndex(videoDetail);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_historicalPosition = StatePlaylists.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong()); _historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"); Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) { if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_layoutResume.visibility = View.VISIBLE; _layoutResume.visibility = View.VISIBLE;
@ -2107,7 +2107,7 @@ class VideoDetailView : ConstraintLayout {
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) { if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
val history = getHistoryIndex(v); val history = getHistoryIndex(v);
StatePlaylists.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong()); StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
} }
_lastPositionSaveTime = currentTime; _lastPositionSaveTime = currentTime;
} }

View file

@ -526,10 +526,8 @@ class StateApp {
StatePlaylists.instance.toMigrateCheck(); StatePlaylists.instance.toMigrateCheck();
StatePlaylists.instance._historyDBStore.deleteAll(); if(StateHistory.instance.shouldMigrateLegacyHistory())
StateHistory.instance.migrateLegacyHistory();
if(StatePlaylists.instance.shouldMigrateLegacyHistory())
StatePlaylists.instance.migrateLegacyHistory();
if(false) { if(false) {
@ -544,63 +542,12 @@ class StateApp {
testHistoryDB(4000); testHistoryDB(4000);
Logger.i(TAG, "TEST:--------(6000)---------"); Logger.i(TAG, "TEST:--------(6000)---------");
testHistoryDB(6000); testHistoryDB(6000);
*/
Logger.i(TAG, "TEST:--------(100000)---------"); Logger.i(TAG, "TEST:--------(100000)---------");
scope.launch(Dispatchers.Default) { scope.launch(Dispatchers.Default) {
testHistoryDB(100000); StateHistory.instance.testHistoryDB(100000);
} }
}
}
fun testHistoryDB(count: Int) {
Logger.i(TAG, "TEST: Starting tests");
StatePlaylists.instance._historyDBStore.deleteAll();
val testHistoryItem = StatePlaylists.instance.getHistoryLegacy().first();
val testItemJson = testHistoryItem.video.toJson();
val now = OffsetDateTime.now();
val testSet = (0..count).map { HistoryVideo(Json.decodeFromString<SerializedPlatformVideo>(testItemJson.replace(testHistoryItem.video.url, UUID.randomUUID().toString())), it.toLong(), now.minusHours(it.toLong())) }
Logger.i(TAG, "TEST: Inserting (${testSet.size})");
val insertMS = measureTimeMillis {
for(item in testSet)
StatePlaylists.instance._historyDBStore.insert(item);
};
Logger.i(TAG, "TEST: Inserting in ${insertMS}ms");
/*
var fetched: List<DBHistory.Index>? = null;
val fetchMS = measureTimeMillis {
fetched = StatePlaylists.instance._historyDBStore.getAll();
Logger.i(TAG, "TEST: Fetched: ${fetched?.size}");
};
Logger.i(TAG, "TEST: Fetch speed ${fetchMS}MS");
val deserializeMS = measureTimeMillis {
val deserialized = StatePlaylists.instance._historyDBStore.convertObjects(fetched!!);
Logger.i(TAG, "TEST: Deserialized: ${deserialized.size}");
};
Logger.i(TAG, "TEST: Deserialize speed ${deserializeMS}MS");
*/ */
var fetchedIndex: List<DBHistory.Index>? = null;
val fetchIndexMS = measureTimeMillis {
fetchedIndex = StatePlaylists.instance._historyDBStore.getAllIndexes();
Logger.i(TAG, "TEST: Fetched Index: ${fetchedIndex!!.size}");
};
Logger.i(TAG, "TEST: Fetched Index speed ${fetchIndexMS}ms");
val fetchFromIndex = measureTimeMillis {
for(preItem in testSet) {
val item = StatePlaylists.instance.historyIndex[preItem.video.url];
if(item == null)
throw IllegalStateException("Missing item [${preItem.video.url}]");
if(item.url != preItem.video.url)
throw IllegalStateException("Mismatch item [${preItem.video.url}]");
} }
};
Logger.i(TAG, "TEST: Index Lookup speed ${fetchFromIndex}ms");
val page1 = StatePlaylists.instance._historyDBStore.getPage(0, 20);
val page2 = StatePlaylists.instance._historyDBStore.getPage(1, 20);
val page3 = StatePlaylists.instance._historyDBStore.getPage(2, 20);
} }
fun mainAppStartedWithExternalFiles(context: Context) { fun mainAppStartedWithExternalFiles(context: Context) {

View file

@ -5,16 +5,12 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.serializers.PlatformContentSerializer import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBChannelCache import com.futo.platformplayer.stores.db.types.DBSubscriptionCache
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.toSafeFileName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -22,27 +18,27 @@ import java.time.OffsetDateTime
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class StateCache { class StateCache {
private val _channelCache = ManagedDBStore.create("channelCache", DBChannelCache.Descriptor(), PlatformContentSerializer()) private val _subscriptionCache = ManagedDBStore.create("subscriptionCache", DBSubscriptionCache.Descriptor(), PlatformContentSerializer())
.load(); .load();
val channelCacheStartupCount = _channelCache.count(); val channelCacheStartupCount = _subscriptionCache.count();
fun clear() { fun clear() {
_channelCache.deleteAll(); _subscriptionCache.deleteAll();
} }
fun clearToday() { fun clearToday() {
val today = _channelCache.queryGreater(DBChannelCache.Index::datetime, OffsetDateTime.now().toEpochSecond()); val today = _subscriptionCache.queryGreater(DBSubscriptionCache.Index::datetime, OffsetDateTime.now().toEpochSecond());
for(content in today) for(content in today)
_channelCache.delete(content); _subscriptionCache.delete(content);
} }
fun getChannelCachePager(channelUrl: String): IPager<IPlatformContent> { fun getChannelCachePager(channelUrl: String): IPager<IPlatformContent> {
return _channelCache.queryPager(DBChannelCache.Index::channelUrl, channelUrl, 20) { return _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, channelUrl, 20) {
it.obj; it.obj;
} }
} }
fun getChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> { fun getChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val pagers = MultiChronoContentPager(channelUrls.map { _channelCache.queryPager(DBChannelCache.Index::channelUrl, it, 20) { val pagers = MultiChronoContentPager(channelUrls.map { _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, it, 20) {
it.obj; it.obj;
} }, false, 20); } }, false, 20);
return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id }); return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id });
@ -70,14 +66,14 @@ class StateCache {
} }
fun getCachedContent(url: String): DBChannelCache.Index? { fun getCachedContent(url: String): DBSubscriptionCache.Index? {
return _channelCache.query(DBChannelCache.Index::url, url).firstOrNull(); return _subscriptionCache.query(DBSubscriptionCache.Index::url, url).firstOrNull();
} }
fun uncacheContent(content: SerializedPlatformContent) { fun uncacheContent(content: SerializedPlatformContent) {
val item = getCachedContent(content.url); val item = getCachedContent(content.url);
if(item != null) if(item != null)
_channelCache.delete(item); _subscriptionCache.delete(item);
} }
fun cacheContents(contents: List<IPlatformContent>, doUpdate: Boolean = false): List<IPlatformContent> { fun cacheContents(contents: List<IPlatformContent>, doUpdate: Boolean = false): List<IPlatformContent> {
return contents.filter { cacheContent(it, doUpdate) }; return contents.filter { cacheContent(it, doUpdate) };
@ -90,11 +86,11 @@ class StateCache {
val existing = getCachedContent(content.url); val existing = getCachedContent(content.url);
if(existing != null && doUpdate) { if(existing != null && doUpdate) {
_channelCache.update(existing.id!!, serialized); _subscriptionCache.update(existing.id!!, serialized);
return true; return true;
} }
else if(existing == null) { else if(existing == null) {
_channelCache.insert(serialized); _subscriptionCache.insert(serialized);
return true; return true;
} }

View file

@ -0,0 +1,215 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory
import com.futo.platformplayer.stores.v2.ReconstructStore
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.system.measureTimeMillis
class StateHistory {
//Legacy
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
= HistoryVideo.fromReconString(backup, null);
})
.load();
private val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex, false, true)
.load();
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();
fun shouldMigrateLegacyHistory(): Boolean {
return _historyDBStore.count() == 0 && _historyStore.count() > 0;
}
fun migrateLegacyHistory() {
Logger.i(StatePlaylists.TAG, "Migrating legacy history");
_historyDBStore.deleteAll();
val allHistory = _historyStore.getItems();
Logger.i(StatePlaylists.TAG, "Migrating legacy history (${allHistory.size}) items");
for(item in allHistory) {
_historyDBStore.insert(item);
}
_historyStore.deleteAll();
}
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 getHistoryLegacy(): List<HistoryVideo> {
return _historyStore.getItems();
}
fun getHistory() : List<HistoryVideo> {
return _historyDBStore.getAllObjects();
//return _historyStore.getItems().sortedByDescending { it.date };
}
fun getHistoryPager(): IPager<HistoryVideo> {
return _historyDBStore.getObjectPager();
}
fun getHistorySearchPager(query: String): IPager<HistoryVideo> {
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::url, "%${query}%", 10);
}
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 if(create) {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, OffsetDateTime.now());
val id = _historyDBStore.insert(newHistItem);
return _historyDBStore.get(id);
}
return null;
}
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.datetime) < 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);*/
}
companion object {
val TAG = "StateHistory";
private var _instance : StateHistory? = null;
val instance : StateHistory
get(){
if(_instance == null)
_instance = StateHistory();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
}
fun testHistoryDB(count: Int) {
Logger.i(TAG, "TEST: Starting tests");
_historyDBStore.deleteAll();
val testHistoryItem = getHistoryLegacy().first();
val testItemJson = testHistoryItem.video.toJson();
val now = OffsetDateTime.now();
val testSet = (0..count).map { HistoryVideo(Json.decodeFromString<SerializedPlatformVideo>(testItemJson.replace(testHistoryItem.video.url, UUID.randomUUID().toString())), it.toLong(), now.minusHours(it.toLong())) }
Logger.i(TAG, "TEST: Inserting (${testSet.size})");
val insertMS = measureTimeMillis {
for(item in testSet)
_historyDBStore.insert(item);
};
Logger.i(TAG, "TEST: Inserting in ${insertMS}ms");
var fetched: List<DBHistory.Index>? = null;
val fetchMS = measureTimeMillis {
fetched = _historyDBStore.getAll();
Logger.i(TAG, "TEST: Fetched: ${fetched?.size}");
};
Logger.i(TAG, "TEST: Fetch speed ${fetchMS}MS");
val deserializeMS = measureTimeMillis {
val deserialized = _historyDBStore.convertObjects(fetched!!);
Logger.i(TAG, "TEST: Deserialized: ${deserialized.size}");
};
Logger.i(TAG, "TEST: Deserialize speed ${deserializeMS}MS");
var fetchedIndex: List<DBHistory.Index>? = null;
val fetchIndexMS = measureTimeMillis {
fetchedIndex = _historyDBStore.getAllIndexes();
Logger.i(TAG, "TEST: Fetched Index: ${fetchedIndex!!.size}");
};
Logger.i(TAG, "TEST: Fetched Index speed ${fetchIndexMS}ms");
val fetchFromIndex = measureTimeMillis {
for(preItem in testSet) {
val item = historyIndex[preItem.video.url];
if(item == null)
throw IllegalStateException("Missing item [${preItem.video.url}]");
if(item.url != preItem.video.url)
throw IllegalStateException("Mismatch item [${preItem.video.url}]");
}
};
Logger.i(TAG, "TEST: Index Lookup speed ${fetchFromIndex}ms");
val page1 = _historyDBStore.getPage(0, 20);
val page2 = _historyDBStore.getPage(1, 20);
val page3 = _historyDBStore.getPage(2, 20);
}
}

View file

@ -45,165 +45,17 @@ class StatePlaylists {
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails); = SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
}) })
.load(); .load();
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
= HistoryVideo.fromReconString(backup, null);
})
.load();
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists") val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
.withRestore(PlaylistBackup()) .withRestore(PlaylistBackup())
.load(); .load();
val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
.withIndex({ it.url }, historyIndex, false, true)
.load();
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares"); val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
var onHistoricVideoChanged = Event2<IPlatformVideo, Long>();
val onWatchLaterChanged = Event0(); val onWatchLaterChanged = Event0();
fun toMigrateCheck(): List<ManagedStore<*>> { fun toMigrateCheck(): List<ManagedStore<*>> {
return listOf(playlistStore, _watchlistStore, _historyStore); return listOf(playlistStore, _watchlistStore);
} }
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 getHistoryLegacy(): List<HistoryVideo> {
return _historyStore.getItems();
}
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();

View file

@ -232,6 +232,12 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj)); val query = SimpleSQLiteQuery(queryStr, arrayOf(obj));
return deserializeIndexes(dbDaoBase.getMultiple(query)); return deserializeIndexes(dbDaoBase.getMultiple(query));
} }
fun queryLike(field: KProperty<*>, obj: String): List<I> = queryLike(validateFieldName(field), obj);
fun queryLike(field: String, obj: String): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryGreater(field: KProperty<*>, obj: Any): List<I> = queryGreater(validateFieldName(field), obj); fun queryGreater(field: KProperty<*>, obj: Any): List<I> = queryGreater(validateFieldName(field), obj);
fun queryGreater(field: String, obj: Any): List<I> { fun queryGreater(field: String, obj: Any): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} > ?"; val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} > ?";
@ -251,17 +257,30 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
return deserializeIndexes(dbDaoBase.getMultiple(query)); return deserializeIndexes(dbDaoBase.getMultiple(query));
} }
//Query Pages
fun queryPage(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List<I> = queryPage(validateFieldName(field), obj, page, pageSize);
fun queryPage(field: String, obj: Any, page: Int, pageSize: Int): List<I> { fun queryPage(field: String, obj: Any, page: Int, pageSize: Int): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} = ? ${_orderSQL} LIMIT ? OFFSET ?"; val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} = ? ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize)); val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize));
return deserializeIndexes(dbDaoBase.getMultiple(query)); return deserializeIndexes(dbDaoBase.getMultiple(query));
} }
fun queryPage(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List<I> = queryPage(validateFieldName(field), obj, page, pageSize);
fun queryLikePage(field: KProperty<*>, obj: String, page: Int, pageSize: Int): List<I> = queryLikePage(validateFieldName(field), obj, page, pageSize);
fun queryLikePage(field: String, obj: String, page: Int, pageSize: Int): List<I> {
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize));
return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryLikeObjectPage(field: String, obj: String, page: Int, pageSize: Int): List<T> {
return convertObjects(queryLikePage(field, obj, page, pageSize));
}
//Query Page Objects
fun queryPageObjects(field: String, obj: Any, page: Int, pageSize: Int): List<T> = convertObjects(queryPage(field, obj, page, pageSize)); fun queryPageObjects(field: String, obj: Any, page: Int, pageSize: Int): List<T> = convertObjects(queryPage(field, obj, page, pageSize));
fun queryPageObjects(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List<T> = queryPageObjects(validateFieldName(field), obj, page, pageSize); fun queryPageObjects(field: KProperty<*>, obj: Any, page: Int, pageSize: Int): List<T> = queryPageObjects(validateFieldName(field), obj, page, pageSize);
//Query Pager
fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager<I> = queryPager(validateFieldName(field), obj, pageSize); fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager<I> = queryPager(validateFieldName(field), obj, pageSize);
fun queryPager(field: String, obj: Any, pageSize: Int): IPager<I> { fun queryPager(field: String, obj: Any, pageSize: Int): IPager<I> {
return AdhocPager({ return AdhocPager({
@ -269,6 +288,23 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
queryPage(field, obj, it - 1, pageSize); queryPage(field, obj, it - 1, pageSize);
}); });
} }
fun queryLikePager(field: KProperty<*>, obj: String, pageSize: Int): IPager<I> = queryLikePager(validateFieldName(field), obj, pageSize);
fun queryLikePager(field: String, obj: String, pageSize: Int): IPager<I> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryLikePage(field, obj, it - 1, pageSize);
});
}
fun queryLikeObjectPager(field: KProperty<*>, obj: String, pageSize: Int): IPager<T> = queryLikeObjectPager(validateFieldName(field), obj, pageSize);
fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager<T> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryLikeObjectPage(field, obj, it - 1, pageSize);
});
}
//Query Pager with convert
fun <X> queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> = queryPager(validateFieldName(field), obj, pageSize, convert); fun <X> queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> = queryPager(validateFieldName(field), obj, pageSize, convert);
fun <X> queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> { fun <X> queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> {
return AdhocPager({ return AdhocPager({
@ -276,6 +312,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
}); });
} }
//Query Object Pager
fun queryObjectPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager<T> = queryObjectPager(validateFieldName(field), obj, pageSize); fun queryObjectPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager<T> = queryObjectPager(validateFieldName(field), obj, pageSize);
fun queryObjectPager(field: String, obj: Any, pageSize: Int): IPager<T> { fun queryObjectPager(field: String, obj: Any, pageSize: Int): IPager<T> {
return AdhocPager({ return AdhocPager({
@ -283,6 +320,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
}); });
} }
//Page
fun getPage(page: Int, length: Int): List<I> { fun getPage(page: Int, length: Int): List<I> {
if(_sqlPage == null) if(_sqlPage == null)
throw IllegalStateException("DB Store [${name}] does not have ordered fields to provide pages"); throw IllegalStateException("DB Store [${name}] does not have ordered fields to provide pages");

View file

@ -28,7 +28,7 @@ class DBHistory {
//These classes solely exist for bounding generics for type erasure //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 = 3)
abstract class DB: ManagedDBDatabase<HistoryVideo, Index, DBDAO>() { abstract class DB: ManagedDBDatabase<HistoryVideo, Index, DBDAO>() {
abstract override fun base(): DBDAO; abstract override fun base(): DBDAO;
} }
@ -40,32 +40,34 @@ class DBHistory {
override fun indexClass(): KClass<Index> = Index::class; override fun indexClass(): KClass<Index> = Index::class;
} }
@Entity(TABLE_NAME) @Entity(TABLE_NAME, indices = [
class Index: ManagedDBIndex<HistoryVideo> { androidx.room.Index(value = ["url"]),
androidx.room.Index(value = ["name"]),
androidx.room.Index(value = ["datetime"], orders = [androidx.room.Index.Order.DESC])
])
class Index(): ManagedDBIndex<HistoryVideo>() {
@PrimaryKey(true) @PrimaryKey(true)
@ColumnOrdered(1) @ColumnOrdered(1)
@ColumnIndex @ColumnIndex
override var id: Long? = null; override var id: Long? = null;
@ColumnIndex @ColumnIndex
var url: String; var url: String = "";
@ColumnIndex @ColumnIndex
var position: Long; var position: Long = 0;
@ColumnIndex @ColumnIndex
@ColumnOrdered(0, true) @ColumnOrdered(0, true)
var date: Long; var datetime: Long = 0;
@ColumnIndex
var name: String = "";
constructor() { constructor(historyVideo: HistoryVideo) : this() {
url = "";
position = 0;
date = 0;
}
constructor(historyVideo: HistoryVideo) {
id = null; id = null;
serialized = null; serialized = null;
url = historyVideo.video.url; url = historyVideo.video.url;
position = historyVideo.position; position = historyVideo.position;
date = historyVideo.date.toEpochSecond(); datetime = historyVideo.date.toEpochSecond();
name = historyVideo.video.name;
} }
} }
} }

View file

@ -1,33 +1,28 @@
package com.futo.platformplayer.stores.db.types package com.futo.platformplayer.stores.db.types
import androidx.room.ColumnInfo
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Database import androidx.room.Database
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
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.stores.db.ColumnIndex import com.futo.platformplayer.stores.db.ColumnIndex
import com.futo.platformplayer.stores.db.ColumnOrdered 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 java.time.OffsetDateTime
import kotlin.reflect.KClass import kotlin.reflect.KClass
class DBChannelCache { class DBSubscriptionCache {
companion object { companion object {
const val TABLE_NAME = "feed_cache"; const val TABLE_NAME = "subscription_cache";
} }
//These classes solely exist for bounding generics for type erasure //These classes solely exist for bounding generics for type erasure
@Dao @Dao
interface DBDAO: ManagedDBDAOBase<SerializedPlatformContent, Index> {} interface DBDAO: ManagedDBDAOBase<SerializedPlatformContent, Index> {}
@Database(entities = [Index::class], version = 4) @Database(entities = [Index::class], version = 5)
abstract class DB: ManagedDBDatabase<SerializedPlatformContent, Index, DBDAO>() { abstract class DB: ManagedDBDatabase<SerializedPlatformContent, Index, DBDAO>() {
abstract override fun base(): DBDAO; abstract override fun base(): DBDAO;
} }

View file

@ -15,7 +15,7 @@ import java.util.UUID
class DBTOs { class DBTOs {
@Dao @Dao
interface DBDAO: ManagedDBDAOBase<TestObject, TestIndex> {} interface DBDAO: ManagedDBDAOBase<TestObject, TestIndex> {}
@Database(entities = [TestIndex::class], version = 2) @Database(entities = [TestIndex::class], version = 3)
abstract class DB: ManagedDBDatabase<TestObject, TestIndex, DBDAO>() { abstract class DB: ManagedDBDatabase<TestObject, TestIndex, DBDAO>() {
abstract override fun base(): DBDAO; abstract override fun base(): DBDAO;
} }

View file

@ -8,6 +8,7 @@ import com.futo.platformplayer.api.media.structures.IPager
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.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -22,7 +23,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
constructor() : super() { constructor() : super() {
updateFilteredVideos(); updateFilteredVideos();
StatePlaylists.instance.onHistoricVideoChanged.subscribe(this) { video, position -> StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
StateApp.instance.scope.launch(Dispatchers.Main) { StateApp.instance.scope.launch(Dispatchers.Main) {
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url }; val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
if (index == -1) { if (index == -1) {
@ -45,7 +46,9 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
} }
fun updateFilteredVideos() { fun updateFilteredVideos() {
val videos = StatePlaylists.instance.getHistory(); val videos = StateHistory.instance.getHistory();
val pager = StateHistory.instance.getHistoryPager();
//filtered val pager = StateHistory.instance.getHistorySearchPager("querrryyyyy");
if (_query.isBlank()) { if (_query.isBlank()) {
_filteredVideos = videos.toMutableList(); _filteredVideos = videos.toMutableList();
@ -57,7 +60,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
} }
fun cleanup() { fun cleanup() {
StatePlaylists.instance.onHistoricVideoChanged.remove(this); StateHistory.instance.onHistoricVideoChanged.remove(this);
} }
override fun getItemCount() = _filteredVideos.size; override fun getItemCount() = _filteredVideos.size;
@ -73,7 +76,7 @@ class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
return@subscribe; return@subscribe;
} }
StatePlaylists.instance.removeHistory(v.video.url); StateHistory.instance.removeHistory(v.video.url);
_filteredVideos.removeAt(index); _filteredVideos.removeAt(index);
notifyItemRemoved(index); notifyItemRemoved(index);
}; };

View file

@ -27,6 +27,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
@ -246,7 +247,7 @@ open class PreviewVideoView : LinearLayout {
val timeBar = _timeBar val timeBar = _timeBar
if (timeBar != null) { if (timeBar != null) {
if (shouldShowTimeBar) { if (shouldShowTimeBar) {
val historyPosition = StatePlaylists.instance.getHistoryPosition(video.url) val historyPosition = StateHistory.instance.getHistoryPosition(video.url)
timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE
timeBar.progress = historyPosition.toFloat() / video.duration.toFloat() timeBar.progress = historyPosition.toFloat() / video.duration.toFloat()
} else { } else {