diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/1-bug_report.yml
similarity index 54%
rename from .github/ISSUE_TEMPLATE/bug_report.yml
rename to .github/ISSUE_TEMPLATE/1-bug_report.yml
index 776ef91f..e2108e3e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/1-bug_report.yml
@@ -1,6 +1,9 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
-labels: ["Bug"]
+labels: ["Bug", "Android"]
+title: "Bug: "
+type: bug
+projects: ["futo-org/19"]
body:
- type: markdown
attributes:
@@ -18,11 +21,33 @@ body:
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
- id: what-happened
+ id: reproduction-steps
attributes:
- label: What happened?
- description: What did you expect to happen?
- placeholder: Tell us what you see!
+ label: Reproduction steps
+ description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
+ placeholder: |
+ 0. Play a Youtube video
+ 1. Press on Download button
+ 2. Select quality 1440p
+ 3. Grayjay crashes when attempting to download
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual-result
+ attributes:
+ label: Actual result
+ description: What happend?
+ placeholder: Tell us what you saw!
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected-result
+ attributes:
+ label: Expected result
+ description: What was suppose to happen?
+ placeholder: Tell us what you expected to happen!
validations:
required: true
@@ -31,7 +56,7 @@ body:
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
- placeholder: "242"
+ placeholder: "311"
validations:
required: true
@@ -42,19 +67,23 @@ body:
multiple: true
options:
- "All"
- - "Youtube"
- - "Odysee"
- - "Rumble"
- - "Kick"
- - "Twitch"
- - "PeerTube"
- - "Patreon"
- - "Nebula"
+ - "Apple Podcasts"
- "BiliBili (CN)"
- "Bitchute"
- - "SoundCloud"
+ - "Crunchyroll"
+ - "CuriosityStream"
- "Dailymotion"
- - "Apple Podcasts"
+ - "Kick"
+ - "Nebula"
+ - "Odysee"
+ - "Patreon"
+ - "PeerTube"
+ - "Rumble"
+ - "SoundCloud"
+ - "Spotify"
+ - "TedTalks"
+ - "Twitch"
+ - "Youtube"
- "Other"
validations:
required: true
@@ -66,6 +95,30 @@ body:
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
+ - type: input
+ id: android-version
+ attributes:
+ label: Which android version are you using?
+ placeholder: "Android 15"
+ validations:
+ required: true
+
+ - type: input
+ id: phone-model
+ attributes:
+ label: Which device are you using?
+ placeholder: "Google Pixel 9"
+ validations:
+ required: true
+
+ - type: input
+ id: os-version
+ attributes:
+ label: Which operating system are you using?
+ placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
+ validations:
+ required: true
+
- type: checkboxes
id: login
attributes:
@@ -86,9 +139,28 @@ body:
validations:
required: true
+ - type: textarea
+ id: grayjay-references
+ attributes:
+ label: References
+ description: |
+ Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
+ ```
+ - #10
+ ```
+ placeholder:
+ value:
+ validations:
+ required: false
+
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
+
+ - type: markdown
+ attributes:
+ value: |
+ **Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/2-feature_request.yml
similarity index 91%
rename from .github/ISSUE_TEMPLATE/feature_request.yml
rename to .github/ISSUE_TEMPLATE/2-feature_request.yml
index ebba5241..2058150f 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/2-feature_request.yml
@@ -1,13 +1,16 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
-labels: ["Enhancement"]
+labels: ["Enhancement", "Android"]
+title: "Feature request: "
+type: feature
+projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a feature request.
- The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
+ The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.yml b/.github/ISSUE_TEMPLATE/3-documentation_issue.yml
similarity index 95%
rename from .github/ISSUE_TEMPLATE/documentation_issue.yml
rename to .github/ISSUE_TEMPLATE/3-documentation_issue.yml
index c416d012..40d245dc 100644
--- a/.github/ISSUE_TEMPLATE/documentation_issue.yml
+++ b/.github/ISSUE_TEMPLATE/3-documentation_issue.yml
@@ -1,13 +1,16 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["Documentation"]
+title: "Documentation: "
+type: task
+projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a documentation change request.
- The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
+ The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
diff --git a/app/build.gradle b/app/build.gradle
index c20ba29f..25d458d4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -181,6 +181,7 @@ dependencies {
//JS
implementation("com.caoccao.javet:javet-android:3.0.2")
+ //implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
//Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1'
diff --git a/app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt b/app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt
new file mode 100644
index 00000000..66686260
--- /dev/null
+++ b/app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt
@@ -0,0 +1,38 @@
+package com.futo.platformplayer
+
+import android.graphics.Color
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import toAndroidColor
+
+class CSSColorTests {
+ @Test
+ fun test1() {
+ val androidHex = "#80336699"
+ val androidColorInt = Color.parseColor(androidHex)
+
+ val cssHex = "#33669980"
+ val cssColor = CSSColor.parseColor(cssHex)
+
+ assertEquals(
+ "CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
+ androidColorInt,
+ cssColor.toAndroidColor(),
+ )
+ }
+
+ @Test
+ fun test2() {
+ val androidHex = "#123ABC"
+ val androidColorInt = Color.parseColor(androidHex)
+
+ val cssHex = "#123ABCFF"
+ val cssColor = CSSColor.parseColor(cssHex)
+
+ assertEquals(
+ "CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
+ androidColorInt,
+ cssColor.toAndroidColor()
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt b/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt
index 7607a2c9..f3e12645 100644
--- a/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt
+++ b/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt
@@ -11,7 +11,7 @@ import java.nio.ByteBuffer
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
-
+/*
class SyncServerTests {
//private val relayHost = "relay.grayjay.app"
@@ -335,4 +335,4 @@ class SyncServerTests {
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
-}
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt b/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt
index 1b9f19cd..d34bfad4 100644
--- a/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt
+++ b/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt
@@ -13,7 +13,7 @@ import kotlin.random.Random
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds
-
+/*
data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream,
@@ -509,4 +509,4 @@ class Authorized : IAuthorizable {
class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false
-}
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 176dc044..ea6f3e5b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,7 +15,6 @@
-
0f
+ max == r -> ((g - b) / d % 6f) * 60f
+ max == g -> (((b - r) / d) + 2f) * 60f
+ else -> (((r - g) / d) + 4f) * 60f
+ }.let { if (it < 0f) it + 360f else it }
+ _hslDirty = false
+ }
+
+ /**
+ * Set all three HSL channels at once.
+ * Hue in degrees [0...360), s/l [0...1].
+ */
+ fun setHsl(h: Float, s: Float, l: Float) {
+ val hh = ((h % 360f) + 360f) % 360f
+ val cc = (1f - abs(2f * l - 1f)) * s
+ val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
+ val m = l - cc / 2f
+
+ val (rp, gp, bp) = when {
+ hh < 60f -> Triple(cc, x, 0f)
+ hh < 120f -> Triple(x, cc, 0f)
+ hh < 180f -> Triple(0f, cc, x)
+ hh < 240f -> Triple(0f, x, cc)
+ hh < 300f -> Triple(x, 0f, cc)
+ else -> Triple(cc, 0f, x)
+ }
+
+ r = rp + m; g = gp + m; b = bp + m
+ _h = hh; _s = s; _l = l; _hslDirty = false
+ }
+
+ /** Return 0xRRGGBBAA int */
+ fun toRgbaInt(): Int {
+ val ai = (a * 255).roundToInt() and 0xFF
+ val ri = (r * 255).roundToInt() and 0xFF
+ val gi = (g * 255).roundToInt() and 0xFF
+ val bi = (b * 255).roundToInt() and 0xFF
+ return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
+ }
+
+ /** Return 0xAARRGGBB int */
+ fun toArgbInt(): Int {
+ val ai = (a * 255).roundToInt() and 0xFF
+ val ri = (r * 255).roundToInt() and 0xFF
+ val gi = (g * 255).roundToInt() and 0xFF
+ val bi = (b * 255).roundToInt() and 0xFF
+ return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
+ }
+
+ // — Convenience modifiers (chainable) —
+
+ /** Lighten by fraction [0...1] */
+ fun lighten(fraction: Float): CSSColor = apply {
+ lightness = (lightness + fraction).coerceIn(0f, 1f)
+ }
+
+ /** Darken by fraction [0...1] */
+ fun darken(fraction: Float): CSSColor = apply {
+ lightness = (lightness - fraction).coerceIn(0f, 1f)
+ }
+
+ /** Increase saturation by fraction [0...1] */
+ fun saturate(fraction: Float): CSSColor = apply {
+ saturation = (saturation + fraction).coerceIn(0f, 1f)
+ }
+
+ /** Decrease saturation by fraction [0...1] */
+ fun desaturate(fraction: Float): CSSColor = apply {
+ saturation = (saturation - fraction).coerceIn(0f, 1f)
+ }
+
+ /** Rotate hue by degrees (can be negative) */
+ fun rotateHue(degrees: Float): CSSColor = apply {
+ hue = (hue + degrees) % 360f
+ }
+
+ companion object {
+ /** Create from Android 0xAARRGGBB */
+ @JvmStatic fun fromArgb(color: Int): CSSColor {
+ val a = ((color ushr 24) and 0xFF) / 255f
+ val r = ((color ushr 16) and 0xFF) / 255f
+ val g = ((color ushr 8) and 0xFF) / 255f
+ val b = ( color and 0xFF) / 255f
+ return CSSColor(r, g, b, a)
+ }
+
+ /** Create from Android 0xRRGGBBAA */
+ @JvmStatic fun fromRgba(color: Int): CSSColor {
+ val r = ((color ushr 24) and 0xFF) / 255f
+ val g = ((color ushr 16) and 0xFF) / 255f
+ val b = ((color ushr 8) and 0xFF) / 255f
+ val a = ( color and 0xFF) / 255f
+ return CSSColor(r, g, b, a)
+ }
+
+ @JvmStatic fun fromAndroidColor(color: Int): CSSColor {
+ return fromArgb(color)
+ }
+
+ private val NAMED_HEX = mapOf(
+ "aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
+ "aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
+ "bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
+ "blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
+ "burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
+ "chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
+ "cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
+ "darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
+ "darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
+ "darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
+ "darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
+ "darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
+ "darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
+ "darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
+ "dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
+ "firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
+ "fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
+ "gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
+ "green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
+ "honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
+ "indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
+ "lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
+ "lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
+ "lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
+ "lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
+ "lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
+ "lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
+ "lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
+ "linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
+ "mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
+ "mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
+ "mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
+ "midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
+ "moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
+ "oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
+ "orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
+ "palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
+ "palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
+ "peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
+ "powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
+ "red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
+ "saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
+ "seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
+ "silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
+ "slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
+ "springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
+ "teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
+ "turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
+ "white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
+ "yellowgreen" to "9ACD32"
+ )
+ private val NAMED: Map = NAMED_HEX
+ .mapValues { (_, hexRgb) ->
+ // parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
+ val rgb = hexRgb.toInt(16)
+ (rgb shl 8) or 0xFF
+ } + ("transparent" to 0x00000000)
+
+ private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
+ private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
+ private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
+
+ @JvmStatic
+ fun parseColor(s: String): CSSColor {
+ val str = s.trim()
+ // named
+ NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
+
+ // hex
+ HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
+ return parseHexPart(part)
+ }
+
+ // rgb/rgba
+ RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
+ return parseRgbParts(it.split(',').map(String::trim))
+ }
+
+ // hsl/hsla
+ HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
+ return parseHslParts(it.split(',').map(String::trim))
+ }
+
+ error("Cannot parse color: \"$s\"")
+ }
+
+ private fun parseHexPart(p: String): CSSColor {
+ // expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
+ val hex = when (p.length) {
+ 3 -> p.map { "$it$it" }.joinToString("") + "FF"
+ 4 -> p.map { "$it$it" }.joinToString("")
+ 6 -> p + "FF"
+ 8 -> p
+ else -> error("Invalid hex color: #$p")
+ }
+
+ val parsed = hex.toLong(16).toInt()
+ val alpha = (parsed and 0xFF) shl 24
+ val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
+ val argb = alpha or rgbOnly
+ return fromArgb(argb)
+ }
+
+ private fun parseRgbParts(parts: List): CSSColor {
+ require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
+
+ // r/g/b: "128" → 128/255, "50%" → 0.5
+ fun channel(ch: String): Float =
+ if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
+ else ch.toFloat().coerceIn(0f, 255f) / 255f
+
+ // alpha: "0.5" → 0.5, "50%" → 0.5
+ fun alpha(a: String): Float =
+ if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
+ else a.toFloat().coerceIn(0f, 1f)
+
+ val r = channel(parts[0])
+ val g = channel(parts[1])
+ val b = channel(parts[2])
+ val a = if (parts.size == 4) alpha(parts[3]) else 1f
+
+ return CSSColor(r, g, b, a)
+ }
+
+ private fun parseHslParts(parts: List): CSSColor {
+ require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
+
+ fun hueOf(h: String): Float = when {
+ h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
+ h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
+ h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
+ h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
+ else -> h.toFloat()
+ }
+
+ // for s and l you only ever see percentages
+ fun pct(p: String): Float =
+ p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
+
+ // alpha: "0.5" → 0.5, "50%" → 0.5
+ fun alpha(a: String): Float =
+ if (a.endsWith("%")) pct(a)
+ else a.toFloat().coerceIn(0f, 1f)
+
+ val h = hueOf(parts[0])
+ val s = pct(parts[1])
+ val l = pct(parts[2])
+ val a = if (parts.size == 4) alpha(parts[3]) else 1f
+
+ return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
+ }
+ }
+}
+
+fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
+fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
+fun CSSColor.toAndroidColor(): Int = toArgbInt()
diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt
index 9fe21ec8..fca7deda 100644
--- a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt
+++ b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt
@@ -219,9 +219,7 @@ private fun ByteArray.toInetAddress(): InetAddress {
fun getConnectedSocket(attemptAddresses: List, port: Int): Socket? {
ensureNotMainThread()
- val timeout = 2000
-
-
+ val timeout = 10000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance() else attemptAddresses;
if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
@@ -243,8 +241,11 @@ fun getConnectedSocket(attemptAddresses: List, port: Int): Socket?
return null;
}
+ val sortedAddresses: List = addresses
+ .sortedBy { addr -> addressScore(addr) }
+
val sockets: ArrayList = arrayListOf();
- for (i in addresses.indices) {
+ for (i in sortedAddresses.indices) {
sockets.add(Socket());
}
@@ -252,7 +253,7 @@ fun getConnectedSocket(attemptAddresses: List, port: Int): Socket?
var connectedSocket: Socket? = null;
val threads: ArrayList = arrayListOf();
for (i in 0 until sockets.size) {
- val address = addresses[i];
+ val address = sortedAddresses[i];
val socket = sockets[i];
val thread = Thread {
try {
diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt
index e31d3dac..d8427dc6 100644
--- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt
+++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt
@@ -2,10 +2,30 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.*
+import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueArray
+import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject
+import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8Plugin
+import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
+import com.futo.platformplayer.logging.Logger
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.selects.SelectClause0
+import kotlinx.coroutines.selects.SelectClause1
+import java.util.concurrent.CancellationException
+import java.util.concurrent.CountDownLatch
+import kotlin.coroutines.AbstractCoroutineContextElement
+import kotlin.coroutines.CoroutineContext
+import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
//V8
@@ -24,6 +44,10 @@ fun V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
return handler(this);
}
+inline fun V8Value.getSourcePlugin(): V8Plugin? {
+ return V8Plugin.getPluginFromRuntime(this.v8Runtime);
+}
+
inline fun V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
@@ -89,7 +113,29 @@ inline fun V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
.map { kv-> kv.second.orNull { it.expectV8Variant(config, contextName + "[${kv.first}]", ) } as T };
}
+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()
+ );
+ }
+ }
+}
+inline fun V8Value.ensureIsBusy() {
+ this?.getSourcePlugin()?.let {
+ it.ensureIsBusy();
+ }
+}
+
inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
+ if(false)
+ ensureIsBusy();
return when(T::class) {
String::class -> this.expectOrThrow(config, contextName).value as T;
Int::class -> {
@@ -146,4 +192,119 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get(it).toString() })
map.put(prop, obj.getString(prop));
return map;
+}
+
+
+fun V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
+ val latch = CountDownLatch(1);
+ var promiseResult: T? = null;
+ var promiseException: Throwable? = null;
+ plugin.busy {
+ this.register(object: IV8ValuePromise.IListener {
+ override fun onFulfilled(p0: V8Value?) {
+ if(p0 is V8ValueError)
+ promiseException = ScriptExecutionException(plugin.config, p0.message);
+ else
+ promiseResult = p0 as T;
+ latch.countDown();
+ }
+ override fun onRejected(p0: V8Value?) {
+ promiseException = (NotImplementedError("onRejected promise not implemented.."));
+ latch.countDown();
+ }
+ override fun onCatch(p0: V8Value?) {
+ promiseException = (NotImplementedError("onCatch promise not implemented.."));
+ latch.countDown();
+ }
+ });
+ }
+
+ plugin.registerPromise(this) {
+ promiseException = CancellationException("Cancelled by system");
+ latch.countDown();
+ }
+ plugin.unbusy {
+ latch.await();
+ }
+ if(promiseException != null)
+ throw promiseException!!;
+ return promiseResult!!;
+}
+fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred {
+ val underlyingDef = CompletableDeferred();
+ val def = if(this.has("estDuration"))
+ V8Deferred(underlyingDef,
+ this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
+ else
+ V8Deferred(underlyingDef);
+
+ val promise = this;
+ plugin.busy {
+ this.register(object: IV8ValuePromise.IListener {
+ override fun onFulfilled(p0: V8Value?) {
+ plugin.resolvePromise(promise);
+ underlyingDef.complete(p0 as T);
+ }
+ override fun onRejected(p0: V8Value?) {
+ plugin.resolvePromise(promise);
+ underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
+ }
+ override fun onCatch(p0: V8Value?) {
+ plugin.resolvePromise(promise);
+ underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
+ }
+ });
+ }
+ plugin.registerPromise(promise) {
+ if(def.isActive)
+ def.cancel("Cancelled by system");
+ }
+ return def;
+}
+
+class V8Deferred(val deferred: Deferred, val estDuration: Int = -1): Deferred by deferred {
+
+ fun convert(conversion: (result: T)->R): V8Deferred{
+ val newDef = CompletableDeferred()
+ this.invokeOnCompletion {
+ if(it != null)
+ newDef.completeExceptionally(it);
+ else
+ newDef.complete(conversion(this@V8Deferred.getCompleted()));
+ }
+
+ return V8Deferred(newDef, estDuration);
+ }
+
+
+ companion object {
+ fun merge(scope: CoroutineScope, defs: List>, conversion: (result: List)->R): V8Deferred {
+
+ var amount = -1;
+ for(def in defs)
+ amount = Math.max(amount, def.estDuration);
+
+ val def = scope.async {
+ val results = defs.map { it.await() };
+ return@async conversion(results);
+ }
+ return V8Deferred(def, amount);
+ }
+ }
+}
+
+
+fun V8ValueObject.invokeV8(method: String, vararg obj: Any): T {
+ var result = this.invoke(method, *obj);
+ if(result is V8ValuePromise) {
+ return result.toV8ValueBlocking(this.getSourcePlugin()!!);
+ }
+ return result as T;
+}
+fun V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred {
+ var result = this.invoke(method, *obj);
+ if(result is V8ValuePromise) {
+ return result.toV8ValueAsync(this.getSourcePlugin()!!);
+ }
+ return V8Deferred(CompletableDeferred(result as T));
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt
index 7f827f66..da414d8f 100644
--- a/app/src/main/java/com/futo/platformplayer/Settings.kt
+++ b/app/src/main/java/com/futo/platformplayer/Settings.kt
@@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
+import com.futo.platformplayer.views.fields.AdvancedField
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
@@ -175,6 +176,10 @@ class Settings : FragmentedStorageFileJson() {
}
}*/
+
+ @FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
+ var advancedSettings: Boolean = false;
+
@FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings();
@Serializable
@@ -221,10 +226,11 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false;
+ @AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
-
+ @AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@@ -253,9 +259,11 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1;
+ @AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true;
+ @AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@@ -277,6 +285,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class ChannelSettings {
+ @AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
}
@@ -302,16 +311,20 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
+ @AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
+ @AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
+ @AdvancedField
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
+ @AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@@ -342,13 +355,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
+ @AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
+ @AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
+ @AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@@ -425,9 +441,11 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
+ @AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true;
+ @AdvancedField
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@@ -438,6 +456,7 @@ class Settings : FragmentedStorageFileJson() {
fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
+ @AdvancedField
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
@DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1;
@@ -464,14 +483,10 @@ class Settings : FragmentedStorageFileJson() {
};
}
+ @AdvancedField
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true;
-
-
- @FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
- var backgroundSwitchToAudio: Boolean = true;
-
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1;
@@ -497,6 +512,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
+ @AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
@@ -515,6 +531,78 @@ class Settings : FragmentedStorageFileJson() {
else -> 10_000L;
}
}
+
+
+ @FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
+ @DropdownFieldOptionsId(R.array.min_playback_speed)
+ var minimumPlaybackSpeed: Int = 0;
+ @FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
+ @DropdownFieldOptionsId(R.array.max_playback_speed)
+ var maximumPlaybackSpeed: Int = 2;
+ @FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
+ @DropdownFieldOptionsId(R.array.step_playback_speed)
+ var stepPlaybackSpeed: Int = 1;
+
+ fun getPlaybackSpeedStep(): Double {
+ return when(stepPlaybackSpeed) {
+ 0 -> 0.05
+ 1 -> 0.1
+ 2 -> 0.25
+ else -> 0.1;
+ }
+ }
+ fun getPlaybackSpeeds(): List {
+ val playbackSpeeds = mutableListOf();
+ playbackSpeeds.add(1.0);
+ val minSpeed = when(minimumPlaybackSpeed) {
+ 0 -> 0.25
+ 1 -> 0.5
+ 2 -> 1.0
+ else -> 0.25
+ }
+ val maxSpeed = when(maximumPlaybackSpeed) {
+ 0 -> 2.0
+ 1 -> 2.25
+ 2 -> 3.0
+ 3 -> 4.0
+ 4 -> 5.0
+ else -> 2.25;
+ }
+ var testSpeed = 1.0;
+
+ while(testSpeed > minSpeed) {
+ val nextSpeed = (testSpeed - 0.25) as Double;
+ testSpeed = Math.max(nextSpeed, minSpeed);
+ playbackSpeeds.add(testSpeed);
+ }
+ testSpeed = 1.0;
+ while(testSpeed < maxSpeed) {
+ val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
+ testSpeed = Math.min(nextSpeed, maxSpeed);
+ playbackSpeeds.add(testSpeed);
+ }
+ playbackSpeeds.sort();
+ return playbackSpeeds;
+ }
+
+ @FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
+ @DropdownFieldOptionsId(R.array.hold_playback_speeds)
+ var holdPlaybackSpeed: Int = 4;
+
+ fun getHoldPlaybackSpeed(): Double {
+ return when(holdPlaybackSpeed) {
+ 0 -> 1.0
+ 1 -> 1.25
+ 2 -> 1.5
+ 3 -> 1.75
+ 4 -> 2.0
+ 5 -> 2.25
+ 6 -> 2.5
+ 7 -> 2.75
+ 8 -> 3.0
+ else -> 2.0
+ }
+ }
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -530,6 +618,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
+ @AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@@ -566,10 +655,12 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
+ @AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true;
+ @AdvancedField
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
@DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3;
@@ -599,15 +690,21 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
+ @AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
-
+ @AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true;
+ @AdvancedField
+ @FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
+ @Serializable(with = FlexibleBooleanSerializer::class)
+ var allowLinkLocalIpv4: Boolean = false;
+
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@@ -675,9 +772,11 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class Plugins {
+ @AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
+ @AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true;
@@ -878,7 +977,23 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
- @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
+ @FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
+ fun viewLicenseStatus() {
+ SettingsActivity.getActivity()?.let {
+ try {
+ if (StatePayment.instance.hasPaid) {
+ val paymentKey = StatePayment.instance.getPaymentKey()
+ UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
+ } else {
+ UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
+ }
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to show license status dialog", e)
+ }
+ }
+ }
+
+ @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
fun clearPayment() {
SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
@@ -896,15 +1011,20 @@ class Settings : FragmentedStorageFileJson() {
var other = Other();
@Serializable
class Other {
+ @AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true;
+ @AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true;
- @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
+ @FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
+ var watchLaterAddStart: Boolean = true;
+
+ @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true;
- @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
+ @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
var polycentricLocalCache: Boolean = true;
}
diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
index 3db07410..83387081 100644
--- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
+++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
@@ -1151,6 +1151,8 @@ class UISlideOverlays {
call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false);
+ else
+ UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}),
)
);
diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt
index bfa7925b..0875aadb 100644
--- a/app/src/main/java/com/futo/platformplayer/Utility.kt
+++ b/app/src/main/java/com/futo/platformplayer/Utility.kt
@@ -339,6 +339,33 @@ fun ByteArray.fromGzip(): ByteArray {
return outputStream.toByteArray()
}
+fun findCandidateAddresses(): List {
+ val candidates = NetworkInterface.getNetworkInterfaces()
+ .toList()
+ .asSequence()
+ .filter(::isUsableInterface)
+ .flatMap { nif ->
+ nif.interfaceAddresses
+ .asSequence()
+ .mapNotNull { ia ->
+ ia.address.takeIf(::isUsableAddress)?.let { addr ->
+ nif to ia
+ }
+ }
+ }
+ .toList()
+
+ return candidates
+ .sortedWith(
+ compareBy>(
+ { addressScore(it.second.address) },
+ { interfaceScore(it.first) },
+ { -it.second.networkPrefixLength.toInt() },
+ { -it.first.mtu }
+ )
+ ).map { it.second.address }
+}
+
fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
@@ -407,7 +434,7 @@ private fun interfaceScore(nif: NetworkInterface): Int {
}
}
-private fun addressScore(addr: InetAddress): Int {
+fun addressScore(addr: InetAddress): Int {
return when (addr) {
is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF }
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index 199598bd..ec89eac1 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -116,6 +116,7 @@ import java.io.StringWriter
import java.lang.reflect.InvocationTargetException
import java.util.LinkedList
import java.util.Queue
+import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
@@ -186,7 +187,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment;
//State
- private val _queue: Queue> = LinkedList();
+ private val _queue: LinkedList> = LinkedList();
lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null;
@@ -220,6 +221,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
+ val mainId = UUID.randomUUID().toString().substring(0, 5)
+
constructor() : super() {
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(
@@ -271,8 +274,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi
override fun onCreate(savedInstanceState: Bundle?) {
- Logger.i(TAG, "MainActivity Starting");
- StateApp.instance.setGlobalContext(this, lifecycleScope);
+ Logger.w(TAG, "MainActivity Starting [$mainId]");
+ StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState);
@@ -678,13 +681,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onResume() {
super.onResume();
- Logger.v(TAG, "onResume")
+ Logger.w(TAG, "onResume [$mainId]")
_isVisible = true;
}
override fun onPause() {
super.onPause();
- Logger.v(TAG, "onPause")
+ Logger.w(TAG, "onPause [$mainId]")
_isVisible = false;
_qrCodeLoadingDialog?.dismiss()
@@ -693,7 +696,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onStop() {
super.onStop()
- Logger.v(TAG, "_wasStopped = true");
+ Logger.w(TAG, "onStop [$mainId]");
_wasStopped = true;
}
@@ -1110,8 +1113,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onDestroy() {
super.onDestroy();
- Logger.v(TAG, "onDestroy")
- StateApp.instance.mainAppDestroyed(this);
+ Logger.w(TAG, "onDestroy [$mainId]")
+ StateApp.instance.mainAppDestroyed(this, mainId);
}
inline fun isFragmentActive(): Boolean {
@@ -1192,7 +1195,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent;
-
fragCurrent = segment;
_parameterCurrent = parameter;
}
diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
index a0a0fac1..9cf58134 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
@@ -14,10 +14,12 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType
@@ -29,6 +31,9 @@ import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo
@@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
+ private lateinit var _loader: View
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons();
- _buttonShare = findViewById(R.id.button_share);
- _buttonCopy = findViewById(R.id.button_copy);
- _imageQR = findViewById(R.id.image_qr);
- _textQR = findViewById(R.id.text_qr);
+ _buttonShare = findViewById(R.id.button_share)
+ _buttonCopy = findViewById(R.id.button_copy)
+ _imageQR = findViewById(R.id.image_qr)
+ _textQR = findViewById(R.id.text_qr)
+ _loader = findViewById(R.id.progress_loader)
findViewById(R.id.button_back).setOnClickListener {
finish();
};
- _exportBundle = createExportBundle();
+ _imageQR.visibility = View.INVISIBLE
+ _textQR.visibility = View.INVISIBLE
+ _loader.visibility = View.VISIBLE
+ _buttonShare.visibility = View.INVISIBLE
+ _buttonCopy.visibility = View.INVISIBLE
- try {
- val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
- val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
- _imageQR.setImageBitmap(qrCodeBitmap);
- } catch (e: Exception) {
- Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
- _imageQR.visibility = View.INVISIBLE;
- _textQR.visibility = View.INVISIBLE;
+ lifecycleScope.launch {
+ try {
+ val pair = withContext(Dispatchers.IO) {
+ val bundle = createExportBundle()
+ val dimension = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
+ ).toInt()
+ val qr = generateQRCode(bundle, dimension, dimension)
+ Pair(bundle, qr)
+ }
+
+ _exportBundle = pair.first
+ _imageQR.setImageBitmap(pair.second)
+ _imageQR.visibility = View.VISIBLE
+ _textQR.visibility = View.VISIBLE
+ _buttonShare.visibility = View.VISIBLE
+ _buttonCopy.visibility = View.VISIBLE
+ } catch (e: Exception) {
+ Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
+ _imageQR.visibility = View.INVISIBLE
+ _textQR.visibility = View.INVISIBLE
+ _buttonShare.visibility = View.INVISIBLE
+ _buttonCopy.visibility = View.INVISIBLE
+ } finally {
+ _loader.visibility = View.GONE
+ }
}
_buttonShare.onClick.subscribe {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt
index 211f83a6..ce3a720e 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt
@@ -34,8 +34,10 @@ class PlatformClientPool {
isDead = true;
onDead.emit(parentClient, this);
- for(clientPair in _pool) {
- clientPair.key.disable();
+ synchronized(_pool) {
+ for (clientPair in _pool) {
+ clientPair.key.disable();
+ }
}
};
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt
index 9b063c9b..4ff3a549 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
@@ -44,6 +45,7 @@ class PlatformID {
val NONE = PlatformID("Unknown", null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
+ value.ensureIsBusy();
val contextName = "PlatformID";
return PlatformID(
value.getOrThrow(config, "platform", contextName),
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt
index 330597a4..831f8ef7 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -33,6 +34,7 @@ open class PlatformAuthorLink {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
+ value.ensureIsBusy();
if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt
index 03abad1a..6b73842f 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
+ value.ensureIsBusy();
val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context),
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt
index fd24de30..e95b3fe0 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.expectV8Variant
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -46,6 +47,7 @@ class ResultCapabilities(
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
val contextName = "ResultCapabilities";
+ value.ensureIsBusy();
return ResultCapabilities(
value.getOrThrow(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
value.getOrThrow(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
@@ -69,6 +71,7 @@ class FilterGroup(
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
+ value.ensureIsBusy();
return FilterGroup(
value.getString("name"),
value.getOrDefault(config, "filters", "FilterGroup", null)
@@ -90,6 +93,7 @@ class FilterCapability(
companion object {
fun fromV8(obj: V8ValueObject): FilterCapability {
+ obj.ensureIsBusy();
val value = obj.get("value") as V8Value;
return FilterCapability(
obj.getString("name"),
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt
index a30d31c9..b25936a0 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -31,6 +32,7 @@ class Thumbnails {
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
+ value.ensureIsBusy();
return Thumbnails((value.getOrThrow(config, "sources", "Thumbnails"))
.toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt
index 89826b01..19b4bbb9 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
interface IPlatformLiveEvent {
@@ -10,6 +11,7 @@ interface IPlatformLiveEvent {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
+ obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow(config, "type", contextName));
return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt
index 28bbe15a..8b9883ef 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -27,6 +28,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
+ obj.ensureIsBusy();
+
val contextName = "LiveEventComment"
val colorName = obj.getOrDefault(config, "colorName", contextName, null);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt
index a4ac5d47..f8cbafe6 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -37,6 +38,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
+ obj.ensureIsBusy();
val contextName = "LiveEventDonation"
return LiveEventDonation(
obj.getOrThrow(config, "name", contextName),
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt
index 6e29bac5..ebd75b44 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class LiveEventEmojis: IPlatformLiveEvent {
@@ -15,9 +16,9 @@ class LiveEventEmojis: IPlatformLiveEvent {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
+ obj.ensureIsBusy();
val contextName = "LiveEventEmojis"
- return LiveEventEmojis(
- obj.getOrThrow(config, "emojis", contextName));
+ return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt
index ff5dd36f..6663852d 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
+import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent {
@@ -10,20 +12,24 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String;
val targetThumbnail: String;
val targetUrl: String;
+ val isOutgoing: Boolean;
- constructor(name: String, url: String, thumbnail: String) {
+ constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
this.targetName = name;
this.targetUrl = url;
this.targetThumbnail = thumbnail;
+ this.isOutgoing = isOutgoing;
}
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
+ obj.ensureIsBusy();
val contextName = "LiveEventRaid"
return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName),
- obj.getOrThrow(config, "targetThumbnail", contextName));
+ obj.getOrThrow(config, "targetThumbnail", contextName),
+ obj.getOrDefault(config, "isOutgoing", contextName, true) ?: true);
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt
index adcfb883..5e48e984 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class LiveEventViewCount: IPlatformLiveEvent {
@@ -15,6 +16,7 @@ class LiveEventViewCount: IPlatformLiveEvent {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
+ obj.ensureIsBusy();
val contextName = "LiveEventViewCount"
return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName));
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt
index 75286b44..1fdbb442 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault
import com.futo.platformplayer.serializers.IRatingSerializer
@@ -13,8 +14,12 @@ interface IRating {
companion object {
- fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
+ fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
+ obj?.ensureIsBusy();
+ return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
+ };
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
+ obj.ensureIsBusy();
val t = RatingType.fromInt(obj.getOrThrow(config, "type", contextName));
return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt
index 6d0e787b..8ccc6b2e 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
+ obj.ensureIsBusy();
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt
index e40169f2..0a45f15b 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
+ obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt
index 7646cf24..d656df5f 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
/**
@@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
+ obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt
index b26abe45..1f29bf2a 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt
@@ -56,6 +56,7 @@ class DevJSClient : JSClient {
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
+ client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
index 1ac4c13e..fd3c9dde 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
@@ -61,9 +61,13 @@ import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
+import java.util.Random
import kotlin.Exception
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction
@@ -85,6 +89,8 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List? = null;
+ private var _usedReloadData: String? = null;
+
protected val _script: String;
private var _initialized: Boolean = false;
@@ -100,14 +106,14 @@ open class JSClient : IPlatformClient {
override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
- private val _busyLock = Object();
- private var _busyCounter = 0;
private var _busyAction = "";
- val isBusy: Boolean get() = _busyCounter > 0;
+ val isBusy: Boolean get() = _plugin.isBusy;
val isBusyAction: String get() {
return _busyAction;
}
+ val declareOnEnable = HashMap();
+
val settings: HashMap get() = descriptor.settings;
val flags: Array;
@@ -200,6 +206,7 @@ open class JSClient : IPlatformClient {
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
+ client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
@@ -216,14 +223,31 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id];
}
+ fun setReloadData(data: String?) {
+ if(data == null) {
+ if(declareOnEnable.containsKey("__reloadData"))
+ declareOnEnable.remove("__reloadData");
+ }
+ else
+ declareOnEnable.put("__reloadData", data ?: "");
+ }
+ fun getReloadData(orLast: Boolean): String? {
+ if(declareOnEnable.containsKey("__reloadData"))
+ return declareOnEnable["__reloadData"];
+ else if(orLast)
+ return _usedReloadData;
+ return null;
+ }
+
override fun initialize() {
if (_initialized) return
- Logger.i(TAG, "Plugin [${config.name}] initializing");
plugin.start();
+
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
+
descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true;
@@ -263,19 +287,28 @@ open class JSClient : IPlatformClient {
}
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
- fun enable() {
+ fun enable() = isBusyWith("enable") {
if(!_initialized)
initialize();
+ for(toDeclare in declareOnEnable) {
+ plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
+ }
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
+
+ if(declareOnEnable.containsKey("__reloadData")) {
+ Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
+ _usedReloadData = declareOnEnable["__reloadData"];
+ declareOnEnable.remove("__reloadData");
+ }
_enabled = true;
}
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
- fun saveState(): String? {
+ fun saveState(): String? = isBusyWith("saveState") {
ensureEnabled();
if(!capabilities.hasSaveState)
- return null;
+ return@isBusyWith null;
val resp = plugin.executeTyped("source.saveState()").value;
- return resp;
+ return@isBusyWith resp;
}
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@@ -323,8 +356,10 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!;
}
- _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
- return _searchCapabilities!!;
+ return busy {
+ _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
+ return@busy _searchCapabilities!!;
+ }
}
catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex);
@@ -352,8 +387,10 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!;
- _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
- return _searchChannelContentsCapabilities!!;
+ return busy {
+ _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
+ return@busy _searchChannelContentsCapabilities!!;
+ }
}
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search")
@@ -385,14 +422,14 @@ open class JSClient : IPlatformClient {
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)")
- override fun isChannelUrl(url: String): Boolean {
+ override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") {
try {
- return plugin.executeTyped("source.isChannelUrl(${Json.encodeToString(url)})")
+ return@isBusyWith plugin.executeTyped("source.isChannelUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex);
- return false;
+ return@isBusyWith false;
}
}
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@@ -410,9 +447,10 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) {
return _channelCapabilities!!;
}
-
- _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
- return _channelCapabilities!!;
+ return busy {
+ _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
+ return@busy _channelCapabilities!!;
+ };
}
catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex);
@@ -523,14 +561,14 @@ open class JSClient : IPlatformClient {
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
@JSDocsParameter("url", "A content url (May not be your platform)")
- override fun isContentDetailsUrl(url: String): Boolean {
+ override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") {
try {
- return plugin.executeTyped("source.isContentDetailsUrl(${Json.encodeToString(url)})")
+ return@isBusyWith plugin.executeTyped("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex);
- return false;
+ return@isBusyWith false;
}
}
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@@ -562,7 +600,7 @@ open class JSClient : IPlatformClient {
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject)
- return@isBusyWith JSPlaybackTracker(config, tracker);
+ return@isBusyWith JSPlaybackTracker(this, tracker);
else
return@isBusyWith null;
}
@@ -604,7 +642,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
-
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager? = isBusyWith("getContentRecommendations") {
@@ -632,17 +669,19 @@ open class JSClient : IPlatformClient {
@JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist")
- override fun isPlaylistUrl(url: String): Boolean {
+ override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
if (!capabilities.hasGetPlaylist)
- return false;
+ return@isBusyWith false;
try {
- return plugin.executeTyped("source.isPlaylistUrl(${Json.encodeToString(url)})")
- .value;
+ return@isBusyWith busy {
+ return@busy plugin.executeTyped("source.isPlaylistUrl(${Json.encodeToString(url)})")
+ .value;
+ }
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
- return false;
+ return@isBusyWith false;
}
}
@JSOptional
@@ -744,19 +783,29 @@ open class JSClient : IPlatformClient {
return urls;
}
-
- private fun isBusyWith(actionName: String, handle: ()->T): T {
- try {
- synchronized(_busyLock) {
- _busyCounter++;
- }
- _busyAction = actionName;
- return handle();
+ fun busy(handle: ()->T): T {
+ return _plugin.busy {
+ return@busy handle();
}
- finally {
- _busyAction = "";
- synchronized(_busyLock) {
- _busyCounter--;
+ }
+ fun busyBlockingSuspended(handle: suspend ()->T): T {
+ return _plugin.busy {
+ return@busy runBlocking {
+ return@runBlocking handle();
+ }
+ }
+ }
+
+ fun isBusyWith(actionName: String, handle: ()->T): T {
+ //val busyId = kotlin.random.Random.nextInt(9999);
+ return busy {
+ try {
+ _busyAction = actionName;
+ return@busy handle();
+
+ }
+ finally {
+ _busyAction = "";
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
index ff67e56e..8d5675b6 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
@@ -4,6 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual
@@ -169,12 +170,17 @@ class SourcePluginConfig(
}
fun validate(text: String): Boolean {
- if(scriptPublicKey.isNullOrEmpty())
- throw IllegalStateException("No public key present");
- if(scriptSignature.isNullOrEmpty())
- throw IllegalStateException("No signature present");
+ try {
+ if (scriptPublicKey.isNullOrEmpty())
+ throw IllegalStateException("No public key present");
+ if (scriptSignature.isNullOrEmpty())
+ throw IllegalStateException("No signature present");
- return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
+ return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
+ return false
+ }
}
fun isUrlAllowed(url: String): Boolean {
@@ -205,6 +211,8 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl;
return obj;
}
+
+ private val TAG = "SourcePluginConfig"
}
@kotlinx.serialization.Serializable
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt
index 6f835304..03c5c2c6 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt
@@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient {
}
+ fun resetAuthCookies() {
+ _currentCookieMap.clear();
+ if(!_auth?.cookieMap.isNullOrEmpty()) {
+ for(domainCookies in _auth!!.cookieMap!!)
+ _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
+ }
+ if(!_captcha?.cookieMap.isNullOrEmpty()) {
+ for(domainCookies in _captcha!!.cookieMap!!) {
+ if(_currentCookieMap.containsKey(domainCookies.key))
+ _currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
+ else
+ _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
+ }
+ }
+ }
+ fun clearOtherCookies() {
+ _otherCookieMap.clear();
+ }
+
override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt
index 777981bf..326b4086 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -13,6 +14,7 @@ interface IJSContent: IPlatformContent {
companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
+ obj.ensureIsBusy();
val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt
index 21b475ff..16470c17 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt
@@ -6,12 +6,14 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent {
companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
+ obj.ensureIsBusy();
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt
index 27731fea..dc2ba7b2 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt
@@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager, IPlatformLiveEventPager {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
- override fun nextPage() {
+ override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") {
super.nextPage();
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
index 8782b742..e81a288d 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt
@@ -29,7 +29,9 @@ abstract class JSPager : IPager {
this.pager = pager;
this.config = config;
- _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
+ plugin.busy {
+ _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
+ }
getResults();
}
@@ -44,11 +46,14 @@ abstract class JSPager : IPager {
override fun nextPage() {
warnIfMainThread("JSPager.nextPage");
- pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
- pager.invoke("nextPage", arrayOf());
- };
- _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
- _resultChanged = true;
+ val pluginV8 = plugin.getUnderlyingPlugin();
+ pluginV8.busy {
+ pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
+ pager.invoke("nextPage", arrayOf());
+ };
+ _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
+ _resultChanged = true;
+ }
/*
try {
}
@@ -70,15 +75,18 @@ abstract class JSPager : IPager {
return previousResults;
warnIfMainThread("JSPager.getResults");
- val items = pager.getOrThrow(config, "results", "JSPager");
- if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
- throw IllegalStateException("Runtime closed");
- val newResults = items.toArray()
- .map { convertResult(it as V8ValueObject) }
- .toList();
- _lastResults = newResults;
- _resultChanged = false;
- return newResults;
+
+ return plugin.getUnderlyingPlugin().busy {
+ val items = pager.getOrThrow(config, "results", "JSPager");
+ if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
+ throw IllegalStateException("Runtime closed");
+ val newResults = items.toArray()
+ .map { convertResult(it as V8ValueObject) }
+ .toList();
+ _lastResults = newResults;
+ _resultChanged = false;
+ return@busy newResults;
+ }
}
abstract fun convertResult(obj: V8ValueObject): T;
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt
index e5ee7b68..15a7d854 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt
@@ -2,37 +2,50 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
+import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread
class JSPlaybackTracker: IPlaybackTracker {
- private val _config: IV8PluginConfig;
- private val _obj: V8ValueObject;
+ private lateinit var _client: JSClient;
+ private lateinit var _config: IV8PluginConfig;
+ private lateinit var _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false;
- private val _hasInit: Boolean;
+ private var _hasInit: Boolean = false;
private var _lastRequest: Long = Long.MIN_VALUE;
- private val _hasOnConcluded: Boolean;
+ private var _hasOnConcluded: Boolean = false;
override var nextRequest: Int = 1000
private set;
- constructor(config: IV8PluginConfig, obj: V8ValueObject) {
+ constructor(client: JSClient, obj: V8ValueObject) {
warnIfMainThread("JSPlaybackTracker.constructor");
- if(!obj.has("onProgress"))
- throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
- if(!obj.has("nextRequest"))
- throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
- _hasOnConcluded = obj.has("onConcluded");
- this._config = config;
- this._obj = obj;
- this._hasInit = obj.has("onInit");
+ client.busy {
+ if (!obj.has("onProgress"))
+ throw ScriptImplementationException(
+ client.config,
+ "Missing onProgress on PlaybackTracker"
+ );
+ if (!obj.has("nextRequest"))
+ throw ScriptImplementationException(
+ client.config,
+ "Missing nextRequest on PlaybackTracker"
+ );
+ _hasOnConcluded = obj.has("onConcluded");
+
+ this._client = client;
+ this._config = client.config;
+ this._obj = obj;
+ this._hasInit = obj.has("onInit");
+ }
}
override fun onInit(seconds: Double) {
@@ -40,12 +53,15 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) {
if(_hasCalledInit)
return;
- if (_hasInit) {
- Logger.i("JSPlaybackTracker", "onInit (${seconds})");
- _obj.invokeVoid("onInit", seconds);
+
+ _client.busy {
+ if (_hasInit) {
+ Logger.i("JSPlaybackTracker", "onInit (${seconds})");
+ _obj.invokeVoid("onInit", seconds);
+ }
+ nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
+ _hasCalledInit = true;
}
- nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
- _hasCalledInit = true;
}
}
@@ -55,10 +71,12 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit)
onInit(seconds);
else {
- Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
- _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
- nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
- _lastRequest = System.currentTimeMillis();
+ _client.busy {
+ Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
+ _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
+ nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
+ _lastRequest = System.currentTimeMillis();
+ }
}
}
}
@@ -67,7 +85,9 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
- _obj.invokeVoid("onConcluded", -1);
+ _client.busy {
+ _obj.invokeVoid("onConcluded", -1);
+ }
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt
index 70dfecfd..36cfc7db 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt
@@ -46,16 +46,18 @@ class JSRequestExecutor {
if (_executor.isClosed)
throw IllegalStateException("Executor object is closed");
- val result = if(_plugin is DevJSClient)
- StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
- V8Plugin.catchScriptErrors(
- _config,
- "[${_config.name}] JSRequestExecutor",
- "builder.modifyRequest()"
- ) {
- _executor.invoke("executeRequest", url, headers, method, body);
- } as V8Value;
- }
+ return _plugin.getUnderlyingPlugin().busy {
+
+ val result = if(_plugin is DevJSClient)
+ StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
+ V8Plugin.catchScriptErrors(
+ _config,
+ "[${_config.name}] JSRequestExecutor",
+ "builder.modifyRequest()"
+ ) {
+ _executor.invoke("executeRequest", url, headers, method, body);
+ } as V8Value;
+ }
else V8Plugin.catchScriptErrors(
_config,
"[${_config.name}] JSRequestExecutor",
@@ -64,34 +66,35 @@ class JSRequestExecutor {
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
- try {
- if(result is V8ValueString) {
- val base64Result = Base64.getDecoder().decode(result.value);
- return base64Result;
- }
- if(result is V8ValueTypedArray) {
- val buffer = result.buffer;
- val byteBuffer = buffer.byteBuffer;
- val bytesResult = ByteArray(result.byteLength);
- byteBuffer.get(bytesResult, 0, result.byteLength);
- buffer.close();
- return bytesResult;
- }
- if(result is V8ValueObject && result.has("type")) {
- val type = result.getOrThrow(_config, "type", "JSRequestModifier");
- when(type) {
- //TODO: Buffer type?
+ try {
+ if(result is V8ValueString) {
+ val base64Result = Base64.getDecoder().decode(result.value);
+ return@busy base64Result;
}
+ if(result is V8ValueTypedArray) {
+ val buffer = result.buffer;
+ val byteBuffer = buffer.byteBuffer;
+ val bytesResult = ByteArray(result.byteLength);
+ byteBuffer.get(bytesResult, 0, result.byteLength);
+ buffer.close();
+ return@busy bytesResult;
+ }
+ if(result is V8ValueObject && result.has("type")) {
+ val type = result.getOrThrow(_config, "type", "JSRequestModifier");
+ when(type) {
+ //TODO: Buffer type?
+ }
+ }
+ if(result is V8ValueUndefined) {
+ if(_plugin is DevJSClient)
+ StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
+ throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
+ }
+ throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
}
- if(result is V8ValueUndefined) {
- if(_plugin is DevJSClient)
- StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
- throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
+ finally {
+ result.close();
}
- throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
- }
- finally {
- result.close();
}
}
@@ -99,24 +102,25 @@ class JSRequestExecutor {
open fun cleanup() {
if (!hasCleanup || _executor.isClosed)
return;
-
- if(_plugin is DevJSClient)
- StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
- V8Plugin.catchScriptErrors(
- _config,
- "[${_config.name}] JSRequestExecutor",
- "builder.modifyRequest()"
- ) {
- _executor.invokeVoid("cleanup", null);
- };
- }
- else V8Plugin.catchScriptErrors(
- _config,
- "[${_config.name}] JSRequestExecutor",
- "builder.modifyRequest()"
- ) {
- _executor.invokeVoid("cleanup", null);
- };
+ _plugin.busy {
+ if(_plugin is DevJSClient)
+ StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
+ V8Plugin.catchScriptErrors(
+ _config,
+ "[${_config.name}] JSRequestExecutor",
+ "builder.modifyRequest()"
+ ) {
+ _executor.invokeVoid("cleanup", null);
+ };
+ }
+ else V8Plugin.catchScriptErrors(
+ _config,
+ "[${_config.name}] JSRequestExecutor",
+ "builder.modifyRequest()"
+ ) {
+ _executor.invokeVoid("cleanup", null);
+ };
+ }
}
protected fun finalize() {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt
index 150189e7..f7d169af 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt
@@ -16,7 +16,7 @@ class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject;
- override var allowByteSkip: Boolean;
+ override var allowByteSkip: Boolean = false;
constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin;
@@ -24,10 +24,13 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config;
val config = plugin.config;
- allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
+ plugin.busy {
+ allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
+
+ if(!modifier.has("modifyRequest"))
+ throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
+ }
- if(!modifier.has("modifyRequest"))
- throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
override fun modifyRequest(url: String, headers: Map): IRequest {
@@ -35,13 +38,15 @@ class JSRequestModifier: IRequestModifier {
return Request(url, headers);
}
- val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
- _modifier.invoke("modifyRequest", url, headers);
- } as V8ValueObject;
+ return _plugin.busy {
+ val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
+ _modifier.invoke("modifyRequest", url, headers);
+ } as V8ValueObject;
- val req = JSRequest(_plugin, result, url, headers);
- result.close();
- return req;
+ val req = JSRequest(_plugin, result, url, headers);
+ result.close();
+ return@busy req;
+ }
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt
index bb4650f6..259a89e4 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt
@@ -6,6 +6,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow
+import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -35,8 +36,11 @@ class JSSubtitleSource : ISubtitleSource {
override fun getSubtitles(): String {
if(!hasFetch)
throw IllegalStateException("This subtitle doesn't support getSubtitles..");
- val v8String = _obj.invoke("getSubtitles", arrayOf());
- return v8String.value;
+
+ return _obj.getSourcePlugin()?.busy {
+ val v8String = _obj.invoke("getSubtitles", arrayOf());
+ return@busy v8String.value;
+ } ?: "";
}
override suspend fun getSubtitlesURI(): Uri? {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
index da495498..cecb2913 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
@@ -27,6 +27,7 @@ import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
+ private val _plugin: JSClient;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean;
@@ -48,6 +49,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
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));
@@ -82,14 +84,16 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS();
}
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
- return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
- val tracker = _content.invoke("getPlaybackTracker", arrayOf())
- ?: return@catchScriptErrors null;
- if(tracker is V8ValueObject)
- return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker);
- else
- return@catchScriptErrors null;
- };
+ return _plugin.busy {
+ V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
+ val tracker = _content.invoke("getPlaybackTracker", arrayOf())
+ ?: return@catchScriptErrors null;
+ if(tracker is V8ValueObject)
+ return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
+ else
+ return@catchScriptErrors null;
+ }
+ }
}
override fun getContentRecommendations(client: IPlatformClient): IPager? {
@@ -106,8 +110,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
- val contentPager = _content.invoke("getContentRecommendations", arrayOf());
- return JSContentPager(_pluginConfig, client, contentPager);
+ return _plugin.busy {
+ val contentPager = _content.invoke("getContentRecommendations", arrayOf());
+ return@busy JSContentPager(_pluginConfig, client, contentPager);
+ }
}
override fun getComments(client: IPlatformClient): IPager? {
@@ -123,10 +129,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
}
private fun getCommentsJS(client: JSClient): IPager? {
- val commentPager = _content.invoke("getComments", arrayOf());
- if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
- return null;
+ return _plugin.busy {
+ val commentPager = _content.invoke("getComments", arrayOf());
+ if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
+ return@busy null;
- return JSCommentPager(_pluginConfig, client, commentPager);
+ return@busy JSCommentPager(_pluginConfig, client, commentPager);
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt
index ae35207b..7b6388cd 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt
@@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
+import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.V8Deferred
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
@@ -13,8 +15,13 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
+import com.futo.platformplayer.invokeV8
+import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String;
@@ -50,6 +57,44 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate");
}
+ override fun generateAsync(scope: CoroutineScope): V8Deferred {
+ if(!hasGenerate)
+ return V8Deferred(CompletableDeferred(manifest));
+ if(_obj.isClosed)
+ throw IllegalStateException("Source object already closed");
+
+ val plugin = _plugin.getUnderlyingPlugin();
+
+ var result: V8Deferred? = null;
+ if(_plugin is DevJSClient)
+ result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
+ _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
+ _plugin.isBusyWith("dashAudio.generate") {
+ _obj.invokeV8Async("generate");
+ }
+ }
+ }
+ else
+ result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
+ _plugin.isBusyWith("dashAudio.generate") {
+ _obj.invokeV8Async("generate");
+ }
+ }
+
+ return plugin.busy {
+ val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
+ val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
+ val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
+ val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
+ if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
+ streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
+ }
+
+ return@busy result.convert {
+ it.value
+ };
+ }
+ }
override fun generate(): String? {
if(!hasGenerate)
return manifest;
@@ -62,21 +107,27 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
- _obj.invokeString("generate");
+ _plugin.isBusyWith("dashAudio.generate") {
+ _obj.invokeV8("generate").value;
+ }
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
- _obj.invokeString("generate");
+ _plugin.isBusyWith("dashAudio.generate") {
+ _obj.invokeV8("generate").value;
+ }
}
if(result != null){
- val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
- val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
- val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
- val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
- if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
- streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
+ plugin.busy {
+ val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
+ val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
+ val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
+ val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
+ if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
+ streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
+ }
}
}
return result;
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt
index d6ff7455..aebaab23 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.V8Deferred
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
@@ -15,11 +16,18 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
+import com.futo.platformplayer.invokeV8
+import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.states.StateDeveloper
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
interface IJSDashManifestRawSource {
val hasGenerate: Boolean;
var manifest: String?;
+ fun generateAsync(scope: CoroutineScope): Deferred;
fun generate(): String?;
}
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
@@ -32,7 +40,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
override val duration: Long;
override val priority: Boolean;
- var url: String?;
+ val url: String?;
override var manifest: String?;
override val hasGenerate: Boolean;
@@ -57,6 +65,45 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate");
}
+ override fun generateAsync(scope: CoroutineScope): V8Deferred {
+ if(!hasGenerate)
+ return V8Deferred(CompletableDeferred(manifest));
+ if(_obj.isClosed)
+ throw IllegalStateException("Source object already closed");
+
+ val plugin = _plugin.getUnderlyingPlugin();
+
+ var result: V8Deferred? = null;
+ if(_plugin is DevJSClient) {
+ result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
+ _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
+ _plugin.isBusyWith("dashVideo.generate") {
+ _obj.invokeV8Async("generate");
+ }
+ });
+ }
+ }
+ else
+ result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
+ _plugin.isBusyWith("dashVideo.generate") {
+ _obj.invokeV8Async("generate");
+ }
+ });
+
+ return plugin.busy {
+ val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
+ val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
+ val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
+ val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
+ if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
+ streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
+ }
+
+ return@busy result.convert {
+ it.value
+ };
+ }
+ }
override open fun generate(): String? {
if(!hasGenerate)
return manifest;
@@ -67,22 +114,28 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
- _obj.invokeString("generate");
+ _plugin.isBusyWith("dashVideo.generate") {
+ _obj.invokeV8("generate").value;
+ }
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
- _obj.invokeString("generate");
+ _plugin.isBusyWith("dashVideo.generate") {
+ _obj.invokeV8("generate").value;
+ }
});
if(result != null){
- val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
- val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
- val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
- val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
- if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
- streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
+ _plugin.busy {
+ val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
+ val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
+ val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
+ val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
+ if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
+ streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
+ }
}
}
return result;
@@ -110,6 +163,32 @@ class JSDashManifestMergingRawSource(
override val priority: Boolean
get() = video.priority;
+ override fun generateAsync(scope: CoroutineScope): V8Deferred {
+ val videoDashDef = video.generateAsync(scope);
+ val audioDashDef = audio.generateAsync(scope);
+
+ return V8Deferred.merge(scope, listOf(videoDashDef, audioDashDef)) {
+ val (videoDash: String?, audioDash: String?) = it;
+
+ if (videoDash != null && audioDash == null) return@merge videoDash;
+ if (audioDash != null && videoDash == null) return@merge audioDash;
+ if (videoDash == null) return@merge null;
+
+ //TODO: Temporary simple solution..make more reliable version
+
+ var result: String? = null;
+ val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
+ if (audioAdaptationSet != null) {
+ result = videoDash.replace(
+ "",
+ "\n" + audioAdaptationSet.value
+ )
+ } else
+ result = videoDash;
+
+ return@merge result;
+ };
+ }
override fun generate(): String? {
val videoDash = video.generate();
val audioDash = audio.generate();
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt
index 9e328df3..18cd71fc 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orNull
@@ -38,7 +39,13 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
companion object {
- fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
- fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
+ fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? {
+ obj?.ensureIsBusy();
+ return obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }
+ };
+ fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource {
+ obj.ensureIsBusy();
+ return JSHLSManifestAudioSource(plugin, obj)
+ };
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt
index 3c76e23d..22bf2a60 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull
@@ -53,36 +54,39 @@ abstract class JSSource {
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
}
- fun getRequestModifier(): IRequestModifier? {
+ fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
if(_requestModifier != null)
- return AdhocRequestModifier { url, headers ->
+ return@isBusyWith AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
};
if (!hasRequestModifier || _obj.isClosed)
- return null;
+ return@isBusyWith null;
val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invoke("getRequestModifier", arrayOf());
};
if (result !is V8ValueObject)
- return null;
+ return@isBusyWith null;
- return JSRequestModifier(_plugin, result)
+ return@isBusyWith JSRequestModifier(_plugin, result)
}
- open fun getRequestExecutor(): JSRequestExecutor? {
+ open fun getRequestExecutor(): JSRequestExecutor? = _plugin.isBusyWith("getRequestExecutor") {
if (!hasRequestExecutor || _obj.isClosed)
- return null;
+ return@isBusyWith null;
+ Logger.v("JSSource", "Request executor for [${type}] requesting");
val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf());
};
- if (result !is V8ValueObject)
- return null;
+ Logger.v("JSSource", "Request executor for [${type}] received");
- return JSRequestExecutor(_plugin, result)
+ if (result !is V8ValueObject)
+ return@isBusyWith null;
+
+ return@isBusyWith JSRequestExecutor(_plugin, result)
}
fun getUnderlyingPlugin(): JSClient? {
@@ -105,8 +109,12 @@ abstract class JSSource {
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
- fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
+ fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? {
+ obj?.ensureIsBusy();
+ return obj.orNull { fromV8Video(plugin, it as V8ValueObject) }
+ };
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
+ obj.ensureIsBusy()
val type = obj.getString("plugin_type");
return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
@@ -123,13 +131,26 @@ abstract class JSSource {
}
}
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
- fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
- fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
- fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
+ fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource{
+ obj.ensureIsBusy();
+ return JSDashManifestSource(plugin, obj)
+ };
+ fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource{
+ obj.ensureIsBusy()
+ return JSDashManifestRawSource(plugin, obj);
+ }
+ fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource {
+ obj?.ensureIsBusy();
+ return JSDashManifestRawAudioSource(plugin, obj)
+ };
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
- fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
+ fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource {
+ obj.ensureIsBusy();
+ return JSHLSManifestSource(plugin, obj)
+ };
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
+ obj.ensureIsBusy();
val type = obj.getString("plugin_type");
return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt
index e68f0ae0..e7c0fe50 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
@@ -31,6 +32,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
+ obj.ensureIsBusy();
val type = obj.getString("plugin_type")
return when(type) {
TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
diff --git a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
index 3d362efd..226a0a66 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
@@ -35,7 +35,7 @@ class ChromecastCastingDevice : CastingDevice {
override var usedRemoteAddress: InetAddress? = null;
override var localAddress: InetAddress? = null;
override val canSetVolume: Boolean get() = true;
- override val canSetSpeed: Boolean get() = false; //TODO: Implement
+ override val canSetSpeed: Boolean get() = true;
var addresses: Array? = null;
var port: Int = 0;
@@ -144,6 +144,23 @@ class ChromecastCastingDevice : CastingDevice {
sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json);
}
+ override fun changeSpeed(speed: Double) {
+ if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return
+
+ val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0)
+ setSpeed(speedClamped)
+ val mediaSessionId = _mediaSessionId ?: return
+ val transportId = _transportId ?: return
+ val setSpeedObject = JSONObject().apply {
+ put("type", "SET_PLAYBACK_RATE")
+ put("mediaSessionId", mediaSessionId)
+ put("playbackRate", speedClamped)
+ put("requestId", _requestId++)
+ }
+
+ sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString())
+ }
+
override fun changeVolume(volume: Double) {
if (invokeInIOScopeIfRequired({ changeVolume(volume) })) {
return;
diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
index 58bd772c..1e8e1830 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
@@ -166,10 +166,11 @@ class StateCasting {
Logger.i(TAG, "CastingService started.");
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
+ startDiscovering()
}
@Synchronized
- fun startDiscovering() {
+ private fun startDiscovering() {
_nsdManager?.apply {
_discoveryListeners.forEach {
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
@@ -178,7 +179,7 @@ class StateCasting {
}
@Synchronized
- fun stopDiscovering() {
+ private fun stopDiscovering() {
_nsdManager?.apply {
_discoveryListeners.forEach {
try {
@@ -1220,9 +1221,16 @@ class StateCasting {
private fun getLocalUrl(ad: CastingDevice): String {
var address = ad.localAddress!!
- if (address.isLinkLocalAddress) {
- address = findPreferredAddress() ?: address
- Logger.i(TAG, "Selected casting address: $address")
+ if (Settings.instance.casting.allowLinkLocalIpv4) {
+ if (address.isLinkLocalAddress && address is Inet6Address) {
+ address = findPreferredAddress() ?: address
+ Logger.i(TAG, "Selected casting address: $address")
+ }
+ } else {
+ if (address.isLinkLocalAddress) {
+ address = findPreferredAddress() ?: address
+ Logger.i(TAG, "Selected casting address: $address")
+ }
}
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
}
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
index 1691b9df..5e2b72de 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
@@ -7,9 +7,7 @@ import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
-import android.content.pm.PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED
import android.graphics.drawable.Animatable
-import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -157,9 +155,6 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller;
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
- }
val sessionId = packageInstaller.createSession(params);
session = packageInstaller.openSession(sessionId)
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
index f00bd191..87375779 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
@@ -103,7 +103,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
super.show();
Logger.i(TAG, "Dialog shown.");
- StateCasting.instance.startDiscovering()
(_imageLoader.drawable as Animatable?)?.start();
synchronized(StateCasting.instance.devices) {
@@ -148,7 +147,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
override fun dismiss() {
super.dismiss()
(_imageLoader.drawable as Animatable?)?.stop()
- StateCasting.instance.stopDiscovering()
StateCasting.instance.onDeviceAdded.remove(this)
StateCasting.instance.onDeviceChanged.remove(this)
StateCasting.instance.onDeviceRemoved.remove(this)
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt
index 2e79b0b4..4c0ccb7a 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt
@@ -6,12 +6,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Button
import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.views.buttons.BigButton
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class ImportOptionsDialog: AlertDialog {
private val _context: MainActivity;
@@ -41,8 +45,17 @@ class ImportOptionsDialog: AlertDialog {
_button_import_zip.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
- val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
- StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+ val zipBytes = it?.readBytes(context) ?: return@launch;
+ withContext(Dispatchers.Main) {
+ try {
+ StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
+ }
+ catch(ex: Throwable) {
+ UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
+ }
+ }
+ }
};
}
_button_import_ezip.setOnClickListener {
@@ -51,17 +64,35 @@ class ImportOptionsDialog: AlertDialog {
_button_import_txt.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
- val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
- val txt = String(txtBytes);
- StateBackup.importTxt(_context, txt);
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+ val txtBytes = it?.readBytes(context) ?: return@launch;
+ val txt = String(txtBytes);
+ withContext(Dispatchers.Main) {
+ try {
+ StateBackup.importTxt(_context, txt);
+ }
+ catch(ex: Throwable) {
+ UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
+ }
+ }
+ }
};
}
_button_import_newpipe_subs.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
- val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
- val json = String(jsonBytes);
- StateBackup.importNewPipeSubs(_context, json);
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+ val jsonBytes = it?.readBytes(context) ?: return@launch;
+ val json = String(jsonBytes);
+ withContext(Dispatchers.Main) {
+ try {
+ StateBackup.importNewPipeSubs(_context, json);
+ }
+ catch(ex: Throwable) {
+ UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
+ }
+ }
+ }
};
};
_button_import_platform.onClick.subscribe {
diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
index f5cf534a..5e64c3e3 100644
--- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
+++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
@@ -724,7 +724,7 @@ class VideoDownload {
val t = cue.groupValues[1];
val d = cue.groupValues[2];
- val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
+ val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
val data = if(executor != null)
executor.executeRequest("GET", url, null, mapOf());
diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt
index 15412fd9..9b888bff 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt
@@ -6,13 +6,13 @@ import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime
-import com.caoccao.javet.interop.options.V8Flags
-import com.caoccao.javet.interop.options.V8RuntimeOptions
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
+import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueObject
+import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
@@ -26,6 +26,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
+import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.engine.internal.V8Converter
@@ -38,8 +39,18 @@ import com.futo.platformplayer.engine.packages.V8Package
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets
+import com.futo.platformplayer.toList
+import com.futo.platformplayer.toV8ValueBlocking
+import com.futo.platformplayer.toV8ValueAsync
import com.futo.platformplayer.warnIfMainThread
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
class V8Plugin {
val config: IV8PluginConfig;
@@ -47,10 +58,14 @@ class V8Plugin {
private val _clientAuth: ManagedHttpClient;
private val _clientOthers: ConcurrentHashMap = ConcurrentHashMap();
+ private val _promises = ConcurrentHashMapUnit)?>();
+
val httpClient: ManagedHttpClient get() = _client;
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
val httpClientOthers: Map get() = _clientOthers;
+ var runtimeId: Int = 0;
+
fun registerHttpClient(client: JSHttpClient) {
synchronized(_clientOthers) {
_clientOthers.put(client.clientId, client);
@@ -67,10 +82,8 @@ class V8Plugin {
var isStopped = true;
val onStopped = Event1();
- //TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial
- private val _busyCounterLock = Object();
- private var _busyCounter = 0;
- val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
+ private val _busyLock = ReentrantLock()
+ val isBusy get() = _busyLock.isLocked;
var allowDevSubmit: Boolean = false
private set(value) {
@@ -140,6 +153,7 @@ class V8Plugin {
synchronized(_runtimeLock) {
if (_runtime != null)
return;
+ runtimeId = runtimeId + 1;
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
val host = V8Host.getV8Instance();
val options = host.jsRuntimeType.getRuntimeOptions();
@@ -148,6 +162,8 @@ class V8Plugin {
if (!host.isIsolateCreated)
throw IllegalStateException("Isolate not created");
+ _runtimeMap.put(_runtime!!, this);
+
//Setup bridge
_runtime?.let {
it.converter = V8Converter();
@@ -184,10 +200,13 @@ class V8Plugin {
}
fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]");
- isStopped = true;
- whenNotBusy {
+ busy {
+ Logger.i(TAG, "Plugin stopping");
synchronized(_runtimeLock) {
+ if(isStopped)
+ return@busy;
isStopped = true;
+ runtimeId = runtimeId + 1;
//Cleanup http
for(pack in _depsPackages) {
@@ -197,6 +216,7 @@ class V8Plugin {
}
_runtime?.let {
+ _runtimeMap.remove(it);
_runtime = null;
if(!it.isClosed && !it.isDead) {
try {
@@ -211,62 +231,147 @@ class V8Plugin {
Logger.i(TAG, "Stopped plugin [${config.name}]");
};
}
+ Logger.i(TAG, "Plugin stopped");
onStopped.emit(this);
}
+ cancelAllPromises();
}
+ fun isThreadAlreadyBusy(): Boolean {
+ return _busyLock.isHeldByCurrentThread;
+ }
+ fun busy(handle: ()->T): T {
+ _busyLock.lock();
+ try {
+ return handle();
+ }
+ finally {
+ _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 unbusy(handle: ()->T): T {
+ val wasLocked = isThreadAlreadyBusy();
+ if(!wasLocked)
+ return handle();
+ val lockCount = _busyLock.holdCount;
+ for(i in 1..lockCount)
+ _busyLock.unlock();
+ try {
+ Logger.w(TAG, "Unlocking V8 thread for [${config.name}] for a blocking resolve of a promise")
+ return handle();
+ }
+ finally {
+ Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise")
+
+ for(i in 1..lockCount)
+ _busyLock.lock();
+ }
+ }
fun execute(js: String) : V8Value {
return executeTyped(js);
}
+
+ suspend fun executeTypedAsync(js: String) : Deferred {
+ warnIfMainThread("V8Plugin.executeTypedAsync");
+ if(isStopped)
+ throw PluginEngineStoppedException(config, "Instance is stopped", js);
+
+ return withContext(IO) {
+ return@withContext busy {
+ try {
+ val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
+ val result = catchScriptErrors("Plugin[${config.name}]", js) {
+ runtime.getExecutor(js).execute()
+ };
+
+ if (result is V8ValuePromise) {
+ return@busy result.toV8ValueAsync(this@V8Plugin);
+ } else
+ return@busy CompletableDeferred(result as T);
+ }
+ catch(ex: Throwable) {
+ val def = CompletableDeferred();
+ def.completeExceptionally(ex);
+ return@busy def;
+ }
+ }
+ }
+ }
fun executeTyped(js: String) : T {
warnIfMainThread("V8Plugin.executeTyped");
if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js);
- synchronized(_busyCounterLock) {
- _busyCounter++;
- }
-
- val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
- try {
- return catchScriptErrors("Plugin[${config.name}]", js) {
+ val result = busy {
+ val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
+ return@busy catchScriptErrors("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute()
};
+ };
+ if(result is V8ValuePromise) {
+ return result.toV8ValueBlocking(this@V8Plugin);
}
- finally {
- synchronized(_busyCounterLock) {
- //Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
- try {
- afterBusy.emit(_busyCounter - 1);
- }
- catch(ex: Throwable) {
- Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
- }
- _busyCounter--;
- }
- }
+ return result as T;
}
- fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value };
- fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value };
- fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value };
+ fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } }
+ fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } }
+ fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } }
- fun whenNotBusy(handler: (V8Plugin)->Unit) {
- synchronized(_busyCounterLock) {
- if(_busyCounter == 0)
- handler(this);
- else {
- val tag = Object();
- afterBusy.subscribe(tag) {
- if(it == 0) {
- Logger.w(TAG, "V8Plugin afterBusy handled");
- afterBusy.remove(tag);
- handler(this);
- }
- }
+
+ fun handlePromise(result: V8ValuePromise): CompletableDeferred {
+ val def = CompletableDeferred();
+ result.register(object: IV8ValuePromise.IListener {
+ override fun onFulfilled(p0: V8Value?) {
+ resolvePromise(result);
+ def.complete(p0 as T);
}
+ override fun onRejected(p0: V8Value?) {
+ resolvePromise(result);
+ def.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
+ }
+ override fun onCatch(p0: V8Value?) {
+ resolvePromise(result);
+ def.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
+ }
+ });
+ registerPromise(result) {
+ if(def.isActive)
+ def.cancel("Cancelled by system");
+ }
+ return def;
+ }
+ fun registerPromise(promise: V8ValuePromise, onCancelled: ((V8ValuePromise)->Unit)? = null) {
+ Logger.v(TAG, "Promise registered for plugin [${config.name}]: ${promise.hashCode()}");
+ if (onCancelled != null) {
+ _promises.put(promise, onCancelled)
+ };
+ }
+ fun resolvePromise(promise: V8ValuePromise, cancelled: Boolean = false) {
+ Logger.v(TAG, "Promise resolved for plugin [${config.name}]: ${promise.hashCode()}");
+ val found = synchronized(_promises) {
+ val found = _promises.getOrDefault(promise, null);
+ _promises.remove(promise);
+ return@synchronized found;
+ };
+ if(found != null && cancelled)
+ found(promise);
+ }
+ fun cancelAllPromises(){
+ val promises = _promises.keys().toList();
+ for(key in promises) {
+ try {
+ resolvePromise(key, true);
+ }
+ catch(ex: Throwable) {}
}
}
+
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
//TODO: Auto get all package types?
return when(packageName) {
@@ -292,8 +397,14 @@ class V8Plugin {
private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*");
private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*");
+ private val _runtimeMap = ConcurrentHashMap();
+
val TAG = "V8Plugin";
+ fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? {
+ return _runtimeMap.getOrDefault(runtime, null);
+ }
+
fun catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
var codeStripped = code;
if(codeStripped != null) { //TODO: Improve code stripped
@@ -327,14 +438,23 @@ class V8Plugin {
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
}
catch(executeEx: JavetExecutionException) {
- if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) {
- val pluginType = executeEx.scriptingError.context["plugin_type"].toString();
+ val obj = executeEx.scriptingError?.context
+ if(obj != null && obj.containsKey("plugin_type") == true) {
+ val pluginType = obj["plugin_type"].toString();
//Captcha
if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config,
- executeEx.scriptingError.context["url"]?.toString(),
- executeEx.scriptingError.context["body"]?.toString(),
+ obj["url"]?.toString(),
+ obj["body"]?.toString(),
+ executeEx, executeEx.scriptingError?.stack, codeStripped);
+ }
+
+ //Reload Required
+ if (pluginType == "ReloadRequiredException") {
+ throw ScriptReloadRequiredException(config,
+ obj["msg"]?.toString(),
+ obj["reloadData"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
@@ -348,6 +468,41 @@ class V8Plugin {
codeStripped
);
}
+ /* //Required for newer V8 versions
+ if(executeEx.scriptingError?.context is IJavetEntityError) {
+ val obj = executeEx.scriptingError?.context as IJavetEntityError
+ if(obj.context.containsKey("plugin_type") == true) {
+ val pluginType = obj.context["plugin_type"].toString();
+
+ //Captcha
+ if (pluginType == "CaptchaRequiredException") {
+ throw ScriptCaptchaRequiredException(config,
+ obj.context["url"]?.toString(),
+ obj.context["body"]?.toString(),
+ executeEx, executeEx.scriptingError?.stack, codeStripped);
+ }
+
+ //Reload Required
+ if (pluginType == "ReloadRequiredException") {
+ throw ScriptReloadRequiredException(config,
+ obj.context["msg"]?.toString(),
+ obj.context["reloadData"]?.toString(),
+ executeEx, executeEx.scriptingError?.stack, codeStripped);
+ }
+
+ //Others
+ throwExceptionFromV8(
+ config,
+ pluginType,
+ (extractJSExceptionMessage(executeEx) ?: ""),
+ executeEx,
+ executeEx.scriptingError?.stack,
+ codeStripped
+ );
+ }
+
+ }
+ */
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
}
catch(ex: Exception) {
@@ -398,9 +553,4 @@ class V8Plugin {
return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found");
}
}
-
-
- /**
- * Methods available for scripts (bridge object)
- */
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt
index bce39025..4011b0a8 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
open class NoInternetException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class NoInternetException(config: IV8PluginConfig, error: String, ex: Excep
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException {
+ obj.ensureIsBusy();
return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt
index ef1ca13f..48c3142f 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Except
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt
index 8aa7f2c8..6bbf536b 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@@ -9,6 +10,7 @@ class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?,
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
val contextName = "ScriptCaptchaRequiredException";
return ScriptCaptchaRequiredException(config,
obj.getOrDefault(config, "url", contextName, null),
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt
index 2db245d3..26b2eebc 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException {
+ obj.ensureIsBusy();
return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt
index 6581ec25..d8eda509 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: E
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt
index cf038a23..de777a9f 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt
index 28b9b0e9..8bfd49d6 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
@@ -11,6 +12,7 @@ open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex:
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException {
+ obj.ensureIsBusy();
return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt
index dd2aaf7a..943b4fe9 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException {
+ obj.ensureIsBusy();
return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt
index 423d5786..4acf0c55 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt
new file mode 100644
index 00000000..6c792a32
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt
@@ -0,0 +1,22 @@
+package com.futo.platformplayer.engine.exceptions
+
+import com.caoccao.javet.values.reference.V8ValueObject
+import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
+import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.engine.V8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
+import com.futo.platformplayer.getOrDefault
+import com.futo.platformplayer.getOrThrow
+
+class ScriptReloadRequiredException(config: IV8PluginConfig, val msg: String?, val reloadData: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, msg ?: "ReloadRequired", ex, stack, code) {
+
+ companion object {
+ fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
+ val contextName = "ScriptReloadRequiredException";
+ return ScriptReloadRequiredException(config,
+ obj.getOrThrow(config, "message", contextName),
+ obj.getOrDefault(config, "reloadData", contextName, null));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt
index 6f883854..17d02073 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt
@@ -2,11 +2,13 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException {
+ obj.ensureIsBusy();
return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt
index 5d331b8b..feb47c35 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
+ obj.ensureIsBusy();
return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException"));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt b/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt
index 4e861b72..fd30af6f 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt
@@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable {
override fun toV8(runtime: V8Runtime): V8Value? {
synchronized(this) {
- if(_runtimeObj != null)
- return _runtimeObj;
+ //if(_runtimeObj != null)
+ // return _runtimeObj;
val v8Obj = runtime.createV8ValueObject();
v8Obj.bind(this);
diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt
index d2d7cf04..db44c1fc 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt
@@ -4,6 +4,7 @@ import android.media.MediaCodec
import android.media.MediaCodecList
import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property
+import com.caoccao.javet.interop.callback.JavetCallbackContext
import com.caoccao.javet.utils.JavetResourceUtils
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueFunction
@@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import java.util.concurrent.ConcurrentHashMap
class PackageBridge : V8Package {
@Transient
@@ -78,6 +80,15 @@ class PackageBridge : V8Package {
return "android";
}
+ @V8Property
+ fun supportedFeatures(): Array {
+ return arrayOf(
+ "ReloadRequiredException",
+ "HttpBatchClient",
+ "Async"
+ );
+ }
+
@V8Property
fun supportedContent(): Array {
return arrayOf(
@@ -101,45 +112,54 @@ class PackageBridge : V8Package {
}
var timeoutCounter = 0;
- var timeoutMap = HashSet();
+ var timeoutMap = ConcurrentHashMap();
@V8Function
fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
val id = timeoutCounter++;
-
val funcClone = func.toClone()
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
delay(timeout);
- synchronized(timeoutMap) {
- if(!timeoutMap.contains(id)) {
- JavetResourceUtils.safeClose(funcClone);
- return@launch;
+ if (_plugin.isStopped)
+ return@launch;
+ if (!timeoutMap.containsKey(id)) {
+ _plugin.busy {
+ if (!_plugin.isStopped)
+ JavetResourceUtils.safeClose(funcClone);
}
- timeoutMap.remove(id);
+ return@launch;
}
+ timeoutMap.remove(id);
try {
- _plugin.whenNotBusy {
- funcClone.callVoid(null, arrayOf());
+ Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}");
+ _plugin.busy {
+ Logger.w(TAG, "setTimeout in busy");
+ if (!_plugin.isStopped)
+ funcClone.callVoid(null, arrayOf());
+ Logger.w(TAG, "setTimeout after");
}
- }
- catch(ex: Throwable) {
+ } catch (ex: Throwable) {
Logger.e(TAG, "Failed timeout callback", ex);
- }
- finally {
- JavetResourceUtils.safeClose(funcClone);
+ } finally {
+ _plugin.busy {
+ if (!_plugin.isStopped)
+ JavetResourceUtils.safeClose(funcClone);
+ }
+ //_plugin.whenNotBusy {
+ //}
}
};
- synchronized(timeoutMap) {
- timeoutMap.add(id);
- }
+ timeoutMap.put(id, true);
return id;
}
@V8Function
fun clearTimeout(id: Int) {
- synchronized(timeoutMap) {
- if(timeoutMap.contains(id))
- timeoutMap.remove(id);
- }
+ if (timeoutMap.containsKey(id))
+ timeoutMap.remove(id);
+ }
+ @V8Function
+ fun sleep(length: Int) {
+ Thread.sleep(length.toLong());
}
@V8Function
@@ -147,7 +167,7 @@ class PackageBridge : V8Package {
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try {
- UIDialogs.toast(str);
+ UIDialogs.appToast(str);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e);
}
diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
index 900eb6f0..82edb023 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
@@ -44,6 +44,17 @@ class PackageHttp: V8Package {
private val aliveSockets = mutableListOf();
private var _cleanedUp = false;
+ private val _clients = mutableMapOf()
+
+ fun getClient(id: String?): PackageHttpClient {
+ if(id == null)
+ throw IllegalArgumentException("Http client ${id} doesn't exist");
+ if(_packageClient.clientId() == id)
+ return _packageClient;
+ if(_packageClientAuth.clientId() == id)
+ return _packageClientAuth;
+ return _clients.getOrDefault(id, null) ?: throw IllegalArgumentException("Http client ${id} doesn't exist");
+ }
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
_config = config;
@@ -112,6 +123,8 @@ class PackageHttp: V8Package {
_plugin.registerHttpClient(httpClient);
val client = PackageHttpClient(this, httpClient);
+ _clients.put(client.clientId() ?: "", client);
+
return client;
}
@V8Function
@@ -246,18 +259,18 @@ class PackageHttp: V8Package {
@V8Function
fun request(method: String, url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder {
- return clientRequest(_package.getDefaultClient(useAuth), method, url, headers);
+ return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers);
}
@V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder {
- return clientRequestWithBody(_package.getDefaultClient(useAuth), method, url, body, headers);
+ return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers);
}
@V8Function
fun GET(url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder
- = clientGET(_package.getDefaultClient(useAuth), url, headers);
+ = clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers);
@V8Function
fun POST(url: String, body: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder
- = clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
+ = clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers);
@V8Function
fun DUMMY(): BatchBuilder {
@@ -268,21 +281,21 @@ class PackageHttp: V8Package {
//Client-specific
@V8Function
- fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap = HashMap()) : BatchBuilder {
- _reqs.add(Pair(client, RequestDescriptor(method, url, headers)));
+ fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap = HashMap()) : BatchBuilder {
+ _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)));
return BatchBuilder(_package, _reqs);
}
@V8Function
- fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap = HashMap()) : BatchBuilder {
- _reqs.add(Pair(client, RequestDescriptor(method, url, headers, body)));
+ fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap = HashMap()) : BatchBuilder {
+ _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body)));
return BatchBuilder(_package, _reqs);
}
@V8Function
- fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap = HashMap()) : BatchBuilder
- = clientRequest(client, "GET", url, headers);
+ fun clientGET(clientId: String?, url: String, headers: MutableMap = HashMap()) : BatchBuilder
+ = clientRequest(clientId, "GET", url, headers);
@V8Function
- fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap = HashMap()) : BatchBuilder
- = clientRequestWithBody(client, "POST", url, body, headers);
+ fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap = HashMap()) : BatchBuilder
+ = clientRequestWithBody(clientId, "POST", url, body, headers);
//Finalizer
@@ -321,6 +334,7 @@ class PackageHttp: V8Package {
@Transient
private val _clientId: String?;
+
@V8Property
fun clientId(): String? {
return _clientId;
@@ -333,6 +347,17 @@ class PackageHttp: V8Package {
_clientId = if(_client is JSHttpClient) _client.clientId else null;
}
+ @V8Function
+ fun resetAuthCookies(){
+ if(_client is JSHttpClient)
+ _client.resetAuthCookies();
+ }
+ @V8Function
+ fun clearOtherCookies(){
+ if(_client is JSHttpClient)
+ _client.clearOtherCookies();
+ }
+
@V8Function
fun setDefaultHeaders(defaultHeaders: Map) {
for(pair in defaultHeaders)
@@ -429,8 +454,23 @@ class PackageHttp: V8Package {
};
}
@V8Function
- fun POST(url: String, body: String, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
- = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
+ fun POST(url: String, body: Any, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse {
+ if(body is V8ValueString)
+ return POSTInternal(url, body.value, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
+ else if(body is String)
+ return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
+ else if(body is V8ValueTypedArray)
+ return POSTInternal(url, body.toBytes(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
+ else if(body is ByteArray)
+ return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
+ else if(body is ArrayList<*>) //Avoid this case, used purely for testing
+ return POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
+ else
+ throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
+ }
+
+
+ // = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: String, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
@@ -452,9 +492,6 @@ class PackageHttp: V8Package {
}
};
}
- @V8Function
- fun POST(url: String, body: ByteArray, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
- = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: ByteArray, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
@@ -630,7 +667,9 @@ class PackageHttp: V8Package {
_isOpen = true;
if(hasOpen && _listeners?.isClosed != true) {
try {
- _listeners?.invokeVoid("open", arrayOf());
+ _package._plugin.busy {
+ _listeners?.invokeVoid("open", arrayOf());
+ }
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
@@ -640,7 +679,9 @@ class PackageHttp: V8Package {
override fun message(msg: String) {
if(hasMessage && _listeners?.isClosed != true) {
try {
- _listeners?.invokeVoid("message", msg);
+ _package._plugin.busy {
+ _listeners?.invokeVoid("message", msg);
+ }
}
catch(ex: Throwable) {}
}
@@ -649,7 +690,9 @@ class PackageHttp: V8Package {
if(hasClosing && _listeners?.isClosed != true)
{
try {
- _listeners?.invokeVoid("closing", code, reason);
+ _package._plugin.busy {
+ _listeners?.invokeVoid("closing", code, reason);
+ }
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
@@ -660,7 +703,9 @@ class PackageHttp: V8Package {
_isOpen = false;
if(hasClosed && _listeners?.isClosed != true) {
try {
- _listeners?.invokeVoid("closed", code, reason);
+ _package._plugin.busy {
+ _listeners?.invokeVoid("closed", code, reason);
+ }
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
@@ -676,7 +721,9 @@ class PackageHttp: V8Package {
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure && _listeners?.isClosed != true) {
try {
- _listeners?.invokeVoid("failure", exception.message);
+ _package._plugin.busy {
+ _listeners?.invokeVoid("failure", exception.message);
+ }
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt
index 6244004b..c9b89d82 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt
@@ -23,7 +23,7 @@ open class MainActivityFragment : Fragment() {
fun navigate(frag: MainFragment, parameter: Any? = null, withHistory: Boolean = true) {
val a = activity
if (a is MainActivity)
- (activity as MainActivity).navigate(frag, parameter, withHistory)
+ (activity as MainActivity).navigate(frag, parameter, withHistory, false)
else
Log.e(TAG, "Failed to navigate due to activity not being a main activity.")
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
index d43a67f6..b69082ae 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
@@ -331,7 +331,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
}
if (!StatePayment.instance.hasPaid) {
- newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate(withHistory = false) }))
+ newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate(withHistory = true) }))
}
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt
index 989a19e1..d052e0f1 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt
@@ -778,6 +778,8 @@ class ArticleDetailFragment : MainFragment {
view.onAddToWatchLaterClicked.subscribe { a ->
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]")
+ else
+ UIDialogs.toast(context.getString(R.string.already_in_watch_later))
}
}
else if(content is IPlatformPost) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
index e79e0c13..eaa84e3e 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
@@ -1,6 +1,8 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -66,8 +68,7 @@ class BuyFragment : MainFragment() {
_paymentManager = PaymentManager(StatePayment.instance, fragment, _overlayPaying) { success, _, exception ->
if(success) {
- UIDialogs.showDialog(context, R.drawable.ic_check, context.getString(R.string.payment_succeeded), context.getString(R.string.thanks_for_your_purchase_a_key_will_be_sent_to_your_email_after_your_payment_has_been_received), null, 0,
- UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY));
+ UIDialogs.showDialog(context, R.drawable.ic_check, context.getString(R.string.payment_succeeded), context.getString(R.string.thanks_for_your_purchase_a_key_will_be_sent_to_your_email_after_your_payment_has_been_received), null, 0, UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY));
_fragment.close(true);
}
else {
@@ -115,11 +116,14 @@ class BuyFragment : MainFragment() {
val licenseInput = SlideUpMenuTextInput(context, context.getString(R.string.license));
val productLicenseDialog = SlideUpMenuOverlay(context, findViewById(R.id.overlay_paid), context.getString(R.string.enter_license_key), context.getString(R.string.ok), true, licenseInput);
productLicenseDialog.onOK.subscribe {
+ licenseInput.deactivate();
val licenseText = licenseInput.text;
if (licenseText.isNullOrEmpty()) {
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key));
return@subscribe;
}
+ licenseInput.clear();
+ productLicenseDialog.hide(true);
_fragment.lifecycleScope.launch(Dispatchers.IO) {
@@ -127,17 +131,18 @@ class BuyFragment : MainFragment() {
val activationResult = StatePayment.instance.setPaymentLicense(licenseText);
withContext(Dispatchers.Main) {
- if(activationResult) {
- licenseInput.deactivate();
- licenseInput.clear();
- productLicenseDialog.hide(true);
-
- UIDialogs.showDialogOk(context, R.drawable.ic_check, context.getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
- _fragment.close(true);
- }
- else
- {
- UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key));
+ try {
+ if(activationResult) {
+ UIDialogs.showDialogOk(context, R.drawable.ic_check, context.getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required)) {
+ _fragment.close(true)
+ }
+ }
+ else
+ {
+ UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key));
+ }
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to update UI after buy complete", e)
}
}
}
@@ -158,5 +163,6 @@ class BuyFragment : MainFragment() {
companion object {
fun newInstance() = BuyFragment().apply {}
+ private val TAG = "BuyFragment"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
index 72dd3a58..91e6aaa3 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
@@ -172,7 +172,7 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe = findViewById(R.id.button_subscribe)
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
_overlayLoading = findViewById(R.id.channel_loading_overlay)
- _overlayLoadingSpinner = findViewById(R.id.channel_loader)
+ _overlayLoadingSpinner = findViewById(R.id.channel_loader_frag)
_overlayContainer = findViewById(R.id.overlay_container)
_buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
@@ -234,6 +234,8 @@ class ChannelFragment : MainFragment() {
if (content is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]")
+ else
+ UIDialogs.toast(context.getString(R.string.already_in_watch_later))
}
}
adapter.onUrlClicked.subscribe { url ->
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
index cc528a2b..fbb85dac 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
@@ -86,6 +86,8 @@ abstract class ContentFeedView : FeedView("creators_ordering")
+
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false);
@@ -44,7 +48,7 @@ class CreatorsFragment : MainFragment() {
_buttonClearSearch?.visibility = View.INVISIBLE;
}
- val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
+ val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription), _ordering?.value?.toIntOrNull() ?: 5) { subs ->
_textMeta?.let {
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
}
@@ -61,6 +65,7 @@ class CreatorsFragment : MainFragment() {
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
adapter.sortBy = pos;
+ _ordering.setAndSave(pos.toString())
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
};
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt
index 536700d8..0e429430 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt
@@ -150,7 +150,7 @@ class DownloadsFragment : MainFragment() {
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
};
- val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc");
+ val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc", "sizeAsc", "sizeDesc");
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
when(pos) {
@@ -160,6 +160,8 @@ class DownloadsFragment : MainFragment() {
3 -> ordering.setAndSave("downloadDateDesc")
4 -> ordering.setAndSave("releasedAsc")
5 -> ordering.setAndSave("releasedDesc")
+ 6 -> ordering.setAndSave("sizeAsc")
+ 7 -> ordering.setAndSave("sizeDesc")
else -> ordering.setAndSave("")
}
updateContentFilters()
@@ -257,6 +259,8 @@ class DownloadsFragment : MainFragment() {
"nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() }
"releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX }
"releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN }
+ "sizeAsc" -> vidsToReturn.sortedBy { it.videoSource.sumOf { it.fileSize } + it.audioSource.sumOf { it.fileSize } }
+ "sizeDesc" -> vidsToReturn.sortedByDescending { it.videoSource.sumOf { it.fileSize } + it.audioSource.sumOf { it.fileSize } }
else -> vidsToReturn
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
index c56585b0..1e90e36e 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
@@ -10,7 +10,6 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
-import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.TaskHandler
@@ -165,14 +164,24 @@ class PlaylistFragment : MainFragment() {
};
}
- private fun copyPlaylist(playlist: Playlist) {
+ private fun savePlaylist(playlist: Playlist) {
StatePlaylists.instance.playlistStore.save(playlist)
- _fragment.topBar?.assume()?.setMenuItems(
- arrayListOf()
- )
UIDialogs.toast("Playlist saved")
}
+ private fun copyPlaylist(playlist: Playlist) {
+ var copyNumber = 1
+ var newName = "${playlist.name} (Copy)"
+ val playlists = StatePlaylists.instance.playlistStore.getItems()
+ while (playlists.any { it.name == newName }) {
+ copyNumber += 1
+ newName = "${playlist.name} (Copy $copyNumber)"
+ }
+ StatePlaylists.instance.playlistStore.save(playlist.makeCopy(newName))
+ _fragment.navigate(withHistory = false)
+ UIDialogs.toast("Playlist copied")
+ }
+
fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel()
@@ -188,12 +197,14 @@ class PlaylistFragment : MainFragment() {
setButtonExportVisible(false)
setButtonEditVisible(true)
- if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
- _fragment.topBar?.assume()
- ?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
+ _fragment.topBar?.assume()
+ ?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
+ if (StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
copyPlaylist(parameter)
- }))
- }
+ } else {
+ savePlaylist(parameter)
+ }
+ }))
} else {
setName(null)
setVideos(null, false)
@@ -259,7 +270,7 @@ class PlaylistFragment : MainFragment() {
val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
- copyPlaylist(playlist)
+ savePlaylist(playlist)
download()
})
return
@@ -292,7 +303,7 @@ class PlaylistFragment : MainFragment() {
val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
- copyPlaylist(playlist)
+ savePlaylist(playlist)
onEditClick()
})
return
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
index 83e39a88..4a3fdf91 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
@@ -191,7 +191,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _bypassRateLimit = false;
private val _lastExceptions: List? = null;
- private val _taskGetPager = TaskHandler>({StateApp.instance.scope}, { withRefresh ->
+ private val _taskGetPager = TaskHandler>({fragment.lifecycleScope}, { withRefresh ->
val group = subGroup;
if(!_bypassRateLimit) {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
@@ -202,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() {
throw RateLimitException(rateLimitPlugins.map { it.key.id });
}
_bypassRateLimit = false;
- val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh, group);
+ val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(fragment.lifecycleScope, withRefresh, group);
val feed = StateSubscriptions.instance.getFeed(group?.id);
val currentExs = feed?.exceptions ?: listOf();
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
index fd3319f0..3404de15 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
@@ -101,7 +101,7 @@ class VideoDetailFragment() : MainFragment() {
}
private fun isSmallWindow(): Boolean {
- return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.column_width_dp) * 2
+ return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.smallest_width_dp)
}
private fun isAutoRotateEnabled(): Boolean {
@@ -467,10 +467,14 @@ class VideoDetailFragment() : MainFragment() {
activity?.enterPictureInPictureMode(params);
}
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, isStop: Boolean, newConfig: Configuration) {
- if (isInPictureInPictureMode) {
- _viewDetail?.startPictureInPicture();
- } else if (isInPictureInPicture) {
- leavePictureInPictureMode(isStop);
+ try {
+ if (isInPictureInPictureMode) {
+ _viewDetail?.startPictureInPicture();
+ } else if (isInPictureInPicture) {
+ leavePictureInPictureMode(isStop);
+ }
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to handle onPictureInPictureModeChanged", e)
}
}
fun leavePictureInPictureMode(isStop: Boolean) {
@@ -623,11 +627,6 @@ class VideoDetailFragment() : MainFragment() {
showSystemUI()
}
- // temporarily force the device to portrait if auto-rotate is disabled to prevent landscape when exiting full screen on a small device
-// @SuppressLint("SourceLockedOrientationActivity")
-// if (!isFullscreen && isSmallWindow() && !isAutoRotateEnabled() && !isMinimizingFromFullScreen) {
-// activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
-// }
updateOrientation();
_view?.allowMotion = !fullscreen;
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index c5d647ff..a1ddc394 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -2,6 +2,8 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.app.PictureInPictureParams
import android.app.RemoteAction
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
@@ -91,6 +93,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
+import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks
@@ -172,6 +175,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import userpackage.Protocol
import java.time.OffsetDateTime
+import java.util.Locale
import kotlin.math.abs
import kotlin.math.roundToLong
@@ -408,6 +412,14 @@ class VideoDetailView : ConstraintLayout {
showChaptersUI();
};
+ _title.setOnLongClickListener {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
+ val clip = ClipData.newPlainText("Video Title", (it as TextView).text);
+ clipboard.setPrimaryClip(clip);
+ UIDialogs.toast(context, "Copied", false)
+ // let other interactions happen based on the touch
+ false
+ }
_buttonSubscribe.onSubscribed.subscribe {
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
@@ -597,6 +609,10 @@ class VideoDetailView : ConstraintLayout {
}
}
+ _player.onReloadRequired.subscribe {
+ fetchVideo();
+ }
+
_player.onPlayChanged.subscribe {
if (StateCasting.instance.activeDevice == null) {
handlePlayChanged(it);
@@ -1115,7 +1131,7 @@ class VideoDetailView : ConstraintLayout {
when (Settings.instance.playback.backgroundPlay) {
0 -> handlePause();
1 -> {
- if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio)
+ if(!(video?.isLive ?: false))
_player.switchToAudioMode();
StatePlayer.instance.startOrUpdateMediaSession(context, video);
}
@@ -1399,8 +1415,8 @@ class VideoDetailView : ConstraintLayout {
onVideoChanged.emit(0, 0)
}
+ val me = this;
if (video is JSVideoDetails) {
- val me = this;
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
//TODO: Implement video.getContentChapters()
@@ -1457,6 +1473,32 @@ class VideoDetailView : ConstraintLayout {
}
};
}
+ else {
+ fragment.lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ if (!StateApp.instance.privateMode) {
+ val stopwatch = com.futo.platformplayer.debug.Stopwatch()
+ var tracker = video.getPlaybackTracker()
+ Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
+
+ if (tracker == null) {
+ stopwatch.reset()
+ tracker = StatePlatform.instance.getPlaybackTracker(video.url);
+ Logger.i(
+ TAG,
+ "StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms"
+ )
+ }
+
+ if (me.video?.url == video.url && !video.url.isNullOrBlank())
+ me._playbackTracker = tracker;
+ } else if (me.video == video)
+ me._playbackTracker = null;
+ } catch (ex: Throwable) {
+ Logger.e(TAG, "Playback tracker failed", ex);
+ }
+ }
+ }
val ref = Models.referenceFromBuffer(video.url.toByteArray())
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
@@ -1897,8 +1939,8 @@ class VideoDetailView : ConstraintLayout {
}
updateQualityFormatsOverlay(
- videoTrackFormats.distinctBy { it.height }.sortedBy { it.height },
- audioTrackFormats.distinctBy { it.bitrate }.sortedBy { it.bitrate });
+ videoTrackFormats.distinctBy { it.height }.sortedByDescending { it.height },
+ audioTrackFormats.distinctBy { it.bitrate }.sortedByDescending { it.bitrate });
}
}
@@ -2149,23 +2191,40 @@ class VideoDetailView : ConstraintLayout {
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
+ val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
R.string.quality), null, true,
- if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null,
+ qualityPlaybackSpeedTitle,
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
- setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString());
+ val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
+ val format = if(playbackSpeeds.size < 20) "%.2f" else "%.1f";
+ val playbackLabels = playbackSpeeds.map { String.format(Locale.US, format, it) }.toMutableList();
+ playbackLabels.add("+");
+ playbackLabels.add(0, "-");
+
+ setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
onClick.subscribe { v ->
+ val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate();
+ var playbackSpeedString = v;
+ val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
+ if(v == "+")
+ playbackSpeedString = String.format(Locale.US, "%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString();
+ else if(v == "-")
+ playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString();
+ val newPlaybackSpeed = playbackSpeedString.toDouble();
if (_isCasting) {
val ad = StateCasting.instance.activeDevice ?: return@subscribe
if (!ad.canSetSpeed) {
return@subscribe
}
- ad.changeSpeed(v.toDouble())
- setSelected(v);
+ qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
+ ad.changeSpeed(newPlaybackSpeed)
+ setSelected(playbackSpeedString);
} else {
- _player.setPlaybackRate(v.toFloat());
- setSelected(v);
+ qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
+ _player.setPlaybackRate(playbackSpeedString.toFloat());
+ setSelected(playbackSpeedString);
}
};
} else null,
@@ -2438,7 +2497,9 @@ class VideoDetailView : ConstraintLayout {
val url = _url;
if (!url.isNullOrBlank()) {
- setLoading(true);
+ fragment.lifecycleScope.launch(Dispatchers.Main) {
+ setLoading(true);
+ }
_taskLoadVideo.run(url);
}
}
@@ -2522,7 +2583,9 @@ class VideoDetailView : ConstraintLayout {
}
fun saveBrightness() {
- _player.gestureControl.saveBrightness()
+ if (Settings.instance.gestureControls.useSystemBrightness) {
+ _player.gestureControl.saveBrightness()
+ }
}
fun restoreBrightness() {
_player.gestureControl.restoreBrightness()
@@ -2702,6 +2765,8 @@ class VideoDetailView : ConstraintLayout {
if(it is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]");
+ else
+ UIDialogs.toast(context.getString(R.string.already_in_watch_later))
}
}
onAddToQueueClicked.subscribe(this) {
@@ -2969,6 +3034,11 @@ class VideoDetailView : ConstraintLayout {
return@TaskHandler result;
})
.success { setVideoDetails(it, true) }
+ .exception {
+ StatePlatform.instance.handleReloadRequired(it, {
+ fetchVideo();
+ });
+ }
.exception {
Logger.w(TAG, "exception", it)
diff --git a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt
index d7b1035f..758929d5 100644
--- a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt
+++ b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
+import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import kotlinx.serialization.Serializable
@@ -35,11 +36,15 @@ class Playlist {
this.videos = ArrayList(list);
}
+ fun makeCopy(newName: String? = null): Playlist {
+ return Playlist(newName ?: name, videos)
+ }
companion object {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? {
if(obj == null)
return null;
+ obj.ensureIsBusy();
val contextName = "Playlist";
diff --git a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt
index d524b7cf..76652236 100644
--- a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt
+++ b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt
@@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.view.MotionEvent
import android.widget.TextView
+import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
@@ -60,31 +63,39 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
val dx = event.x - downX
val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
- runBlocking {
- for (link in pressedLinks!!) {
- Logger.i(TAG) { "Link clicked '${link.url}'." }
+ for (link in pressedLinks!!) {
+ Logger.i(TAG) { "Link clicked '${link.url}'." }
- if (_context is MainActivity) {
- if (_context.handleUrl(link.url)) continue
- if (timestampRegex.matches(link.url)) {
- val tokens = link.url.split(':')
- var time_s = -1L
- when (tokens.size) {
- 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
- 3 -> time_s = tokens[0].toLong() * 3600 +
- tokens[1].toLong() * 60 +
- tokens[2].toLong()
- }
+ val c = _context
+ if (c is MainActivity) {
+ c.lifecycleScope.launch(Dispatchers.IO) {
+ if (c.handleUrl(link.url)) {
+ return@launch
+ }
+ if (timestampRegex.matches(link.url)) {
+ val tokens = link.url.split(':')
+ var time_s = -1L
+ when (tokens.size) {
+ 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
+ 3 -> time_s = tokens[0].toLong() * 3600 +
+ tokens[1].toLong() * 60 +
+ tokens[2].toLong()
+ }
- if (time_s != -1L) {
+ if (time_s != -1L) {
+ withContext(Dispatchers.Main) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
- continue
}
+ return@launch
}
}
- _context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
+
+ withContext(Dispatchers.Main) {
+ c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
+ }
}
}
+ }
pressedLinks = null
linkPressed = false
return true
diff --git a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt
index b39b4592..5ab75011 100644
--- a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt
+++ b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt
@@ -62,7 +62,7 @@ class DownloadService : Service() {
Logger.i(TAG, "onStartCommand");
synchronized(this) {
if(_started)
- return START_STICKY;
+ return START_NOT_STICKY;
if(!FragmentedStorage.isInitialized) {
Logger.i(TAG, "Attempted to start DownloadService without initialized files");
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index e2155e8b..9757f005 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -156,6 +156,8 @@ class StateApp {
return thisContext;
}
+ private var _mainId: String? = null;
+
//Files
private var _tempDirectory: File? = null;
private var _cacheDirectory: File? = null;
@@ -295,9 +297,12 @@ class StateApp {
}
//Lifecycle
- fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
+ fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null, mainId: String? = null) {
+ _mainId = mainId;
_context = context;
_scope = coroutineScope
+ Logger.w(TAG, "Scope initialized ${(coroutineScope != null)}\n ${Log.getStackTraceString(Throwable())}")
+
}
fun initializeFiles(force: Boolean = false) {
@@ -719,7 +724,9 @@ class StateApp {
migrateStores(context, managedStores, index + 1);
}
- fun mainAppDestroyed(context: Context) {
+ fun mainAppDestroyed(context: Context, mainId: String? = null) {
+ if (mainId != null && (_mainId != mainId || _mainId == null))
+ return
Logger.i(TAG, "App ended");
_receiverBecomingNoisy?.let {
_receiverBecomingNoisy = null;
@@ -743,7 +750,8 @@ class StateApp {
fun dispose(){
_context = null;
- _scope = null;
+ // _scope = null;
+ Logger.w(TAG, "StateApp disposed: ${Log.getStackTraceString(Throwable())}")
}
private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
index 3047731d..97ab82c1 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt
@@ -131,8 +131,13 @@ class StateHistory {
fun getHistoryPager(): IPager {
return _historyDBStore.getObjectPager();
}
- fun getHistorySearchPager(query: String): IPager {
- return _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10);
+ fun getHistorySearchPager(query: String, withAuthor: Boolean = false): IPager {
+ return if(!withAuthor)
+ _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10)
+ else
+ _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10)
+ //_historyDBStore.queryLike2ObjectPager(DBHistory.Index::name, DBHistory.Index::auth,"%${query}%", 10)
+ //TODO: See if we can include author name?
}
fun getHistoryIndexByUrl(url: String): DBHistory.Index? {
return historyIndex[url];
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
index a6e07a7b..d2cd0250 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.states
import android.content.Context
import androidx.collection.LruCache
+import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
@@ -38,6 +39,7 @@ import com.futo.platformplayer.awaitFirstNotNullDeferred
import com.futo.platformplayer.constructs.BatchedTaskHandler
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.fromPool
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.getNowDiffSeconds
@@ -316,7 +318,18 @@ class StatePlatform {
_platformOrderPersistent.save();
}
- suspend fun reloadClient(context: Context, id: String) : JSClient? {
+ fun handleReloadRequired(reloadRequiredException: ScriptReloadRequiredException, afterReload: (() -> Unit)? = null) {
+ val id = if(reloadRequiredException.config is SourcePluginConfig) reloadRequiredException.config.id else "";
+ UIDialogs.appToast("Reloading [${reloadRequiredException.config.name}] by plugin request");
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+ if(!reloadRequiredException.reloadData.isNullOrEmpty())
+ reEnableClientWithData(id, reloadRequiredException.reloadData, afterReload);
+ else
+ reEnableClient(id, afterReload);
+ }
+ }
+
+ suspend fun reloadClient(context: Context, id: String, afterReload: (()->Unit)? = null) : JSClient? {
return withContext(Dispatchers.IO) {
val client = getClient(id);
if (client !is JSClient)
@@ -347,10 +360,27 @@ class StatePlatform {
_availableClients.removeIf { it.id == id };
_availableClients.add(newClient);
}
+ afterReload?.invoke();
return@withContext newClient;
};
}
+ suspend fun reEnableClientWithData(id: String, data: String? = null, afterReload: (()->Unit)? = null) {
+ val enabledBefore = getEnabledClients().map { it.id };
+ if(data != null) {
+ val client = getClientOrNull(id);
+ if(client != null && client is JSClient)
+ client.setReloadData(data);
+ }
+ selectClients({
+ _scope.launch(Dispatchers.IO) {
+ selectClients({
+ afterReload?.invoke();
+ }, *(enabledBefore).distinct().toTypedArray());
+ }
+ }, *(enabledBefore.filter { it != id }).distinct().toTypedArray())
+ }
+ suspend fun reEnableClient(id: String, afterReload: (()->Unit)? = null) = reEnableClientWithData(id, null, afterReload);
suspend fun enableClient(ids: List) {
val currentClients = getEnabledClients().map { it.id };
@@ -361,6 +391,9 @@ class StatePlatform {
* If a client is disabled, NO requests are made to said client
*/
suspend fun selectClients(vararg ids: String) {
+ selectClients(null, *ids);
+ }
+ suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
withContext(Dispatchers.IO) {
synchronized(_clientsLock) {
val removed = _enabledClients.toMutableList();
@@ -385,6 +418,7 @@ class StatePlatform {
onSourceDisabled.emit(oldClient);
}
}
+ afterLoad?.invoke();
};
}
@@ -976,7 +1010,7 @@ class StatePlatform {
return EmptyPager();
if(!StateApp.instance.privateMode)
- return client.fromPool(_mainClientPool).getComments(url);
+ return client.fromPool(_pagerClientPool).getComments(url);
else
return client.fromPool(_privateClientPool).getComments(url);
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
index c20375f2..cbe1c518 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
@@ -3,7 +3,6 @@ package com.futo.platformplayer.states
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
-import androidx.fragment.app.Fragment
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
@@ -21,7 +20,6 @@ import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge
-import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringDateMapStorage
@@ -30,15 +28,12 @@ import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
-import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
-import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
-import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
@@ -178,31 +173,30 @@ class StatePlaylists {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
}
}
- fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
- var wasNew = false;
+ fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false): Boolean {
synchronized(_watchlistStore) {
- if(!_watchlistStore.hasItem { it.url == video.url })
- wasNew = true;
- _watchlistStore.saveAsync(video);
- if(orderPosition == -1)
- _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
- else {
- val existing = _watchlistOrderStore.getAllValues().toMutableList();
- existing.add(orderPosition, video.url);
- _watchlistOrderStore.set(*existing.toTypedArray());
+ if (_watchlistStore.hasItem { it.url == video.url }) {
+ return false
}
- _watchlistOrderStore.save();
+
+ _watchlistStore.saveAsync(video)
+ if (Settings.instance.other.watchLaterAddStart) {
+ _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray())
+ } else {
+ _watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray())
+ }
+ _watchlistOrderStore.save()
}
onWatchLaterChanged.emit();
- if(isUserInteraction) {
+ if (isUserInteraction) {
val now = OffsetDateTime.now();
_watchLaterAdds.setAndSave(video.url, now);
broadcastWatchLaterAddition(video, now);
}
StateDownloads.instance.checkForOutdatedPlaylists();
- return wasNew;
+ return true;
}
fun getLastPlayedPlaylist() : Playlist? {
@@ -423,17 +417,25 @@ class StatePlaylists {
class PlaylistBackup: ReconstructStore() {
override fun toReconstruction(obj: Playlist): String {
val items = ArrayList();
- items.add(obj.name);
+ items.add(obj.name + ":::" + obj.id);
items.addAll(obj.videos.map { it.url });
return items.map { it.replace("\n","") }.joinToString("\n");
}
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
+ var idToUse = id;
val items = backup.split("\n");
if(items.size <= 0) {
throw IllegalStateException("Cannot reconstructor playlist ${id}");
}
- val name = items[0];
+ var name = items[0];
+ if(name.contains(":::")){
+ val splitIndex = name.indexOf(":::");
+ val foundId = name.substring(splitIndex + 3);
+ if(!foundId.isNullOrEmpty())
+ idToUse = foundId;
+ name = name.substring(0, splitIndex);
+ }
val videos = items.drop(1).filter { it.isNotEmpty() }.map {
try {
val videoUrl = it;
@@ -465,7 +467,7 @@ class StatePlaylists {
throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex);
}
}.filter { it != null }.map { it!! }
- return Playlist(id, name, videos);
+ return Playlist(idToUse, name, videos);
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
index 1d1acff6..60026ea6 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt
@@ -329,8 +329,19 @@ class StateSubscriptions {
}
}
- if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url))
- getSubscriptionOtherOrCreate(sub.channel.url, sub.channel.name, sub.channel.thumbnail);
+ if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) {
+ val subGroups = StateSubscriptionGroups.instance.getSubscriptionGroups().filter { it.urls.contains(sub.channel.url) };
+ for(group in subGroups) {
+ group.urls.remove(sub.channel.url);
+ StateSubscriptionGroups.instance.updateSubscriptionGroup(group);
+ }
+ /*
+ getSubscriptionOtherOrCreate(
+ sub.channel.url,
+ sub.channel.name,
+ sub.channel.thumbnail
+ ); */
+ }
}
return sub;
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt
index fd08165c..25cff055 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt
@@ -78,22 +78,32 @@ class StateSync {
onAuthorized = { sess, isNewlyAuthorized, isNewSession ->
if (isNewSession) {
deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess)
- StateApp.instance.scope.launch(Dispatchers.IO) { checkForSync(sess) }
+ StateApp.instance.scope.launch(Dispatchers.IO) {
+ try {
+ checkForSync(sess)
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to check for sync.", e)
+ }
+ }
}
}
onUnauthorized = { sess ->
StateApp.instance.scope.launch(Dispatchers.Main) {
- UIDialogs.showConfirmationDialog(
- context,
- "Device Unauthorized: ${sess.displayName}",
- action = {
- Logger.i(TAG, "${sess.remotePublicKey} unauthorized received")
- removeAuthorizedDevice(sess.remotePublicKey)
- deviceRemoved.emit(sess.remotePublicKey)
- },
- cancelAction = {}
- )
+ try {
+ UIDialogs.showConfirmationDialog(
+ context,
+ "Device Unauthorized: ${sess.displayName}",
+ action = {
+ Logger.i(TAG, "${sess.remotePublicKey} unauthorized received")
+ removeAuthorizedDevice(sess.remotePublicKey)
+ deviceRemoved.emit(sess.remotePublicKey)
+ },
+ cancelAction = {}
+ )
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to show unauthorized dialog.", e)
+ }
}
}
@@ -118,30 +128,38 @@ class StateSync {
if (scope != null && activity != null) {
scope.launch(Dispatchers.Main) {
- UIDialogs.showConfirmationDialog(activity, "Allow connection from $remotePublicKey?",
- action = {
- scope.launch(Dispatchers.IO) {
- try {
- callback(true)
- Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation")
+ try {
+ UIDialogs.showConfirmationDialog(
+ activity, "Allow connection from $remotePublicKey?",
+ action = {
+ scope.launch(Dispatchers.IO) {
+ try {
+ callback(true)
+ Logger.i(
+ TAG,
+ "Connection authorized for $remotePublicKey by confirmation"
+ )
- activity.finish()
- } catch (e: Throwable) {
- Logger.e(TAG, "Failed to send authorize", e)
+ activity.finish()
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to send authorize", e)
+ }
+ }
+ },
+ cancelAction = {
+ scope.launch(Dispatchers.IO) {
+ try {
+ callback(false)
+ Logger.i(TAG, "$remotePublicKey unauthorized received")
+ } catch (e: Throwable) {
+ Logger.w(TAG, "Failed to send unauthorize", e)
+ }
}
}
- },
- cancelAction = {
- scope.launch(Dispatchers.IO) {
- try {
- callback(false)
- Logger.i(TAG, "$remotePublicKey unauthorized received")
- } catch (e: Throwable) {
- Logger.w(TAG, "Failed to send unauthorize", e)
- }
- }
- }
- )
+ )
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to show authorized dialog.", e)
+ }
}
} else {
callback(false)
@@ -224,6 +242,11 @@ class StateSync {
}
}
+ private val _lockSubscriptions = Any();
+ private val _lockSubscriptionGroups = Any();
+ private val _lockPlaylists = Any();
+ private val _lockWatchlater = Any();
+
private fun handleData(session: SyncSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
val remotePublicKey = session.remotePublicKey
when (subOpcode) {
@@ -307,7 +330,9 @@ class StateSync {
data.get(dataBody);
val json = String(dataBody, Charsets.UTF_8);
val subPackage = Serializer.json.decodeFromString(json);
- handleSyncSubscriptionPackage(session, subPackage);
+ synchronized(_lockSubscriptions) {
+ handleSyncSubscriptionPackage(session, subPackage);
+ }
if(subPackage.subscriptions.size > 0) {
val newestSub = subPackage.subscriptions.maxOf { it.creationTime };
@@ -327,21 +352,23 @@ class StateSync {
val pack = Serializer.json.decodeFromString(json);
var lastSubgroupChange = OffsetDateTime.MIN;
- for(group in pack.groups){
- if(group.lastChange > lastSubgroupChange)
- lastSubgroupChange = group.lastChange;
+ synchronized(_lockSubscriptionGroups) {
+ for(group in pack.groups){
+ if(group.lastChange > lastSubgroupChange)
+ lastSubgroupChange = group.lastChange;
- val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
+ val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
- if(existing == null)
- StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true);
- else if(existing.lastChange < group.lastChange) {
- existing.name = group.name;
- existing.urls = group.urls;
- existing.image = group.image;
- existing.priority = group.priority;
- existing.lastChange = group.lastChange;
- StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
+ if(existing == null)
+ StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true);
+ else if(existing.lastChange < group.lastChange) {
+ existing.name = group.name;
+ existing.urls = group.urls;
+ existing.image = group.image;
+ existing.priority = group.priority;
+ existing.lastChange = group.lastChange;
+ StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
+ }
}
}
for(removal in pack.groupRemovals) {
@@ -358,18 +385,20 @@ class StateSync {
val json = String(dataBody, Charsets.UTF_8);
val pack = Serializer.json.decodeFromString(json);
- for(playlist in pack.playlists) {
- val existing = StatePlaylists.instance.getPlaylist(playlist.id);
+ synchronized(_lockPlaylists) {
+ for(playlist in pack.playlists) {
+ val existing = StatePlaylists.instance.getPlaylist(playlist.id);
- if(existing == null)
- StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
- else if(existing.dateUpdate < playlist.dateUpdate) {
- existing.dateUpdate = playlist.dateUpdate;
- existing.name = playlist.name;
- existing.videos = playlist.videos;
- existing.dateCreation = playlist.dateCreation;
- existing.datePlayed = playlist.datePlayed;
- StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
+ if(existing == null)
+ StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
+ else if(existing.dateUpdate < playlist.dateUpdate) {
+ existing.dateUpdate = playlist.dateUpdate;
+ existing.name = playlist.name;
+ existing.videos = playlist.videos;
+ existing.dateCreation = playlist.dateCreation;
+ existing.datePlayed = playlist.datePlayed;
+ StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
+ }
}
}
for(removal in pack.playlistRemovals) {
@@ -390,14 +419,16 @@ class StateSync {
Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})");
val allExisting = StatePlaylists.instance.getWatchLater();
- for(video in pack.videos) {
- val existing = allExisting.firstOrNull { it.url == video.url };
- val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN;
- val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN;
- if(existing == null && time > removalTime) {
- StatePlaylists.instance.addToWatchLater(video, false);
- if(time > OffsetDateTime.MIN)
- StatePlaylists.instance.setWatchLaterAddTime(video.url, time);
+ synchronized(_lockWatchlater) {
+ for(video in pack.videos) {
+ val existing = allExisting.firstOrNull { it.url == video.url };
+ val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN;
+ val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN;
+ if(existing == null && time > removalTime) {
+ StatePlaylists.instance.addToWatchLater(video, false);
+ if(time > OffsetDateTime.MIN)
+ StatePlaylists.instance.setWatchLaterAddTime(video.url, time);
+ }
}
}
for(removal in pack.videoRemovals) {
diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt
index 2e493eef..6b9ed65f 100644
--- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt
+++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt
@@ -274,10 +274,17 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize));
return deserializeIndexes(dbDaoBase.getMultiple(query));
+ }fun queryLike2Page(field: String, field2: String, obj: String, page: Int, pageSize: Int): List {
+ val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? OR ${field2} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?";
+ val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, obj, pageSize, page * pageSize));
+ return deserializeIndexes(dbDaoBase.getMultiple(query));
}
fun queryLikeObjectPage(field: String, obj: String, page: Int, pageSize: Int): List {
return convertObjects(queryLikePage(field, obj, page, pageSize));
}
+ fun queryLike2ObjectPage(field: String, field2: String, obj: String, page: Int, pageSize: Int): List {
+ return convertObjects(queryLike2Page(field, field2, obj, page, pageSize));
+ }
//Query Page Objects
@@ -336,6 +343,13 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA
queryLikePage(field, obj, it - 1, pageSize);
});
}
+ fun queryLike2Pager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager = queryLike2Pager(validateFieldName(field), validateFieldName(field2), obj, pageSize);
+ fun queryLike2Pager(field: String, field2: String, obj: String, pageSize: Int): IPager {
+ return AdhocPager({
+ Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
+ queryLike2Page(field, field2, obj, it - 1, pageSize);
+ });
+ }
fun queryLikeObjectPager(field: KProperty<*>, obj: String, pageSize: Int): IPager = queryLikeObjectPager(validateFieldName(field), obj, pageSize);
fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager {
return AdhocPager({
@@ -344,6 +358,14 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA
});
}
+ fun queryLike2ObjectPager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager = queryLike2ObjectPager(validateFieldName(field), validateFieldName(field2), obj, pageSize);
+ fun queryLike2ObjectPager(field: String, field2: String, obj: String, pageSize: Int): IPager {
+ return AdhocPager({
+ Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
+ queryLike2ObjectPage(field, field2, obj, it - 1, pageSize);
+ });
+ }
+
//Query Pager with convert
fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager