Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Koen 2023-10-20 12:21:33 +02:00
commit c3ff897ef4
24 changed files with 165 additions and 29 deletions

View file

@ -95,7 +95,7 @@ android {
} }
defaultConfig { defaultConfig {
minSdk 29 minSdk 28
targetSdk 33 targetSdk 33
versionCode gitVersionCode versionCode gitVersionCode
versionName gitVersionName versionName gitVersionName

View file

@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.* import com.futo.platformplayer.activities.*
@ -63,7 +64,8 @@ class Settings : FragmentedStorageFileJson() {
try { try {
val i = Intent(Intent.ACTION_VIEW); val i = Intent(Intent.ACTION_VIEW);
val subject = "Feedback Grayjay"; val subject = "Feedback Grayjay";
val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n\n"; val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n" +
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n";
val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body)); val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body));
i.data = data; i.data = data;
@ -140,7 +142,11 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL; return FeedStyle.THUMBNAIL;
} }
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 6) @FormField("Fetch on app boot", FieldForm.TOGGLE, "Shortly after opening the app, start fetching subscriptions.", 6)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 7)
@DropdownFieldOptionsId(R.array.background_interval) @DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0; var subscriptionsBackgroundUpdateInterval: Int = 0;
@ -156,7 +162,7 @@ class Settings : FragmentedStorageFileJson() {
}; };
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 7) @FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 8)
@DropdownFieldOptionsId(R.array.thread_count) @DropdownFieldOptionsId(R.array.thread_count)
var subscriptionConcurrency: Int = 3; var subscriptionConcurrency: Int = 3;
@ -213,7 +219,7 @@ class Settings : FragmentedStorageFileJson() {
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate()); fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "Auto-rotate deadzone in degrees", 5) @FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5)
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone) @DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
var autoRotateDeadZone: Int = 0; var autoRotateDeadZone: Int = 0;

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
@ -40,7 +41,8 @@ class ExceptionActivity : AppCompatActivity() {
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context"; val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?"; val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?";
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n\n" + val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack"); Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack");
try { try {
val file = File(filesDir, "log.txt"); val file = File(filesDir, "log.txt");

View file

@ -3,7 +3,9 @@ package com.futo.platformplayer.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.webkit.ConsoleMessage
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -68,9 +70,15 @@ class LoginActivity : AppCompatActivity() {
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {}); view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
} }
} }
//TODO: Required for some...TBD what to do with it. Clear on finish?
_webView.settings.domStorageEnabled = true; _webView.settings.domStorageEnabled = true;
/*
_webView.webChromeClient = object: WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
Logger.w(TAG, "Login Console: " + consoleMessage?.message());
return super.onConsoleMessage(consoleMessage);
}
}*/
_webView.webViewClient = webViewClient; _webView.webViewClient = webViewClient;
_webView.loadUrl(authConfig.loginUrl); _webView.loadUrl(authConfig.loginUrl);
} }

View file

@ -27,6 +27,7 @@ class ResultCapabilities(
const val TYPE_VIDEOS = "VIDEOS"; const val TYPE_VIDEOS = "VIDEOS";
const val TYPE_STREAMS = "STREAMS"; const val TYPE_STREAMS = "STREAMS";
const val TYPE_LIVE = "LIVE"; const val TYPE_LIVE = "LIVE";
const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED"; const val TYPE_MIXED = "MIXED";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL"; const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";

View file

@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
override val contentProvider: String?, override val contentProvider: String?,
override val contentThumbnails: Thumbnails override val contentThumbnails: Thumbnails
) : IPlatformNestedContent, SerializedPlatformContent { ) : IPlatformNestedContent, SerializedPlatformContent {
final override val contentType: ContentType get() = ContentType.MEDIA; final override val contentType: ContentType get() = ContentType.NESTED_VIDEO;
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id; override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
override val contentSupported: Boolean get() = contentPlugin != null; override val contentSupported: Boolean get() = contentPlugin != null;

View file

@ -41,6 +41,7 @@ class SourcePluginConfig(
val constants: HashMap<String, String> = hashMapOf(), val constants: HashMap<String, String> = hashMapOf(),
//TODO: These should be vals...but prob for serialization reasons cannot be changed. //TODO: These should be vals...but prob for serialization reasons cannot be changed.
var subscriptionRateLimit: Int? = null,
var enableInSearch: Boolean = true, var enableInSearch: Boolean = true,
var enableInHome: Boolean = true, var enableInHome: Boolean = true,
var supportedClaimTypes: List<Int> = listOf() var supportedClaimTypes: List<Int> = listOf()

View file

@ -0,0 +1,9 @@
package com.futo.platformplayer.exceptions
class RateLimitException : Throwable {
val pluginIds: List<String>;
constructor(pluginIds: List<String>): super() {
this.pluginIds = pluginIds ?: listOf();
}
}

View file

@ -77,7 +77,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
}; };
_textName?.text = channel.name; _textName?.text = channel.name;
val metadata = "${channel.subscribers.toHumanNumber()} subscribers"; val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
_textMetadata?.text = metadata; _textMetadata?.text = metadata;
_lastChannel = channel; _lastChannel = channel;
setLinks(channel.links, channel.name); setLinks(channel.links, channel.name);

