diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index 0a17d96142345d29e78c198529b03cc434dca489..3d93437c6941672436807b779a58d7844ff0bd5d 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -68,6 +68,12 @@ fun ensureNotMainThread() { } } +private val _regexUrl = Regex("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)"); +fun String.isHttpUrl(): Boolean { + return _regexUrl.matchEntire(this) != null; +} + + private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})"); fun String.isHexColor(): Boolean { return _regexHexColor.matches(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 index d0ff0b4f31a13f0cde42cf2f9e06b44dd4c18ebd..1464b035c7a1f0e7494844de93d8f5f527544dd7 100644 --- a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt +++ b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt @@ -12,19 +12,43 @@ 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 com.futo.polycentric.core.toUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.time.OffsetDateTime +import kotlin.system.measureTimeMillis class ChannelContentCache { + private val _targetCacheSize = 2000; val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache"); - val _channelContents = HashMap(_channelCacheDir.listFiles() - .filter { it.isDirectory } - .associate { Pair(it.name, FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, it.name, PlatformContentSerializer()) - .withoutBackup() - .load()) }); + val _channelContents: HashMap<String, ManagedStore<SerializedPlatformContent>>; + init { + val allFiles = _channelCacheDir.listFiles() ?: arrayOf(); + val initializeTime = measureTimeMillis { + _channelContents = HashMap(allFiles + .filter { it.isDirectory } + .associate { + Pair(it.name, FragmentedStorage.storeJson(_channelCacheDir, it.name, PlatformContentSerializer()) + .withoutBackup() + .load()) + }); + } + 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) } + .sortedByDescending { 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 getChannelCachePager(channelUrl: String): PlatformContentPager { val validID = channelUrl.toSafeFileName(); @@ -38,7 +62,9 @@ class ChannelContentCache { 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)) @@ -46,6 +72,7 @@ class ChannelContentCache { else return@map otherUrls; }.flatten().distinct(); + Logger.i(TAG, "Subscriptions CachePager compiling"); val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet(); val validStores = _channelContents @@ -58,7 +85,11 @@ class ChannelContentCache { return DedupContentPager(PlatformContentPager(items, Math.min(150, items.size)), StatePlatform.instance.getEnabledClients().map { it.id }); } - fun cacheVideos(contents: List<IPlatformContent>): List<IPlatformContent> { + fun uncacheContent(content: SerializedPlatformContent) { + val store = getContentStore(content); + store?.delete(content); + } + fun cacheContents(contents: List<IPlatformContent>): List<IPlatformContent> { return contents.filter { cacheContent(it) }; } fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean { @@ -66,14 +97,14 @@ class ChannelContentCache { return false; val channelId = content.author.url.toSafeFileName(); - val store = synchronized(_channelContents) { - var channelStore = _channelContents.get(channelId); - if(channelStore == null) { - Logger.i(TAG, "New Subscription Cache for channel ${content.author.name}"); - channelStore = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load(); - _channelContents.put(channelId, channelStore); + val store = getContentStore(channelId).let { + if(it == null) { + Logger.i(TAG, "New Channel Cache for channel ${content.author.name}"); + val store = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load(); + _channelContents.put(channelId, store); + return@let store; } - return@synchronized channelStore; + else return@let it; } val serialized = SerializedPlatformContent.fromContent(content); val existing = store.findItems { it.url == content.url }; @@ -88,6 +119,17 @@ class ChannelContentCache { return existing.isEmpty(); } + private fun getContentStore(content: IPlatformContent): ManagedStore<SerializedPlatformContent>? { + val channelId = content.author.url.toSafeFileName(); + return getContentStore(channelId); + } + private fun getContentStore(channelId: String): ManagedStore<SerializedPlatformContent>? { + return synchronized(_channelContents) { + var channelStore = _channelContents.get(channelId); + return@synchronized channelStore; + } + } + companion object { private val TAG = "ChannelCache"; @@ -95,10 +137,11 @@ class ChannelContentCache { private var _instance: ChannelContentCache? = null; val instance: ChannelContentCache get() { synchronized(_lock) { - if(_instance == null) + if(_instance == null) { _instance = ChannelContentCache(); - return _instance!!; + } } + return _instance!!; } fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> { @@ -114,7 +157,7 @@ class ChannelContentCache { Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]"); scope.launch(Dispatchers.IO) { try { - val newCacheItems = instance.cacheVideos(results); + val newCacheItems = instance.cacheContents(results); if(onNewCacheItem != null) newCacheItems.forEach { onNewCacheItem!!(it) } } catch (e: Throwable) { @@ -134,7 +177,7 @@ class ChannelContentCache { Logger.i(TAG, "Caching ${results.size} subscription results"); scope.launch(Dispatchers.IO) { try { - val newCacheItems = instance.cacheVideos(results); + val newCacheItems = instance.cacheContents(results); if(onNewCacheItem != null) newCacheItems.forEach { onNewCacheItem!!(it) } } catch (e: Throwable) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt index a22c2ab3b1cbb410a9041f52e68192dfe10239e0..6ca87a28077721dc08bb812e201e8c689a4015e6 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt @@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment +import com.futo.platformplayer.isHttpUrl import com.futo.platformplayer.views.FeedStyle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -143,7 +144,10 @@ class ContentSearchResultsFragment : MainFragment() { }; onSearch.subscribe(this) { - setQuery(it, true); + if(it.isHttpUrl()) + navigate<VideoDetailFragment>(it); + else + setQuery(it, true); }; } } 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 3f0938cc0d7c4ef52e79703db0137c80945b5733..0f70aef32d77ae298eb41beefb9a9a110bceb665 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 @@ -141,7 +141,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L val filteredResults = filterResults(it); recyclerData.results.addAll(filteredResults); recyclerData.resultsUnfiltered.addAll(it); - recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size); + if(filteredResults.isEmpty()) + loadNextPage() + else + recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size); }.exception<Throwable> { Logger.w(TAG, "Failed to load next page.", it); UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt index 0693f9d8e72dd5e312753a6db844a105b6608455..07eff4105185bbb0914cad5f77c93d3528d610f3 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt @@ -218,6 +218,13 @@ class SourceDetailFragment : MainFragment() { BigButtonGroup(c, context.getString(R.string.authentication), BigButton(c, context.getString(R.string.logout), context.getString(R.string.sign_out_of_the_platform), R.drawable.ic_logout) { logoutSource(); + }, + BigButton(c, "Logout without Clear", "Logout but keep the browser cookies.\nThis allows for quick re-logging.", R.drawable.ic_logout) { + logoutSource(false); + }.apply { + this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { + setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0); + }; } ) ); @@ -286,12 +293,22 @@ class SourceDetailFragment : MainFragment() { _sourceButtons.addView(group); } + val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id }; val advancedButtons = BigButtonGroup(c, "Advanced", BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) { }.apply { this.alpha = 0.5f; - } + }, + if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) { + StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true); + reloadSource(config.id); + UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh"); + }.apply { + this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply { + setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0); + }; + } else null ) _sourceAdvancedButtons.removeAllViews(); @@ -311,7 +328,7 @@ class SourceDetailFragment : MainFragment() { reloadSource(config.id); }; } - private fun logoutSource() { + private fun logoutSource(clear: Boolean = true) { val config = _config ?: return; StatePlugins.instance.setPluginAuth(config.id, null); @@ -319,7 +336,7 @@ class SourceDetailFragment : MainFragment() { //TODO: Maybe add a dialog option.. - if(Settings.instance.plugins.clearCookiesOnLogout) { + if(Settings.instance.plugins.clearCookiesOnLogout && clear) { val cookieManager: CookieManager = CookieManager.getInstance(); cookieManager.removeAllCookies(null); } 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 f65c6931f8fe0e93d3170f9e125a326ec23691b6..e8bf35d8677323dfb524dd0db97160495db4e5a0 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 @@ -87,6 +87,7 @@ class SubscriptionsFeedFragment : MainFragment() { @SuppressLint("ViewConstructor") class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> { constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) { + Logger.i(TAG, "SubscriptionsFeedFragment constructor()"); StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total -> fragment.lifecycleScope.launch(Dispatchers.Main) { try { @@ -110,6 +111,7 @@ class SubscriptionsFeedFragment : MainFragment() { } fun onShown() { + Logger.i(TAG, "SubscriptionsFeedFragment onShown()"); val currentProgress = StateSubscriptions.instance.getGlobalSubscriptionProgress(); setProgress(currentProgress.first, currentProgress.second); @@ -176,6 +178,7 @@ class SubscriptionsFeedFragment : MainFragment() { if(rateLimitPlugins.any()) throw RateLimitException(rateLimitPlugins.map { it.key.id }); } + _bypassRateLimit = false; val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh); val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions; @@ -279,9 +282,10 @@ class SubscriptionsFeedFragment : MainFragment() { setLoading(true); Logger.i(TAG, "Subscriptions load"); if(recyclerData.results.size == 0) { + Logger.i(TAG, "Subscriptions load cache"); val cachePager = ChannelContentCache.instance.getSubscriptionCachePager(); val results = cachePager.getResults(); - Logger.i(TAG, "Subscription show cache (${results.size})"); + 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); } else { @@ -291,7 +295,7 @@ class SubscriptionsFeedFragment : MainFragment() { } private fun loadedResult(pager: IPager<IPlatformContent>) { - Logger.i(TAG, "Subscriptions new pager loaded"); + Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})"); fragment.lifecycleScope.launch(Dispatchers.Main) { try { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt index 27a960945e03d22a852f7df164cd0cd937cba4f3..e5499062cb36b13ee456128eea5c8b7e07c14c21 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt @@ -117,7 +117,10 @@ class SuggestionsFragment : MainFragment { } else if (_searchType == SearchType.PLAYLIST) { navigate<PlaylistSearchResultsFragment>(it); } else { - navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl)); + if(it.isHttpUrl()) + navigate<VideoDetailFragment>(it); + else + navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl)); } }; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index beacbf0f060646f0f7b39496758369ffb731e98d..94664802b42e4472102af20109354ffc53c0ceb0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -38,6 +38,7 @@ import com.futo.platformplayer.api.media.LiveChatManager import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException +import com.futo.platformplayer.api.media.models.chapters.ChapterType import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent @@ -173,6 +174,8 @@ class VideoDetailView : ConstraintLayout { private val _addCommentView: AddCommentView; private val _toggleCommentType: Toggle; + private val _layoutSkip: LinearLayout; + private val _textSkip: TextView; private val _textResume: TextView; private val _layoutResume: LinearLayout; private var _jobHideResume: Job? = null; @@ -296,6 +299,8 @@ class VideoDetailView : ConstraintLayout { _addCommentView = findViewById(R.id.add_comment_view); _commentsList = findViewById(R.id.comments_list); + _layoutSkip = findViewById(R.id.layout_skip); + _textSkip = findViewById(R.id.text_skip); _layoutResume = findViewById(R.id.layout_resume); _textResume = findViewById(R.id.text_resume); _layoutPlayerContainer = findViewById(R.id.layout_player_container); @@ -403,6 +408,21 @@ class VideoDetailView : ConstraintLayout { _cast.onSettingsClick.subscribe { showVideoSettings() }; _player.onVideoSettings.subscribe { showVideoSettings() }; _player.onToggleFullScreen.subscribe(::handleFullScreen); + _player.onChapterChanged.subscribe { chapter, isScrub -> + if(_layoutSkip.visibility == VISIBLE && chapter?.type != ChapterType.SKIPPABLE) + _layoutSkip.visibility = GONE; + + if(!isScrub) { + if(chapter?.type == ChapterType.SKIPPABLE) { + _layoutSkip.visibility = VISIBLE; + } + else if(chapter?.type == ChapterType.SKIP) { + _player.seekTo(chapter.timeEnd.toLong() * 1000); + UIDialogs.toast(context, "Skipped chapter [${chapter.name}]", false); + } + } + } + _cast.onMinimizeClick.subscribe { _player.setFullScreen(false); onMinimize.emit(); @@ -581,6 +601,13 @@ class VideoDetailView : ConstraintLayout { _layoutResume.visibility = View.GONE; }; + + _layoutSkip.setOnClickListener { + val currentChapter = _player.getCurrentChapter(_player.position); + if(currentChapter?.type == ChapterType.SKIPPABLE) { + _player.seekTo(currentChapter.timeEnd.toLong() * 1000); + } + } } fun updateMoreButtons() { @@ -961,9 +988,10 @@ class VideoDetailView : ConstraintLayout { } catch(ex: Throwable) { Logger.e(TAG, "Failed to get chapters", ex); - withContext(Dispatchers.Main) { + + /*withContext(Dispatchers.Main) { UIDialogs.toast(context, "Failed to get chapters\n" + ex.message); - } + }*/ } try { val stopwatch = com.futo.platformplayer.debug.Stopwatch() 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 0545d2faa0d6da37aaf9d6ba011798d874e5db03..371c15ff8837a0e7e636bf0cef9489af98aa34f1 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -33,6 +33,7 @@ import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient 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 @@ -54,6 +55,8 @@ import java.io.File import java.time.OffsetDateTime import java.util.* import java.util.concurrent.TimeUnit +import kotlin.system.measureTimeMillis +import kotlin.time.measureTime /*** * This class contains global context for unconventional cases where obtaining context is hard. @@ -380,6 +383,18 @@ class StateApp { fun mainAppStarted(context: Context) { Logger.i(TAG, "App started"); + //Start loading cache + instance.scopeOrNull?.launch(Dispatchers.IO) { + try { + val time = measureTimeMillis { + ChannelContentCache.instance; + } + Logger.i(TAG, "ChannelContentCache initialized in ${time}ms"); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to load announcements.", e) + } + } + StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now()) if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot) @@ -441,7 +456,7 @@ class StateApp { val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }; if (isRateLimitReached) { Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}"); - delay(5000); + delay(8000); StateSubscriptions.instance.updateSubscriptionFeed(scope, false); } else diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt index 27b030e1e085954a3a3b92349e502f7d6b2e8bb5..58454cc6e6b30aae4df8920643a6bf1b721e283b 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt @@ -108,14 +108,12 @@ class StatePlugins { instance.deletePlugin(embedded.key); StatePlatform.instance.updateAvailableClients(context); } - fun updateEmbeddedPlugins(context: Context) { - for(embedded in getEmbeddedSources(context)) { + fun updateEmbeddedPlugins(context: Context, subset: List<String>? = null, force: Boolean = false) { + for(embedded in getEmbeddedSources(context).filter { subset == null || subset.contains(it.key) }) { val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value); - if(FORCE_REINSTALL_EMBEDDED) - deletePlugin(embedded.key); - else if(embeddedConfig != null) { + if(embeddedConfig != null) { val existing = getPlugin(embedded.key); - if(existing != null && existing.config.version < embeddedConfig.version ) { + if(existing != null && (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) { Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig?.version}), reinstalling"); //deletePlugin(embedded.key); installEmbeddedPlugin(context, embedded.value) 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 a6274d266976cf60811cd1841bdaeee846fc9805..b686e659cedd14d91c066fe38c9448b3eb724890 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -242,8 +242,12 @@ class StateSubscriptions { } + val usePolycentric = false; val subUrls = getSubscriptions().associateWith { - StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id) + if(usePolycentric) + StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id); + else + listOf(it.channel.url); }; val result = algo.getSubscriptions(subUrls); diff --git a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt index a76af3a78a83fdb6ac0c877b36287877c90ba3b2..596f3be24d8b464aef531c5e5ace941a639e8c1a 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt @@ -181,6 +181,12 @@ class ManagedStore<T>{ return ReconstructionResult(successes, exs, builder.messages); } + + fun count(): Int { + synchronized(_files) { + return _files.size; + } + } fun getItems(): List<T> { synchronized(_files) { return _files.map { it.key }; diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt index 60e2bb5ee85aa492da1fd9472dcb989fbe06f93d..9132f6db22f5fb1bf0b6fd41923da04f131e565f 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt @@ -49,8 +49,11 @@ class SmartSubscriptionAlgorithm( }; }; + for(task in allTasks) + task.urgency = calculateUpdateUrgency(task.sub, task.type); + val ordering = allTasks.groupBy { it.client } - .map { Pair(it.key, it.value.sortedBy { calculateUpdateUrgency(it.sub, it.type) }) }; + .map { Pair(it.key, it.value.sortedBy { it.urgency }) }; val finalTasks = mutableListOf<SubscriptionTask>(); @@ -100,7 +103,7 @@ class SmartSubscriptionAlgorithm( }; val lastItemDaysAgo = lastItem.getNowDiffHours(); val lastUpdateHoursAgo = lastUpdate.getNowDiffHours(); - val expectedHours = lastUpdateHoursAgo.toDouble() - (interval*24); + val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble(); return (expectedHours * 100).toInt(); } 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 2a9de5bc17659fc54d3b14555c3105e4b73c696d..1b70d014a2e5b3228135c8a902ab3fad8e11f827 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent 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.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 @@ -42,24 +43,89 @@ abstract class SubscriptionsTaskFetchAlgorithm( override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result { val tasks = getSubscriptionTasks(subs); + val tasksGrouped = tasks.groupBy { it.client } + val taskCount = tasks.filter { !it.fromCache }.size; + val cacheCount = tasks.size - taskCount; Logger.i(TAG, "Starting Subscriptions Fetch:\n" + - " Tasks: ${tasks.filter { !it.fromCache }.size}\n" + - " Cached: ${tasks.filter { it.fromCache }.size}"); + " Tasks: ${taskCount}\n" + + " Cached: ${cacheCount}"); try { - //TODO: Remove this - UIDialogs.toast("Tasks: ${tasks.filter { !it.fromCache }.size}\n" + - "Cached: ${tasks.filter { it.fromCache }.size}", false); + for(clientTasks in tasksGrouped) { + val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size; + val clientCacheCount = clientTasks.value.size - clientTaskCount; + if(clientCacheCount > 0) { + UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels.") + } + } + } catch (ex: Throwable){} val exs: ArrayList<Throwable> = arrayListOf(); - val taskResults = arrayListOf<IPager<IPlatformContent>>(); + val failedPlugins = mutableListOf<String>(); + val cachedChannels = mutableListOf<String>() + val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels); + + + val taskResults = arrayListOf<SubscriptionTaskResult>(); + val timeTotal = measureTimeMillis { + for(task in forkTasks) { + try { + val result = task.get(); + if(result != null) { + if(result.pager != null) + taskResults.add(result); + else if(result.exception != null) { + val ex = result.exception; + if(ex != null) { + val nonRuntimeEx = findNonRuntimeException(ex); + if (nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException)) + exs.add(nonRuntimeEx); + else + throw ex.cause ?: ex; + } + } + } + } catch (ex: ExecutionException) { + val nonRuntimeEx = findNonRuntimeException(ex.cause); + if(nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException)) + exs.add(nonRuntimeEx); + else + throw ex.cause ?: ex; + }; + } + } + Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms") + + //Cache pagers grouped by channel + val groupedPagers = taskResults.groupBy { it.task.sub.channel.url } + .map { entry -> + 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() }, { + onNewCacheHit.emit(sub!!, it); + }) else null; + val cachedPager = if(!cachedTasks.isEmpty()) MultiChronoContentPager(cachedTasks.map { it.pager!! }, true).apply { this.initialize() } else null; + if(livePager != null && cachedPager == null) + return@map livePager; + else if(cachedPager != null && livePager == null) + return@map cachedPager; + else if(cachedPager == null && livePager == null) + return@map EmptyPager(); + else + return@map MultiChronoContentPager(listOf(livePager!!, cachedPager!!), true).apply { this.initialize() } + } + + val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15); + pager.initialize(); + + return Result(DedupContentPager(pager), exs); + } + + fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> { val forkTasks = mutableListOf<ForkJoinTask<SubscriptionTaskResult>>(); var finished = 0; - val exceptionMap: HashMap<Subscription, Throwable> = hashMapOf(); - val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency(); - val failedPlugins = arrayListOf<String>(); - val cachedChannels = arrayListOf<String>(); for(task in tasks) { val forkTask = threadPool.submit<SubscriptionTaskResult> { @@ -87,10 +153,6 @@ abstract class SubscriptionsTaskFetchAlgorithm( pager = StatePlatform.instance.getChannelContent(task.client, task.url, task.type, ResultCapabilities.ORDER_CHONOLOGICAL); - pager = ChannelContentCache.cachePagerResults(scope, pager) { - onNewCacheHit.emit(task.sub, it); - }; - val initialPage = pager.getResults(); task.sub.updateSubscriptionState(task.type, initialPage); StateSubscriptions.instance.saveSubscription(task.sub); @@ -105,6 +167,27 @@ abstract class SubscriptionsTaskFetchAlgorithm( val channelEx = ChannelException(task.sub.channel, ex); finished++; onProgress.emit(finished, forkTasks.size); + + + if(ex is ScriptCaptchaRequiredException) { + synchronized(failedPlugins) { + //Fail all subscription calls to plugin if it has a captcha issue + if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) { + Logger.w(StateSubscriptions.TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to Captcha"); + failedPlugins.add(ex.config.id); + } + } + } + else if(ex is ScriptCriticalException) { + synchronized(failedPlugins) { + //Fail all subscription calls to plugin if it has a critical issue + if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) { + Logger.w(StateSubscriptions.TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to critical exception:\n" + ex.message); + failedPlugins.add(ex.config.id); + } + } + } + if (!withCacheFallback) throw channelEx; else { @@ -117,39 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( } forkTasks.add(forkTask); } - - val timeTotal = measureTimeMillis { - for(task in forkTasks) { - try { - val result = task.get(); - if(result != null) { - if(result.pager != null) - taskResults.add(result.pager!!); - if(exceptionMap.containsKey(result.task.sub)) { - val ex = exceptionMap[result.task.sub]; - if(ex != null) { - val nonRuntimeEx = findNonRuntimeException(ex); - if (nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException)) - exs.add(nonRuntimeEx); - else - throw ex.cause ?: ex; - } - } - } - } catch (ex: ExecutionException) { - val nonRuntimeEx = findNonRuntimeException(ex.cause); - if(nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException)) - exs.add(nonRuntimeEx); - else - throw ex.cause ?: ex; - }; - } - } - Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms") - val pager = MultiChronoContentPager(taskResults, allowFailure, 15); - pager.initialize(); - - return Result(DedupContentPager(pager), exs); + return forkTasks; } abstract fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask>; @@ -160,7 +211,8 @@ abstract class SubscriptionsTaskFetchAlgorithm( val sub: Subscription, val url: String, val type: String, - var fromCache: Boolean = false + var fromCache: Boolean = false, + var urgency: Int = 0 ); class SubscriptionTaskResult( diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index f55742859558d30dc15e90fc605107e30973af22..b186afef76c3f1da3eb7c7aefac405b231bc6ae3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -100,6 +100,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase { val onSourceChanged = Event3<IVideoSource?, IAudioSource?, Boolean>(); val onSourceEnded = Event0(); + val onChapterChanged = Event2<IChapter?, Boolean>(); + val onVideoClicked = Event0(); val onTimeBarChanged = Event2<Long, Long>(); @@ -185,6 +187,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase { override fun onScrubMove(timeBar: TimeBar, position: Long) { gestureControl.restartHideJob(); + + updateCurrentChapter(position); } override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { @@ -233,17 +237,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { val delta = position - lastPos; if(delta > 1000 || delta < 0) { lastPos = position; - val currentChapter = getCurrentChapter(position) - if(_currentChapter != currentChapter) { - _currentChapter = currentChapter; - if (currentChapter != null) { - _control_chapter.text = " • " + currentChapter.name; - _control_chapter_fullscreen.text = " • " + currentChapter.name; - } else { - _control_chapter.text = ""; - _control_chapter_fullscreen.text = ""; - } - } + updateCurrentChapter(); } } @@ -256,6 +250,22 @@ class FutoVideoPlayer : FutoVideoPlayerBase { exoPlayer?.attach(_videoView, PLAYER_STATE_NAME); } + fun updateCurrentChapter(pos: Long? = null) { + val chaptPos = pos ?: position; + val currentChapter = getCurrentChapter(chaptPos); + if(_currentChapter != currentChapter) { + _currentChapter = currentChapter; + if (currentChapter != null) { + _control_chapter.text = " • " + currentChapter.name; + _control_chapter_fullscreen.text = " • " + currentChapter.name; + } else { + _control_chapter.text = ""; + _control_chapter_fullscreen.text = ""; + } + onChapterChanged.emit(currentChapter, pos != null); + } + } + fun setArtwork(drawable: Drawable?) { if (drawable != null) { _videoView.defaultArtwork = drawable; diff --git a/app/src/main/res/layout/big_button.xml b/app/src/main/res/layout/big_button.xml index 8867d1f80941a8a02a37d1474d5e2a89e633ab5f..1cf84f1e17aaae0af0a5cacc9b4f765d1930b612 100644 --- a/app/src/main/res/layout/big_button.xml +++ b/app/src/main/res/layout/big_button.xml @@ -3,10 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="60dp" + android:layout_height="wrap_content" android:gravity="center_vertical" - android:paddingTop="6dp" - android:paddingBottom="7dp" android:paddingStart="7dp" android:paddingEnd="12dp" android:background="@drawable/background_big_button" @@ -26,6 +24,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:orientation="vertical"> <TextView android:id="@+id/button_text" @@ -47,7 +47,7 @@ android:textSize="12dp" android:gravity="center_vertical" android:fontFamily="@font/inter_extra_light" - android:maxLines="1" + android:maxLines="2" android:ellipsize="end" tools:text="Attempts to fetch your subscriptions from this source" /> </LinearLayout> diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml index c2a09caab1c3c693a7f1342e4c12168f38b570a3..be40a8645fec9e6106429bfb77c5806c8600f300 100644 --- a/app/src/main/res/layout/fragview_video_detail.xml +++ b/app/src/main/res/layout/fragview_video_detail.xml @@ -150,6 +150,30 @@ android:textSize="12dp" android:fontFamily="@font/inter_light" /> </LinearLayout> + <LinearLayout + android:id="@+id/layout_skip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="5dp" + android:background="@drawable/background_button_transparent_round" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:layout_marginTop="10dp" + android:visibility="gone"> + + <TextView + android:id="@+id/text_skip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Skip" + android:textSize="12dp" + android:fontFamily="@font/inter_light" /> + </LinearLayout> <FrameLayout android:id="@+id/contentContainer" diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 5011bfcddb084007b938e6276b11f63e940006eb..ce7d9d0bc7cd18c4f50338e30a82ca132e24f70d 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 5011bfcddb084007b938e6276b11f63e940006eb +Subproject commit ce7d9d0bc7cd18c4f50338e30a82ca132e24f70d