mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
PeekChannelContent and initial algorithm support, DevSubmit support, Prevent crash url search before init, Documentation url scanning for devportal, limit ongoing downloads ui to 4
This commit is contained in:
parent
6b57878275
commit
8be7b1272b
21 changed files with 258 additions and 49 deletions
|
@ -311,7 +311,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||
var alwaysReloadFromCache: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
|
||||
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||
var peekChannelContents: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||
fun clearChannelCache() {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||
StateCache.instance.clear();
|
||||
|
|
|
@ -60,6 +60,9 @@ class CachedPlatformClient : IPlatformClient {
|
|||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||
|
||||
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||
|
||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||
|
||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||
|
|
|
@ -84,6 +84,15 @@ interface IPlatformClient {
|
|||
*/
|
||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
||||
|
||||
/**
|
||||
* Describes what the plugin is capable on peek channel results
|
||||
*/
|
||||
fun getPeekChannelTypes(): List<String>;
|
||||
/**
|
||||
* Peeks contents of a channel, upload time descending
|
||||
*/
|
||||
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||
|
||||
/**
|
||||
* Gets the channel url associated with a claimType
|
||||
*/
|
||||
|
|
|
@ -17,7 +17,8 @@ data class PlatformClientCapabilities(
|
|||
val hasGetChannelCapabilities: Boolean = false,
|
||||
val hasGetLiveEvents: Boolean = false,
|
||||
val hasGetLiveChatWindow: Boolean = false,
|
||||
val hasGetContentChapters: Boolean = false
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ open class PlatformAuthorLink {
|
|||
val id: PlatformID;
|
||||
val name: String;
|
||||
val url: String;
|
||||
val thumbnail: String?;
|
||||
var thumbnail: String?;
|
||||
var subscribers: Long? = null; //Optional
|
||||
|
||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
|||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||
|
@ -56,7 +57,6 @@ import com.futo.platformplayer.states.StatePlugins
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Dictionary
|
||||
import kotlin.reflect.full.findAnnotations
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
import kotlin.streams.asSequence
|
||||
|
@ -75,6 +75,7 @@ open class JSClient : IPlatformClient {
|
|||
private var _searchCapabilities: ResultCapabilities? = null;
|
||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||
private var _channelCapabilities: ResultCapabilities? = null;
|
||||
private var _peekChannelTypes: List<String>? = null;
|
||||
|
||||
protected val _script: String;
|
||||
|
||||
|
@ -93,7 +94,11 @@ open class JSClient : IPlatformClient {
|
|||
|
||||
private val _busyLock = Object();
|
||||
private var _busyCounter = 0;
|
||||
private var _busyAction = "";
|
||||
val isBusy: Boolean get() = _busyCounter > 0;
|
||||
val isBusyAction: String get() {
|
||||
return _busyAction;
|
||||
}
|
||||
|
||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||
|
||||
|
@ -152,6 +157,8 @@ open class JSClient : IPlatformClient {
|
|||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||
this._context = context;
|
||||
|
@ -175,6 +182,8 @@ open class JSClient : IPlatformClient {
|
|||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
|
||||
open fun getCopy(): JSClient {
|
||||
|
@ -220,6 +229,7 @@ open class JSClient : IPlatformClient {
|
|||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -263,7 +273,7 @@ open class JSClient : IPlatformClient {
|
|||
}
|
||||
|
||||
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getHome()"));
|
||||
|
@ -271,7 +281,7 @@ open class JSClient : IPlatformClient {
|
|||
|
||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
||||
.toArray()
|
||||
|
@ -301,7 +311,7 @@ open class JSClient : IPlatformClient {
|
|||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||
|
@ -325,7 +335,7 @@ open class JSClient : IPlatformClient {
|
|||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSearchChannelContents)
|
||||
throw IllegalStateException("This plugin does not support channel search");
|
||||
|
@ -337,7 +347,7 @@ open class JSClient : IPlatformClient {
|
|||
@JSOptional
|
||||
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
||||
@JSDocsParameter("query", "Query that channels should match")
|
||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSChannelPager(config, this,
|
||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||
|
@ -357,7 +367,7 @@ open class JSClient : IPlatformClient {
|
|||
}
|
||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSChannel(config,
|
||||
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
||||
|
@ -384,12 +394,46 @@ open class JSClient : IPlatformClient {
|
|||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||
}
|
||||
|
||||
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||
override fun getPeekChannelTypes(): List<String> {
|
||||
if(!capabilities.hasPeekChannelContents)
|
||||
return listOf();
|
||||
try {
|
||||
if (_peekChannelTypes != null) {
|
||||
return _peekChannelTypes!!;
|
||||
}
|
||||
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||
|
||||
_peekChannelTypes = arr.keys.mapNotNull {
|
||||
val str = arr.get<V8ValueString>(it);
|
||||
return@mapNotNull str.value;
|
||||
};
|
||||
return _peekChannelTypes ?: listOf();
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||
return listOf();
|
||||
}
|
||||
}
|
||||
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
|
||||
ensureEnabled();
|
||||
|
||||
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
|
||||
return@isBusyWith items.keys.mapNotNull {
|
||||
val obj = items.get<V8ValueObject>(it);
|
||||
return@mapNotNull IJSContent.fromV8(this, obj);
|
||||
};
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
||||
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
||||
|
@ -450,7 +494,7 @@ open class JSClient : IPlatformClient {
|
|||
}
|
||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||
|
@ -459,7 +503,7 @@ open class JSClient : IPlatformClient {
|
|||
@JSOptional //getContentChapters = function(url, initialData)
|
||||
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||
if(!capabilities.hasGetContentChapters)
|
||||
return@isBusyWith listOf();
|
||||
ensureEnabled();
|
||||
|
@ -470,7 +514,7 @@ open class JSClient : IPlatformClient {
|
|||
@JSOptional
|
||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
|
||||
if(!capabilities.hasGetPlaybackTracker)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
|
@ -484,7 +528,7 @@ open class JSClient : IPlatformClient {
|
|||
|
||||
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
|
||||
ensureEnabled();
|
||||
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||
|
@ -502,7 +546,7 @@ open class JSClient : IPlatformClient {
|
|||
|
||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||
if(!capabilities.hasGetLiveChatWindow)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
|
@ -511,7 +555,7 @@ open class JSClient : IPlatformClient {
|
|||
}
|
||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||
if(!capabilities.hasGetLiveEvents)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
|
@ -524,7 +568,7 @@ open class JSClient : IPlatformClient {
|
|||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSearchPlaylists)
|
||||
throw IllegalStateException("This plugin does not support playlist search");
|
||||
|
@ -534,15 +578,22 @@ open class JSClient : IPlatformClient {
|
|||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun isPlaylistUrl(url: String): Boolean {
|
||||
ensureEnabled();
|
||||
if (!capabilities.hasGetPlaylist)
|
||||
return false;
|
||||
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
|
||||
|
||||
try {
|
||||
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@JSOptional
|
||||
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
||||
}
|
||||
|
@ -639,19 +690,24 @@ open class JSClient : IPlatformClient {
|
|||
}
|
||||
|
||||
|
||||
private fun <T> isBusyWith(handle: ()->T): T {
|
||||
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||
try {
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter++;
|
||||
}
|
||||
_busyAction = actionName;
|
||||
return handle();
|
||||
}
|
||||
finally {
|
||||
_busyAction = "";
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter--;
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun <T> isBusyWith(handle: ()->T): T {
|
||||
return isBusyWith("Unknown", handle);
|
||||
}
|
||||
|
||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||
if(ex is PluginEngineException)
|
||||
|
|
|
@ -45,7 +45,8 @@ class SourcePluginConfig(
|
|||
var enableInSearch: Boolean = true,
|
||||
var enableInHome: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf(),
|
||||
var primaryClaimFieldType: Int? = null
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.futo.platformplayer.states.StateAnnouncement
|
|||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
@ -90,7 +91,7 @@ class SourcePluginDescriptor {
|
|||
@Serializable
|
||||
class AppPluginSettings {
|
||||
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1)
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
||||
var checkForUpdates: Boolean = true;
|
||||
|
||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||
|
@ -130,6 +131,11 @@ class SourcePluginDescriptor {
|
|||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
|
||||
var allowDeveloperSubmit: Boolean = false;
|
||||
|
||||
|
||||
fun loadDefaults(config: SourcePluginConfig) {
|
||||
if(tabEnabled.enableHome == null)
|
||||
tabEnabled.enableHome = config.enableInHome
|
||||
|
|
|
@ -11,7 +11,6 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
|
|||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
@ -45,7 +44,6 @@ class V8Plugin {
|
|||
private val _clientAuth: ManagedHttpClient;
|
||||
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||
|
||||
|
||||
val httpClient: ManagedHttpClient get() = _client;
|
||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
||||
|
@ -71,6 +69,11 @@ class V8Plugin {
|
|||
private var _busyCounter = 0;
|
||||
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||
|
||||
var allowDevSubmit: Boolean = false
|
||||
private set(value) {
|
||||
field = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a busy counter is about to be removed.
|
||||
* Is primarily used to prevent additional calls to dead runtimes.
|
||||
|
@ -92,6 +95,10 @@ class V8Plugin {
|
|||
withDependency(getPackage(pack));
|
||||
}
|
||||
|
||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||
allowDevSubmit = isAllowed;
|
||||
}
|
||||
|
||||
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
||||
if(!_deps.containsKey(assetPath))
|
||||
_deps.put(assetPath, getAssetFile(context, assetPath));
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.futo.platformplayer.logging.Logger
|
|||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
|
@ -12,6 +13,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class PackageBridge : V8Package {
|
||||
@Transient
|
||||
|
@ -21,6 +25,7 @@ class PackageBridge : V8Package {
|
|||
@Transient
|
||||
private val _clientAuth: ManagedHttpClient
|
||||
|
||||
|
||||
override val name: String get() = "Bridge";
|
||||
override val variableName: String get() = "bridge";
|
||||
|
||||
|
@ -47,6 +52,44 @@ class PackageBridge : V8Package {
|
|||
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
||||
}
|
||||
|
||||
private val _jsonSerializer = Json { this.prettyPrintIndent = " "; this.prettyPrint = true; };
|
||||
private var _devSubmitClient: ManagedHttpClient? = null;
|
||||
@V8Function
|
||||
fun devSubmit(label: String, data: String) {
|
||||
if(_plugin.config !is SourcePluginConfig)
|
||||
return;
|
||||
if(!_plugin.allowDevSubmit)
|
||||
return;
|
||||
val devUrl = _plugin.config.developerSubmitUrl ?: return;
|
||||
if(_devSubmitClient == null)
|
||||
_devSubmitClient = ManagedHttpClient();
|
||||
|
||||
val stackTrace = Thread.currentThread().stackTrace;
|
||||
val callerMethod = stackTrace.findLast {
|
||||
it.className == JSClient::class.java.name
|
||||
}?.methodName ?: "";
|
||||
val session = StateApp.instance.sessionId;
|
||||
val pluginId = _plugin.config.id;
|
||||
val pluginVersion = _plugin.config.version;
|
||||
|
||||
val obj = DevSubmitData(pluginId, pluginVersion, callerMethod, session, label, data);
|
||||
|
||||
UIDialogs.toast("DevSubmit [${callerMethod}] (${_plugin.config.name})", false);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val json = _jsonSerializer.encodeToString(obj);
|
||||
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl}\n" + json);
|
||||
val resp = _devSubmitClient?.post(devUrl, json, mutableMapOf(Pair("Content-Type", "application/json")));
|
||||
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl} Status: " + (resp?.code?.toString() ?: "-1"))
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e(TAG, "DevSubmission to [${devUrl}] failed due to:\n" + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Serializable
|
||||
class DevSubmitData(val pluginId: String, val pluginVersion: Int, val caller: String, val session: String, val label: String, val data: String)
|
||||
|
||||
@V8Function
|
||||
fun throwTest(str: String) {
|
||||
throw IllegalStateException(str);
|
||||
|
|
|
@ -22,12 +22,12 @@ class PackageUtilities : V8Package {
|
|||
|
||||
@V8Function
|
||||
fun toBase64(arr: ByteArray): String {
|
||||
return Base64.encodeToString(arr, Base64.NO_WRAP);
|
||||
return Base64.encodeToString(arr, Base64.NO_PADDING or Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun fromBase64(str: String): ByteArray {
|
||||
return Base64.decode(str, Base64.DEFAULT)
|
||||
return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
|
|
|
@ -153,10 +153,10 @@ class DownloadsFragment : MainFragment() {
|
|||
_listActiveDownloadsContainer.visibility = GONE;
|
||||
else {
|
||||
_listActiveDownloadsContainer.visibility = VISIBLE;
|
||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
|
||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
|
||||
|
||||
_listActiveDownloads.removeAllViews();
|
||||
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||
_listActiveDownloads.addView(view);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.webkit.CookieManager
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
|
@ -107,17 +108,20 @@ class SourceDetailFragment : MainFragment() {
|
|||
fun onHide() {
|
||||
val id = _config?.id ?: return;
|
||||
|
||||
if(_settingsChanged && _settings != null) {
|
||||
_settingsChanged = false;
|
||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||
reloadSource(id);
|
||||
|
||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||
}
|
||||
var shouldReload = false;
|
||||
if(_settingsAppChanged) {
|
||||
_settingsAppForm.setObjectValues();
|
||||
StatePlugins.instance.savePlugin(id);
|
||||
shouldReload = true;
|
||||
}
|
||||
if(_settingsChanged && _settings != null) {
|
||||
_settingsChanged = false;
|
||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||
shouldReload = true;
|
||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||
}
|
||||
if(shouldReload)
|
||||
reloadSource(id);
|
||||
}
|
||||
|
||||
|
||||
|
@ -137,9 +141,25 @@ class SourceDetailFragment : MainFragment() {
|
|||
//App settings
|
||||
try {
|
||||
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
||||
if(source.config.developerSubmitUrl.isNullOrEmpty()) {
|
||||
val field = _settingsAppForm.findField("devSubmit");
|
||||
field?.setValue(false);
|
||||
if(field is View)
|
||||
field.isVisible = false;
|
||||
}
|
||||
_settingsAppForm.onChanged.clear();
|
||||
_settingsAppForm.onChanged.subscribe { _, _ ->
|
||||
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||
_settingsAppChanged = true;
|
||||
if(field.descriptor?.id == "devSubmit") {
|
||||
if(value is Boolean && value) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow,
|
||||
"Are you sure you trust the developer?",
|
||||
"Developers may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.\nThe following domain is used:",
|
||||
source.config.developerSubmitUrl ?: "", 0,
|
||||
UIDialogs.Action("Cancel", { field.setValue(false); }, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Enable", { }, UIDialogs.ActionStyle.DANGEROUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
||||
|
|
|
@ -40,6 +40,9 @@ class Subscription {
|
|||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPeekVideo : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
//Last video interval
|
||||
var uploadInterval : Int = 0;
|
||||
var uploadStreamInterval : Int = 0;
|
||||
|
@ -126,6 +129,7 @@ class Subscription {
|
|||
else if(lastVideo.year > 3000)
|
||||
lastVideo = OffsetDateTime.MIN;
|
||||
lastVideoUpdate = OffsetDateTime.now();
|
||||
lastPeekVideo = OffsetDateTime.MIN;
|
||||
}
|
||||
ResultCapabilities.TYPE_MIXED -> {
|
||||
uploadInterval = interval;
|
||||
|
@ -134,6 +138,7 @@ class Subscription {
|
|||
else if(lastVideo.year > 3000)
|
||||
lastVideo = OffsetDateTime.MIN;
|
||||
lastVideoUpdate = OffsetDateTime.now();
|
||||
lastPeekVideo = OffsetDateTime.MIN;
|
||||
}
|
||||
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
||||
uploadInterval = interval;
|
||||
|
|
|
@ -54,6 +54,9 @@ import kotlin.system.measureTimeMillis
|
|||
class StateApp {
|
||||
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
||||
|
||||
val sessionId = UUID.randomUUID().toString();
|
||||
|
||||
|
||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
if(isValidStorageUri(context, generalUri))
|
||||
|
|
|
@ -797,6 +797,10 @@ class StatePlatform {
|
|||
return client.getChannelContents(channelUrl, type, ordering) ;
|
||||
}
|
||||
|
||||
fun peekChannelContents(baseClient: IPlatformClient, channelUrl: String, type: String?): List<IPlatformContent> {
|
||||
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||
return client.peekChannelContents(channelUrl, type) ;
|
||||
}
|
||||
|
||||
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
||||
val channel = getChannelClient(url).getChannel(url);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.futo.platformplayer.subscription
|
||||
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.getNowDiffHours
|
||||
|
@ -7,6 +8,7 @@ import com.futo.platformplayer.logging.Logger
|
|||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
||||
class SmartSubscriptionAlgorithm(
|
||||
|
@ -70,18 +72,30 @@ class SmartSubscriptionAlgorithm(
|
|||
} else {
|
||||
val fetchTasks = mutableListOf<SubscriptionTask>();
|
||||
val cacheTasks = mutableListOf<SubscriptionTask>();
|
||||
var peekTasks = mutableListOf<SubscriptionTask>();
|
||||
|
||||
for(task in clientTasks.second) {
|
||||
if (!task.fromCache && fetchTasks.size < limit) {
|
||||
fetchTasks.add(task);
|
||||
} else {
|
||||
task.fromCache = true;
|
||||
cacheTasks.add(task);
|
||||
if(peekTasks.size < 100 &&
|
||||
Settings.instance.subscriptions.peekChannelContents &&
|
||||
(task.sub.lastPeekVideo.year < 1971 || task.sub.lastPeekVideo < task.sub.lastVideoUpdate) &&
|
||||
task.client.capabilities.hasPeekChannelContents &&
|
||||
task.client.getPeekChannelTypes().contains(task.type)) {
|
||||
task.fromPeek = true;
|
||||
task.fromCache = true;
|
||||
peekTasks.add(task);
|
||||
}
|
||||
else {
|
||||
task.fromCache = true;
|
||||
cacheTasks.add(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.i(TAG, "Subscription Client Budget [${clientTasks.first.name}]: ${fetchTasks.size}/${limit}")
|
||||
|
||||
finalTasks.addAll(fetchTasks + cacheTasks);
|
||||
finalTasks.addAll(fetchTasks + peekTasks + cacheTasks);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,6 +129,9 @@ class SmartSubscriptionAlgorithm(
|
|||
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
||||
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
|
||||
|
||||
return (expectedHours * 100).toInt();
|
||||
if((type == ResultCapabilities.TYPE_MIXED || type == ResultCapabilities.TYPE_VIDEOS) && (sub.lastPeekVideo.year > 1970 && sub.lastPeekVideo > sub.lastVideoUpdate))
|
||||
return 0;
|
||||
else
|
||||
return (expectedHours * 100).toInt();
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import com.futo.platformplayer.states.StateCache
|
|||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
|
@ -48,15 +49,17 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
val tasksGrouped = tasks.groupBy { it.client }
|
||||
|
||||
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } })" }.joinToString("\n"));
|
||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } - it.value.count { it.fromPeek && it.fromCache }}), Peek(${it.value.count { it.fromPeek }})" }.joinToString("\n"));
|
||||
|
||||
try {
|
||||
for(clientTasks in tasksGrouped) {
|
||||
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||
val clientTaskCount = clientTasks.value.count { !it.fromCache };
|
||||
val clientCacheCount = clientTasks.value.count { it.fromCache && !it.fromPeek };
|
||||
val clientPeekCount = clientTasks.value.count { it.fromPeek };
|
||||
val limit = clientTasks.key.getSubscriptionRateLimit();
|
||||
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). " +
|
||||
"(${if(clientPeekCount > 0) "${clientPeekCount} peek, " else ""}${clientCacheCount} cached)");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,8 +138,30 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
|
||||
for(task in tasks) {
|
||||
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
||||
if(task.fromPeek) {
|
||||
try {
|
||||
|
||||
val time = measureTimeMillis {
|
||||
val peekResults = StatePlatform.instance.peekChannelContents(task.client, task.url, task.type);
|
||||
val mostRecent = peekResults.firstOrNull();
|
||||
task.sub.lastPeekVideo = mostRecent?.datetime ?: OffsetDateTime.MIN;
|
||||
task.sub.saveAsync();
|
||||
val cacheItems = peekResults.filter { it.datetime != null && it.datetime!! > task.sub.lastVideoUpdate };
|
||||
//Fix for current situation
|
||||
for(item in cacheItems) {
|
||||
if(item.author.thumbnail.isNullOrEmpty())
|
||||
item.author.thumbnail = task.sub.channel.thumbnail;
|
||||
}
|
||||
StateCache.instance.cacheContents(cacheItems, false);
|
||||
}
|
||||
Logger.i("StateSubscriptions", "Subscription peek [${task.sub.channel.name}]:${task.type} results in ${time}ms");
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||
}
|
||||
}
|
||||
synchronized(cachedChannels) {
|
||||
if(task.fromCache) {
|
||||
if(task.fromCache || task.fromPeek) {
|
||||
finished++;
|
||||
onProgress.emit(finished, forkTasks.size);
|
||||
if(cachedChannels.contains(task.url)) {
|
||||
|
@ -218,6 +243,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
val url: String,
|
||||
val type: String,
|
||||
var fromCache: Boolean = false,
|
||||
var fromPeek: Boolean = false,
|
||||
var urgency: Int = 0
|
||||
);
|
||||
|
||||
|
|
|
@ -342,6 +342,8 @@
|
|||
<string name="fetch_on_tab_opened_description">Fetch new results when the tab is opened (if no results yet, disabling is not recommended unless you have issues)</string>
|
||||
<string name="always_reload_from_cache">Always reload from cache</string>
|
||||
<string name="always_reload_from_cache_description">This is not recommended, but a possible workaround for some issues.</string>
|
||||
<string name="peek_channel_contents">Peek Channel Contents</string>
|
||||
<string name="peek_channel_contents_description">Peek channel contents if supported by plugin of rate-limited calls, may increase subscription reload time.</string>
|
||||
<string name="get_answers_to_common_questions">Get answers to common questions</string>
|
||||
<string name="give_feedback_on_the_application">Give feedback on the application</string>
|
||||
<string name="info">Info</string>
|
||||
|
@ -487,6 +489,9 @@
|
|||
<string name="visibility">Visibility</string>
|
||||
<string name="check_for_updates_setting">Check for updates</string>
|
||||
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
||||
<string name="allow_developer_submit">Allow Developer Submissions</string>
|
||||
<string name="allow_developer_submit_description">Allows the developer to send data to their server, be careful as this might include sensitive data.</string>
|
||||
<string name="allow_developer_submit_warning">Make sure you trust the developer. They may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.</string>
|
||||
<string name="ratelimit">Rate-limit</string>
|
||||
<string name="ratelimit_description">Settings related to rate-limiting this plugin\'s behavior</string>
|
||||
<string name="ratelimit_sub_setting">Rate-limit Subscriptions</string>
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit bc13b38411bdb8ad7c48d869ec9bc2068e671bd0
|
||||
Subproject commit cee1fda4e875a46315a9d4492e2e3b541d98f39f
|
|
@ -1 +1 @@
|
|||
Subproject commit bef199baa9df5cb3192c7a3f8baf8c57e9fbdaea
|
||||
Subproject commit 940bb554d5fca5d8c3e2c6f501a74b7f03c9011b
|
Loading…
Add table
Reference in a new issue