View file

@ -361,7 +361,7 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe.setSubscribeChannel(channel); _buttonSubscribe.setSubscribeChannel(channel);
_textChannel.text = channel.name; _textChannel.text = channel.name;
_textChannelSub.text = "${channel.subscribers.toHumanNumber()} subscribers"; _textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
_creatorThumbnail.setThumbnail(channel.thumbnail, true); _creatorThumbnail.setThumbnail(channel.thumbnail, true);
Glide.with(_imageBanner) Glide.with(_imageBanner)

View file

@ -601,7 +601,7 @@ class PostDetailFragment : MainFragment {
val subscribers = value?.author?.subscribers; val subscribers = value?.author?.subscribers;
if(subscribers != null && subscribers > 0) { if(subscribers != null && subscribers > 0) {
_channelMeta.visibility = View.VISIBLE; _channelMeta.visibility = View.VISIBLE;
_channelMeta.text = value.author.subscribers!!.toHumanNumber() + " subscribers"; _channelMeta.text = if((value.author?.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " subscribers" else "";
} else { } else {
_channelMeta.visibility = View.GONE; _channelMeta.visibility = View.GONE;
_channelMeta.text = ""; _channelMeta.text = "";

View file

@ -12,13 +12,16 @@ import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo 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.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.exceptions.RateLimitException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
@ -31,6 +34,7 @@ import com.futo.platformplayer.views.subscriptions.SubscriptionBar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -160,8 +164,17 @@ class SubscriptionsFeedFragment : MainFragment() {
private val _filterLock = Object(); private val _filterLock = Object();
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter"); private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
private var _bypassRateLimit = false;
private val _lastExceptions: List<Throwable>? = null; private val _lastExceptions: List<Throwable>? = null;
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh -> private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
if(!_bypassRateLimit) {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }
Logger.w(TAG, "Refreshing subscriptions with requests:\n" + reqCountStr);
if(rateLimitPlugins.any())
throw RateLimitException(rateLimitPlugins.map { it.key.id });
}
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh); val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions; val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
@ -171,6 +184,29 @@ class SubscriptionsFeedFragment : MainFragment() {
return@TaskHandler resp; return@TaskHandler resp;
}) })
.success { loadedResult(it); } .success { loadedResult(it); }
.exception<RateLimitException> {
fragment.lifecycleScope.launch(Dispatchers.IO) {
val subs = StateSubscriptions.instance.getSubscriptions();
val subsByLimited = it.pluginIds.map{ StatePlatform.instance.getClientOrNull(it) }
.filterIsInstance<JSClient>()
.associateWith { client -> subs.filter { it.getClient() == client } }
.map { Pair(it.key, it.value) }
withContext(Dispatchers.Main) {
UIDialogs.showDialog(context, R.drawable.ic_security_pred,
"Rate Limit Warning", "This is a temporary measure to prevent people from hitting rate limit until we have better support for lots of subscriptions." +
"\n\nYou have too many subscriptions for the following plugins:\n",
subsByLimited.map { "${it.first.config.name}: ${it.second.size} Subscriptions" } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
_bypassRateLimit = true;
loadResults();
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
UIDialogs.Action("OK", {
finishRefreshLayoutLoader();
setLoading(false);
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
.exception<Throwable> { .exception<Throwable> {
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it); Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
if(it !is CancellationException) if(it !is CancellationException)

View file

@ -873,7 +873,7 @@ class VideoDetailView : ConstraintLayout {
_channelName.text = video.author.name; _channelName.text = video.author.name;
_playWhenReady = true; _playWhenReady = true;
if(video.author.subscribers != null) { if(video.author.subscribers != null) {
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers"; _channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0); (_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
} else { } else {
_channelMeta.text = ""; _channelMeta.text = "";
@ -982,7 +982,7 @@ class VideoDetailView : ConstraintLayout {
_title.text = video.name; _title.text = video.name;
_channelName.text = video.author.name; _channelName.text = video.author.name;
if(video.author.subscribers != null) { if(video.author.subscribers != null) {
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers"; _channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0); (_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
} else { } else {
_channelMeta.text = ""; _channelMeta.text = "";
@ -1610,10 +1610,10 @@ class VideoDetailView : ConstraintLayout {
_lastSubtitleSource = toSet; _lastSubtitleSource = toSet;
} }
private fun handleUnavailableVideo() { private fun handleUnavailableVideo(msg: String? = null) {
if (!nextVideo()) { if (!nextVideo()) {
if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1)) if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1))
UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", "This video is unavailable.", null, 0, UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", msg ?: "This video is unavailable.", null, 0,
UIDialogs.Action("Back", { UIDialogs.Action("Back", {
this@VideoDetailView.onClose.emit(); this@VideoDetailView.onClose.emit();
}, UIDialogs.ActionStyle.PRIMARY)); }, UIDialogs.ActionStyle.PRIMARY));
@ -2092,7 +2092,7 @@ class VideoDetailView : ConstraintLayout {
} }
.exception<ScriptUnavailableException> { .exception<ScriptUnavailableException> {
Logger.w(TAG, "exception<ScriptUnavailableException>", it); Logger.w(TAG, "exception<ScriptUnavailableException>", it);
handleUnavailableVideo(); handleUnavailableVideo(it.message);
} }
.exception<ScriptException> { .exception<ScriptException> {
Logger.w(TAG, "exception<ScriptException>", it) Logger.w(TAG, "exception<ScriptException>", it)

View file

@ -3,24 +3,48 @@ package com.futo.platformplayer.models
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.StatePlatform
import java.time.OffsetDateTime import java.time.OffsetDateTime
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class Subscription { class Subscription {
var channel: SerializedChannel; var channel: SerializedChannel;
//Last found content
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastVideo : OffsetDateTime = OffsetDateTime.MAX; var lastVideo : OffsetDateTime = OffsetDateTime.MAX;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastLiveStream : OffsetDateTime = OffsetDateTime.MAX; var lastLiveStream : OffsetDateTime = OffsetDateTime.MAX;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastPost : OffsetDateTime = OffsetDateTime.MAX;
//Last update time
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastVideoUpdate : OffsetDateTime = OffsetDateTime.MIN;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastStreamUpdate : OffsetDateTime = OffsetDateTime.MIN;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastLiveStreamUpdate : OffsetDateTime = OffsetDateTime.MIN;
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
//Last video interval
var uploadInterval : Int = 0; var uploadInterval : Int = 0;
var uploadPostInterval : Int = 0;
constructor(channel : SerializedChannel) { constructor(channel : SerializedChannel) {
this.channel = channel; this.channel = channel;
} }
fun shouldFetchStreams() = lastLiveStream.getNowDiffDays() < 7;
fun shouldFetchLiveStreams() = lastLiveStream.getNowDiffDays() < 14;
fun shouldFetchPosts() = lastPost.getNowDiffDays() < 2;
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
fun updateChannel(channel: IPlatformChannel) { fun updateChannel(channel: IPlatformChannel) {
this.channel = SerializedChannel.fromChannel(channel); this.channel = SerializedChannel.fromChannel(channel);
} }

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class LoginWebViewClient : WebViewClient { class LoginWebViewClient : WebViewClient {
private val LOG_VERBOSE = false; private val LOG_VERBOSE = false;

View file

@ -20,10 +20,10 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
if(obj?.jsonPrimitive?.isString ?: true) if(obj?.jsonPrimitive?.isString ?: true)
return when(obj?.jsonPrimitive?.contentOrNull) { return when(obj?.jsonPrimitive?.contentOrNull) {
"MEDIA" -> SerializedPlatformVideo.serializer(); "MEDIA" -> SerializedPlatformVideo.serializer();
"NESTED" -> SerializedPlatformNestedContent.serializer(); "NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented"); "ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
"POST" -> throw NotImplementedError("Post not yet implemented"); "POST" -> throw NotImplementedError("Post not yet implemented");
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}") else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
}; };
else else
return when(obj?.jsonPrimitive?.int) { return when(obj?.jsonPrimitive?.int) {

View file

@ -28,6 +28,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.activities.CaptchaActivity import com.futo.platformplayer.activities.CaptchaActivity
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
@ -44,7 +45,10 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.stripe.android.core.utils.encodeToJson
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.* import java.util.*
@ -429,9 +433,19 @@ class StateApp {
StatePlaylists.instance.toMigrateCheck() StatePlaylists.instance.toMigrateCheck()
).flatten(), 0); ).flatten(), 0);
scope.launch { if(Settings.instance.subscriptions.fetchOnAppBoot) {
delay(5000); scope.launch(Dispatchers.IO) {
StateSubscriptions.instance.updateSubscriptionFeed(scope, false); val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
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);
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
}
else
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
}
} }
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes(); val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();

View file

@ -664,19 +664,24 @@ class StatePlatform {
toQuery.add(ResultCapabilities.TYPE_STREAMS); toQuery.add(ResultCapabilities.TYPE_STREAMS);
if(clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE)) if(clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE))
toQuery.add(ResultCapabilities.TYPE_LIVE); toQuery.add(ResultCapabilities.TYPE_LIVE);
if(clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS))
toQuery.add(ResultCapabilities.TYPE_POSTS);
if(isSubscriptionOptimized) { if(isSubscriptionOptimized) {
val sub = StateSubscriptions.instance.getSubscription(channelUrl); val sub = StateSubscriptions.instance.getSubscription(channelUrl);
if(sub != null) { if(sub != null) {
val daysSinceLiveStream = sub.lastLiveStream.getNowDiffDays() if(!sub.shouldFetchStreams()) {
if(daysSinceLiveStream > 7) { Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 7 days, skipping live streams [${sub.lastLiveStream.getNowDiffDays()} days ago]");
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 7 days, skipping live streams [${daysSinceLiveStream} days ago]");
toQuery.remove(ResultCapabilities.TYPE_LIVE); toQuery.remove(ResultCapabilities.TYPE_LIVE);
} }
if(daysSinceLiveStream > 14) { if(!sub.shouldFetchLiveStreams()) {
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 15 days, skipping streams [${daysSinceLiveStream} days ago]"); Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 15 days, skipping streams [${sub.lastLiveStream.getNowDiffDays()} days ago]");
toQuery.remove(ResultCapabilities.TYPE_STREAMS); toQuery.remove(ResultCapabilities.TYPE_STREAMS);
} }
if(!sub.shouldFetchPosts()) {
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 5 days, skipping posts [${sub.lastPost.getNowDiffDays()} days ago]");
toQuery.remove(ResultCapabilities.TYPE_POSTS);
}
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.states
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -18,6 +19,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
@ -219,6 +221,31 @@ class StateSubscriptions {
} }
} }
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
val subs = getSubscriptions();
val pluginReqCounts = mutableMapOf<JSClient, Int>();
for(sub in subs) {
val client = StatePlatform.instance.getChannelClientOrNull(sub.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.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;
}
return pluginReqCounts;
}
fun getSubscriptionsFeed(allowFailure: Boolean = false): IPager<IPlatformContent> { fun getSubscriptionsFeed(allowFailure: Boolean = false): IPager<IPlatformContent> {
val result = getSubscriptionsFeedWithExceptions(allowFailure, true); val result = getSubscriptionsFeedWithExceptions(allowFailure, true);
if(result.second.any()) if(result.second.any())

View file

@ -50,7 +50,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L) if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
_textMetadata.visibility = View.GONE; _textMetadata.visibility = View.GONE;
else { else {
_textMetadata.text = authorLink.subscribers!!.toHumanNumber() + " subscribers"; _textMetadata.text = if(authorLink?.subscribers ?: 0 > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
_textMetadata.visibility = View.VISIBLE; _textMetadata.visibility = View.VISIBLE;
} }
_buttonSubscribe.setSubscribeChannel(authorLink.url); _buttonSubscribe.setSubscribeChannel(authorLink.url);

View file

@ -69,6 +69,7 @@
android:layout_marginRight="30dp" android:layout_marginRight="30dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:singleLine="true"
android:inputType="textPassword"
android:hint="Backup Password" /> android:hint="Backup Password" />
<LinearLayout <LinearLayout

View file

@ -51,6 +51,7 @@
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginRight="30dp" android:layout_marginRight="30dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true" android:singleLine="true"
android:hint="Backup Password" /> android:hint="Backup Password" />

@ -1 +1 @@
Subproject commit aa2a4f297027903b6182254def885fd82e1bb5c4 Subproject commit 8ea9393634b13b478916e4dade664427d8f9a0fe

@ -1 +1 @@
Subproject commit eff873edf344663f7dd2741ea9f1b8eca17afa24 Subproject commit d05a959174bcceb616c9f42043466e9e1258f519