mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'possiblebusyfix' into 'master'
Possible busy fix See merge request videostreaming/grayjay!173
This commit is contained in:
@@ -118,14 +118,13 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
|
||||
inline fun V8Plugin.ensureIsBusy() {
|
||||
this.let {
|
||||
if (!it.isThreadAlreadyBusy()) {
|
||||
//throw IllegalStateException("Tried to access V8Plugin without busy");
|
||||
val stacktrace = Thread.currentThread().stackTrace;
|
||||
Logger.w("Extensions_V8",
|
||||
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
|
||||
", " + stacktrace.drop(4)?.firstOrNull().toString() +
|
||||
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
|
||||
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
|
||||
);
|
||||
val message = "V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
|
||||
", " + stacktrace.drop(4)?.firstOrNull().toString() +
|
||||
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
|
||||
", " + stacktrace.drop(6)?.firstOrNull()?.toString();
|
||||
Logger.w("Extensions_V8", message);
|
||||
throw IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,8 +135,7 @@ inline fun V8Value.ensureIsBusy() {
|
||||
}
|
||||
|
||||
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
|
||||
if(false)
|
||||
ensureIsBusy();
|
||||
ensureIsBusy();
|
||||
return when(T::class) {
|
||||
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
|
||||
Int::class -> {
|
||||
@@ -186,10 +184,14 @@ inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextN
|
||||
else -> throw NotImplementedError("Type ${T::class.simpleName} not implemented conversion");
|
||||
}
|
||||
}
|
||||
fun V8ArrayToStringList(obj: V8ValueArray): List<String> = obj.keys.map { obj.getString(it) };
|
||||
fun V8ArrayToStringList(obj: V8ValueArray): List<String> {
|
||||
obj.ensureIsBusy();
|
||||
return obj.keys.map { obj.getString(it) };
|
||||
}
|
||||
fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
|
||||
if(obj == null)
|
||||
return hashMapOf();
|
||||
obj.ensureIsBusy();
|
||||
val map = hashMapOf<String, String>();
|
||||
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
|
||||
map.put(prop, obj.getString(prop));
|
||||
@@ -203,21 +205,27 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
||||
plugin.busy {
|
||||
this.register(object: IV8ValuePromise.IListener {
|
||||
override fun onFulfilled(p0: V8Value?) {
|
||||
if(p0 is V8ValueError)
|
||||
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
||||
else {
|
||||
if(p0 is V8ValueObject)
|
||||
p0.setWeak();
|
||||
promiseResult = p0 as T;
|
||||
plugin.busy {
|
||||
if(p0 is V8ValueError)
|
||||
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
||||
else {
|
||||
if(p0 is V8ValueObject)
|
||||
p0.setWeak();
|
||||
promiseResult = p0 as T;
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
override fun onRejected(p0: V8Value?) {
|
||||
promiseException = p0?.toException(plugin.config);
|
||||
plugin.busy {
|
||||
promiseException = p0?.toException(plugin.config);
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
override fun onCatch(p0: V8Value?) {
|
||||
promiseException = p0?.toException(plugin.config);
|
||||
plugin.busy {
|
||||
promiseException = p0?.toException(plugin.config);
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
@@ -229,20 +237,23 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
||||
}
|
||||
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
|
||||
|
||||
|
||||
if(!promise.isPending) {
|
||||
try {
|
||||
Logger.i("V8", "V8Promise resolved synchronously");
|
||||
if(promise.isFulfilled)
|
||||
promiseResult = promise.getResult<T>();
|
||||
else
|
||||
promiseException = promise.getResult<V8Value>().toException(plugin.config);
|
||||
val isPending = plugin.busy {
|
||||
promise.isPending
|
||||
};
|
||||
if(!isPending) {
|
||||
plugin.busy {
|
||||
try {
|
||||
Logger.i("V8", "V8Promise resolved synchronously");
|
||||
if(promise.isFulfilled)
|
||||
promiseResult = promise.getResult<T>();
|
||||
else
|
||||
promiseException = promise.getResult<V8Value>().toException(plugin.config);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
promiseException = ex;
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
promiseException = ex;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
plugin.unbusy {
|
||||
latch.await();
|
||||
}
|
||||
@@ -266,15 +277,19 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
||||
plugin.busy {
|
||||
this.register(object: IV8ValuePromise.IListener {
|
||||
override fun onFulfilled(p0: V8Value?) {
|
||||
plugin.resolvePromise(promise);
|
||||
underlyingDef.complete(p0 as T);
|
||||
plugin.busy {
|
||||
plugin.resolvePromise(promise);
|
||||
underlyingDef.complete(p0 as T);
|
||||
}
|
||||
}
|
||||
override fun onRejected(p0: V8Value?) {
|
||||
try {
|
||||
plugin.resolvePromise(promise);
|
||||
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
|
||||
Logger.i("V8", "Promise rejected, setting exception");
|
||||
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||
plugin.busy {
|
||||
plugin.resolvePromise(promise);
|
||||
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
|
||||
Logger.i("V8", "Promise rejected, setting exception");
|
||||
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e("V8", "Rejection handling failed?" , ex);
|
||||
@@ -282,9 +297,11 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
||||
}
|
||||
override fun onCatch(p0: V8Value?) {
|
||||
try {
|
||||
plugin.resolvePromise(promise);
|
||||
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
|
||||
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||
plugin.busy {
|
||||
plugin.resolvePromise(promise);
|
||||
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
|
||||
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e("V8", "Catching handling failed?" , ex);
|
||||
@@ -300,6 +317,7 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
||||
}
|
||||
|
||||
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
||||
ensureIsBusy();
|
||||
val p0 = this;
|
||||
if(p0 is V8ValueObject) {
|
||||
return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:");
|
||||
@@ -349,6 +367,7 @@ class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Defer
|
||||
|
||||
|
||||
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
|
||||
ensureIsBusy();
|
||||
var result = this.invoke<V8Value>(method, *obj);
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
|
||||
@@ -356,6 +375,7 @@ fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
|
||||
return result as T;
|
||||
}
|
||||
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
|
||||
ensureIsBusy();
|
||||
var result = this.invoke<V8Value>(method, *obj);
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueAsync(this.getSourcePlugin()!!);
|
||||
@@ -363,6 +383,7 @@ fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?):
|
||||
return V8Deferred(CompletableDeferred(result as T));
|
||||
}
|
||||
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
|
||||
ensureIsBusy();
|
||||
var result = this.invoke<V8Value>(method, *obj);
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
|
||||
@@ -370,6 +391,7 @@ fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
|
||||
return result;
|
||||
}
|
||||
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
|
||||
ensureIsBusy();
|
||||
var result = this.invoke<V8Value>(method, *obj);
|
||||
if(result is V8ValuePromise) {
|
||||
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
|
||||
@@ -399,4 +421,4 @@ fun <T> IPager<T>.toList(): List<T> {
|
||||
}
|
||||
|
||||
return list.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1543,4 +1543,4 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
return sourcesIntent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,13 +488,14 @@ open class JSClient : IPlatformClient {
|
||||
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();
|
||||
return busy {
|
||||
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||
_peekChannelTypes = arr.keys.mapNotNull {
|
||||
val str = arr.get<V8ValueString>(it);
|
||||
return@mapNotNull str.value;
|
||||
};
|
||||
return@busy _peekChannelTypes ?: listOf();
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||
@@ -523,10 +524,12 @@ open class JSClient : IPlatformClient {
|
||||
if(!capabilities.hasGetChannelUrlByClaim)
|
||||
throw IllegalStateException("This plugin does not support channel url by claim");
|
||||
|
||||
val value = plugin.executeTyped<V8Value>("source.getChannelUrlByClaim(${claimType}, ${Json.encodeToString(claimValues)})");
|
||||
if(value !is V8ValueString)
|
||||
return null;
|
||||
return value.value;
|
||||
return busy {
|
||||
val value = plugin.executeTyped<V8Value>("source.getChannelUrlByClaim(${claimType}, ${Json.encodeToString(claimValues)})");
|
||||
if(value !is V8ValueString)
|
||||
return@busy null;
|
||||
return@busy value.value;
|
||||
}
|
||||
}
|
||||
@JSOptional
|
||||
@JSDocs(12, "source.getChannelTemplateByClaimMap()", "Get a map for every supported claimtype mapping field to urls")
|
||||
@@ -536,28 +539,30 @@ open class JSClient : IPlatformClient {
|
||||
if(!capabilities.hasGetChannelTemplateByClaimMap)
|
||||
throw IllegalStateException("This plugin does not support channel template by claim map");
|
||||
|
||||
val value = plugin.executeTyped<V8Value>("source.getChannelTemplateByClaimMap()");
|
||||
if(value !is V8ValueObject)
|
||||
return mapOf();
|
||||
return busy {
|
||||
val value = plugin.executeTyped<V8Value>("source.getChannelTemplateByClaimMap()");
|
||||
if(value !is V8ValueObject)
|
||||
return@busy mapOf();
|
||||
|
||||
val claimTypes = mutableMapOf<Int, Map<Int, String>>();
|
||||
val claimTypes = mutableMapOf<Int, Map<Int, String>>();
|
||||
|
||||
val keys = value.ownPropertyNames;
|
||||
for(key in keys.toArray()) {
|
||||
if(key is V8ValueInteger) {
|
||||
val map = value.get<V8ValueObject>(key);
|
||||
val mapKeys = map.ownPropertyNames;
|
||||
val keys = value.ownPropertyNames;
|
||||
for(key in keys.toArray()) {
|
||||
if(key is V8ValueInteger) {
|
||||
val map = value.get<V8ValueObject>(key);
|
||||
val mapKeys = map.ownPropertyNames;
|
||||
|
||||
claimTypes[key.value] = mapKeys.toArray().filter {
|
||||
it is V8ValueInteger
|
||||
}.associate {
|
||||
val mapKey = (it as V8ValueInteger).value;
|
||||
return@associate Pair(mapKey, map.getString(mapKey));
|
||||
};
|
||||
claimTypes[key.value] = mapKeys.toArray().filter {
|
||||
it is V8ValueInteger
|
||||
}.associate {
|
||||
val mapKey = (it as V8ValueInteger).value;
|
||||
return@associate Pair(mapKey, map.getString(mapKey));
|
||||
};
|
||||
}
|
||||
}
|
||||
channelClaimTemplates = claimTypes.toMap();
|
||||
return@busy claimTypes;
|
||||
}
|
||||
channelClaimTemplates = claimTypes.toMap();
|
||||
return claimTypes;
|
||||
}
|
||||
|
||||
|
||||
@@ -698,27 +703,33 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocs(22, "source.getUserPlaylists()", "Gets the playlist of the current user")
|
||||
override fun getUserPlaylists(): Array<String> {
|
||||
ensureEnabled();
|
||||
return plugin.executeTyped<V8ValueArray>("source.getUserPlaylists()")
|
||||
.toArray()
|
||||
.map { (it as V8ValueString).value }
|
||||
.toTypedArray();
|
||||
return busy {
|
||||
return@busy plugin.executeTyped<V8ValueArray>("source.getUserPlaylists()")
|
||||
.toArray()
|
||||
.map { (it as V8ValueString).value }
|
||||
.toTypedArray();
|
||||
}
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(23, "source.getUserSubscriptions()", "Gets the subscriptions of the current user")
|
||||
override fun getUserSubscriptions(): Array<String> {
|
||||
ensureEnabled();
|
||||
return plugin.executeTyped<V8ValueArray>("source.getUserSubscriptions()")
|
||||
.toArray()
|
||||
.map { (it as V8ValueString).value }
|
||||
.toTypedArray();
|
||||
return busy {
|
||||
return@busy plugin.executeTyped<V8ValueArray>("source.getUserSubscriptions()")
|
||||
.toArray()
|
||||
.map { (it as V8ValueString).value }
|
||||
.toTypedArray();
|
||||
}
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
|
||||
override fun getUserHistory(): IPager<IPlatformContent> {
|
||||
ensureEnabled();
|
||||
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
||||
return isBusyWith("getUserHistory") {
|
||||
return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
||||
}
|
||||
}
|
||||
|
||||
fun validate() {
|
||||
@@ -894,4 +905,4 @@ open class JSClient : IPlatformClient {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.getSourcePlugin
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticle(
|
||||
@@ -34,7 +35,7 @@ open class JSArticle(
|
||||
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
|
||||
|
||||
override val thumbnails: Thumbnails? =
|
||||
if (obj.has("thumbnails"))
|
||||
if (obj.getSourcePlugin()?.busy { obj.has("thumbnails") } ?: obj.has("thumbnails"))
|
||||
Thumbnails.fromV8(
|
||||
config,
|
||||
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
|
||||
|
||||
+37
-21
@@ -31,18 +31,20 @@ open class JSArticleDetails(
|
||||
|
||||
final override val contentType: ContentType = ContentType.ARTICLE
|
||||
|
||||
private val _hasGetComments: Boolean = _content.has("getComments")
|
||||
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations")
|
||||
private val _hasGetComments: Boolean = client.busy { _content.has("getComments") }
|
||||
private val _hasGetContentRecommendations: Boolean = client.busy { _content.has("getContentRecommendations") }
|
||||
|
||||
override val rating: IRating =
|
||||
override val rating: IRating = client.busy {
|
||||
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null)
|
||||
?.let { IRating.fromV8(client.config, it, "PlatformArticle") }
|
||||
?: RatingLikes(0)
|
||||
}
|
||||
|
||||
override val summary: String =
|
||||
override val summary: String = client.busy {
|
||||
_content.getOrThrow(client.config, "summary", "PlatformArticle")
|
||||
}
|
||||
|
||||
override val thumbnails: Thumbnails? =
|
||||
override val thumbnails: Thumbnails? = client.busy {
|
||||
if (_content.has("thumbnails"))
|
||||
Thumbnails.fromV8(
|
||||
client.config,
|
||||
@@ -50,14 +52,19 @@ open class JSArticleDetails(
|
||||
)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
override val segments: List<IJSArticleSegment> =
|
||||
override val segments: List<IJSArticleSegment> = client.busy {
|
||||
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle")
|
||||
?.mapNotNull { fromV8Segment(client, it) }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(!_hasGetComments || _content.isClosed)
|
||||
val canGetComments = this.client.busy {
|
||||
_hasGetComments && !_content.isClosed
|
||||
}
|
||||
if(!canGetComments)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
@@ -73,7 +80,10 @@ open class JSArticleDetails(
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
val canGetContentRecommendations = this.client.busy {
|
||||
_hasGetContentRecommendations && !_content.isClosed
|
||||
}
|
||||
if(!canGetContentRecommendations)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
@@ -87,25 +97,31 @@ open class JSArticleDetails(
|
||||
}
|
||||
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
return client.busy {
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return@busy JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
return client.busy {
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return@busy JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
|
||||
if(!obj.has("type"))
|
||||
throw IllegalArgumentException("Object missing type field");
|
||||
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||
SegmentType.HEADER -> JSHeaderSegment(client, obj);
|
||||
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||
else -> null;
|
||||
return client.busy {
|
||||
if(!obj.has("type"))
|
||||
throw IllegalArgumentException("Object missing type field");
|
||||
return@busy when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||
SegmentType.HEADER -> JSHeaderSegment(client, obj);
|
||||
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||
else -> null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,4 +192,4 @@ class JSNestedSegment: IJSArticleSegment {
|
||||
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
|
||||
nested = IJSContent.fromV8(client, nestedObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
-12
@@ -46,23 +46,45 @@ class JSComment : IPlatformComment {
|
||||
_comment = obj;
|
||||
_plugin = plugin;
|
||||
|
||||
val contextName = "Comment";
|
||||
contextUrl = _comment!!.getOrThrow(config, "contextUrl", contextName);
|
||||
author = PlatformAuthorLink.fromV8(_config!!, _comment!!.getOrThrow(config, "author", contextName));
|
||||
message = _comment!!.getOrThrow(config, "message", contextName);
|
||||
rating = IRating.fromV8(config, _comment!!.getOrThrow(config, "rating", contextName));
|
||||
date = _comment!!.getOrThrowNullable<Int>(config, "date", contextName)?.let { OffsetDateTime.of(LocalDateTime.ofEpochSecond(it.toLong(), 0, ZoneOffset.UTC), ZoneOffset.UTC) }
|
||||
replyCount = _comment!!.getOrThrowNullable(config, "replyCount", contextName);
|
||||
context = _comment!!.getOrDefault(config, "context", contextName, hashMapOf()) ?: hashMapOf();
|
||||
_hasGetReplies = _comment!!.has("getReplies");
|
||||
var parsedContextUrl: String? = null;
|
||||
var parsedAuthor: PlatformAuthorLink? = null;
|
||||
var parsedMessage: String? = null;
|
||||
var parsedRating: IRating? = null;
|
||||
var parsedDate: OffsetDateTime? = null;
|
||||
var parsedReplyCount: Int? = null;
|
||||
var parsedContext: Map<String, String>? = null;
|
||||
var parsedHasGetReplies = false;
|
||||
|
||||
plugin.busy {
|
||||
val contextName = "Comment";
|
||||
parsedContextUrl = _comment!!.getOrThrow(config, "contextUrl", contextName);
|
||||
parsedAuthor = PlatformAuthorLink.fromV8(_config!!, _comment!!.getOrThrow(config, "author", contextName));
|
||||
parsedMessage = _comment!!.getOrThrow(config, "message", contextName);
|
||||
parsedRating = IRating.fromV8(config, _comment!!.getOrThrow(config, "rating", contextName));
|
||||
parsedDate = _comment!!.getOrThrowNullable<Int>(config, "date", contextName)?.let { OffsetDateTime.of(LocalDateTime.ofEpochSecond(it.toLong(), 0, ZoneOffset.UTC), ZoneOffset.UTC) };
|
||||
parsedReplyCount = _comment!!.getOrThrowNullable(config, "replyCount", contextName);
|
||||
parsedContext = _comment!!.getOrDefault(config, "context", contextName, hashMapOf()) ?: hashMapOf();
|
||||
parsedHasGetReplies = _comment!!.has("getReplies");
|
||||
}
|
||||
|
||||
contextUrl = parsedContextUrl ?: "";
|
||||
author = parsedAuthor ?: PlatformAuthorLink.UNKNOWN;
|
||||
message = parsedMessage ?: "";
|
||||
rating = parsedRating ?: throw IllegalStateException("Missing comment rating");
|
||||
date = parsedDate;
|
||||
replyCount = parsedReplyCount;
|
||||
context = parsedContext ?: hashMapOf();
|
||||
_hasGetReplies = parsedHasGetReplies;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(!_hasGetReplies)
|
||||
return null;
|
||||
|
||||
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
|
||||
return JSCommentPager(_config!!, plugin, obj);
|
||||
return plugin.busy {
|
||||
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||
return@busy JSCommentPager(_config!!, plugin, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.decodeUnicode
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getSourcePlugin
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
@@ -23,7 +24,8 @@ open class JSContent(
|
||||
|
||||
override val contentType: ContentType = ContentType.UNKNOWN
|
||||
|
||||
protected val _hasGetDetails: Boolean = _content.has("getDetails")
|
||||
protected val _hasGetDetails: Boolean =
|
||||
_content.getSourcePlugin()?.busy { _content.has("getDetails") } ?: false
|
||||
|
||||
override val id: PlatformID =
|
||||
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX))
|
||||
|
||||
@@ -41,7 +41,9 @@ abstract class JSPager<T> : IPager<T> {
|
||||
}
|
||||
|
||||
override fun hasMorePages(): Boolean {
|
||||
return _hasMorePages && !pager.isClosed;
|
||||
return plugin.getUnderlyingPlugin().busy {
|
||||
_hasMorePages && !pager.isClosed;
|
||||
}
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
@@ -91,4 +93,4 @@ abstract class JSPager<T> : IPager<T> {
|
||||
}
|
||||
|
||||
abstract fun convertResult(obj: V8ValueObject): T;
|
||||
}
|
||||
}
|
||||
|
||||
+49
-21
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getSourcePlugin
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
@@ -30,52 +31,79 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
val contextName = "PlatformPostDetails";
|
||||
var parsedRating: IRating? = null;
|
||||
var parsedTextType: TextType? = null;
|
||||
var parsedContent: String? = null;
|
||||
var parsedHasGetComments = false;
|
||||
var parsedHasGetContentRecommendations = false;
|
||||
|
||||
rating = obj.getOrDefault<V8ValueObject>(config, "rating", contextName, null)?.let { IRating.fromV8(config, it, contextName) } ?: RatingLikes(0);
|
||||
textType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
|
||||
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
||||
val parse = {
|
||||
val contextName = "PlatformPostDetails";
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
parsedRating = obj.getOrDefault<V8ValueObject>(config, "rating", contextName, null)?.let { IRating.fromV8(config, it, contextName) } ?: RatingLikes(0);
|
||||
parsedTextType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
|
||||
parsedContent = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
||||
|
||||
parsedHasGetComments = _content.has("getComments");
|
||||
parsedHasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
};
|
||||
obj.getSourcePlugin()?.busy {
|
||||
parse();
|
||||
} ?: parse()
|
||||
|
||||
rating = parsedRating ?: RatingLikes(0);
|
||||
textType = parsedTextType ?: TextType.RAW;
|
||||
content = parsedContent ?: "";
|
||||
_hasGetComments = parsedHasGetComments;
|
||||
_hasGetContentRecommendations = parsedHasGetContentRecommendations;
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(!_hasGetComments || _content.isClosed)
|
||||
val jsClient = client as? JSClient;
|
||||
if(jsClient == null)
|
||||
return null;
|
||||
val canGetComments = jsClient.busy {
|
||||
_hasGetComments && !_content.isClosed
|
||||
}
|
||||
if(!canGetComments)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
|
||||
return@handleDevCall getCommentsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getCommentsJS(client);
|
||||
|
||||
return null;
|
||||
return getCommentsJS(jsClient);
|
||||
}
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
val jsClient = client as? JSClient;
|
||||
if(jsClient == null)
|
||||
return null;
|
||||
val canGetContentRecommendations = jsClient.busy {
|
||||
_hasGetContentRecommendations && !_content.isClosed
|
||||
}
|
||||
if(!canGetContentRecommendations)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
|
||||
return@handleDevCall getContentRecommendationsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getContentRecommendationsJS(client);
|
||||
|
||||
return null;
|
||||
return getContentRecommendationsJS(jsClient);
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
return client.busy {
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return@busy JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
return client.busy {
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return@busy JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+20
-12
@@ -41,20 +41,26 @@ class JSRequestExecutor: AutoCloseable {
|
||||
this._config = plugin.config;
|
||||
val config = plugin.config;
|
||||
|
||||
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
|
||||
var parsedUrlPrefix: String? = null;
|
||||
var parsedHasCleanup = false;
|
||||
plugin.busy {
|
||||
parsedUrlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
|
||||
|
||||
if(!executor.has("executeRequest"))
|
||||
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
|
||||
hasCleanup = executor.has("cleanup");
|
||||
if(!executor.has("executeRequest"))
|
||||
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
|
||||
parsedHasCleanup = executor.has("cleanup");
|
||||
}
|
||||
|
||||
urlPrefix = parsedUrlPrefix;
|
||||
hasCleanup = parsedHasCleanup;
|
||||
}
|
||||
|
||||
//TODO: Executor properties?
|
||||
@Throws(ScriptException::class)
|
||||
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
|
||||
if (_executor.isClosed)
|
||||
throw IllegalStateException("Executor object is closed");
|
||||
|
||||
return _plugin.getUnderlyingPlugin().busy {
|
||||
if (_executor.isClosed)
|
||||
throw IllegalStateException("Executor object is closed");
|
||||
|
||||
val result = if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
@@ -108,10 +114,12 @@ class JSRequestExecutor: AutoCloseable {
|
||||
|
||||
|
||||
open fun cleanup() {
|
||||
synchronized(_cleanLock) {
|
||||
if (!hasCleanup || _executor.isClosed || _cleaned)
|
||||
return;
|
||||
_cleaned = true;
|
||||
_plugin.busy {
|
||||
synchronized(_cleanLock) {
|
||||
if (!hasCleanup || _executor.isClosed || _cleaned)
|
||||
return@busy;
|
||||
_cleaned = true;
|
||||
}
|
||||
}
|
||||
Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested");
|
||||
_plugin.busy {
|
||||
@@ -163,4 +171,4 @@ class ExecutorParameters {
|
||||
var rangeEnd: Int = -1;
|
||||
|
||||
var segment: Int = -1;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -36,11 +36,11 @@ class JSRequestModifier: IRequestModifier {
|
||||
}
|
||||
|
||||
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
|
||||
if (_modifier.isClosed) {
|
||||
return Request(url, headers);
|
||||
}
|
||||
|
||||
return _plugin.busy {
|
||||
if (_modifier.isClosed) {
|
||||
return@busy Request(url, headers);
|
||||
}
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
|
||||
_modifier.invokeV8("modifyRequest", url, headers);
|
||||
} as V8ValueObject;
|
||||
@@ -53,4 +53,4 @@ class JSRequestModifier: IRequestModifier {
|
||||
|
||||
|
||||
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -29,12 +29,28 @@ class JSSubtitleSource : ISubtitleSource {
|
||||
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
|
||||
_obj = v8Value;
|
||||
|
||||
val context = "JSSubtitles";
|
||||
name = v8Value.getOrThrow(config, "name", context, false);
|
||||
language = v8Value.getOrDefault(config, "language", context, null);
|
||||
url = v8Value.getOrThrow(config, "url", context, true);
|
||||
format = v8Value.getOrThrow(config, "format", context, true);
|
||||
hasFetch = v8Value.has("getSubtitles");
|
||||
var parsedName: String? = null;
|
||||
var parsedLanguage: String? = null;
|
||||
var parsedUrl: String? = null;
|
||||
var parsedFormat: String? = null;
|
||||
var parsedHasFetch = false;
|
||||
val parse = {
|
||||
val context = "JSSubtitles";
|
||||
parsedName = v8Value.getOrThrow(config, "name", context, false);
|
||||
parsedLanguage = v8Value.getOrDefault(config, "language", context, null);
|
||||
parsedUrl = v8Value.getOrThrow(config, "url", context, true);
|
||||
parsedFormat = v8Value.getOrThrow(config, "format", context, true);
|
||||
parsedHasFetch = v8Value.has("getSubtitles");
|
||||
};
|
||||
v8Value.getSourcePlugin()?.busy {
|
||||
parse();
|
||||
} ?: parse()
|
||||
|
||||
name = parsedName ?: "";
|
||||
language = parsedLanguage;
|
||||
url = parsedUrl;
|
||||
format = parsedFormat;
|
||||
hasFetch = parsedHasFetch;
|
||||
}
|
||||
|
||||
override fun getSubtitles(): String {
|
||||
@@ -69,4 +85,4 @@ class JSSubtitleSource : ISubtitleSource {
|
||||
return JSSubtitleSource(config, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-24
@@ -52,34 +52,63 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
override val subtitles: List<ISubtitleSource>;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
|
||||
val contextName = "VideoDetails";
|
||||
_plugin = plugin;
|
||||
val config = plugin.config;
|
||||
description = _content.getOrThrow(config, "description", contextName);
|
||||
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
|
||||
dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
|
||||
hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
|
||||
live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
|
||||
rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
|
||||
var parsedDescription: String? = null;
|
||||
var parsedVideo: IVideoSourceDescriptor? = null;
|
||||
var parsedDash: IDashManifestSource? = null;
|
||||
var parsedHls: IHLSManifestSource? = null;
|
||||
var parsedLive: IVideoSource? = null;
|
||||
var parsedRating: IRating? = null;
|
||||
var parsedSubtitles: List<ISubtitleSource>? = null;
|
||||
var parsedHasGetComments = false;
|
||||
var parsedHasGetPlaybackTracker = false;
|
||||
var parsedHasGetContentRecommendations = false;
|
||||
var parsedHasGetVODEvents = false;
|
||||
|
||||
if(!_content.has("subtitles"))
|
||||
subtitles = listOf();
|
||||
else {
|
||||
val subArrs = _content.getOrThrowNullable<V8ValueArray>(config, "subtitles", contextName);
|
||||
if(subArrs != null)
|
||||
subtitles = subArrs.keys.map { JSSubtitleSource.fromV8(config, subArrs.get(it)) };
|
||||
else
|
||||
subtitles = listOf();
|
||||
plugin.busy {
|
||||
val contextName = "VideoDetails";
|
||||
val config = plugin.config;
|
||||
parsedDescription = _content.getOrThrow(config, "description", contextName);
|
||||
parsedVideo = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
|
||||
parsedDash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
|
||||
parsedHls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
|
||||
parsedLive = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
|
||||
parsedRating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
|
||||
|
||||
if(!_content.has("subtitles"))
|
||||
parsedSubtitles = listOf();
|
||||
else {
|
||||
val subArrs = _content.getOrThrowNullable<V8ValueArray>(config, "subtitles", contextName);
|
||||
if(subArrs != null)
|
||||
parsedSubtitles = subArrs.keys.map { JSSubtitleSource.fromV8(config, subArrs.get(it)) };
|
||||
else
|
||||
parsedSubtitles = listOf();
|
||||
}
|
||||
|
||||
parsedHasGetComments = _content.has("getComments");
|
||||
parsedHasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||
parsedHasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
parsedHasGetVODEvents = _content.has("getVODEvents");
|
||||
}
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
_hasGetVODEvents = _content.has("getVODEvents");
|
||||
description = parsedDescription ?: "";
|
||||
video = parsedVideo ?: throw IllegalStateException("Missing video source descriptor");
|
||||
dash = parsedDash;
|
||||
hls = parsedHls;
|
||||
live = parsedLive;
|
||||
rating = parsedRating ?: RatingLikes(0);
|
||||
subtitles = parsedSubtitles ?: listOf();
|
||||
_hasGetComments = parsedHasGetComments;
|
||||
_hasGetPlaybackTracker = parsedHasGetPlaybackTracker;
|
||||
_hasGetContentRecommendations = parsedHasGetContentRecommendations;
|
||||
_hasGetVODEvents = parsedHasGetVODEvents;
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||
if(!_hasGetPlaybackTracker || _content.isClosed)
|
||||
val canGetPlaybackTracker = _plugin.busy {
|
||||
_hasGetPlaybackTracker && !_content.isClosed
|
||||
}
|
||||
if(!canGetPlaybackTracker)
|
||||
return null;
|
||||
if(_pluginConfig.id == StateDeveloper.DEV_ID)
|
||||
return StateDeveloper.instance.handleDevCall(_pluginConfig.id, "videoDetail.getComments()") {
|
||||
@@ -102,7 +131,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
}
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
val canGetContentRecommendations = _plugin.busy {
|
||||
_hasGetContentRecommendations && !_content.isClosed
|
||||
}
|
||||
if(!canGetContentRecommendations)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
@@ -122,7 +154,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(client !is JSClient || !_hasGetComments || _content.isClosed)
|
||||
if(client !is JSClient)
|
||||
return null;
|
||||
val canGetComments = _plugin.busy {
|
||||
_hasGetComments && !_content.isClosed
|
||||
}
|
||||
if(!canGetComments)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
@@ -153,4 +190,4 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
return@busy JSVODEventPager(_plugin.config, _plugin,
|
||||
_content.invokeV8<V8ValueObject>("getVODEvents", arrayOf<Any>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -39,10 +39,10 @@ open class JSAudioUrlSource(
|
||||
?: "$container $bitrate"
|
||||
|
||||
override var priority: Boolean =
|
||||
if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false
|
||||
plugin.busy { if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false }
|
||||
|
||||
override var original: Boolean =
|
||||
if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false
|
||||
plugin.busy { if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false }
|
||||
|
||||
override fun getAudioUrl(): String = url
|
||||
|
||||
|
||||
+12
-10
@@ -19,21 +19,23 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
val config = plugin.config
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") }
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
return _plugin.busy {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return@busy null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return@busy null
|
||||
|
||||
return@busy JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
||||
+4
-4
@@ -55,7 +55,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||
original = obj.getOrNull(config, "original", contextName) ?: false;
|
||||
hasGenerate = _obj.has("generate");
|
||||
hasGenerate = plugin.busy { _obj.has("generate") };
|
||||
}
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null;
|
||||
@@ -67,7 +67,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||
if(!hasGenerate)
|
||||
return V8Deferred(CompletableDeferred(manifest));
|
||||
if(_obj.isClosed)
|
||||
if(_plugin.busy { _obj.isClosed })
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val pregenerated = _pregenerate;
|
||||
@@ -111,7 +111,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
override fun generate(): String? {
|
||||
if(!hasGenerate)
|
||||
return manifest;
|
||||
if(_obj.isClosed)
|
||||
if(_plugin.busy { _obj.isClosed })
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
@@ -145,4 +145,4 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -73,7 +73,7 @@ open class JSDashManifestRawSource(
|
||||
override var manifest: String? =
|
||||
_obj.getOrDefault<String>(cfg, "manifest", ctx, null)
|
||||
|
||||
override val hasGenerate: Boolean = _obj.has("generate")
|
||||
override val hasGenerate: Boolean = plugin.busy { _obj.has("generate") }
|
||||
|
||||
val canMerge: Boolean =
|
||||
_obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false
|
||||
@@ -89,7 +89,7 @@ open class JSDashManifestRawSource(
|
||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||
if(!hasGenerate)
|
||||
return V8Deferred(CompletableDeferred(manifest));
|
||||
if(_obj.isClosed)
|
||||
if(_plugin.busy { _obj.isClosed })
|
||||
throw IllegalStateException("Source object already closed");
|
||||
val pregenerated = _pregenerate;
|
||||
if(pregenerated != null) {
|
||||
@@ -133,7 +133,7 @@ open class JSDashManifestRawSource(
|
||||
override open fun generate(): String? {
|
||||
if(!hasGenerate)
|
||||
return manifest;
|
||||
if(_obj.isClosed)
|
||||
if(_plugin.busy { _obj.isClosed })
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
var result: String? = null;
|
||||
@@ -241,4 +241,4 @@ class JSDashManifestMergingRawSource(
|
||||
companion object {
|
||||
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-11
@@ -42,27 +42,29 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") }
|
||||
|
||||
language = _obj.getOrNull(config, "language", contextName);
|
||||
original = _obj.getOrNull(config, "original", contextName);
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
return _plugin.busy {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return@busy null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return@busy null
|
||||
|
||||
return@busy JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun getVideoUrl(): String {
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-8
@@ -44,15 +44,26 @@ abstract class JSSource {
|
||||
this._obj = obj;
|
||||
this.type = type;
|
||||
|
||||
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
}
|
||||
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
||||
var parsedRequestModifier: JSRequest? = null;
|
||||
var parsedHasRequestModifier = false;
|
||||
var parsedRequestExecutor: JSRequest? = null;
|
||||
var parsedHasRequestExecutor = false;
|
||||
plugin.busy {
|
||||
parsedRequestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
};
|
||||
parsedHasRequestModifier = parsedRequestModifier != null || obj.has("getRequestModifier");
|
||||
|
||||
_requestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
parsedRequestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
};
|
||||
parsedHasRequestExecutor = parsedRequestExecutor != null || obj.has("getRequestExecutor");
|
||||
}
|
||||
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
|
||||
|
||||
_requestModifier = parsedRequestModifier;
|
||||
hasRequestModifier = parsedHasRequestModifier;
|
||||
_requestExecutor = parsedRequestExecutor;
|
||||
hasRequestExecutor = parsedHasRequestExecutor;
|
||||
}
|
||||
|
||||
fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
|
||||
@@ -166,4 +177,4 @@ abstract class JSSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-11
@@ -18,25 +18,27 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||
val config = plugin.config
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") }
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
return _plugin.busy {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return@busy null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return@busy null
|
||||
|
||||
return@busy JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getVideoUrl()
|
||||
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +139,17 @@ class VideoDownload {
|
||||
@Contextual
|
||||
@kotlinx.serialization.Transient
|
||||
var videoSourceLive: JSSource? = null;
|
||||
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingPlugin()?.busy {
|
||||
videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
} ?: false;
|
||||
|
||||
var requiresLiveAudioSource: Boolean = false;
|
||||
@Contextual
|
||||
@kotlinx.serialization.Transient
|
||||
var audioSourceLive: JSSource? = null;
|
||||
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingPlugin()?.busy {
|
||||
audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
} ?: false;
|
||||
|
||||
var hasVideoRequestExecutor: Boolean = false;
|
||||
var hasAudioRequestExecutor: Boolean = false;
|
||||
@@ -1603,4 +1607,4 @@ class VideoDownload {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
@@ -95,6 +96,9 @@ class V8Plugin {
|
||||
private val _busyLock = ReentrantLock()
|
||||
val isBusy get() = _busyLock.isLocked;
|
||||
|
||||
@Volatile
|
||||
private var _busyHolder: Thread? = null;
|
||||
|
||||
var allowDevSubmit: Boolean = false
|
||||
private set(value) {
|
||||
field = value;
|
||||
@@ -161,51 +165,53 @@ class V8Plugin {
|
||||
|
||||
fun start() {
|
||||
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
|
||||
synchronized(_runtimeLock) {
|
||||
if (_runtime != null)
|
||||
return;
|
||||
runtimeId = runtimeId + 1;
|
||||
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||
val host = V8Host.getV8Instance();
|
||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||
tryBusy(BUSY_STARTUP_MS) {
|
||||
synchronized(_runtimeLock) {
|
||||
if (_runtime != null)
|
||||
return@tryBusy;
|
||||
runtimeId = runtimeId + 1;
|
||||
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||
val host = V8Host.getV8Instance();
|
||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||
|
||||
_runtime = host.createV8Runtime(options);
|
||||
if (!host.isIsolateCreated)
|
||||
throw IllegalStateException("Isolate not created");
|
||||
_runtime = host.createV8Runtime(options);
|
||||
if (!host.isIsolateCreated)
|
||||
throw IllegalStateException("Isolate not created");
|
||||
|
||||
_runtimeMap.put(_runtime!!, this);
|
||||
_runtimeMap.put(_runtime!!, this);
|
||||
|
||||
//Setup bridge
|
||||
_runtime?.let {
|
||||
it.converter = V8Converter();
|
||||
//Setup bridge
|
||||
_runtime?.let {
|
||||
it.converter = V8Converter();
|
||||
|
||||
for (pack in _depsPackages) {
|
||||
if (pack.variableName != null)
|
||||
it.createV8ValueObject().use { v8valueObject ->
|
||||
it.globalObject.set(pack.variableName, v8valueObject);
|
||||
v8valueObject.bind(pack);
|
||||
};
|
||||
catchScriptErrors("Package Dep[${pack.name}]") {
|
||||
for (packScript in pack.getScripts())
|
||||
it.getExecutor(packScript).executeVoid();
|
||||
for (pack in _depsPackages) {
|
||||
if (pack.variableName != null)
|
||||
it.createV8ValueObject().use { v8valueObject ->
|
||||
it.globalObject.set(pack.variableName, v8valueObject);
|
||||
v8valueObject.bind(pack);
|
||||
};
|
||||
catchScriptErrors("Package Dep[${pack.name}]") {
|
||||
for (packScript in pack.getScripts())
|
||||
it.getExecutor(packScript).executeVoid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Load deps
|
||||
for (dep in _deps)
|
||||
catchScriptErrors("Dep[${dep.key}]") {
|
||||
it.getExecutor(dep.value).executeVoid()
|
||||
//Load deps
|
||||
for (dep in _deps)
|
||||
catchScriptErrors("Dep[${dep.key}]") {
|
||||
it.getExecutor(dep.value).executeVoid()
|
||||
};
|
||||
|
||||
|
||||
if (config.allowEval)
|
||||
it.allowEval(true);
|
||||
|
||||
//Load plugin
|
||||
catchScriptErrors("Plugin[${config.name}]") {
|
||||
it.getExecutor(script).executeVoid()
|
||||
};
|
||||
|
||||
|
||||
if (config.allowEval)
|
||||
it.allowEval(true);
|
||||
|
||||
//Load plugin
|
||||
catchScriptErrors("Plugin[${config.name}]") {
|
||||
it.getExecutor(script).executeVoid()
|
||||
};
|
||||
isStopped = false;
|
||||
isStopped = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,27 +260,30 @@ class V8Plugin {
|
||||
fun isThreadAlreadyBusy(): Boolean {
|
||||
return _busyLock.isHeldByCurrentThread;
|
||||
}
|
||||
fun <T> busy(handle: ()->T): T {
|
||||
_busyLock.lock();
|
||||
//Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||
fun <T> busy(handle: ()->T): T = busyInternal(BUSY_FATAL_MS, true, "busy(enter)", handle)
|
||||
|
||||
fun <T> tryBusy(maxWaitMs: Long, handle: ()->T): T = busyInternal(maxWaitMs, false, "tryBusy(enter)", handle)
|
||||
|
||||
private fun <T> busyInternal(maxWaitMs: Long, allowUnwedge: Boolean, context: String, handle: ()->T): T {
|
||||
acquireBusyOrThrow(context, maxWaitMs, allowUnwedge);
|
||||
_busyHolder = Thread.currentThread();
|
||||
try {
|
||||
return handle();
|
||||
}
|
||||
finally {
|
||||
//Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||
_busyLock.unlock();
|
||||
if (_busyLock.isHeldByCurrentThread) {
|
||||
if (_busyLock.holdCount == 1)
|
||||
_busyHolder = null;
|
||||
_busyLock.unlock();
|
||||
}
|
||||
}
|
||||
/*
|
||||
_busyLock.withLock {
|
||||
//Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString());
|
||||
return handle();
|
||||
}*/
|
||||
}
|
||||
fun <T> unbusy(handle: ()->T): T {
|
||||
val wasLocked = isThreadAlreadyBusy();
|
||||
if(!wasLocked)
|
||||
return handle();
|
||||
val lockCount = _busyLock.holdCount;
|
||||
_busyHolder = null;
|
||||
for(i in 1..lockCount)
|
||||
_busyLock.unlock();
|
||||
try {
|
||||
@@ -283,9 +292,90 @@ class V8Plugin {
|
||||
}
|
||||
finally {
|
||||
Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise")
|
||||
var acquired = 0;
|
||||
try {
|
||||
for (i in 1..lockCount) {
|
||||
acquireBusyOrThrow("unbusy(relock)");
|
||||
acquired++;
|
||||
}
|
||||
_busyHolder = Thread.currentThread();
|
||||
}
|
||||
catch (timeout: Throwable) {
|
||||
for (j in 1..acquired)
|
||||
_busyLock.unlock();
|
||||
throw timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(i in 1..lockCount)
|
||||
_busyLock.lock();
|
||||
private fun acquireBusyOrThrow(context: String, maxWaitMs: Long = BUSY_FATAL_MS, allowUnwedge: Boolean = true) {
|
||||
val warnAt = Math.min(BUSY_WARN_MS, maxWaitMs);
|
||||
if (_busyLock.tryLock(warnAt, TimeUnit.MILLISECONDS))
|
||||
return;
|
||||
logBusyContention(context);
|
||||
val remaining = maxWaitMs - warnAt;
|
||||
if (remaining > 0 && _busyLock.tryLock(remaining, TimeUnit.MILLISECONDS))
|
||||
return;
|
||||
if (!allowUnwedge)
|
||||
throw IllegalStateException("V8 busy lock for [${config.name}] timed out after ${maxWaitMs}ms in $context (fast-fail)");
|
||||
unwedgeBusyHolder(context);
|
||||
if (_busyLock.tryLock(BUSY_RECOVERY_MS, TimeUnit.MILLISECONDS))
|
||||
return;
|
||||
throw IllegalStateException("V8 busy lock for [${config.name}] timed out after ${maxWaitMs + BUSY_RECOVERY_MS}ms in $context; holder did not release after recovery");
|
||||
}
|
||||
|
||||
private fun unwedgeBusyHolder(context: String) {
|
||||
val holder = _busyHolder;
|
||||
Logger.w(TAG, "V8 busy lock for [${config.name}] still held in $context after ${BUSY_FATAL_MS}ms; attempting to unwedge holder ${holder?.name ?: "unknown"}");
|
||||
try {
|
||||
val rt = _runtime;
|
||||
if (rt != null && !rt.isClosed && !rt.isDead) {
|
||||
Logger.w(TAG, "Calling terminateExecution() on [${config.name}] runtime");
|
||||
rt.terminateExecution();
|
||||
}
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
Logger.e(TAG, "terminateExecution() failed for [${config.name}]", ex);
|
||||
}
|
||||
try {
|
||||
holder?.interrupt();
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Interrupting holder thread for [${config.name}] failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private fun logBusyContention(context: String) {
|
||||
try {
|
||||
val holder = _busyHolder;
|
||||
val sb = StringBuilder();
|
||||
sb.append("V8 BUSY CONTENTION [${config.name}] in $context: queueLength=${_busyLock.queueLength}, holdCount=${_busyLock.holdCount}, waited>${BUSY_WARN_MS}ms\n");
|
||||
if (holder != null) {
|
||||
sb.append("Lock holder: ${holder.name} (id=${holder.id}, state=${holder.state})\n");
|
||||
for (frame in holder.stackTrace.take(40))
|
||||
sb.append(" at ").append(frame.toString()).append("\n");
|
||||
} else {
|
||||
sb.append("Lock holder unknown (likely already released or never set)\n");
|
||||
}
|
||||
sb.append("Suspect waiting/blocked threads:\n");
|
||||
val cur = Thread.currentThread();
|
||||
for ((thread, stack) in Thread.getAllStackTraces()) {
|
||||
if (thread == cur || thread == holder) continue;
|
||||
if (thread.state != Thread.State.WAITING && thread.state != Thread.State.BLOCKED && thread.state != Thread.State.TIMED_WAITING) continue;
|
||||
if (stack.none {
|
||||
val cn = it.className;
|
||||
cn.contains("V8Plugin") || cn.contains("JSClient") || cn.contains("Extensions_V8")
|
||||
|| cn.contains("Subscription") || cn.contains("PackageHttp") || cn.contains("JSPager")
|
||||
|| cn.contains("JSContent")
|
||||
}) continue;
|
||||
sb.append(" ${thread.name} (state=${thread.state}):\n");
|
||||
for (frame in stack.take(20))
|
||||
sb.append(" at ").append(frame.toString()).append("\n");
|
||||
}
|
||||
Logger.w(TAG, sb.toString());
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to log busy contention", ex);
|
||||
}
|
||||
}
|
||||
fun execute(js: String) : V8Value {
|
||||
@@ -430,6 +520,11 @@ class V8Plugin {
|
||||
|
||||
val TAG = "V8Plugin";
|
||||
|
||||
private const val BUSY_WARN_MS = 10_000L;
|
||||
private const val BUSY_FATAL_MS = 60_000L;
|
||||
private const val BUSY_RECOVERY_MS = 5_000L;
|
||||
const val BUSY_STARTUP_MS = 5_000L;
|
||||
|
||||
fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? {
|
||||
return _runtimeMap.getOrDefault(runtime, null);
|
||||
}
|
||||
@@ -567,4 +662,4 @@ class V8Plugin {
|
||||
return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,9 @@ class PackageBridge : V8Package {
|
||||
@V8Function
|
||||
fun dispose(value: V8Value) {
|
||||
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||
value.close();
|
||||
_plugin.busy {
|
||||
value.close();
|
||||
}
|
||||
}
|
||||
|
||||
var timeoutCounter = 0;
|
||||
@@ -294,4 +296,4 @@ class PackageBridge : V8Package {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,14 +651,17 @@ class PackageHttp: V8Package {
|
||||
|
||||
@V8Function
|
||||
fun connect(socketObj: V8ValueObject) {
|
||||
val hasOpen = socketObj.has("open");
|
||||
val hasMessage = socketObj.has("message");
|
||||
val hasClosing = socketObj.has("closing");
|
||||
val hasClosed = socketObj.has("closed");
|
||||
val hasFailure = socketObj.has("failure");
|
||||
val (hasOpen, hasMessage, hasClosing, hasClosed, hasFailure) = _package._plugin.busy {
|
||||
val open = socketObj.has("open");
|
||||
val message = socketObj.has("message");
|
||||
val closing = socketObj.has("closing");
|
||||
val closed = socketObj.has("closed");
|
||||
val failure = socketObj.has("failure");
|
||||
|
||||
socketObj.setWeak(); //We have to manage this lifecycle
|
||||
_listeners = socketObj;
|
||||
socketObj.setWeak(); //We have to manage this lifecycle
|
||||
_listeners = socketObj;
|
||||
Quintuple(open, message, closing, closed, failure);
|
||||
};
|
||||
|
||||
_socket = _packageClient.logExceptions {
|
||||
val client = _client;
|
||||
@@ -666,51 +669,50 @@ class PackageHttp: V8Package {
|
||||
override fun open() {
|
||||
Logger.i(TAG, "Websocket opened: " + _url);
|
||||
_isOpen = true;
|
||||
if(hasOpen && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
if(hasOpen && _listeners?.isClosed != true) {
|
||||
_listeners?.invokeV8Void("open", arrayOf<Any>());
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
override fun message(msg: String) {
|
||||
if(hasMessage && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
if(hasMessage && _listeners?.isClosed != true) {
|
||||
_listeners?.invokeV8Void("message", msg);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {}
|
||||
}
|
||||
catch(ex: Throwable) {}
|
||||
}
|
||||
override fun closing(code: Int, reason: String) {
|
||||
if(hasClosing && _listeners?.isClosed != true)
|
||||
{
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
if(hasClosing && _listeners?.isClosed != true) {
|
||||
_listeners?.invokeV8Void("closing", code, reason);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
override fun closed(code: Int, reason: String) {
|
||||
_isOpen = false;
|
||||
if(hasClosed && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
if(hasClosed && _listeners?.isClosed != true) {
|
||||
_listeners?.invokeV8Void("closed", code, reason);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
}
|
||||
Logger.w(TAG, "PackageHttp Socket removed");
|
||||
synchronized(_package.aliveSockets) {
|
||||
@@ -720,15 +722,15 @@ class PackageHttp: V8Package {
|
||||
override fun failure(exception: Throwable) {
|
||||
_isOpen = false;
|
||||
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
|
||||
if(hasFailure && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
try {
|
||||
_package._plugin.busy {
|
||||
if(hasFailure && _listeners?.isClosed != true) {
|
||||
_listeners?.invokeV8Void("failure", exception.message);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -747,10 +749,20 @@ class PackageHttp: V8Package {
|
||||
@V8Function
|
||||
fun close(code: Int?, reason: String?) {
|
||||
_socket?.close(code ?: 1000, reason ?: "");
|
||||
_listeners?.close()
|
||||
_package._plugin.busy {
|
||||
_listeners?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Quintuple<A, B, C, D, E>(
|
||||
val first: A,
|
||||
val second: B,
|
||||
val third: C,
|
||||
val fourth: D,
|
||||
val fifth: E
|
||||
)
|
||||
|
||||
data class RequestDescriptor(
|
||||
val method: String,
|
||||
val url: String,
|
||||
@@ -780,4 +792,4 @@ class PackageHttp: V8Package {
|
||||
private const val TAG = "PackageHttp";
|
||||
private val WHITELISTED_RESPONSE_HEADERS = listOf("content-type", "date", "content-length", "last-modified", "etag", "cache-control", "content-encoding", "content-disposition", "connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
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.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.api.media.platforms.local.LocalClient
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -181,11 +182,14 @@ class StatePlatform {
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
var toDisables = mutableListOf<IPlatformClient>();
|
||||
val toDispose = mutableListOf<IPlatformClient>();
|
||||
var enabled: Array<String>;
|
||||
synchronized(_clientsLock) {
|
||||
for(e in _enabledClients) {
|
||||
toDisables.add(e);
|
||||
val previousAvailable = _availableClients.toList();
|
||||
val reusableByDescriptor = HashMap<SourcePluginDescriptor, JSClient>();
|
||||
for (prev in previousAvailable) {
|
||||
if (prev is JSClient)
|
||||
reusableByDescriptor[prev.descriptor] = prev;
|
||||
}
|
||||
|
||||
_enabledClients.clear();
|
||||
@@ -200,9 +204,16 @@ class StatePlatform {
|
||||
|
||||
for (plugin in StatePlugins.instance.getPlugins()) {
|
||||
try {
|
||||
val client = JSClient(context, plugin);
|
||||
client.onCaptchaException.subscribe { c, ex ->
|
||||
StateApp.instance.handleCaptchaException(c, ex);
|
||||
val reused = reusableByDescriptor[plugin];
|
||||
val isReused = reused != null && reused.descriptor === plugin;
|
||||
val client: JSClient = if (isReused) {
|
||||
reused!!;
|
||||
} else {
|
||||
JSClient(context, plugin).also { fresh ->
|
||||
fresh.onCaptchaException.subscribe { c, ex ->
|
||||
StateApp.instance.handleCaptchaException(c, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
|
||||
@@ -210,6 +221,9 @@ class StatePlatform {
|
||||
_iconsByName[plugin.config.name.lowercase()] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
|
||||
ImageVariable(plugin.config.absoluteIconUrl, null);
|
||||
_availableClients.add(client);
|
||||
|
||||
if (isReused)
|
||||
reusableByDescriptor.remove(plugin);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to initialize plugin [${plugin.config.name}]", ex);
|
||||
@@ -219,6 +233,8 @@ class StatePlatform {
|
||||
}
|
||||
}
|
||||
|
||||
toDispose.addAll(reusableByDescriptor.values);
|
||||
|
||||
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) {
|
||||
val dups = _availableClients.filter { x-> _availableClients.count { it.id == x.id } > 1 };
|
||||
val overrideClients = _availableClients.distinctBy { it.id }
|
||||
@@ -244,7 +260,7 @@ class StatePlatform {
|
||||
}
|
||||
selectClients(*enabled);
|
||||
|
||||
for(toDisable in toDisables) {
|
||||
for(toDisable in toDispose) {
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
toDisable.disable();
|
||||
@@ -1144,4 +1160,4 @@ class StatePlatform {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import kotlin.streams.asSequence
|
||||
* Used to maintain subscriptions
|
||||
*/
|
||||
class StateSubscriptions {
|
||||
|
||||
private val _subscriptions = FragmentedStorage.storeJson<Subscription>("subscriptions")
|
||||
.withUnique { it.channel.url }
|
||||
.withRestore(object: ReconstructStore<Subscription>(){
|
||||
@@ -489,4 +490,4 @@ class StateSubscriptions {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -40,6 +40,8 @@ import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
@@ -125,7 +127,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val timeTotal = measureTimeMillis {
|
||||
for(task in forkTasks) {
|
||||
try {
|
||||
val result = task.get();
|
||||
val result = task.get(TASK_TIMEOUT_S, TimeUnit.SECONDS);
|
||||
if(result != null) {
|
||||
if(result.pager != null) {
|
||||
taskResults.add(result);
|
||||
@@ -148,6 +150,10 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
} else {
|
||||
throw ex.cause ?: ex;
|
||||
}
|
||||
} catch (ex: TimeoutException) {
|
||||
Logger.w(TAG, "Subscription task timed out after ${TASK_TIMEOUT_S}s, abandoning");
|
||||
task.cancel(true);
|
||||
exs.add(ex);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -382,4 +388,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val pager: IPager<IPlatformContent>?,
|
||||
val exception: Throwable?
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TASK_TIMEOUT_S = 90L;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user