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

This commit is contained in:
Koen 2024-01-19 20:24:35 +01:00
commit 362c7f5b2c
14 changed files with 280 additions and 87 deletions

View file

@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
function pluginRemoteCall(objID, methodName, args) { function pluginRemoteCall(objID, methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args))); return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
} }
function pluginRemoteTest(methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
}
function pluginIsLoggedIn(cb, err) { function pluginIsLoggedIn(cb, err) {
fetch("/plugin/isLoggedIn", { fetch("/plugin/isLoggedIn", {

View file

@ -385,8 +385,8 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<div style="width: 50%" v-if="Plugin.currentPlugin"> <div style="width: 50%" v-if="Plugin.currentPlugin">
<!--Get Home--> <v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
<v-card class="requestCard" v-for="req in Testing.requests"> <v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
<v-card-text> <v-card-text>
<div class="title"> <div class="title">
<span v-if="req.isOptional">(Optional)</span> <span v-if="req.isOptional">(Optional)</span>
@ -416,6 +416,9 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="testSourceRemotely(req)">
Test Android
</v-btn>
<v-btn @click="testSource(req)"> <v-btn @click="testSource(req)">
Test Test
</v-btn> </v-btn>
@ -545,6 +548,7 @@
new Vue({ new Vue({
el: '#app', el: '#app',
data: { data: {
searchTestMethods: "",
page: "Plugin", page: "Plugin",
pastPluginUrls: [], pastPluginUrls: [],
settings: {}, settings: {},
@ -860,6 +864,53 @@
"Error: " + ex; "Error: " + ex;
} }
}, },
testSourceRemotely(req) {
const name = req.title;
const parameterVals = req.parameters.map(x=>{
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
return JSON.parse(x.value.substring(5));
return x.value
});
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = __DEV_SETTINGS;
else
parameterVals.push(__DEV_SETTINGS);
}
const func = source[name];
if(!func)
alert("Test func not found");
try {
const remoteResult = pluginRemoteTest(name, parameterVals);
console.log("Result for " + req.title, remoteResult);
this.Testing.lastResult = "//Results [" + name + "]\n" +
JSON.stringify(remoteResult, null, 3);
this.Testing.lastResultError = "";
}
catch(ex) {
if(ex.plugin_type == "CaptchaRequiredException") {
let shouldCaptcha = confirm("Do you want to request captcha?");
if(shouldCaptcha) {
pluginCaptchaTestPlugin(ex.url, ex.body);
}
}
console.error("Failed to run test for " + req.title, ex);
this.Testing.lastResult = ""
if(ex.message)
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex.message + "\n\n" + ex.stack;
else
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex;
}
},
showTestResults(results) { showTestResults(results) {
}, },

View file

@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@ -141,7 +142,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
try { try {
handleUrlAll(content) runBlocking {
handleUrlAll(content)
}
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.i(TAG, "Failed to handle URL.", e) Logger.i(TAG, "Failed to handle URL.", e)
UIDialogs.toast(this, "Failed to handle URL: ${e.message}") UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
@ -540,7 +543,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Pair("grayjay") { req -> Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let { StateApp.instance.contextOrNull?.let {
if(it is MainActivity) { if(it is MainActivity) {
it.handleUrlAll(req.url.toString()); runBlocking {
it.handleUrlAll(req.url.toString());
}
} }
}; };
} }
@ -552,7 +557,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
try { try {
if (targetData != null) { if (targetData != null) {
handleUrlAll(targetData) runBlocking {
handleUrlAll(targetData)
}
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
@ -560,7 +567,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
fun handleUrlAll(url: String) { suspend fun handleUrlAll(url: String) {
val uri = Uri.parse(url) val uri = Uri.parse(url)
when (uri.scheme) { when (uri.scheme) {
"grayjay" -> { "grayjay" -> {
@ -644,31 +651,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
fun handleUrl(url: String): Boolean { suspend fun handleUrl(url: String): Boolean {
Logger.i(TAG, "handleUrl(url=$url)") Logger.i(TAG, "handleUrl(url=$url)")
if (StatePlatform.instance.hasEnabledVideoClient(url)) { return withContext(Dispatchers.IO) {
navigate(_fragVideoDetail, url); Logger.i(TAG, "handleUrl(url=$url) on IO");
_fragVideoDetail.maximizeVideoDetail(true); if (StatePlatform.instance.hasEnabledVideoClient(url)) {
return true; Logger.i(TAG, "handleUrl(url=$url) found video client");
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) { lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url); navigate(_fragVideoDetail, url);
lifecycleScope.launch { _fragVideoDetail.maximizeVideoDetail(true);
delay(100); }
_fragVideoDetail.minimizeVideoDetail(); return@withContext true;
}; } else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
return true; Logger.i(TAG, "handleUrl(url=$url) found channel client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainPlaylist, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
}
return@withContext false;
} }
else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) {
navigate(_fragMainPlaylist, url);
lifecycleScope.launch {
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return true;
}
return false;
} }
fun handleContent(file: String, mime: String? = null): Boolean { fun handleContent(file: String, mime: String? = null): Boolean {
Logger.i(TAG, "handleContent(url=$file)"); Logger.i(TAG, "handleContent(url=$file)");

View file

@ -9,7 +9,10 @@ import com.futo.platformplayer.api.http.server.HttpGET
import com.futo.platformplayer.api.http.server.HttpPOST import com.futo.platformplayer.api.http.server.HttpPOST
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig 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.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.dev.V8RemoteObject import com.futo.platformplayer.engine.dev.V8RemoteObject
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
@ -20,18 +23,29 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateAssets import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier
import java.util.UUID import java.util.UUID
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
class DeveloperEndpoints(private val context: Context) { class DeveloperEndpoints(private val context: Context) {
private val TAG = "DeveloperEndpoints"; private val TAG = "DeveloperEndpoints";
private val _client = ManagedHttpClient(); private val _client = ManagedHttpClient();
private var _testPlugin: V8Plugin? = null; private var _testPlugin: V8Plugin? = null;
private var _testPluginFull: JSClient? = null;
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin"); private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf(); private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
@ -190,6 +204,17 @@ class DeveloperEndpoints(private val context: Context) {
val client = JSHttpClient(null, null, null, config); val client = JSHttpClient(null, null, null, config);
val clientAuth = JSHttpClient(null, null, null, config); val clientAuth = JSHttpClient(null, null, null, config);
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth); _testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
try {
val script = _client.get(config.absoluteScriptUrl);
_testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor(
config, null, null, null
), null, script.body?.string() ?: "");
_testPluginFull!!.initialize();
}
catch (ex: Throwable) {
Logger.e(TAG, "Loading full client failed", ex);
_testPluginFull = null;
}
context.respondJson(200, testPluginOrThrow.getPackageVariables()); context.respondJson(200, testPluginOrThrow.getPackageVariables());
} }
@ -440,6 +465,68 @@ class DeveloperEndpoints(private val context: Context) {
} }
} }
private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field");
init {
_fieldAttributesField.isAccessible = true;
}
private val _remoteTestGson = GsonBuilder()
.setExclusionStrategies(object : ExclusionStrategy {
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
return clazz?.simpleName == "JSClient" ||
clazz?.simpleName == "KSerializer[]" ||
clazz?.simpleName == "V8ValueObject";
}
override fun shouldSkipField(f: FieldAttributes?): Boolean {
val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true;
if(!isPublic) {
val underlyingField = _fieldAttributesField.get(f) as Field;
return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) };
}
else
return !isPublic;
}
}).create();
@HttpPOST("/plugin/remoteTest")
fun pluginRemoteTest(context: HttpContext) {
val method = context.query.getOrDefault("method", "");
try {
val parameters = context.readContentString();
val paras = JsonParser.parseString(parameters);
if(!paras.isJsonArray)
throw IllegalArgumentException("Expected json array as body");
val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded");
val function = plugin::class.memberFunctions.filter { it.findAnnotation<JSDocs>() != null }
.find { it.name == method };
if(function == null)
throw java.lang.IllegalArgumentException("Plugin method [${function}] not found");
val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement ->
//For now, manual conversion.
val parameter = function.parameters[index + 1];
val value = _remoteTestGson.fromJson<Any>(jsonElement, parameter.type.javaType);
return@mapIndexed value;
}).toTypedArray());
val json = if(callResult is IPager<*>)
_remoteTestGson.toJson(callResult.getResults())
else
_remoteTestGson.toJson(callResult);
//val json = wrapRemoteResult(callResult, false);
context.respondCode(200, json);
}
catch(ex: InvocationTargetException) {
Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException);
context.respondCode(500, ex.targetException.message ?: "", "text/plain")
}
catch(ex: Exception) {
Logger.e(TAG, "Remote test for [${method}] is failed", ex);
context.respondCode(500, ex.message ?: "", "text/plain")
}
}
//Internal calls //Internal calls
@HttpPOST("/get") @HttpPOST("/get")
fun get(context: HttpContext) { fun get(context: HttpContext) {

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
import android.content.Context import android.content.Context
import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetCompilationException
import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
@ -10,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@ -173,8 +175,16 @@ class V8Plugin {
isStopped = true; isStopped = true;
_runtime?.let { _runtime?.let {
_runtime = null; _runtime = null;
if(!it.isClosed && !it.isDead) if(!it.isClosed && !it.isDead) {
it.close(); try {
it.close();
}
catch(ex: JavetException) {
//In case race conditions are going on, already closed runtimes are fine.
if(ex.message?.contains("Runtime is already closed") != true)
throw ex;
}
}
Logger.i(TAG, "Stopped plugin [${config.name}]"); Logger.i(TAG, "Stopped plugin [${config.name}]");
}; };
} }

View file

@ -32,6 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID import java.util.UUID
@ -168,7 +171,9 @@ class HomeFragment : MainFragment() {
Pair("grayjay") { req -> Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let { StateApp.instance.contextOrNull?.let {
if(it is MainActivity) { if(it is MainActivity) {
it.handleUrlAll(req.url.toString()); runBlocking {
it.handleUrlAll(req.url.toString());
}
} }
}; };
} }

View file

@ -859,11 +859,11 @@ class VideoDetailView : ConstraintLayout {
private val _historyIndexLock = Mutex(false); private val _historyIndexLock = Mutex(false);
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){ suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index? = withContext(Dispatchers.IO){
_historyIndexLock.withLock { _historyIndexLock.withLock {
val current = _historyIndex; val current = _historyIndex;
if(current == null || current.url != video.url) { if(current == null || current.url != video.url) {
val index = StateHistory.instance.getHistoryByVideo(video, true)!!; val index = StateHistory.instance.getHistoryByVideo(video, true);
_historyIndex = index; _historyIndex = index;
return@withContext index; return@withContext index;
} }
@ -1390,7 +1390,7 @@ class VideoDetailView : ConstraintLayout {
if (video !is TutorialFragment.TutorialVideo) { if (video !is TutorialFragment.TutorialVideo) {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
val historyItem = getHistoryIndex(videoDetail); val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong()); _historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
@ -2252,7 +2252,7 @@ class VideoDetailView : ConstraintLayout {
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) { if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
if (v !is TutorialFragment.TutorialVideo) { if (v !is TutorialFragment.TutorialVideo) {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
val history = getHistoryIndex(v); val history = getHistoryIndex(v) ?: return@launch;
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong()); StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
} }
} }

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking
class PlatformLinkMovementMethod : LinkMovementMethod { class PlatformLinkMovementMethod : LinkMovementMethod {
private val _context: Context; private val _context: Context;
@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
val links = buffer.getSpans(off, off, URLSpan::class.java); val links = buffer.getSpans(off, off, URLSpan::class.java);
if (links.isNotEmpty()) { if (links.isNotEmpty()) {
for (link in links) { runBlocking {
Logger.i(TAG) { "Link clicked '${link.url}'." }; for (link in links) {
Logger.i(TAG) { "Link clicked '${link.url}'." };
if (_context is MainActivity) { if (_context is MainActivity) {
if (_context.handleUrl(link.url)) { if (_context.handleUrl(link.url)) {
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue; continue;
} }
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s =
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
}
} }
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
} }
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
} }
return true; return true;

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
@ -92,14 +93,20 @@ class StateHistory {
} }
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? { fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
val existing = historyIndex[video.url]; val existing = historyIndex[video.url];
if(existing != null) var result: DBHistory.Index? = null;
return _historyDBStore.get(existing.id!!); if(existing != null) {
result = _historyDBStore.getOrNull(existing.id!!);
if(result == null)
UIDialogs.toast("History item null?\nNo history tracking..");
}
else if(create) { else if(create) {
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now()); val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
val id = _historyDBStore.insert(newHistItem); val id = _historyDBStore.insert(newHistItem);
return _historyDBStore.get(id); result = _historyDBStore.getOrNull(id);
if(result == null)
UIDialogs.toast("History creation failed?\nNo history tracking..");
} }
return null; return result;
} }
fun removeHistory(url: String) { fun removeHistory(url: String) {

View file

@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringStorage import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -166,8 +167,13 @@ class StatePlatform {
var enabled: Array<String>; var enabled: Array<String>;
synchronized(_clientsLock) { synchronized(_clientsLock) {
for(e in _enabledClients) { for(e in _enabledClients) {
e.disable(); try {
onSourceDisabled.emit(e); e.disable();
onSourceDisabled.emit(e);
}
catch(ex: Throwable) {
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
}
} }
_enabledClients.clear(); _enabledClients.clear();

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.v2.JsonStoreSerializer import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import java.lang.IllegalArgumentException
import java.lang.reflect.Field import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap import java.util.concurrent.ConcurrentMap
@ -209,7 +210,9 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
fun getObject(id: Long) = get(id).obj!!; fun getObject(id: Long) = get(id).obj!!;
fun get(id: Long): I { fun get(id: Long): I {
return deserializeIndex(dbDaoBase.get(_sqlGet(id))); val result = dbDaoBase.getNullable(_sqlGet(id))
?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}");
return deserializeIndex(result);
} }
fun getOrNull(id: Long): I? { fun getOrNull(id: Long): I? {
val result = dbDaoBase.getNullable(_sqlGet(id)); val result = dbDaoBase.getNullable(_sqlGet(id));

View file

@ -13,6 +13,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.others.PlatformLinkMovementMethod
import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
constructor(context: Context) : super(context) {} constructor(context: Context) : super(context) {}
@ -40,32 +41,34 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
if (text is Spannable) { if (text is Spannable) {
val links = text.getSpans(offset, offset, URLSpan::class.java) val links = text.getSpans(offset, offset, URLSpan::class.java)
if (links.isNotEmpty()) { if (links.isNotEmpty()) {
for (link in links) { runBlocking {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }; for (link in links) {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
val c = context; val c = context;
if (c is MainActivity) { if (c is MainActivity) {
if (c.handleUrl(link.url)) { if (c.handleUrl(link.url)) {
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue; continue;
} }
}
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))); if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
}
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
}
} }
} }

@ -1 +1 @@
Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03 Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d

@ -1 +1 @@
Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03 Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d