diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index f1eb09eb..f0d52d39 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -1,8 +1,12 @@ package com.futo.platformplayer import android.content.ContentResolver +import android.graphics.Color +import android.util.TypedValue import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource @@ -16,6 +20,7 @@ import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Playlist +import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.states.* import com.futo.platformplayer.views.Loader import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup @@ -45,6 +50,64 @@ class UISlideOverlays { menu.show(); } + fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) { + val items = arrayListOf(); + var menu: SlideUpMenuOverlay? = null; + + val originalNotif = subscription.doNotifications; + val originalLive = subscription.doFetchLive; + val originalStream = subscription.doFetchStreams; + val originalVideo = subscription.doFetchVideos; + val originalPosts = subscription.doFetchPosts; + + items.addAll(listOf( + SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", { + subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications; + }, false), + SlideUpMenuGroup(container.context, "Fetch Settings", + "Depending on the platform you might not need to enable a type for it to be available.", + -1, listOf()), + SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", { + subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive; + }, false), + SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for finished streams", "fetchStreams", { + subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchLive; + }, false), + SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", { + subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchLive; + }, false), + SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", { + subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchLive; + }, false))); + + menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items); + + if(subscription.doFetchLive) + menu.selectOption(null, "fetchLive", true, true); + if(subscription.doFetchStreams) + menu.selectOption(null, "fetchStreams", true, true); + if(subscription.doFetchVideos) + menu.selectOption(null, "fetchVideos", true, true); + if(subscription.doFetchPosts) + menu.selectOption(null, "fetchPosts", true, true); + + menu.onOK.subscribe { + StateSubscriptions.instance.saveSubscription(subscription); + menu.hide(true); + }; + menu.onCancel.subscribe { + subscription.doNotifications = originalNotif; + subscription.doFetchLive = originalLive; + subscription.doFetchStreams = originalStream; + subscription.doFetchVideos = originalVideo; + subscription.doFetchPosts = originalPosts; + }; + + menu.setOk("Save"); + + menu.show(); + } + fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? { val items = arrayListOf(); var menu: SlideUpMenuOverlay? = null; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index bfc938af..2250570d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -9,6 +9,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView @@ -41,6 +42,7 @@ import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.subscriptions.SubscribeButton import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter @@ -100,6 +102,7 @@ class ChannelFragment : MainFragment() { private var _viewPager: ViewPager2; private var _tabLayoutMediator: TabLayoutMediator; private var _buttonSubscribe: SubscribeButton; + private var _buttonSubscriptionSettings: ImageButton; private var _overlayContainer: FrameLayout; private var _overlay_loading: LinearLayout; @@ -160,10 +163,21 @@ class ChannelFragment : MainFragment() { _creatorThumbnail = findViewById(R.id.creator_thumbnail); _imageBanner = findViewById(R.id.image_channel_banner); _buttonSubscribe = findViewById(R.id.button_subscribe); + _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings); _overlay_loading = findViewById(R.id.channel_loading_overlay); _overlay_loading_spinner = findViewById(R.id.channel_loader); _overlayContainer = findViewById(R.id.overlay_container); + _buttonSubscribe.onSubscribed.subscribe { + UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); + } + + _buttonSubscriptionSettings.setOnClickListener { + val url = channel?.url ?: _url ?: return@setOnClickListener; + val sub = StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener; + UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer); + }; + //TODO: Determine if this is really the only solution (isSaveEnabled=false) viewPager.isSaveEnabled = false; viewPager.registerOnPageChangeCallback(_onPageChangeCallback); @@ -246,6 +260,7 @@ class ChannelFragment : MainFragment() { if (parameter is String) { _buttonSubscribe.setSubscribeChannel(parameter); + _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; setPolycentricProfileOr(parameter) { _textChannel.text = ""; _textChannelSub.text = ""; @@ -377,6 +392,7 @@ class ChannelFragment : MainFragment() { _fragment.topBar?.assume()?.setMenuItems(buttons); _buttonSubscribe.setSubscribeChannel(channel); + _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; _textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else ""; //TODO: Find a better way to access the adapter fragments.. diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt index 7f7e6109..4d86ce3b 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment @@ -210,6 +211,11 @@ class PostDetailFragment : MainFragment { _repliesOverlay = findViewById(R.id.replies_overlay); + _buttonSubscribe.onSubscribed.subscribe { + //TODO: add overlay to layout + //UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); + }; + val layoutTop: LinearLayout = findViewById(R.id.layout_top); root.removeView(layoutTop); _commentsList.setPrependedView(layoutTop); 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 1bb98f2e..5582ab96 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 @@ -314,6 +314,11 @@ class VideoDetailView : ConstraintLayout { _layoutMonetization.visibility = View.GONE; _player.attachPlayer(); + + _buttonSubscribe.onSubscribed.subscribe { + UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); + }; + _container_content_liveChat.onRaidNow.subscribe { StatePlayer.instance.clearQueue(); fragment.navigate(it.targetUrl); diff --git a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt index d2db99ab..de6c5ee2 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt @@ -12,6 +12,13 @@ import java.time.OffsetDateTime class Subscription { var channel: SerializedChannel; + //Settings + var doNotifications: Boolean = false; + var doFetchLive: Boolean = false; + var doFetchStreams: Boolean = true; + var doFetchVideos: Boolean = true; + var doFetchPosts: Boolean = false; + //Last found content @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) var lastVideo : OffsetDateTime = OffsetDateTime.MAX; @@ -48,8 +55,4 @@ class Subscription { fun updateChannel(channel: IPlatformChannel) { this.channel = SerializedChannel.fromChannel(channel); } - - fun updateVideoStatus(allVideos: List? = null, liveStreams: List? = null) { - - } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index 88dcaf91..b5155c4a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -651,11 +651,8 @@ class StatePlatform { return _scope.async { getChannelLive(url, updateSubscriptions) }; } - fun getChannelContent(channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List? = null): IPager { - Logger.i(TAG, "Platform - getChannelVideos"); - val baseClient = getChannelClient(channelUrl, ignorePlugins); + fun getChannelContent(baseClient: IPlatformClient, channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List? = null): IPager { val clientCapabilities = baseClient.getChannelCapabilities(); - val client = if(usePooledClients > 1) _channelClientPool.getClientPooled(baseClient, usePooledClients); else baseClient; @@ -770,6 +767,20 @@ class StatePlatform { return pagerResult; } + fun getChannelContent(channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List? = null): IPager { + Logger.i(TAG, "Platform - getChannelVideos"); + val baseClient = getChannelClient(channelUrl, ignorePlugins); + return getChannelContent(baseClient, channelUrl, isSubscriptionOptimized, usePooledClients, ignorePlugins); + } + fun getChannelContent(channelUrl: String, type: String?, ordering: String = ResultCapabilities.ORDER_CHONOLOGICAL): IPager { + val client = getChannelClient(channelUrl); + return getChannelContent(client, channelUrl, type, ordering); + } + fun getChannelContent(baseClient: IPlatformClient, channelUrl: String, type: String?, ordering: String = ResultCapabilities.ORDER_CHONOLOGICAL): IPager { + val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency()); + return client.getChannelContents(channelUrl, type, ordering) ; + } + fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel { val channel = getChannelClient(url).getChannel(url); diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt index 0602c82d..92ca426a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt @@ -144,6 +144,33 @@ class StatePolycentric { return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id }); } + fun getChannelUrls(url: String, channelId: PlatformID? = null): List { + + var polycentricProfile: PolycentricProfile? = null; + try { + polycentricProfile = PolycentricCache.instance.getCachedProfile(url)?.profile; + if (polycentricProfile == null && channelId != null) { + Logger.i("StateSubscriptions", "Get polycentric profile not cached"); + polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId) }?.profile; + } else { + Logger.i("StateSubscriptions", "Get polycentric profile cached"); + } + } + catch(ex: Throwable) { + Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex); + //TODO: Some way to communicate polycentric failing without blocking here + } + if(polycentricProfile != null) { + val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType } + .mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList(); + if(urls.any { it.equals(url, true) }) + return urls; + else + return listOf(url) + urls; + } + else + return listOf(url); + } fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager? { //TODO: Currently abusing subscription concurrency for parallelism val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency; 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 ed57be6a..40b153e9 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -29,6 +29,8 @@ import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.v2.ReconstructStore import com.futo.platformplayer.stores.v2.ManagedStore +import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm +import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms import kotlinx.coroutines.* import java.util.concurrent.ExecutionException import java.util.concurrent.ForkJoinPool @@ -53,7 +55,6 @@ class StateSubscriptions { private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency()); private val _legacySubscriptions = FragmentedStorage.get(); - val onSubscriptionsChanged = Event2, Boolean>(); private var _globalSubscriptionsLock = Object(); private var _globalSubscriptionFeed: ReusablePager? = null; @@ -62,6 +63,8 @@ class StateSubscriptions { var globalSubscriptionExceptions: List = listOf() private set; + private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SIMPLE; + private var _lastGlobalSubscriptionProgress: Int = 0; private var _lastGlobalSubscriptionTotal: Int = 0; val onGlobalSubscriptionsUpdateProgress = Event2(); @@ -69,6 +72,8 @@ class StateSubscriptions { val onGlobalSubscriptionsUpdatedOnce = Event1(); val onGlobalSubscriptionsException = Event1>(); + val onSubscriptionsChanged = Event2, Boolean>(); + fun getGlobalSubscriptionProgress(): Pair { return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal); } @@ -223,37 +228,27 @@ class StateSubscriptions { } fun getSubscriptionRequestCount(): Map { - val subs = getSubscriptions(); - val pluginReqCounts = mutableMapOf(); + return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope) + .countRequests(getSubscriptions()); + } - for(sub in subs) { - val client = StatePlatform.instance.getChannelClientOrNull(sub.channel.url); - if(client !is JSClient) - continue; + fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair, List> { + val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool); - val channelCaps = client.getChannelCapabilities(); - if(!pluginReqCounts.containsKey(client)) - pluginReqCounts[client] = 1; - else - pluginReqCounts[client] = pluginReqCounts[client]!! + 1; - - if(channelCaps.hasType(ResultCapabilities.TYPE_STREAMS) && sub.shouldFetchStreams()) - pluginReqCounts[client] = pluginReqCounts[client]!! + 1; - if(channelCaps.hasType(ResultCapabilities.TYPE_LIVE) && sub.shouldFetchLiveStreams()) - pluginReqCounts[client] = pluginReqCounts[client]!! + 1; - if(channelCaps.hasType(ResultCapabilities.TYPE_POSTS) && sub.shouldFetchPosts()) - pluginReqCounts[client] = pluginReqCounts[client]!! + 1; + algo.onProgress.subscribe { progress, total -> + onProgress?.invoke(progress, total); } - return pluginReqCounts; - } + algo.onNewCacheHit.subscribe { sub, content -> - fun getSubscriptionsFeed(allowFailure: Boolean = false): IPager { - val result = getSubscriptionsFeedWithExceptions(allowFailure, true); - if(result.second.any()) - throw result.second.first(); - return result.first; - } - fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope? = null, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair, List> { + } + + val subUrls = getSubscriptions().associateWith { + StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id) + }; + + val result = algo.getSubscriptions(subUrls); + return Pair(result.pager, result.exceptions); + /* val subsPager: Array>; val exs: ArrayList = arrayListOf(); @@ -384,6 +379,7 @@ class StateSubscriptions { pager.initialize(); //return Pair(pager, exs); return Pair(DedupContentPager(pager), exs); + */ } //New Migration diff --git a/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt new file mode 100644 index 00000000..0f4bf008 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/subscription/CachedSubscriptionAlgorithm.kt @@ -0,0 +1,39 @@ +package com.futo.platformplayer.subscription + +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +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.StatePlatform +import com.futo.platformplayer.states.StateSubscriptions +import com.futo.platformplayer.toSafeFileName +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ForkJoinPool + +class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = true, threadPool: ForkJoinPool? = null) + : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) { + + private val _pageSize: Int = pageSize; + + override fun countRequests(subs: Map>): Map { + return mapOf(); + } + + override fun getSubscriptions(subs: Map>): Result { + val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet(); + + val validStores = ChannelContentCache.instance._channelContents + .filter { validSubIds.contains(it.key) } + .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()); + } +} \ 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 new file mode 100644 index 00000000..86965492 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/subscription/SimpleSubscriptionAlgorithm.kt @@ -0,0 +1,186 @@ +package com.futo.platformplayer.subscription + +import com.futo.platformplayer.Settings +import com.futo.platformplayer.api.media.models.ResultCapabilities +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.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 +import com.futo.platformplayer.exceptions.ChannelException +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.StatePlatform +import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.states.StateSubscriptions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import java.lang.Exception +import java.lang.IllegalStateException +import java.util.concurrent.ExecutionException +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.ForkJoinTask +import kotlin.system.measureTimeMillis + +class SimpleSubscriptionAlgorithm( + scope: CoroutineScope, + allowFailure: Boolean = false, + withCacheFallback: Boolean = true, + threadPool: ForkJoinPool? = null +): SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) { + + override fun countRequests(subs: Map>): Map { + val pluginReqCounts = mutableMapOf(); + + for(sub in subs) { + for(subUrl in sub.value) { + val client = StatePlatform.instance.getChannelClientOrNull(sub.key.channel.url); + if (client !is JSClient) + continue; + + val channelCaps = client.getChannelCapabilities(); + if (!pluginReqCounts.containsKey(client)) + pluginReqCounts[client] = 1; + else + pluginReqCounts[client] = pluginReqCounts[client]!! + 1; + + if (channelCaps.hasType(ResultCapabilities.TYPE_STREAMS) && sub.key.shouldFetchStreams()) + pluginReqCounts[client] = pluginReqCounts[client]!! + 1; + if (channelCaps.hasType(ResultCapabilities.TYPE_LIVE) && sub.key.shouldFetchLiveStreams()) + pluginReqCounts[client] = pluginReqCounts[client]!! + 1; + if (channelCaps.hasType(ResultCapabilities.TYPE_POSTS) && sub.key.shouldFetchPosts()) + pluginReqCounts[client] = pluginReqCounts[client]!! + 1; + } + } + return pluginReqCounts; + } + + override fun getSubscriptions(subs: Map>): Result { + val subsPager: Array>; + val exs: ArrayList = arrayListOf(); + + val tasks = mutableListOf?>>>(); + var finished = 0; + val exceptionMap: HashMap = hashMapOf(); + val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency(); + val failedPlugins = arrayListOf(); + for (sub in subs.filter { StatePlatform.instance.hasEnabledChannelClient(it.key.channel.url) }) + tasks.add(getSubscription(sub.key, sub.value, failedPlugins){ channelEx -> + finished++; + onProgress.emit(finished, tasks.size); + + val ex = channelEx?.cause; + + if(channelEx != null) { + synchronized(exceptionMap) { + exceptionMap.put(sub.key, channelEx); + } + 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, "Subscriptionsgnoring 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); + } + } + } + } + }); + + val timeTotal = measureTimeMillis { + val taskResults = arrayListOf>(); + for(task in tasks) { + try { + val result = task.get(); + if(result != null) { + if(result.second != null) + taskResults.add(result.second!!); + if(exceptionMap.containsKey(result.first)) { + val ex = exceptionMap[result.first]; + 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; + }; + } + subsPager = taskResults.toTypedArray(); + } + Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms") + + if(subsPager.size <= 0 && exs.any()) + throw exs.first(); + + Logger.i(StateSubscriptions.TAG, "Subscription pager with ${subsPager.size} channels"); + val pager = MultiChronoContentPager(subsPager, allowFailure, 15); + pager.initialize(); + //return Pair(pager, exs); + return Result(DedupContentPager(pager), exs); + } + + private fun getSubscription(sub: Subscription, urls: List, failedPlugins: List, onFinished: (ChannelException?)->Unit): ForkJoinTask?>> { + return threadPool.submit?>> { + val toIgnore = synchronized(failedPlugins){ failedPlugins.toList() }; + + var pager: IPager? = null; + for(url in urls) { + try { + val platformClient = StatePlatform.instance.getChannelClientOrNull(url, toIgnore) ?: continue; + val time = measureTimeMillis { + pager = StatePlatform.instance.getChannelContent(platformClient, url, true, threadPool.poolSize, toIgnore); + + pager = ChannelContentCache.cachePagerResults(scope, pager!!) { + onNewCacheHit.emit(sub, it); + }; + + onFinished(null); + } + Logger.i( + "StateSubscriptions", + "Subscription [${sub.channel.name}] results in ${time}ms" + ); + } + catch(ex: Throwable) { + Logger.e(StateSubscriptions.TAG, "Subscription [${sub.channel.name}] failed", ex); + val channelEx = ChannelException(sub.channel, ex); + onFinished(channelEx); + if(!withCacheFallback) + throw channelEx; + else { + Logger.i(StateSubscriptions.TAG, "Channel ${sub.channel.name} failed, substituting with cache"); + pager = ChannelContentCache.instance.getChannelCachePager(sub.channel.url); + } + } + } + if(pager == null) + throw IllegalStateException("Uncaught nullable pager"); + return@submit Pair(sub, pager); + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt new file mode 100644 index 00000000..77889254 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/subscription/SmartSubscriptionAlgorithm.kt @@ -0,0 +1,23 @@ +package com.futo.platformplayer.subscription + +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.models.Subscription +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ForkJoinPool + +class SmartSubscriptionAlgorithm( + scope: CoroutineScope, + allowFailure: Boolean = false, + withCacheFallback: Boolean = true, + threadPool: ForkJoinPool? = null +): SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) { + override fun countRequests(subs: Map>): Map { + TODO("Not yet implemented") + } + + override fun getSubscriptions(subs: Map>): Result { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionFetchAlgorithm.kt new file mode 100644 index 00000000..187cf0e4 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionFetchAlgorithm.kt @@ -0,0 +1,46 @@ +package com.futo.platformplayer.subscription + +import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.constructs.Event2 +import com.futo.platformplayer.models.Subscription +import kotlinx.coroutines.CoroutineScope +import java.lang.Exception +import java.lang.IllegalStateException +import java.util.concurrent.ForkJoinPool + +abstract class SubscriptionFetchAlgorithm( + val scope: CoroutineScope, + val allowFailure: Boolean = false, + val withCacheFallback: Boolean = true, + private val _threadPool: ForkJoinPool? = null +) { + val threadPool: ForkJoinPool get() = _threadPool ?: throw IllegalStateException("Require thread pool parameter"); + val onNewCacheHit = Event2(); + val onProgress = Event2(); + + fun countRequests(subs: List): Map = countRequests(subs.associateWith { listOf(it.channel.url) }); + abstract fun countRequests(subs: Map>): Map; + + fun getSubscriptions(subs: List): Result = getSubscriptions(subs.associateWith { listOf(it.channel.url) }); + abstract fun getSubscriptions(subs: Map>): Result; + + + class Result( + val pager: IPager, + val exceptions: List + ); + + companion object { + fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm { + return when(algo) { + SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(150, scope, allowFailure, withCacheFallback, pool); + SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool); + SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool); + else -> throw IllegalStateException("Unknown algorithm ${algo}"); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionFetchAlgorithms.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionFetchAlgorithms.kt new file mode 100644 index 00000000..c5f9cb55 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionFetchAlgorithms.kt @@ -0,0 +1,7 @@ +package com.futo.platformplayer.subscription + +enum class SubscriptionFetchAlgorithms(val value: Int) { + CACHE(1), + SIMPLE(2), + SMART(3); +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuGroup.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuGroup.kt index a6d5dc3a..32e36027 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuGroup.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuGroup.kt @@ -11,6 +11,7 @@ import com.futo.platformplayer.R class SlideUpMenuGroup : LinearLayout { private lateinit var title: TextView; + private lateinit var description: TextView; private lateinit var itemContainer: LinearLayout; private var parentClickListener: (()->Unit)? = null; private val items: List; @@ -28,6 +29,16 @@ class SlideUpMenuGroup : LinearLayout { groupTag = tag; this.items = items.toList(); addItems(items); + description.visibility = View.GONE; + } + constructor(context: Context, titleText: String, descriptionText: String, tag: Any, items: List) : super(context){ + init(); + title.text = titleText; + groupTag = tag; + this.items = items.toList(); + addItems(items); + description.text = descriptionText; + description.visibility = View.VISIBLE; } constructor(context: Context, titleText: String, tag: Any, vararg items: SlideUpMenuItem) @@ -37,6 +48,7 @@ class SlideUpMenuGroup : LinearLayout { LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_group, this, true); title = findViewById(R.id.slide_up_menu_group_title); + description = findViewById(R.id.slide_up_menu_group_description); itemContainer = findViewById(R.id.slide_up_menu_group_items); } diff --git a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscribeButton.kt b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscribeButton.kt index dba1707d..c956b331 100644 --- a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscribeButton.kt +++ b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscribeButton.kt @@ -11,13 +11,12 @@ import android.widget.LinearLayout import android.widget.TextView import com.futo.platformplayer.* import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StateSubscriptions -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel class SubscribeButton : LinearLayout { private val _root: FrameLayout; @@ -29,12 +28,15 @@ class SubscribeButton : LinearLayout { var url : String? = null private set; - private var _isSubscribed: Boolean = false; + var isSubscribed: Boolean = false + private set; private val _subscribeTask = if (!isInEditMode) { TaskHandler(StateApp.instance.scopeGetter, StatePlatform.instance::getChannelLive).success(::handleSubscribe) } else { null }; + val onSubscribed = Event1(); + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { LayoutInflater.from(context).inflate(R.layout.button_subscribe, this, true); @@ -69,9 +71,10 @@ class SubscribeButton : LinearLayout { private fun handleSubscribe(channel: IPlatformChannel) { setIsLoading(false); - StateSubscriptions.instance.addSubscription(channel); + val sub = StateSubscriptions.instance.addSubscription(channel); UIDialogs.toast(context, context.getString(R.string.subscribed_to) + channel.name); setIsSubscribed(true); + onSubscribed.emit(sub); } private fun handleUnSubscribe(url: String) { setIsLoading(false); @@ -118,6 +121,6 @@ class SubscribeButton : LinearLayout { else _root.visibility = INVISIBLE; - _isSubscribed = isSubcribed; + isSubscribed = isSubcribed; } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_live_tv.xml b/app/src/main/res/drawable/ic_live_tv.xml new file mode 100644 index 00000000..560d1cb7 --- /dev/null +++ b/app/src/main/res/drawable/ic_live_tv.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 00000000..77461925 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 54661bf3..5ce19a54 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -82,7 +82,7 @@ tools:text="CHANNEL NAME" app:layout_constraintLeft_toRightOf="@id/creator_thumbnail" app:layout_constraintBottom_toTopOf="@id/text_metadata" - app:layout_constraintRight_toLeftOf="@id/button_subscribe" /> + app:layout_constraintRight_toLeftOf="@id/button_sub_settings" /> + +