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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 literal 0 HcmV?d00001