From c5541b1747e34db94137b64e59548bd5e66d7a24 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 30 Nov 2023 20:58:37 +0100 Subject: [PATCH] Working DBCache, test plugin --- .../java/com/futo/platformplayer/Settings.kt | 3 +- .../com/futo/platformplayer/SettingsDev.kt | 74 +++++- .../cache/ChannelContentCache.kt | 213 ------------------ .../channel/tab/ChannelContentsFragment.kt | 4 +- .../fragment/mainactivity/main/FeedView.kt | 7 + .../main/SubscriptionsFeedFragment.kt | 24 +- .../futo/platformplayer/states/StateApp.kt | 3 +- .../futo/platformplayer/states/StateCache.kt | 81 ++++++- .../states/StateSubscriptions.kt | 1 - .../stores/db/ManagedDBStore.kt | 2 + .../stores/db/types/DBChannelCache.kt | 11 +- .../CachedSubscriptionAlgorithm.kt | 11 +- .../SimpleSubscriptionAlgorithm.kt | 6 +- .../SubscriptionsTaskFetchAlgorithm.kt | 8 +- app/src/main/res/values/strings.xml | 3 + .../assets/sources/test/TestConfig.json | 24 ++ .../assets/sources/test/TestScript.js | 45 ++++ .../unstable/assets/sources/test/odysee.png | Bin 0 -> 47198 bytes 18 files changed, 266 insertions(+), 254 deletions(-) delete mode 100644 app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt create mode 100644 app/src/unstable/assets/sources/test/TestConfig.json create mode 100644 app/src/unstable/assets/sources/test/TestScript.js create mode 100644 app/src/unstable/assets/sources/test/odysee.png diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 8daa4736..541c2844 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -8,7 +8,6 @@ import android.webkit.CookieManager import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.activities.* import com.futo.platformplayer.api.http.ManagedHttpClient -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.logging.Logger @@ -276,7 +275,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14) fun clearChannelCache() { UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing.."); - ChannelContentCache.instance.clear(); + StateCache.instance.clear(); UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing"); } } diff --git a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt index 8be80aa9..111911f1 100644 --- a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt +++ b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer import android.content.Context import android.webkit.CookieManager +import androidx.lifecycle.lifecycleScope import androidx.work.Constraints import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy @@ -20,12 +21,12 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.background.BackgroundWorker -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateSubscriptions @@ -82,26 +83,74 @@ class SettingsDev : FragmentedStorageFileJson() { var backgroundSubscriptionFetching: Boolean = false; } + + @FormField(R.string.cache, FieldForm.GROUP, -1, 3) + val cache: Cache = Cache(); + @Serializable + class Cache { + + @FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1) + fun subscriptionsCache5000() { + StateApp.instance.scope.launch(Dispatchers.IO) { + try { + val subsCache = + StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this)?.first; + + var total = 0; + var page = 0; + var lastToast = System.currentTimeMillis(); + while(subsCache!!.hasMorePages() && total < 5000) { + subsCache!!.nextPage(); + total += subsCache!!.getResults().size; + page++; + + if(page % 10 == 0) + withContext(Dispatchers.Main) { + val diff = System.currentTimeMillis() - lastToast; + lastToast = System.currentTimeMillis(); + UIDialogs.toast( + SettingsActivity.getActivity()!!, + "Page: ${page}, Total: ${total}, Speed: ${diff}ms" + ); + } + Thread.sleep(250); + } + + withContext(Dispatchers.Main) { + UIDialogs.toast( + SettingsActivity.getActivity()!!, + "FINISHED Page: ${page}, Total: ${total}" + ); + } + } + catch(ex: Throwable) { + Logger.e("SettingsDev", ex.message, ex); + Logger.i("SettingsDev", "Failed: ${ex.message}"); + } + } + } + } + @FormField(R.string.crash_me, FieldForm.BUTTON, - R.string.crashes_the_application_on_purpose, 2) + R.string.crashes_the_application_on_purpose, 3) fun crashMe() { throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!"); } @FormField(R.string.delete_announcements, FieldForm.BUTTON, - R.string.delete_all_announcements, 2) + R.string.delete_all_announcements, 3) fun deleteAnnouncements() { StateAnnouncement.instance.deleteAllAnnouncements(); } @FormField(R.string.clear_cookies, FieldForm.BUTTON, - R.string.clear_all_cookies_from_the_cookieManager, 2) + R.string.clear_all_cookies_from_the_cookieManager, 3) fun clearCookies() { val cookieManager: CookieManager = CookieManager.getInstance() cookieManager.removeAllCookies(null); } @FormField(R.string.test_background_worker, FieldForm.BUTTON, - R.string.test_background_worker_description, 3) + R.string.test_background_worker_description, 4) fun triggerBackgroundUpdate() { val act = SettingsActivity.getActivity()!!; UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker"); @@ -113,10 +162,10 @@ class SettingsDev : FragmentedStorageFileJson() { wm.enqueue(req); } @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, - R.string.test_background_worker_description, 3) + R.string.test_background_worker_description, 4) fun clearChannelContentCache() { UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache"); - ChannelContentCache.instance.clearToday(); + StateCache.instance.clearToday(); UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared"); } @@ -363,6 +412,17 @@ class SettingsDev : FragmentedStorageFileJson() { } } + + @Contextual + @Transient + @FormField(R.string.info, FieldForm.GROUP, -1, 19) + var info = Info(); + @Serializable + class Info { + @FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize") + var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount; + } + //region BOILERPLATE override fun encode(): String { return Json.encodeToString(this); diff --git a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt deleted file mode 100644 index 87614cc0..00000000 --- a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt +++ /dev/null @@ -1,213 +0,0 @@ -package com.futo.platformplayer.cache - -import com.futo.platformplayer.api.media.models.contents.IPlatformContent -import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent -import com.futo.platformplayer.api.media.structures.DedupContentPager -import com.futo.platformplayer.api.media.structures.IPager -import com.futo.platformplayer.api.media.structures.PlatformContentPager -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache -import com.futo.platformplayer.resolveChannelUrl -import com.futo.platformplayer.serializers.PlatformContentSerializer -import com.futo.platformplayer.states.StatePlatform -import com.futo.platformplayer.states.StateSubscriptions -import com.futo.platformplayer.stores.FragmentedStorage -import com.futo.platformplayer.stores.v2.ManagedStore -import com.futo.platformplayer.toSafeFileName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.time.OffsetDateTime -import kotlin.streams.toList -import kotlin.system.measureTimeMillis - -class ChannelContentCache { - private val _targetCacheSize = 3000; - val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache"); - val _channelContents: HashMap>; - init { - val allFiles = _channelCacheDir.listFiles() ?: arrayOf(); - val initializeTime = measureTimeMillis { - _channelContents = HashMap(allFiles - .filter { it.isDirectory } - .parallelStream().map { - Pair(it.name, FragmentedStorage.storeJson(_channelCacheDir, it.name, PlatformContentSerializer()) - .withoutBackup() - .load()) - }.toList().associate { it }) - } - val minDays = OffsetDateTime.now().minusDays(10); - val totalItems = _channelContents.map { it.value.count() }.sum(); - val toTrim = totalItems - _targetCacheSize; - val trimmed: Int; - if(toTrim > 0) { - val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) } - .sortedBy { it.datetime!! }.take(toTrim); - for(content in redundantContent) - uncacheContent(content); - trimmed = redundantContent.size; - } - else trimmed = 0; - Logger.i(TAG, "ChannelContentCache time: ${initializeTime}ms channels: ${allFiles.size}, videos: ${totalItems}, trimmed: ${trimmed}, total: ${totalItems - trimmed}"); - } - - fun clear() { - synchronized(_channelContents) { - for(channel in _channelContents) - for(content in channel.value.getItems()) - uncacheContent(content); - } - } - fun clearToday() { - val yesterday = OffsetDateTime.now().minusDays(1); - synchronized(_channelContents) { - for(channel in _channelContents) - for(content in channel.value.getItems().filter { it.datetime?.isAfter(yesterday) == true }) - uncacheContent(content); - } - } - - fun getChannelCachePager(channelUrl: String): PlatformContentPager { - val validID = channelUrl.toSafeFileName(); - - val validStores = _channelContents - .filter { it.key == validID } - .map { it.value }; - - val items = validStores.flatMap { it.getItems() } - .sortedByDescending { it.datetime }; - return PlatformContentPager(items, Math.min(150, items.size)); - } - fun getSubscriptionCachePager(): DedupContentPager { - Logger.i(TAG, "Subscriptions CachePager get subscriptions"); - val subs = StateSubscriptions.instance.getSubscriptions(); - Logger.i(TAG, "Subscriptions CachePager polycentric urls"); - val allUrls = subs.map { - val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf(); - if(!otherUrls.contains(it.channel.url)) - return@map listOf(listOf(it.channel.url), otherUrls).flatten(); - else - return@map otherUrls; - }.flatten().distinct(); - Logger.i(TAG, "Subscriptions CachePager compiling"); - val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet(); - - val validStores = _channelContents - .filter { validSubIds.contains(it.key) } - .map { it.value }; - - val items = validStores.flatMap { it.getItems() } - .sortedByDescending { it.datetime }; - - return DedupContentPager(PlatformContentPager(items, Math.min(30, items.size)), StatePlatform.instance.getEnabledClients().map { it.id }); - } - - fun uncacheContent(content: SerializedPlatformContent) { - val store = getContentStore(content); - store?.delete(content); - } - fun cacheContents(contents: List): List { - return contents.filter { cacheContent(it) }; - } - fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean { - if(content.author.url.isEmpty()) - return false; - - val channelId = content.author.url.toSafeFileName(); - val store = getContentStore(channelId).let { - if(it == null) { - Logger.i(TAG, "New Channel Cache for channel ${content.author.name}"); - val store = FragmentedStorage.storeJson(_channelCacheDir, channelId, PlatformContentSerializer()).load(); - _channelContents.put(channelId, store); - return@let store; - } - else return@let it; - } - val serialized = SerializedPlatformContent.fromContent(content); - val existing = store.findItems { it.url == content.url }; - - if(existing.isEmpty() || doUpdate) { - if(existing.isNotEmpty()) - existing.forEach { store.delete(it) }; - - store.save(serialized); - } - - return existing.isEmpty(); - } - - private fun getContentStore(content: IPlatformContent): ManagedStore? { - val channelId = content.author.url.toSafeFileName(); - return getContentStore(channelId); - } - private fun getContentStore(channelId: String): ManagedStore? { - return synchronized(_channelContents) { - var channelStore = _channelContents.get(channelId); - return@synchronized channelStore; - } - } - - companion object { - private val TAG = "ChannelCache"; - - private val _lock = Object(); - private var _instance: ChannelContentCache? = null; - val instance: ChannelContentCache get() { - synchronized(_lock) { - if(_instance == null) { - _instance = ChannelContentCache(); - } - } - return _instance!!; - } - - fun cachePagerResults(scope: CoroutineScope, pager: IPager, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager { - return ChannelVideoCachePager(pager, scope, onNewCacheHit); - } - } - - class ChannelVideoCachePager(val pager: IPager, private val scope: CoroutineScope, private val onNewCacheItem: ((IPlatformContent)->Unit)? = null): IPager { - - init { - val results = pager.getResults(); - - Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]"); - scope.launch(Dispatchers.IO) { - try { - val newCacheItems = instance.cacheContents(results); - if(onNewCacheItem != null) - newCacheItems.forEach { onNewCacheItem!!(it) } - } catch (e: Throwable) { - Logger.e(TAG, "Failed to cache videos.", e); - } - } - } - - override fun hasMorePages(): Boolean { - return pager.hasMorePages(); - } - - override fun nextPage() { - pager.nextPage(); - val results = pager.getResults(); - - Logger.i(TAG, "Caching ${results.size} subscription results"); - scope.launch(Dispatchers.IO) { - try { - val newCacheItems = instance.cacheContents(results); - if(onNewCacheItem != null) - newCacheItems.forEach { onNewCacheItem!!(it) } - } catch (e: Throwable) { - Logger.e(TAG, "Failed to cache videos.", e); - } - } - } - - override fun getResults(): List { - val results = pager.getResults(); - - return results; - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt index 490e7447..bdc3a3f8 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt @@ -24,7 +24,6 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IRefreshPager import com.futo.platformplayer.api.media.structures.IReplacerPager import com.futo.platformplayer.api.media.structures.MultiPager -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.TaskHandler @@ -32,6 +31,7 @@ import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.fragment.mainactivity.main.FeedView import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.views.FeedStyle @@ -78,7 +78,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment { private val _taskLoadVideos = TaskHandler>({lifecycleScope}, { val livePager = getContentPager(it); return@TaskHandler if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true) - ChannelContentCache.cachePagerResults(lifecycleScope, livePager); + StateCache.cachePagerResults(lifecycleScope, livePager); else livePager; }).success { livePager -> setLoading(false); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index 1dad57ef..c4c28a73 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -352,6 +352,7 @@ abstract class FeedView : L } private fun loadPagerInternal(pager: TPager, cache: ItemCache? = null) { + Logger.i(TAG, "Setting new internal pager on feed"); _cache = cache; detachPagerEvents(); @@ -397,6 +398,7 @@ abstract class FeedView : L } } + var _lastNextPage = false; private fun loadNextPage() { synchronized(_pager_lock) { val pager: TPager = recyclerData.pager ?: return; @@ -405,9 +407,14 @@ abstract class FeedView : L //loadCachedPage(); if (pager.hasMorePages()) { + _lastNextPage = true; setLoading(true); _nextPageHandler.run(pager); } + else if(_lastNextPage) { + Logger.i(TAG, "End of page reached"); + _lastNextPage = false; + } } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index 21b75c83..25f44a3a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -15,13 +15,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.IPager -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.RateLimitException import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.stores.FragmentedStorage @@ -132,8 +132,10 @@ class SubscriptionsFeedFragment : MainFragment() { if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) loadResults(false); - else if(recyclerData.results.size == 0) + else if(recyclerData.results.size == 0) { loadCache(); + setLoading(false); + } } val announcementsView = _announcementsView; @@ -306,12 +308,18 @@ class SubscriptionsFeedFragment : MainFragment() { private fun loadCache() { - Logger.i(TAG, "Subscriptions load cache"); - val cachePager = ChannelContentCache.instance.getSubscriptionCachePager(); - val results = cachePager.getResults(); - Logger.i(TAG, "Subscriptions show cache (${results.size})"); - setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null); - setPager(cachePager); + fragment.lifecycleScope.launch(Dispatchers.IO) { + Logger.i(TAG, "Subscriptions retrieving cache"); + val cachePager = StateCache.instance.getSubscriptionCachePager(); + Logger.i(TAG, "Subscriptions retrieved cache"); + + withContext(Dispatchers.Main) { + val results = cachePager.getResults(); + Logger.i(TAG, "Subscriptions show cache (${results.size})"); + setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null); + setPager(cachePager); + } + } } private fun loadResults(withRefetch: Boolean = false) { setLoading(true); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index 0fde6ff8..b85fc51b 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -26,7 +26,6 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.background.BackgroundWorker -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException @@ -387,7 +386,7 @@ class StateApp { try { Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]"); val time = measureTimeMillis { - ChannelContentCache.instance; + StateCache.instance; } Logger.i(TAG, "ChannelContentCache initialized in ${time}ms"); } catch (e: Throwable) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateCache.kt b/app/src/main/java/com/futo/platformplayer/states/StateCache.kt index 699f8b10..899e5717 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateCache.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateCache.kt @@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.PlatformContentPager -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.resolveChannelUrl @@ -16,12 +15,18 @@ import com.futo.platformplayer.stores.db.ManagedDBStore import com.futo.platformplayer.stores.db.types.DBChannelCache import com.futo.platformplayer.stores.db.types.DBHistory import com.futo.platformplayer.toSafeFileName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.time.OffsetDateTime +import kotlin.system.measureTimeMillis class StateCache { private val _channelCache = ManagedDBStore.create("channelCache", DBChannelCache.Descriptor(), PlatformContentSerializer()) .load(); + val channelCacheStartupCount = _channelCache.count(); + fun clear() { _channelCache.deleteAll(); } @@ -36,6 +41,12 @@ class StateCache { it.obj; } } + fun getChannelCachePager(channelUrls: List): IPager { + val pagers = MultiChronoContentPager(channelUrls.map { _channelCache.queryPager(DBChannelCache.Index::channelUrl, it, 20) { + it.obj; + } }, false, 20); + return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id }); + } fun getSubscriptionCachePager(): DedupContentPager { Logger.i(TAG, "Subscriptions CachePager get subscriptions"); val subs = StateSubscriptions.instance.getSubscriptions(); @@ -47,10 +58,15 @@ class StateCache { else return@map otherUrls; }.flatten().distinct(); - Logger.i(TAG, "Subscriptions CachePager compiling"); - val pagers = MultiChronoContentPager(allUrls.map { getChannelCachePager(it) }, false, 20); - return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id }); + Logger.i(TAG, "Subscriptions CachePager get pagers"); + val pagers = allUrls.parallelStream().map { getChannelCachePager(it) }.toList(); + + Logger.i(TAG, "Subscriptions CachePager compiling"); + val pager = MultiChronoContentPager(pagers, false, 20); + pager.initialize(); + Logger.i(TAG, "Subscriptions CachePager compiled"); + return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id }); } @@ -63,8 +79,8 @@ class StateCache { if(item != null) _channelCache.delete(item); } - fun cacheContents(contents: List): List { - return contents.filter { cacheContent(it) }; + fun cacheContents(contents: List, doUpdate: Boolean = false): List { + return contents.filter { cacheContent(it, doUpdate) }; } fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean { if(content.author.url.isEmpty()) @@ -102,5 +118,58 @@ class StateCache { _instance = null; } } + + + fun cachePagerResults(scope: CoroutineScope, pager: IPager, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager { + return ChannelContentCachePager(pager, scope, onNewCacheHit); + } + } + class ChannelContentCachePager(val pager: IPager, private val scope: CoroutineScope, private val onNewCacheItem: ((IPlatformContent)->Unit)? = null): IPager { + + init { + val results = pager.getResults(); + + Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]"); + scope.launch(Dispatchers.IO) { + try { + val newCacheItems = StateCache.instance.cacheContents(results, true); + if(onNewCacheItem != null) + newCacheItems.forEach { onNewCacheItem!!(it) } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to cache videos.", e); + } + } + } + + override fun hasMorePages(): Boolean { + return pager.hasMorePages(); + } + + override fun nextPage() { + pager.nextPage(); + val results = pager.getResults(); + + scope.launch(Dispatchers.IO) { + try { + val newCacheItemsCount: Int; + val ms = measureTimeMillis { + val newCacheItems = instance.cacheContents(results, true); + newCacheItemsCount = newCacheItems.size; + if(onNewCacheItem != null) + newCacheItems.forEach { onNewCacheItem!!(it) } + } + Logger.i(TAG, "Caching ${results.size} subscription results, updated ${newCacheItemsCount} (${ms}ms)"); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to cache ${results.size} videos.", e); + } + } + } + + override fun getResults(): List { + val results = pager.getResults(); + + return results; + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 11e92b3d..d892cdb6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.structures.* import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt index 50eac84b..fccc93a0 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt @@ -7,6 +7,7 @@ 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.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.stores.v2.JsonStoreSerializer import com.futo.platformplayer.stores.v2.StoreSerializer @@ -264,6 +265,7 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int): IPager = queryPager(validateFieldName(field), obj, pageSize); fun queryPager(field: String, obj: Any, pageSize: Int): IPager { return AdhocPager({ + Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}"); queryPage(field, obj, it - 1, pageSize); }); } diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt b/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt index 9f2af268..a974c97f 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/types/DBChannelCache.kt @@ -3,7 +3,9 @@ package com.futo.platformplayer.stores.db.types import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Database +import androidx.room.Entity import androidx.room.Ignore +import androidx.room.Index import androidx.room.PrimaryKey import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent import com.futo.platformplayer.models.HistoryVideo @@ -25,7 +27,7 @@ class DBChannelCache { //These classes solely exist for bounding generics for type erasure @Dao interface DBDAO: ManagedDBDAOBase {} - @Database(entities = [Index::class], version = 2) + @Database(entities = [Index::class], version = 4) abstract class DB: ManagedDBDatabase() { abstract override fun base(): DBDAO; } @@ -37,6 +39,11 @@ class DBChannelCache { override fun indexClass(): KClass = Index::class; } + @Entity(TABLE_NAME, indices = [ + androidx.room.Index(value = ["url"]), + androidx.room.Index(value = ["channelUrl"]), + androidx.room.Index(value = ["datetime"], orders = [androidx.room.Index.Order.DESC]) + ]) class Index: ManagedDBIndex { @ColumnIndex @PrimaryKey(true) @@ -49,7 +56,7 @@ class DBChannelCache { var channelUrl: String? = null; @ColumnIndex - @ColumnOrdered(0) + @ColumnOrdered(0, true) var datetime: Long? = null; diff --git a/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt index 0f4bf008..cba96ca5 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt @@ -5,10 +5,10 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.PlatformContentPager -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.resolveChannelUrl +import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.toSafeFileName @@ -27,13 +27,16 @@ class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, al override fun getSubscriptions(subs: Map>): Result { val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet(); - val validStores = ChannelContentCache.instance._channelContents + /* + val validStores = StateCache.instance._channelContents .filter { validSubIds.contains(it.key) } - .map { it.value }; + .map { it.value };*/ + /* val items = validStores.flatMap { it.getItems() } .sortedByDescending { it.datetime }; + */ - return Result(DedupContentPager(PlatformContentPager(items, Math.min(_pageSize, items.size)), StatePlatform.instance.getEnabledClients().map { it.id }), listOf()); + return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct()), StatePlatform.instance.getEnabledClients().map { it.id }), listOf()); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SimpleSubscriptionAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SimpleSubscriptionAlgorithm.kt index af96ffcd..bf40d738 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SimpleSubscriptionAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SimpleSubscriptionAlgorithm.kt @@ -8,7 +8,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCriticalException @@ -17,6 +16,7 @@ import com.futo.platformplayer.findNonRuntimeException import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.polycentric.PolycentricCache +import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StateSubscriptions @@ -157,7 +157,7 @@ class SimpleSubscriptionAlgorithm( val time = measureTimeMillis { pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore); - pager = ChannelContentCache.cachePagerResults(scope, pager!!) { + pager = StateCache.cachePagerResults(scope, pager!!) { onNewCacheHit.emit(sub, it); }; @@ -176,7 +176,7 @@ class SimpleSubscriptionAlgorithm( throw channelEx; else { Logger.i(StateSubscriptions.TAG, "Channel ${sub.channel.name} failed, substituting with cache"); - pager = ChannelContentCache.instance.getChannelCachePager(sub.channel.url); + pager = StateCache.instance.getChannelCachePager(sub.channel.url); } } } diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index affdb7c9..d51688df 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager -import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCriticalException @@ -21,6 +20,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StateSubscriptions import kotlinx.coroutines.CoroutineScope @@ -108,7 +108,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null; val liveTasks = entry.value.filter { !it.task.fromCache }; val cachedTasks = entry.value.filter { it.task.fromCache }; - val livePager = if(!liveTasks.isEmpty()) ChannelContentCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, { + val livePager = if(!liveTasks.isEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }, { onNewCacheHit.emit(sub!!, it); }) else null; val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null; @@ -142,7 +142,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( return@submit SubscriptionTaskResult(task, null, null); else { cachedChannels.add(task.url); - return@submit SubscriptionTaskResult(task, ChannelContentCache.instance.getChannelCachePager(task.url), null); + return@submit SubscriptionTaskResult(task, StateCache.instance.getChannelCachePager(task.url), null); } } } @@ -197,7 +197,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( throw channelEx; else { Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache"); - pager = ChannelContentCache.instance.getChannelCachePager(task.sub.channel.url); + pager = StateCache.instance.getChannelCachePager(task.sub.channel.url); taskEx = ex; return@submit SubscriptionTaskResult(task, pager, taskEx); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8552e9c4..583192bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -399,6 +399,7 @@ Version Code Version Name Version Type + Channel Cache Size (Startup) When watching a video in preview mode, resume at the position when opening the video code Please enable logging to submit logs Embedded plugins reinstalled, a reboot is recommended @@ -424,6 +425,7 @@ Developer Mode Development Server Experimental + Cache Fill storage till error Inject Injects a test source config (local) into V8 @@ -432,6 +434,7 @@ Removes all subscriptions Settings related to development server, be careful as it may open your phone to security vulnerabilities Start Server + Subscriptions Cache 5000 Start Server on boot Starts a DevServer on port 11337, may expose vulnerabilities. Test V8 Communication speed diff --git a/app/src/unstable/assets/sources/test/TestConfig.json b/app/src/unstable/assets/sources/test/TestConfig.json new file mode 100644 index 00000000..86eed6ee --- /dev/null +++ b/app/src/unstable/assets/sources/test/TestConfig.json @@ -0,0 +1,24 @@ +{ + "name": "Testing", + "description": "Just for testing.", + "author": "FUTO", + "authorUrl": "https://futo.org", + + "platformUrl": "https://odysee.com", + "sourceUrl": "https://plugins.grayjay.app/Test/TestConfig.json", + "repositoryUrl": "https://futo.org", + "scriptUrl": "./TestScript.js", + "version": 31, + + "iconUrl": "./odysee.png", + "id": "1c05bfc3-08b9-42d0-93d3-6d52e0fd34d8", + + "scriptSignature": "", + "scriptPublicKey": "", + "packages": ["Http"], + + "allowEval": false, + "allowUrls": [], + + "supportedClaimTypes": [] +} diff --git a/app/src/unstable/assets/sources/test/TestScript.js b/app/src/unstable/assets/sources/test/TestScript.js new file mode 100644 index 00000000..45c47d8f --- /dev/null +++ b/app/src/unstable/assets/sources/test/TestScript.js @@ -0,0 +1,45 @@ +var config = {}; + +//Source Methods +source.enable = function(conf){ + config = conf ?? {}; + //log(config); +} +source.getHome = function() { + return new ContentPager([ + source.getContentDetails("whatever") + ]); +}; + +//Video +source.isContentDetailsUrl = function(url) { + return REGEX_DETAILS_URL.test(url) +}; +source.getContentDetails = function(url) { + return new PlatformVideoDetails({ + id: new PlatformID("Test", "Something", config.id), + name: "Test Video", + thumbnails: new Thumbnails([]), + author: new PlatformAuthorLink(new PlatformID("Test", "TestID", config.id), + "TestAuthor", + "None", + ""), + datetime: parseInt(new Date().getTime() / 1000), + duration: 0, + viewCount: 0, + url: "", + isLive: false, + description: "", + rating: new RatingLikes(0), + video: new VideoSourceDescriptor([ + new HLSSource({ + name: "HLS", + url: "", + duration: 0, + priority: true + }) + ]) + }); +}; + +log("LOADED"); \ No newline at end of file diff --git a/app/src/unstable/assets/sources/test/odysee.png b/app/src/unstable/assets/sources/test/odysee.png new file mode 100644 index 0000000000000000000000000000000000000000..472960d00a49401c1d97199ff6fa90cc337bd9bc GIT binary patch literal 47198 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 (a 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