mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Subscriptions ratelimit and warnings, Nebula login requirement, Subscription fetch setting, -1 sub hide
This commit is contained in:
parent
d4317ff06f
commit
229377bd6e
11 changed files with 73 additions and 23 deletions
|
@ -95,7 +95,7 @@ android {
|
|||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk 29
|
||||
minSdk 28
|
||||
targetSdk 33
|
||||
versionCode gitVersionCode
|
||||
versionName gitVersionName
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.CookieManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.*
|
||||
|
@ -63,7 +64,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||
try {
|
||||
val i = Intent(Intent.ACTION_VIEW);
|
||||
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));
|
||||
i.data = data;
|
||||
|
||||
|
@ -217,7 +219,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
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)
|
||||
var autoRotateDeadZone: Int = 0;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
@ -40,7 +41,8 @@ class ExceptionActivity : AppCompatActivity() {
|
|||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
|
||||
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");
|
||||
try {
|
||||
val file = File(filesDir, "log.txt");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -77,7 +77,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||
};
|
||||
_textName?.text = channel.name;
|
||||
|
||||
val metadata = "${channel.subscribers.toHumanNumber()} subscribers";
|
||||
val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
||||
_textMetadata?.text = metadata;
|
||||
_lastChannel = channel;
|
||||
setLinks(channel.links, channel.name);
|
||||
|
|
|
@ -361,7 +361,7 @@ class ChannelFragment : MainFragment() {
|
|||
|
||||
_buttonSubscribe.setSubscribeChannel(channel);
|
||||
_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);
|
||||
Glide.with(_imageBanner)
|
||||
|
|
|
@ -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.IPlatformContent
|
||||
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.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.exceptions.RateLimitException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
|
@ -31,6 +34,7 @@ import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
|||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -160,8 +164,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||
private val _filterLock = Object();
|
||||
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
|
||||
|
||||
private var _bypassRateLimit = false;
|
||||
private val _lastExceptions: List<Throwable>? = null;
|
||||
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 currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
||||
|
@ -171,6 +184,29 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||
return@TaskHandler resp;
|
||||
})
|
||||
.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> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||
if(it !is CancellationException)
|
||||
|
|
|
@ -1610,10 +1610,10 @@ class VideoDetailView : ConstraintLayout {
|
|||
_lastSubtitleSource = toSet;
|
||||
}
|
||||
|
||||
private fun handleUnavailableVideo() {
|
||||
private fun handleUnavailableVideo(msg: String? = null) {
|
||||
if (!nextVideo()) {
|
||||
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", {
|
||||
this@VideoDetailView.onClose.emit();
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
|
@ -2092,7 +2092,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
}
|
||||
.exception<ScriptUnavailableException> {
|
||||
Logger.w(TAG, "exception<ScriptUnavailableException>", it);
|
||||
handleUnavailableVideo();
|
||||
handleUnavailableVideo(it.message);
|
||||
}
|
||||
.exception<ScriptException> {
|
||||
Logger.w(TAG, "exception<ScriptException>", it)
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
|||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.getNowDiffDays
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
@ -42,6 +43,8 @@ class Subscription {
|
|||
fun shouldFetchLiveStreams() = lastLiveStream.getNowDiffDays() < 14;
|
||||
fun shouldFetchPosts() = lastPost.getNowDiffDays() < 2;
|
||||
|
||||
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||
|
||||
fun updateChannel(channel: IPlatformChannel) {
|
||||
this.channel = SerializedChannel.fromChannel(channel);
|
||||
}
|
||||
|
|
|
@ -434,21 +434,19 @@ class StateApp {
|
|||
).flatten(), 0);
|
||||
|
||||
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
||||
if (!subRequestCounts.any { clientCount ->
|
||||
clientCount.key.config.subscriptionRateLimit
|
||||
?.let { rateLimit -> clientCount.value > rateLimit } == true
|
||||
}) {
|
||||
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
||||
scope.launch {
|
||||
delay(5000);
|
||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||
scope.launch(Dispatchers.IO) {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
else
|
||||
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
||||
}
|
||||
|
||||
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
||||
scheduleBackgroundWork(context, interval != 0, interval);
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit aa2a4f297027903b6182254def885fd82e1bb5c4
|
||||
Subproject commit 8ea9393634b13b478916e4dade664427d8f9a0fe
|
Loading…
Add table
Reference in a new issue