mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-02 06:09:13 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
c3ff897ef4
24 changed files with 165 additions and 29 deletions
|
@ -95,7 +95,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 29
|
minSdk 28
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
_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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = "";
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue