Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into shorts-tab

This commit is contained in:
Koen J 2025-07-04 08:16:12 +02:00
commit 4ee127fe13
155 changed files with 3060 additions and 720 deletions

View file

@ -1,6 +1,9 @@
name: Bug Report name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior. 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: body:
- type: markdown - type: markdown
attributes: 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 * 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 - type: textarea
id: what-happened id: reproduction-steps
attributes: attributes:
label: What happened? label: Reproduction steps
description: What did you expect to happen? 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: Tell us what you see! 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: validations:
required: true required: true
@ -31,7 +56,7 @@ body:
attributes: attributes:
label: Grayjay Version label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name". description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "242" placeholder: "311"
validations: validations:
required: true required: true
@ -42,19 +67,23 @@ body:
multiple: true multiple: true
options: options:
- "All" - "All"
- "Youtube" - "Apple Podcasts"
- "Odysee"
- "Rumble"
- "Kick"
- "Twitch"
- "PeerTube"
- "Patreon"
- "Nebula"
- "BiliBili (CN)" - "BiliBili (CN)"
- "Bitchute" - "Bitchute"
- "SoundCloud" - "Crunchyroll"
- "CuriosityStream"
- "Dailymotion" - "Dailymotion"
- "Apple Podcasts" - "Kick"
- "Nebula"
- "Odysee"
- "Patreon"
- "PeerTube"
- "Rumble"
- "SoundCloud"
- "Spotify"
- "TedTalks"
- "Twitch"
- "Youtube"
- "Other" - "Other"
validations: validations:
required: true required: true
@ -66,6 +95,30 @@ body:
description: In the application, select Sources > [the broken plugin], write down the value under "Version". description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12" 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 - type: checkboxes
id: login id: login
attributes: attributes:
@ -86,9 +139,28 @@ body:
validations: validations:
required: true 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 - type: textarea
id: logs id: logs
attributes: attributes:
label: Relevant log output 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. description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell 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.

View file

@ -1,13 +1,16 @@
name: Feature Request name: Feature Request
description: Suggest a new feature or other enhancement. description: Suggest a new feature or other enhancement.
labels: ["Enhancement"] labels: ["Enhancement", "Android"]
title: "Feature request: "
type: feature
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for opening a feature request. # 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) For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

View file

@ -1,13 +1,16 @@
name: Documentation Issue name: Documentation Issue
description: Report an issue or suggest a change in the documentation. description: Report an issue or suggest a change in the documentation.
labels: ["Documentation"] labels: ["Documentation"]
title: "Documentation: "
type: task
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for opening a documentation change request. # 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. 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) For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

View file

@ -181,6 +181,7 @@ dependencies {
//JS //JS
implementation("com.caoccao.javet:javet-android:3.0.2") 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 //Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1' implementation 'androidx.media3:media3-exoplayer:1.2.1'

View file

@ -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()
)
}
}

View file

@ -11,7 +11,7 @@ import java.nio.ByteBuffer
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/*
class SyncServerTests { class SyncServerTests {
//private val relayHost = "relay.grayjay.app" //private val relayHost = "relay.grayjay.app"
@ -335,4 +335,4 @@ class SyncServerTests {
class AlwaysAuthorized : IAuthorizable { class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true override val isAuthorized: Boolean get() = true
} }*/

View file

@ -13,7 +13,7 @@ import kotlin.random.Random
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/*
data class PipeStreams( data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream, val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream, val initiatorOutput: LittleEndianDataOutputStream,
@ -509,4 +509,4 @@ class Authorized : IAuthorizable {
class Unauthorized : IAuthorizable { class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false override val isAuthorized: Boolean = false
} }*/

View file

@ -15,7 +15,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/> <uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View file

@ -103,6 +103,12 @@ class UnavailableException extends ScriptException {
super("UnavailableException", msg); super("UnavailableException", msg);
} }
} }
class ReloadRequiredException extends ScriptException {
constructor(msg, reloadData) {
super("ReloadRequiredException", msg);
this.reloadData = reloadData;
}
}
class AgeException extends ScriptException { class AgeException extends ScriptException {
constructor(msg) { constructor(msg) {
super("AgeException", msg); super("AgeException", msg);
@ -701,11 +707,12 @@ class LiveEventViewCount extends LiveEvent {
} }
} }
class LiveEventRaid extends LiveEvent { class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail) { constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
super(100); super(100);
this.targetUrl = targetUrl; this.targetUrl = targetUrl;
this.targetName = targetName; this.targetName = targetName;
this.targetThumbnail = targetThumbnail; this.targetThumbnail = targetThumbnail;
this.isOutgoing = isOutgoing ?? true;
} }
} }

View file

@ -0,0 +1,319 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 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<String, Int> = 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<String>): 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<String>): 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()

View file

@ -219,9 +219,7 @@ private fun ByteArray.toInetAddress(): InetAddress {
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
ensureNotMainThread() ensureNotMainThread()
val timeout = 2000 val timeout = 10000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses; val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty()) if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})"); throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
@ -243,8 +241,11 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
return null; return null;
} }
val sortedAddresses: List<InetAddress> = addresses
.sortedBy { addr -> addressScore(addr) }
val sockets: ArrayList<Socket> = arrayListOf(); val sockets: ArrayList<Socket> = arrayListOf();
for (i in addresses.indices) { for (i in sortedAddresses.indices) {
sockets.add(Socket()); sockets.add(Socket());
} }
@ -252,7 +253,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
var connectedSocket: Socket? = null; var connectedSocket: Socket? = null;
val threads: ArrayList<Thread> = arrayListOf(); val threads: ArrayList<Thread> = arrayListOf();
for (i in 0 until sockets.size) { for (i in 0 until sockets.size) {
val address = addresses[i]; val address = sortedAddresses[i];
val socket = sockets[i]; val socket = sockets[i];
val thread = Thread { val thread = Thread {
try { try {

View file

@ -2,10 +2,30 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.* 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.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject 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.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.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 //V8
@ -24,6 +44,10 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
return handler(this); return handler(this);
} }
inline fun V8Value.getSourcePlugin(): V8Plugin? {
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
}
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T) if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}"); throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
@ -89,7 +113,29 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T }; .map { kv-> kv.second.orNull { it.expectV8Variant<T>(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 <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
if(false)
ensureIsBusy();
return when(T::class) { return when(T::class) {
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T; String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
Int::class -> { Int::class -> {
@ -147,3 +193,118 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
map.put(prop, obj.getString(prop)); map.put(prop, obj.getString(prop));
return map; return map;
} }
fun <T: V8Value> 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 <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val underlyingDef = CompletableDeferred<T>();
val def = if(this.has("estDuration"))
V8Deferred(underlyingDef,
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(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<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
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 <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any): T {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result as T;
}
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
}

View file

@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle 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.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField 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) @FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings(); var language = LanguageSettings();
@Serializable @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) @FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false; var showHomeFiltersPluginNames: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@ -253,9 +259,11 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style) @DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1; var searchFeedStyle: Int = 1;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@ -277,6 +285,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class ChannelSettings { class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; 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) @FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false; var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true; 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) @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true; var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true; 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) @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false; var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true; var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false; var peekChannelContents: Boolean = false;
@ -425,9 +441,11 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true; 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) @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 var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@ -438,6 +456,7 @@ class Settings : FragmentedStorageFileJson() {
fun isBackgroundContinue() = backgroundPlay == 1; fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2; 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) @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) @DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1; 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) @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; 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) @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) @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1; var restartPlaybackAfterLoss: Int = 1;
@ -497,6 +512,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false; var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true; var deleteFromWatchLaterAuto: Boolean = true;
@ -515,6 +531,78 @@ class Settings : FragmentedStorageFileJson() {
else -> 10_000L; 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<Double> {
val playbackSpeeds = mutableListOf<Double>();
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) @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) @FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false; var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0) @FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false; var hideRecommendations: Boolean = false;
@ -566,10 +655,12 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1; var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0; fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4) @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true; 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) @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) @DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3; var byteRangeConcurrency: Int = 3;
@ -599,15 +690,21 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false; var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true; 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? /*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
@ -675,9 +772,11 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false; var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;
@ -878,7 +977,23 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1) @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"; 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() { fun clearPayment() {
SettingsActivity.getActivity()?.let { context -> SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", { UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
@ -896,15 +1011,20 @@ class Settings : FragmentedStorageFileJson() {
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true; var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true; 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; 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; var polycentricLocalCache: Boolean = true;
} }

View file

@ -1151,6 +1151,8 @@ class UISlideOverlays {
call = { call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false); UIDialogs.appToast("Added to watch later", false);
else
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}), }),
) )
); );

View file

@ -339,6 +339,33 @@ fun ByteArray.fromGzip(): ByteArray {
return outputStream.toByteArray() return outputStream.toByteArray()
} }
fun findCandidateAddresses(): List<InetAddress> {
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<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
).map { it.second.address }
}
fun findPreferredAddress(): InetAddress? { fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces() val candidates = NetworkInterface.getNetworkInterfaces()
.toList() .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) { return when (addr) {
is Inet4Address -> { is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF } val octets = addr.address.map { it.toInt() and 0xFF }

View file

@ -116,6 +116,7 @@ import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.LinkedList import java.util.LinkedList
import java.util.Queue import java.util.Queue
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -186,7 +187,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment; lateinit var _fragVideoDetail: VideoDetailFragment;
//State //State
private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList(); private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList();
lateinit var fragCurrent: MainFragment private set; lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null; private var _parameterCurrent: Any? = null;
@ -220,6 +221,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
val mainId = UUID.randomUUID().toString().substring(0, 5)
constructor() : super() { constructor() : super() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy( StrictMode.setVmPolicy(
@ -271,8 +274,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi @UnstableApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting"); Logger.w(TAG, "MainActivity Starting [$mainId]");
StateApp.instance.setGlobalContext(this, lifecycleScope); StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
StateApp.instance.mainAppStarting(this); StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -678,13 +681,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onResume() { override fun onResume() {
super.onResume(); super.onResume();
Logger.v(TAG, "onResume") Logger.w(TAG, "onResume [$mainId]")
_isVisible = true; _isVisible = true;
} }
override fun onPause() { override fun onPause() {
super.onPause(); super.onPause();
Logger.v(TAG, "onPause") Logger.w(TAG, "onPause [$mainId]")
_isVisible = false; _isVisible = false;
_qrCodeLoadingDialog?.dismiss() _qrCodeLoadingDialog?.dismiss()
@ -693,7 +696,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
Logger.v(TAG, "_wasStopped = true"); Logger.w(TAG, "onStop [$mainId]");
_wasStopped = true; _wasStopped = true;
} }
@ -1110,8 +1113,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy(); super.onDestroy();
Logger.v(TAG, "onDestroy") Logger.w(TAG, "onDestroy [$mainId]")
StateApp.instance.mainAppDestroyed(this); StateApp.instance.mainAppDestroyed(this, mainId);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {
@ -1192,7 +1195,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory) if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent; fragBeforeOverlay = fragCurrent;
fragCurrent = segment; fragCurrent = segment;
_parameterCurrent = parameter; _parameterCurrent = parameter;
} }

View file

@ -14,10 +14,12 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType 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.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix import com.google.zxing.common.BitMatrix
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import userpackage.Protocol.ExportBundle import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo import userpackage.Protocol.URLInfo
@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _imageQR: ImageView; private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String; private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView; private lateinit var _textQR: TextView;
private lateinit var _loader: View
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup); setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
_buttonShare = findViewById(R.id.button_share); _buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy); _buttonCopy = findViewById(R.id.button_copy)
_imageQR = findViewById(R.id.image_qr); _imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr); _textQR = findViewById(R.id.text_qr)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
_exportBundle = createExportBundle(); _imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
lifecycleScope.launch {
try { try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt(); val pair = withContext(Dispatchers.IO) {
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension); val bundle = createExportBundle()
_imageQR.setImageBitmap(qrCodeBitmap); 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) { } catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e); Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE; _imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE; _textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
} }
_buttonShare.onClick.subscribe { _buttonShare.onClick.subscribe {

View file

@ -34,9 +34,11 @@ class PlatformClientPool {
isDead = true; isDead = true;
onDead.emit(parentClient, this); onDead.emit(parentClient, this);
for(clientPair in _pool) { synchronized(_pool) {
for (clientPair in _pool) {
clientPair.key.disable(); clientPair.key.disable();
} }
}
}; };
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
@ -44,6 +45,7 @@ class PlatformID {
val NONE = PlatformID("Unknown", null); val NONE = PlatformID("Unknown", null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
value.ensureIsBusy();
val contextName = "PlatformID"; val contextName = "PlatformID";
return PlatformID( return PlatformID(
value.getOrThrow(config, "platform", contextName), value.getOrThrow(config, "platform", contextName),

View file

@ -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.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -33,6 +34,7 @@ open class PlatformAuthorLink {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null); val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
value.ensureIsBusy();
if(value.has("membershipUrl")) if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value); return PlatformAuthorMembershipLink.fromV8(config, value);

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
companion object { companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
value.ensureIsBusy();
val context = "AuthorMembershipLink" val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)), return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context), value.getOrThrow(config ,"name", context),

View file

@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.expectV8Variant import com.futo.platformplayer.expectV8Variant
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -46,6 +47,7 @@ class ResultCapabilities(
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
val contextName = "ResultCapabilities"; val contextName = "ResultCapabilities";
value.ensureIsBusy();
return ResultCapabilities( return ResultCapabilities(
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") }, value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); }, value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
@ -69,6 +71,7 @@ class FilterGroup(
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
value.ensureIsBusy();
return FilterGroup( return FilterGroup(
value.getString("name"), value.getString("name"),
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null) value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
@ -90,6 +93,7 @@ class FilterCapability(
companion object { companion object {
fun fromV8(obj: V8ValueObject): FilterCapability { fun fromV8(obj: V8ValueObject): FilterCapability {
obj.ensureIsBusy();
val value = obj.get("value") as V8Value; val value = obj.get("value") as V8Value;
return FilterCapability( return FilterCapability(
obj.getString("name"), obj.getString("name"),

View file

@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -31,6 +32,7 @@ class Thumbnails {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
value.ensureIsBusy();
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails")) return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray() .toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) } .map { Thumbnail.fromV8(config, it as V8ValueObject) }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IPlatformLiveEvent { interface IPlatformLiveEvent {
@ -10,6 +11,7 @@ interface IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);

View file

@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -27,6 +28,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
obj.ensureIsBusy();
val contextName = "LiveEventComment" val contextName = "LiveEventComment"
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null); val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -37,6 +38,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
obj.ensureIsBusy();
val contextName = "LiveEventDonation" val contextName = "LiveEventDonation"
return LiveEventDonation( return LiveEventDonation(
obj.getOrThrow(config, "name", contextName), obj.getOrThrow(config, "name", contextName),

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventEmojis: IPlatformLiveEvent { class LiveEventEmojis: IPlatformLiveEvent {
@ -15,9 +16,9 @@ class LiveEventEmojis: IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis" val contextName = "LiveEventEmojis"
return LiveEventEmojis( return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
obj.getOrThrow(config, "emojis", contextName));
} }
} }
} }

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent { class LiveEventRaid: IPlatformLiveEvent {
@ -10,20 +12,24 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String; val targetName: String;
val targetThumbnail: String; val targetThumbnail: String;
val targetUrl: 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.targetName = name;
this.targetUrl = url; this.targetUrl = url;
this.targetThumbnail = thumbnail; this.targetThumbnail = thumbnail;
this.isOutgoing = isOutgoing;
} }
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
obj.ensureIsBusy();
val contextName = "LiveEventRaid" val contextName = "LiveEventRaid"
return LiveEventRaid( return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName), obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName), obj.getOrThrow(config, "targetUrl", contextName),
obj.getOrThrow(config, "targetThumbnail", contextName)); obj.getOrThrow(config, "targetThumbnail", contextName),
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
} }
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventViewCount: IPlatformLiveEvent { class LiveEventViewCount: IPlatformLiveEvent {
@ -15,6 +16,7 @@ class LiveEventViewCount: IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
obj.ensureIsBusy();
val contextName = "LiveEventViewCount" val contextName = "LiveEventViewCount"
return LiveEventViewCount( return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName)); obj.getOrThrow(config, "viewCount", contextName));

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault import com.futo.platformplayer.orDefault
import com.futo.platformplayer.serializers.IRatingSerializer import com.futo.platformplayer.serializers.IRatingSerializer
@ -13,8 +14,12 @@ interface IRating {
companion object { 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 { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
obj.ensureIsBusy();
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj); RatingType.LIKES -> RatingLikes.fromV8(config, obj);

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
obj.ensureIsBusy();
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes")); return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes")); return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler")); return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
} }
} }

View file

@ -56,6 +56,7 @@ class DevJSClient : JSClient {
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): 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); val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState) if (noSaveState)
client.initialize() client.initialize()
return client return client

View file

@ -61,9 +61,13 @@ import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins 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.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Random
import kotlin.Exception import kotlin.Exception
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
@ -85,6 +89,8 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null; private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null; private var _peekChannelTypes: List<String>? = null;
private var _usedReloadData: String? = null;
protected val _script: String; protected val _script: String;
private var _initialized: Boolean = false; private var _initialized: Boolean = false;
@ -100,14 +106,14 @@ open class JSClient : IPlatformClient {
override val icon: ImageVariable; override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities(); override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = ""; private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0; val isBusy: Boolean get() = _plugin.isBusy;
val isBusyAction: String get() { val isBusyAction: String get() {
return _busyAction; return _busyAction;
} }
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings; val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>; val flags: Array<String>;
@ -200,6 +206,7 @@ open class JSClient : IPlatformClient {
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState) if (noSaveState)
client.initialize() client.initialize()
return client return client
@ -216,14 +223,31 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id]; 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() { override fun initialize() {
if (_initialized) return if (_initialized) return
Logger.i(TAG, "Plugin [${config.name}] initializing");
plugin.start(); plugin.start();
plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
descriptor.appSettings.loadDefaults(descriptor.config); descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true; _initialized = true;
@ -263,19 +287,28 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started") @JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
fun enable() { fun enable() = isBusyWith("enable") {
if(!_initialized) if(!_initialized)
initialize(); 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)})"); 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; _enabled = true;
} }
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances") @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(); ensureEnabled();
if(!capabilities.hasSaveState) if(!capabilities.hasSaveState)
return null; return@isBusyWith null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value; val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return resp; return@isBusyWith resp;
} }
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped") @JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@ -323,8 +356,10 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!; return _searchCapabilities!!;
} }
return busy {
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()")); _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return _searchCapabilities!!; return@busy _searchCapabilities!!;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex); announcePluginUnhandledException("getSearchCapabilities", ex);
@ -352,8 +387,10 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;
return busy {
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()")); _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return _searchChannelContentsCapabilities!!; return@busy _searchChannelContentsCapabilities!!;
}
} }
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform") @JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search") @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") @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your 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 { try {
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})") return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex); announcePluginUnhandledException("isChannelUrl", ex);
return false; return@isBusyWith false;
} }
} }
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@ -410,9 +447,10 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) { if (_channelCapabilities != null) {
return _channelCapabilities!!; return _channelCapabilities!!;
} }
return busy {
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()")); _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return _channelCapabilities!!; return@busy _channelCapabilities!!;
};
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex); 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") @JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
@JSDocsParameter("url", "A content url (May not be your 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 { try {
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})") return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex); announcePluginUnhandledException("isContentDetailsUrl", ex);
return false; return@isBusyWith false;
} }
} }
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @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})"); Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})"); val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@isBusyWith JSPlaybackTracker(config, tracker); return@isBusyWith JSPlaybackTracker(this, tracker);
else else
return@isBusyWith null; return@isBusyWith null;
} }
@ -604,7 +642,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
} }
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page") @JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content") @JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") { override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
@ -632,17 +669,19 @@ open class JSClient : IPlatformClient {
@JSOptional @JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean { override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
if (!capabilities.hasGetPlaylist) if (!capabilities.hasGetPlaylist)
return false; return@isBusyWith false;
try { try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})") return@isBusyWith busy {
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value; .value;
} }
}
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex); announcePluginUnhandledException("isPlaylistUrl", ex);
return false; return@isBusyWith false;
} }
} }
@JSOptional @JSOptional
@ -744,19 +783,29 @@ open class JSClient : IPlatformClient {
return urls; return urls;
} }
fun <T> busy(handle: ()->T): T {
private fun <T> isBusyWith(actionName: String, handle: ()->T): T { return _plugin.busy {
try { return@busy handle();
synchronized(_busyLock) {
_busyCounter++;
} }
}
fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
return _plugin.busy {
return@busy runBlocking {
return@runBlocking handle();
}
}
}
fun <T> isBusyWith(actionName: String, handle: ()->T): T {
//val busyId = kotlin.random.Random.nextInt(9999);
return busy {
try {
_busyAction = actionName; _busyAction = actionName;
return handle(); return@busy handle();
} }
finally { finally {
_busyAction = ""; _busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
} }
} }
} }

View file

@ -4,6 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
@ -169,12 +170,17 @@ class SourcePluginConfig(
} }
fun validate(text: String): Boolean { fun validate(text: String): Boolean {
if(scriptPublicKey.isNullOrEmpty()) try {
if (scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present"); throw IllegalStateException("No public key present");
if(scriptSignature.isNullOrEmpty()) if (scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present"); 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 { fun isUrlAllowed(url: String): Boolean {
@ -205,6 +211,8 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl; obj.sourceUrl = sourceUrl;
return obj; return obj;
} }
private val TAG = "SourcePluginConfig"
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable

View file

@ -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 { override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth); val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })

View file

@ -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.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -13,6 +14,7 @@ interface IJSContent: IPlatformContent {
companion object { companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
obj.ensureIsBusy();
val config = plugin.config; val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem"); val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null); val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);

View file

@ -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.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent { interface IJSContentDetails: IPlatformContent {
companion object { companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
obj.ensureIsBusy();
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails"); val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj); ContentType.MEDIA -> JSVideoDetails(plugin, obj);

View file

@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
override fun nextPage() { override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") {
super.nextPage(); super.nextPage();
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }

View file

@ -29,7 +29,9 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager; this.pager = pager;
this.config = config; this.config = config;
plugin.busy {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults(); getResults();
} }
@ -44,11 +46,14 @@ abstract class JSPager<T> : IPager<T> {
override fun nextPage() { override fun nextPage() {
warnIfMainThread("JSPager.nextPage"); warnIfMainThread("JSPager.nextPage");
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { val pluginV8 = plugin.getUnderlyingPlugin();
pluginV8.busy {
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>()); pager.invoke("nextPage", arrayOf<Any>());
}; };
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true; _resultChanged = true;
}
/* /*
try { try {
} }
@ -70,15 +75,18 @@ abstract class JSPager<T> : IPager<T> {
return previousResults; return previousResults;
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
return plugin.getUnderlyingPlugin().busy {
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager"); val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed) if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed"); throw IllegalStateException("Runtime closed");
val newResults = items.toArray() val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) } .map { convertResult(it as V8ValueObject) }
.toList(); .toList();
_lastResults = newResults; _lastResults = newResults;
_resultChanged = false; _resultChanged = false;
return newResults; return@busy newResults;
}
} }
abstract fun convertResult(obj: V8ValueObject): T; abstract fun convertResult(obj: V8ValueObject): T;

View file

@ -2,44 +2,59 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker 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.IV8PluginConfig
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
class JSPlaybackTracker: IPlaybackTracker { class JSPlaybackTracker: IPlaybackTracker {
private val _config: IV8PluginConfig; private lateinit var _client: JSClient;
private val _obj: V8ValueObject; private lateinit var _config: IV8PluginConfig;
private lateinit var _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false; private var _hasCalledInit: Boolean = false;
private val _hasInit: Boolean; private var _hasInit: Boolean = false;
private var _lastRequest: Long = Long.MIN_VALUE; private var _lastRequest: Long = Long.MIN_VALUE;
private val _hasOnConcluded: Boolean; private var _hasOnConcluded: Boolean = false;
override var nextRequest: Int = 1000 override var nextRequest: Int = 1000
private set; private set;
constructor(config: IV8PluginConfig, obj: V8ValueObject) { constructor(client: JSClient, obj: V8ValueObject) {
warnIfMainThread("JSPlaybackTracker.constructor"); warnIfMainThread("JSPlaybackTracker.constructor");
if(!obj.has("onProgress"))
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker"); client.busy {
if(!obj.has("nextRequest")) if (!obj.has("onProgress"))
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker"); throw ScriptImplementationException(
client.config,
"Missing onProgress on PlaybackTracker"
);
if (!obj.has("nextRequest"))
throw ScriptImplementationException(
client.config,
"Missing nextRequest on PlaybackTracker"
);
_hasOnConcluded = obj.has("onConcluded"); _hasOnConcluded = obj.has("onConcluded");
this._config = config; this._client = client;
this._config = client.config;
this._obj = obj; this._obj = obj;
this._hasInit = obj.has("onInit"); this._hasInit = obj.has("onInit");
} }
}
override fun onInit(seconds: Double) { override fun onInit(seconds: Double) {
warnIfMainThread("JSPlaybackTracker.onInit"); warnIfMainThread("JSPlaybackTracker.onInit");
synchronized(_obj) { synchronized(_obj) {
if(_hasCalledInit) if(_hasCalledInit)
return; return;
_client.busy {
if (_hasInit) { if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})"); Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("onInit", seconds); _obj.invokeVoid("onInit", seconds);
@ -48,6 +63,7 @@ class JSPlaybackTracker: IPlaybackTracker {
_hasCalledInit = true; _hasCalledInit = true;
} }
} }
}
override fun onProgress(seconds: Double, isPlaying: Boolean) { override fun onProgress(seconds: Double, isPlaying: Boolean) {
warnIfMainThread("JSPlaybackTracker.onProgress"); warnIfMainThread("JSPlaybackTracker.onProgress");
@ -55,6 +71,7 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit) if(!_hasCalledInit && _hasInit)
onInit(seconds); onInit(seconds);
else { else {
_client.busy {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
@ -62,15 +79,18 @@ class JSPlaybackTracker: IPlaybackTracker {
} }
} }
} }
}
override fun onConcluded() { override fun onConcluded() {
warnIfMainThread("JSPlaybackTracker.onConcluded"); warnIfMainThread("JSPlaybackTracker.onConcluded");
if(_hasOnConcluded) { if(_hasOnConcluded) {
synchronized(_obj) { synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded"); Logger.i("JSPlaybackTracker", "onConcluded");
_client.busy {
_obj.invokeVoid("onConcluded", -1); _obj.invokeVoid("onConcluded", -1);
} }
} }
} }
}
override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest); override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest);

View file

@ -46,6 +46,8 @@ class JSRequestExecutor {
if (_executor.isClosed) if (_executor.isClosed)
throw IllegalStateException("Executor object is closed"); throw IllegalStateException("Executor object is closed");
return _plugin.getUnderlyingPlugin().busy {
val result = if(_plugin is DevJSClient) val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( V8Plugin.catchScriptErrors<Any>(
@ -67,7 +69,7 @@ class JSRequestExecutor {
try { try {
if(result is V8ValueString) { if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value); val base64Result = Base64.getDecoder().decode(result.value);
return base64Result; return@busy base64Result;
} }
if(result is V8ValueTypedArray) { if(result is V8ValueTypedArray) {
val buffer = result.buffer; val buffer = result.buffer;
@ -75,7 +77,7 @@ class JSRequestExecutor {
val bytesResult = ByteArray(result.byteLength); val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength); byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close(); buffer.close();
return bytesResult; return@busy bytesResult;
} }
if(result is V8ValueObject && result.has("type")) { if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier"); val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
@ -94,12 +96,13 @@ class JSRequestExecutor {
result.close(); result.close();
} }
} }
}
open fun cleanup() { open fun cleanup() {
if (!hasCleanup || _executor.isClosed) if (!hasCleanup || _executor.isClosed)
return; return;
_plugin.busy {
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( V8Plugin.catchScriptErrors<Any>(
@ -118,6 +121,7 @@ class JSRequestExecutor {
_executor.invokeVoid("cleanup", null); _executor.invokeVoid("cleanup", null);
}; };
} }
}
protected fun finalize() { protected fun finalize() {
cleanup(); cleanup();

View file

@ -16,7 +16,7 @@ class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient; private val _plugin: JSClient;
private val _config: IV8PluginConfig; private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject; private var _modifier: V8ValueObject;
override var allowByteSkip: Boolean; override var allowByteSkip: Boolean = false;
constructor(plugin: JSClient, modifier: V8ValueObject) { constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin; this._plugin = plugin;
@ -24,24 +24,29 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config; this._config = plugin.config;
val config = plugin.config; val config = plugin.config;
plugin.busy {
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true; allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
if(!modifier.has("modifyRequest")) if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null); throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
} }
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest { override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
if (_modifier.isClosed) { if (_modifier.isClosed) {
return Request(url, headers); return Request(url, headers);
} }
return _plugin.busy {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers); _modifier.invoke("modifyRequest", url, headers);
} as V8ValueObject; } as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers); val req = JSRequest(_plugin, result, url, headers);
result.close(); result.close();
return req; return@busy req;
}
} }

View file

@ -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.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -35,8 +36,11 @@ class JSSubtitleSource : ISubtitleSource {
override fun getSubtitles(): String { override fun getSubtitles(): String {
if(!hasFetch) if(!hasFetch)
throw IllegalStateException("This subtitle doesn't support getSubtitles.."); throw IllegalStateException("This subtitle doesn't support getSubtitles..");
return _obj.getSourcePlugin()?.busy {
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>()); val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
return v8String.value; return@busy v8String.value;
} ?: "";
} }
override suspend fun getSubtitlesURI(): Uri? { override suspend fun getSubtitlesURI(): Uri? {

View file

@ -27,6 +27,7 @@ import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails { class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _plugin: JSClient;
private val _hasGetComments: Boolean; private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean; private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean; private val _hasGetPlaybackTracker: Boolean;
@ -48,6 +49,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails"; val contextName = "VideoDetails";
_plugin = plugin;
val config = plugin.config; val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName); description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
@ -82,14 +84,16 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS(); return getPlaybackTrackerJS();
} }
private fun getPlaybackTrackerJS(): IPlaybackTracker? { private fun getPlaybackTrackerJS(): IPlaybackTracker? {
return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { return _plugin.busy {
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>()) val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>())
?: return@catchScriptErrors null; ?: return@catchScriptErrors null;
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker); return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
else else
return@catchScriptErrors null; return@catchScriptErrors null;
}; }
}
} }
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
@ -106,8 +110,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null; return null;
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return _plugin.busy {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return@busy JSContentPager(_pluginConfig, client, contentPager);
}
} }
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
@ -123,10 +129,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
} }
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? { private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
return _plugin.busy {
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>()); val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return null; return@busy null;
return JSCommentPager(_pluginConfig, client, commentPager); return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
} }
} }

View file

@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources 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.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.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource 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.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow 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.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource { class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String; override val container : String;
@ -50,6 +57,44 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = 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<V8ValueString>("generate");
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_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? { override fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@ -62,15 +107,20 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
} }
if(result != null){ if(result != null){
plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
@ -79,6 +129,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
} }
} }
}
return result; return result;
} }
} }

View file

@ -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.V8Value
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.V8Deferred
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource 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.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource 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.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
interface IJSDashManifestRawSource { interface IJSDashManifestRawSource {
val hasGenerate: Boolean; val hasGenerate: Boolean;
var manifest: String?; var manifest: String?;
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
fun generate(): String?; fun generate(): String?;
} }
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource { open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
@ -32,7 +40,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
override val duration: Long; override val duration: Long;
override val priority: Boolean; override val priority: Boolean;
var url: String?; val url: String?;
override var manifest: String?; override var manifest: String?;
override val hasGenerate: Boolean; override val hasGenerate: Boolean;
@ -57,6 +65,45 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = 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<V8ValueString>("generate");
}
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_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? { override open fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@ -67,16 +114,21 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
if(_plugin is DevJSClient) { if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
}); });
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
}); });
if(result != null){ if(result != null){
_plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
@ -85,6 +137,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
} }
} }
}
return result; return result;
} }
} }
@ -110,6 +163,32 @@ class JSDashManifestMergingRawSource(
override val priority: Boolean override val priority: Boolean
get() = video.priority; get() = video.priority;
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
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(
"</AdaptationSet>",
"</AdaptationSet>\n" + audioAdaptationSet.value
)
} else
result = videoDash;
return@merge result;
};
}
override fun generate(): String? { override fun generate(): String? {
val videoDash = video.generate(); val videoDash = video.generate();
val audioDash = audio.generate(); val audioDash = audio.generate();

View file

@ -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.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
@ -38,7 +39,13 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
companion object { companion object {
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? {
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj); obj?.ensureIsBusy();
return obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }
};
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource {
obj.ensureIsBusy();
return JSHLSManifestAudioSource(plugin, obj)
};
} }
} }

View file

@ -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.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
@ -53,36 +54,39 @@ abstract class JSSource {
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor"); hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
} }
fun getRequestModifier(): IRequestModifier? { fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
if(_requestModifier != null) if(_requestModifier != null)
return AdhocRequestModifier { url, headers -> return@isBusyWith AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers); return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
}; };
if (!hasRequestModifier || _obj.isClosed) if (!hasRequestModifier || _obj.isClosed)
return null; return@isBusyWith null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invoke("getRequestModifier", arrayOf<Any>()); _obj.invoke("getRequestModifier", arrayOf<Any>());
}; };
if (result !is V8ValueObject) 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) if (!hasRequestExecutor || _obj.isClosed)
return null; return@isBusyWith null;
Logger.v("JSSource", "Request executor for [${type}] requesting");
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>()); _obj.invoke("getRequestExecutor", arrayOf<Any>());
}; };
if (result !is V8ValueObject) Logger.v("JSSource", "Request executor for [${type}] received");
return null;
return JSRequestExecutor(_plugin, result) if (result !is V8ValueObject)
return@isBusyWith null;
return@isBusyWith JSRequestExecutor(_plugin, result)
} }
fun getUnderlyingPlugin(): JSClient? { fun getUnderlyingPlugin(): JSClient? {
@ -105,8 +109,12 @@ abstract class JSSource {
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource" const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource" 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? { fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
obj.ensureIsBusy()
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); 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 fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj); fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource{
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj); obj.ensureIsBusy();
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj); 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 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? { fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
obj.ensureIsBusy();
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);

View file

@ -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.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor { class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
@ -31,6 +32,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor { fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
obj.ensureIsBusy();
val type = obj.getString("plugin_type") val type = obj.getString("plugin_type")
return when(type) { return when(type) {
TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj); TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);

View file

@ -35,7 +35,7 @@ class ChromecastCastingDevice : CastingDevice {
override var usedRemoteAddress: InetAddress? = null; override var usedRemoteAddress: InetAddress? = null;
override var localAddress: InetAddress? = null; override var localAddress: InetAddress? = null;
override val canSetVolume: Boolean get() = true; override val canSetVolume: Boolean get() = true;
override val canSetSpeed: Boolean get() = false; //TODO: Implement override val canSetSpeed: Boolean get() = true;
var addresses: Array<InetAddress>? = null; var addresses: Array<InetAddress>? = null;
var port: Int = 0; var port: Int = 0;
@ -144,6 +144,23 @@ class ChromecastCastingDevice : CastingDevice {
sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json); 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) { override fun changeVolume(volume: Double) {
if (invokeInIOScopeIfRequired({ changeVolume(volume) })) { if (invokeInIOScopeIfRequired({ changeVolume(volume) })) {
return; return;

View file

@ -166,10 +166,11 @@ class StateCasting {
Logger.i(TAG, "CastingService started."); Logger.i(TAG, "CastingService started.");
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager _nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
startDiscovering()
} }
@Synchronized @Synchronized
fun startDiscovering() { private fun startDiscovering() {
_nsdManager?.apply { _nsdManager?.apply {
_discoveryListeners.forEach { _discoveryListeners.forEach {
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value) discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
@ -178,7 +179,7 @@ class StateCasting {
} }
@Synchronized @Synchronized
fun stopDiscovering() { private fun stopDiscovering() {
_nsdManager?.apply { _nsdManager?.apply {
_discoveryListeners.forEach { _discoveryListeners.forEach {
try { try {
@ -1220,10 +1221,17 @@ class StateCasting {
private fun getLocalUrl(ad: CastingDevice): String { private fun getLocalUrl(ad: CastingDevice): String {
var address = ad.localAddress!! var address = ad.localAddress!!
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) { if (address.isLinkLocalAddress) {
address = findPreferredAddress() ?: address address = findPreferredAddress() ?: address
Logger.i(TAG, "Selected casting address: $address") Logger.i(TAG, "Selected casting address: $address")
} }
}
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}"; return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
} }

View file

@ -7,9 +7,7 @@ import android.app.PendingIntent.getBroadcast
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -157,9 +155,6 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller; val packageInstaller: PackageInstaller = context.packageManager.packageInstaller;
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL); 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); val sessionId = packageInstaller.createSession(params);
session = packageInstaller.openSession(sessionId) session = packageInstaller.openSession(sessionId)

View file

@ -103,7 +103,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
super.show(); super.show();
Logger.i(TAG, "Dialog shown."); Logger.i(TAG, "Dialog shown.");
StateCasting.instance.startDiscovering()
(_imageLoader.drawable as Animatable?)?.start(); (_imageLoader.drawable as Animatable?)?.start();
synchronized(StateCasting.instance.devices) { synchronized(StateCasting.instance.devices) {
@ -148,7 +147,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
override fun dismiss() { override fun dismiss() {
super.dismiss() super.dismiss()
(_imageLoader.drawable as Animatable?)?.stop() (_imageLoader.drawable as Animatable?)?.stop()
StateCasting.instance.stopDiscovering()
StateCasting.instance.onDeviceAdded.remove(this) StateCasting.instance.onDeviceAdded.remove(this)
StateCasting.instance.onDeviceChanged.remove(this) StateCasting.instance.onDeviceChanged.remove(this)
StateCasting.instance.onDeviceRemoved.remove(this) StateCasting.instance.onDeviceRemoved.remove(this)

View file

@ -6,12 +6,16 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Button import android.widget.Button
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.readBytes import com.futo.platformplayer.readBytes
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImportOptionsDialog: AlertDialog { class ImportOptionsDialog: AlertDialog {
private val _context: MainActivity; private val _context: MainActivity;
@ -41,8 +45,17 @@ class ImportOptionsDialog: AlertDialog {
_button_import_zip.onClick.subscribe { _button_import_zip.onClick.subscribe {
dismiss(); dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") { StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val zipBytes = it?.readBytes(context) ?: return@launch;
withContext(Dispatchers.Main) {
try {
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes); StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
}
catch(ex: Throwable) {
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
}
}
}
}; };
} }
_button_import_ezip.setOnClickListener { _button_import_ezip.setOnClickListener {
@ -51,17 +64,35 @@ class ImportOptionsDialog: AlertDialog {
_button_import_txt.onClick.subscribe { _button_import_txt.onClick.subscribe {
dismiss(); dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") { StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val txtBytes = it?.readBytes(context) ?: return@launch;
val txt = String(txtBytes); val txt = String(txtBytes);
withContext(Dispatchers.Main) {
try {
StateBackup.importTxt(_context, txt); StateBackup.importTxt(_context, txt);
}
catch(ex: Throwable) {
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
}
}
}
}; };
} }
_button_import_newpipe_subs.onClick.subscribe { _button_import_newpipe_subs.onClick.subscribe {
dismiss(); dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/json") { StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val jsonBytes = it?.readBytes(context) ?: return@launch;
val json = String(jsonBytes); val json = String(jsonBytes);
withContext(Dispatchers.Main) {
try {
StateBackup.importNewPipeSubs(_context, json); StateBackup.importNewPipeSubs(_context, json);
}
catch(ex: Throwable) {
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
}
}
}
}; };
}; };
_button_import_platform.onClick.subscribe { _button_import_platform.onClick.subscribe {

View file

@ -724,7 +724,7 @@ class VideoDownload {
val t = cue.groupValues[1]; val t = cue.groupValues[1];
val d = cue.groupValues[2]; val d = cue.groupValues[2];
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString()); val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
val data = if(executor != null) val data = if(executor != null)
executor.executeRequest("GET", url, null, mapOf()); executor.executeRequest("GET", url, null, mapOf());

View file

@ -6,13 +6,13 @@ import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
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.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueObject 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.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@ -26,6 +26,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException 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.ScriptTimeoutException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.engine.internal.V8Converter 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.getOrThrow
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets 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 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.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class V8Plugin { class V8Plugin {
val config: IV8PluginConfig; val config: IV8PluginConfig;
@ -47,10 +58,14 @@ class V8Plugin {
private val _clientAuth: ManagedHttpClient; private val _clientAuth: ManagedHttpClient;
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap(); private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
private val _promises = ConcurrentHashMap<V8ValuePromise, ((V8ValuePromise)->Unit)?>();
val httpClient: ManagedHttpClient get() = _client; val httpClient: ManagedHttpClient get() = _client;
val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientAuth: ManagedHttpClient get() = _clientAuth;
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers; val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
var runtimeId: Int = 0;
fun registerHttpClient(client: JSHttpClient) { fun registerHttpClient(client: JSHttpClient) {
synchronized(_clientOthers) { synchronized(_clientOthers) {
_clientOthers.put(client.clientId, client); _clientOthers.put(client.clientId, client);
@ -67,10 +82,8 @@ class V8Plugin {
var isStopped = true; var isStopped = true;
val onStopped = Event1<V8Plugin>(); val onStopped = Event1<V8Plugin>();
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial private val _busyLock = ReentrantLock()
private val _busyCounterLock = Object(); val isBusy get() = _busyLock.isLocked;
private var _busyCounter = 0;
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
var allowDevSubmit: Boolean = false var allowDevSubmit: Boolean = false
private set(value) { private set(value) {
@ -140,6 +153,7 @@ class V8Plugin {
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if (_runtime != null) if (_runtime != null)
return; return;
runtimeId = runtimeId + 1;
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true); //V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
val host = V8Host.getV8Instance(); val host = V8Host.getV8Instance();
val options = host.jsRuntimeType.getRuntimeOptions(); val options = host.jsRuntimeType.getRuntimeOptions();
@ -148,6 +162,8 @@ class V8Plugin {
if (!host.isIsolateCreated) if (!host.isIsolateCreated)
throw IllegalStateException("Isolate not created"); throw IllegalStateException("Isolate not created");
_runtimeMap.put(_runtime!!, this);
//Setup bridge //Setup bridge
_runtime?.let { _runtime?.let {
it.converter = V8Converter(); it.converter = V8Converter();
@ -184,10 +200,13 @@ class V8Plugin {
} }
fun stop(){ fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]"); Logger.i(TAG, "Stopping plugin [${config.name}]");
isStopped = true; busy {
whenNotBusy { Logger.i(TAG, "Plugin stopping");
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if(isStopped)
return@busy;
isStopped = true; isStopped = true;
runtimeId = runtimeId + 1;
//Cleanup http //Cleanup http
for(pack in _depsPackages) { for(pack in _depsPackages) {
@ -197,6 +216,7 @@ class V8Plugin {
} }
_runtime?.let { _runtime?.let {
_runtimeMap.remove(it);
_runtime = null; _runtime = null;
if(!it.isClosed && !it.isDead) { if(!it.isClosed && !it.isDead) {
try { try {
@ -211,62 +231,147 @@ class V8Plugin {
Logger.i(TAG, "Stopped plugin [${config.name}]"); Logger.i(TAG, "Stopped plugin [${config.name}]");
}; };
} }
Logger.i(TAG, "Plugin stopped");
onStopped.emit(this); onStopped.emit(this);
} }
cancelAllPromises();
} }
fun isThreadAlreadyBusy(): Boolean {
return _busyLock.isHeldByCurrentThread;
}
fun <T> 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 <T> 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 { fun execute(js: String) : V8Value {
return executeTyped<V8Value>(js); return executeTyped<V8Value>(js);
} }
suspend fun <T : V8Value> executeTypedAsync(js: String) : Deferred<T> {
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<V8Value>("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute()
};
if (result is V8ValuePromise) {
return@busy result.toV8ValueAsync<T>(this@V8Plugin);
} else
return@busy CompletableDeferred(result as T);
}
catch(ex: Throwable) {
val def = CompletableDeferred<T>();
def.completeExceptionally(ex);
return@busy def;
}
}
}
}
fun <T : V8Value> executeTyped(js: String) : T { fun <T : V8Value> executeTyped(js: String) : T {
warnIfMainThread("V8Plugin.executeTyped"); warnIfMainThread("V8Plugin.executeTyped");
if(isStopped) if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js); throw PluginEngineStoppedException(config, "Instance is stopped", js);
synchronized(_busyCounterLock) { val result = busy {
_busyCounter++;
}
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
try { return@busy catchScriptErrors<V8Value>("Plugin[${config.name}]", js) {
return catchScriptErrors("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute() runtime.getExecutor(js).execute()
}; };
};
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this@V8Plugin);
} }
finally { return result as T;
synchronized(_busyCounterLock) {
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
try {
afterBusy.emit(_busyCounter - 1);
} }
catch(ex: Throwable) { fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value } }
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex); fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value } }
} fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value } }
_busyCounter--;
}
}
}
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
fun whenNotBusy(handler: (V8Plugin)->Unit) {
synchronized(_busyCounterLock) { fun <T: V8Value> handlePromise(result: V8ValuePromise): CompletableDeferred<T> {
if(_busyCounter == 0) val def = CompletableDeferred<T>();
handler(this); result.register(object: IV8ValuePromise.IListener {
else { override fun onFulfilled(p0: V8Value?) {
val tag = Object(); resolvePromise(result);
afterBusy.subscribe(tag) { def.complete(p0 as T);
if(it == 0) { }
Logger.w(TAG, "V8Plugin afterBusy handled"); override fun onRejected(p0: V8Value?) {
afterBusy.remove(tag); resolvePromise(result);
handler(this); 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? { private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
//TODO: Auto get all package types? //TODO: Auto get all package types?
return when(packageName) { return when(packageName) {
@ -292,8 +397,14 @@ class V8Plugin {
private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*"); private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*");
private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*"); private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*");
private val _runtimeMap = ConcurrentHashMap<V8Runtime, V8Plugin>();
val TAG = "V8Plugin"; val TAG = "V8Plugin";
fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? {
return _runtimeMap.getOrDefault(runtime, null);
}
fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T { fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
var codeStripped = code; var codeStripped = code;
if(codeStripped != null) { //TODO: Improve code stripped 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); 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) { catch(executeEx: JavetExecutionException) {
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { val obj = executeEx.scriptingError?.context
val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); if(obj != null && obj.containsKey("plugin_type") == true) {
val pluginType = obj["plugin_type"].toString();
//Captcha //Captcha
if (pluginType == "CaptchaRequiredException") { if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config, throw ScriptCaptchaRequiredException(config,
executeEx.scriptingError.context["url"]?.toString(), obj["url"]?.toString(),
executeEx.scriptingError.context["body"]?.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); executeEx, executeEx.scriptingError?.stack, codeStripped);
} }
@ -348,6 +468,41 @@ class V8Plugin {
codeStripped 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); throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
} }
catch(ex: Exception) { catch(ex: Exception) {
@ -398,9 +553,4 @@ class V8Plugin {
return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found"); return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found");
} }
} }
/**
* Methods available for scripts (bridge object)
*/
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { 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 { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException {
obj.ensureIsBusy();
return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException")); return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { 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 { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException")); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -9,6 +10,7 @@ class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?,
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
val contextName = "ScriptCaptchaRequiredException"; val contextName = "ScriptCaptchaRequiredException";
return ScriptCaptchaRequiredException(config, return ScriptCaptchaRequiredException(config,
obj.getOrDefault<String>(config, "url", contextName, null), obj.getOrDefault<String>(config, "url", contextName, null),

View file

@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) { class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException {
obj.ensureIsBusy();
return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException")); return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { 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 { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException")); return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { 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 { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException")); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException"));
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { 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 { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException {
obj.ensureIsBusy();
return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException")); return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException"));
} }
} }

View file

@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException {
obj.ensureIsBusy();
return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException")); return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException"));
} }
} }

View file

@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException")); return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException"));
} }
} }

View file

@ -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<String>(config, "reloadData", contextName, null));
}
}
}

View file

@ -2,11 +2,13 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) { class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException {
obj.ensureIsBusy();
return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException")); return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException"));
} }
} }

View file

@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow 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) { class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException")); return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException"));
} }
} }

View file

@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable {
override fun toV8(runtime: V8Runtime): V8Value? { override fun toV8(runtime: V8Runtime): V8Value? {
synchronized(this) { synchronized(this) {
if(_runtimeObj != null) //if(_runtimeObj != null)
return _runtimeObj; // return _runtimeObj;
val v8Obj = runtime.createV8ValueObject(); val v8Obj = runtime.createV8ValueObject();
v8Obj.bind(this); v8Obj.bind(this);

View file

@ -4,6 +4,7 @@ import android.media.MediaCodec
import android.media.MediaCodecList import android.media.MediaCodecList
import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.interop.callback.JavetCallbackContext
import com.caoccao.javet.utils.JavetResourceUtils import com.caoccao.javet.utils.JavetResourceUtils
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueFunction import com.caoccao.javet.values.reference.V8ValueFunction
@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.concurrent.ConcurrentHashMap
class PackageBridge : V8Package { class PackageBridge : V8Package {
@Transient @Transient
@ -78,6 +80,15 @@ class PackageBridge : V8Package {
return "android"; return "android";
} }
@V8Property
fun supportedFeatures(): Array<String> {
return arrayOf(
"ReloadRequiredException",
"HttpBatchClient",
"Async"
);
}
@V8Property @V8Property
fun supportedContent(): Array<Int> { fun supportedContent(): Array<Int> {
return arrayOf( return arrayOf(
@ -101,45 +112,54 @@ class PackageBridge : V8Package {
} }
var timeoutCounter = 0; var timeoutCounter = 0;
var timeoutMap = HashSet<Int>(); var timeoutMap = ConcurrentHashMap<Int, Any?>();
@V8Function @V8Function
fun setTimeout(func: V8ValueFunction, timeout: Long): Int { fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
val id = timeoutCounter++; val id = timeoutCounter++;
val funcClone = func.toClone<V8ValueFunction>() val funcClone = func.toClone<V8ValueFunction>()
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
delay(timeout); delay(timeout);
synchronized(timeoutMap) { if (_plugin.isStopped)
if(!timeoutMap.contains(id)) { return@launch;
if (!timeoutMap.containsKey(id)) {
_plugin.busy {
if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone); JavetResourceUtils.safeClose(funcClone);
}
return@launch; return@launch;
} }
timeoutMap.remove(id); timeoutMap.remove(id);
}
try { try {
_plugin.whenNotBusy { Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}");
_plugin.busy {
Logger.w(TAG, "setTimeout in busy");
if (!_plugin.isStopped)
funcClone.callVoid(null, arrayOf<Any>()); funcClone.callVoid(null, arrayOf<Any>());
Logger.w(TAG, "setTimeout after");
} }
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, "Failed timeout callback", ex); Logger.e(TAG, "Failed timeout callback", ex);
} } finally {
finally { _plugin.busy {
if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone); JavetResourceUtils.safeClose(funcClone);
} }
}; //_plugin.whenNotBusy {
synchronized(timeoutMap) { //}
timeoutMap.add(id);
} }
};
timeoutMap.put(id, true);
return id; return id;
} }
@V8Function @V8Function
fun clearTimeout(id: Int) { fun clearTimeout(id: Int) {
synchronized(timeoutMap) { if (timeoutMap.containsKey(id))
if(timeoutMap.contains(id))
timeoutMap.remove(id); timeoutMap.remove(id);
} }
@V8Function
fun sleep(length: Int) {
Thread.sleep(length.toLong());
} }
@V8Function @V8Function
@ -147,7 +167,7 @@ class PackageBridge : V8Package {
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}"); Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
UIDialogs.toast(str); UIDialogs.appToast(str);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }

View file

@ -44,6 +44,17 @@ class PackageHttp: V8Package {
private val aliveSockets = mutableListOf<SocketResult>(); private val aliveSockets = mutableListOf<SocketResult>();
private var _cleanedUp = false; private var _cleanedUp = false;
private val _clients = mutableMapOf<String, PackageHttpClient>()
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) { constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
_config = config; _config = config;
@ -112,6 +123,8 @@ class PackageHttp: V8Package {
_plugin.registerHttpClient(httpClient); _plugin.registerHttpClient(httpClient);
val client = PackageHttpClient(this, httpClient); val client = PackageHttpClient(this, httpClient);
_clients.put(client.clientId() ?: "", client);
return client; return client;
} }
@V8Function @V8Function
@ -246,18 +259,18 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequest(_package.getDefaultClient(useAuth), method, url, headers); return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers);
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = 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 @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientGET(_package.getDefaultClient(useAuth), url, headers); = clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers);
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers); = clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers);
@V8Function @V8Function
fun DUMMY(): BatchBuilder { fun DUMMY(): BatchBuilder {
@ -268,21 +281,21 @@ class PackageHttp: V8Package {
//Client-specific //Client-specific
@V8Function @V8Function
fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers))); _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers, body))); _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientGET(clientId: String?, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequest(client, "GET", url, headers); = clientRequest(clientId, "GET", url, headers);
@V8Function @V8Function
fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequestWithBody(client, "POST", url, body, headers); = clientRequestWithBody(clientId, "POST", url, body, headers);
//Finalizer //Finalizer
@ -321,6 +334,7 @@ class PackageHttp: V8Package {
@Transient @Transient
private val _clientId: String?; private val _clientId: String?;
@V8Property @V8Property
fun clientId(): String? { fun clientId(): String? {
return _clientId; return _clientId;
@ -333,6 +347,17 @@ class PackageHttp: V8Package {
_clientId = if(_client is JSHttpClient) _client.clientId else null; _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 @V8Function
fun setDefaultHeaders(defaultHeaders: Map<String, String>) { fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
for(pair in defaultHeaders) for(pair in defaultHeaders)
@ -429,8 +454,23 @@ class PackageHttp: V8Package {
}; };
} }
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse {
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) 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<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
@ -452,9 +492,6 @@ class PackageHttp: V8Package {
} }
}; };
} }
@V8Function
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
@ -630,8 +667,10 @@ class PackageHttp: V8Package {
_isOpen = true; _isOpen = true;
if(hasOpen && _listeners?.isClosed != true) { if(hasOpen && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy {
_listeners?.invokeVoid("open", arrayOf<Any>()); _listeners?.invokeVoid("open", arrayOf<Any>());
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
} }
@ -640,8 +679,10 @@ class PackageHttp: V8Package {
override fun message(msg: String) { override fun message(msg: String) {
if(hasMessage && _listeners?.isClosed != true) { if(hasMessage && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy {
_listeners?.invokeVoid("message", msg); _listeners?.invokeVoid("message", msg);
} }
}
catch(ex: Throwable) {} catch(ex: Throwable) {}
} }
} }
@ -649,8 +690,10 @@ class PackageHttp: V8Package {
if(hasClosing && _listeners?.isClosed != true) if(hasClosing && _listeners?.isClosed != true)
{ {
try { try {
_package._plugin.busy {
_listeners?.invokeVoid("closing", code, reason); _listeners?.invokeVoid("closing", code, reason);
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
} }
@ -660,8 +703,10 @@ class PackageHttp: V8Package {
_isOpen = false; _isOpen = false;
if(hasClosed && _listeners?.isClosed != true) { if(hasClosed && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy {
_listeners?.invokeVoid("closed", code, reason); _listeners?.invokeVoid("closed", code, reason);
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
} }
@ -676,8 +721,10 @@ class PackageHttp: V8Package {
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure && _listeners?.isClosed != true) { if(hasFailure && _listeners?.isClosed != true) {
try { try {
_package._plugin.busy {
_listeners?.invokeVoid("failure", exception.message); _listeners?.invokeVoid("failure", exception.message);
} }
}
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
} }

View file

@ -23,7 +23,7 @@ open class MainActivityFragment : Fragment() {
fun navigate(frag: MainFragment, parameter: Any? = null, withHistory: Boolean = true) { fun navigate(frag: MainFragment, parameter: Any? = null, withHistory: Boolean = true) {
val a = activity val a = activity
if (a is MainActivity) if (a is MainActivity)
(activity as MainActivity).navigate(frag, parameter, withHistory) (activity as MainActivity).navigate(frag, parameter, withHistory, false)
else else
Log.e(TAG, "Failed to navigate due to activity not being a main activity.") Log.e(TAG, "Failed to navigate due to activity not being a main activity.")
} }

View file

@ -331,7 +331,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
} }
if (!StatePayment.instance.hasPaid) { 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<BuyFragment>(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<BuyFragment>(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 //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

View file

@ -778,6 +778,8 @@ class ArticleDetailFragment : MainFragment {
view.onAddToWatchLaterClicked.subscribe { a -> view.onAddToWatchLaterClicked.subscribe { a ->
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]") 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) { else if(content is IPlatformPost) {

View file

@ -1,6 +1,8 @@
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -66,8 +68,7 @@ class BuyFragment : MainFragment() {
_paymentManager = PaymentManager(StatePayment.instance, fragment, _overlayPaying) { success, _, exception -> _paymentManager = PaymentManager(StatePayment.instance, fragment, _overlayPaying) { success, _, exception ->
if(success) { 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.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.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY));
_fragment.close(true); _fragment.close(true);
} }
else { else {
@ -115,11 +116,14 @@ class BuyFragment : MainFragment() {
val licenseInput = SlideUpMenuTextInput(context, context.getString(R.string.license)); val licenseInput = SlideUpMenuTextInput(context, context.getString(R.string.license));
val productLicenseDialog = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_paid), context.getString(R.string.enter_license_key), context.getString(R.string.ok), true, licenseInput); val productLicenseDialog = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_paid), context.getString(R.string.enter_license_key), context.getString(R.string.ok), true, licenseInput);
productLicenseDialog.onOK.subscribe { productLicenseDialog.onOK.subscribe {
licenseInput.deactivate();
val licenseText = licenseInput.text; val licenseText = licenseInput.text;
if (licenseText.isNullOrEmpty()) { if (licenseText.isNullOrEmpty()) {
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key)); UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key));
return@subscribe; return@subscribe;
} }
licenseInput.clear();
productLicenseDialog.hide(true);
_fragment.lifecycleScope.launch(Dispatchers.IO) { _fragment.lifecycleScope.launch(Dispatchers.IO) {
@ -127,18 +131,19 @@ class BuyFragment : MainFragment() {
val activationResult = StatePayment.instance.setPaymentLicense(licenseText); val activationResult = StatePayment.instance.setPaymentLicense(licenseText);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
try {
if(activationResult) { if(activationResult) {
licenseInput.deactivate(); UIDialogs.showDialogOk(context, R.drawable.ic_check, context.getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required)) {
licenseInput.clear(); _fragment.close(true)
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 else
{ {
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key)); 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)
}
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
@ -158,5 +163,6 @@ class BuyFragment : MainFragment() {
companion object { companion object {
fun newInstance() = BuyFragment().apply {} fun newInstance() = BuyFragment().apply {}
private val TAG = "BuyFragment"
} }
} }

View file

@ -172,7 +172,7 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe = findViewById(R.id.button_subscribe) _buttonSubscribe = findViewById(R.id.button_subscribe)
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings) _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
_overlayLoading = findViewById(R.id.channel_loading_overlay) _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) _overlayContainer = findViewById(R.id.overlay_container)
_buttonSubscribe.onSubscribed.subscribe { _buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer) UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
@ -234,6 +234,8 @@ class ChannelFragment : MainFragment() {
if (content is IPlatformVideo) { if (content is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]") UIDialogs.toast("Added to watch later\n[${content.name}]")
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
} }
adapter.onUrlClicked.subscribe { url -> adapter.onUrlClicked.subscribe { url ->

View file

@ -86,6 +86,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
if(it is IPlatformVideo) { if(it is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]"); UIDialogs.toast("Added to watch later\n[${it.name}]");
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
}; };
adapter.onLongPress.subscribe(this) { adapter.onLongPress.subscribe(this) {

View file

@ -16,6 +16,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.adapters.SubscriptionAdapter import com.futo.platformplayer.views.adapters.SubscriptionAdapter
class CreatorsFragment : MainFragment() { class CreatorsFragment : MainFragment() {
@ -29,6 +31,8 @@ class CreatorsFragment : MainFragment() {
private var _editSearch: EditText? = null; private var _editSearch: EditText? = null;
private var _textMeta: TextView? = null; private var _textMeta: TextView? = null;
private var _buttonClearSearch: ImageButton? = null private var _buttonClearSearch: ImageButton? = null
private var _ordering = FragmentedStorage.get<StringStorage>("creators_ordering")
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false); val view = inflater.inflate(R.layout.fragment_creators, container, false);
@ -44,7 +48,7 @@ class CreatorsFragment : MainFragment() {
_buttonClearSearch?.visibility = View.INVISIBLE; _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 { _textMeta?.let {
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}"; it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
} }
@ -61,6 +65,7 @@ class CreatorsFragment : MainFragment() {
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
adapter.sortBy = pos; adapter.sortBy = pos;
_ordering.setAndSave(pos.toString())
} }
override fun onNothingSelected(parent: AdapterView<*>?) = Unit override fun onNothingSelected(parent: AdapterView<*>?) = Unit
}; };

View file

@ -150,7 +150,7 @@ class DownloadsFragment : MainFragment() {
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also { spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); 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 { spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
when(pos) { when(pos) {
@ -160,6 +160,8 @@ class DownloadsFragment : MainFragment() {
3 -> ordering.setAndSave("downloadDateDesc") 3 -> ordering.setAndSave("downloadDateDesc")
4 -> ordering.setAndSave("releasedAsc") 4 -> ordering.setAndSave("releasedAsc")
5 -> ordering.setAndSave("releasedDesc") 5 -> ordering.setAndSave("releasedDesc")
6 -> ordering.setAndSave("sizeAsc")
7 -> ordering.setAndSave("sizeDesc")
else -> ordering.setAndSave("") else -> ordering.setAndSave("")
} }
updateContentFilters() updateContentFilters()
@ -257,6 +259,8 @@ class DownloadsFragment : MainFragment() {
"nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() } "nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() }
"releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX } "releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX }
"releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN } "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 else -> vidsToReturn
} }
} }

View file

@ -10,7 +10,6 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist 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.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.TaskHandler 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) StatePlaylists.instance.playlistStore.save(playlist)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved") 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<PlaylistsFragment>(withHistory = false)
UIDialogs.toast("Playlist copied")
}
fun onShown(parameter: Any?) { fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel() _taskLoadPlaylist.cancel()
@ -188,12 +197,14 @@ class PlaylistFragment : MainFragment() {
setButtonExportVisible(false) setButtonExportVisible(false)
setButtonEditVisible(true) setButtonEditVisible(true)
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
_fragment.topBar?.assume<NavigationTopBarFragment>() _fragment.topBar?.assume<NavigationTopBarFragment>()
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) { ?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
if (StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
copyPlaylist(parameter) copyPlaylist(parameter)
})) } else {
savePlaylist(parameter)
} }
}))
} else { } else {
setName(null) setName(null)
setVideos(null, false) setVideos(null, false)
@ -259,7 +270,7 @@ class PlaylistFragment : MainFragment() {
val playlist = _playlist ?: return val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) { if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", { UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
copyPlaylist(playlist) savePlaylist(playlist)
download() download()
}) })
return return
@ -292,7 +303,7 @@ class PlaylistFragment : MainFragment() {
val playlist = _playlist ?: return val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) { if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", { UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
copyPlaylist(playlist) savePlaylist(playlist)
onEditClick() onEditClick()
}) })
return return

View file

@ -191,7 +191,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _bypassRateLimit = false; private var _bypassRateLimit = false;
private val _lastExceptions: List<Throwable>? = null; private val _lastExceptions: List<Throwable>? = null;
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh -> private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({fragment.lifecycleScope}, { withRefresh ->
val group = subGroup; val group = subGroup;
if(!_bypassRateLimit) { if(!_bypassRateLimit) {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group); val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
@ -202,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() {
throw RateLimitException(rateLimitPlugins.map { it.key.id }); throw RateLimitException(rateLimitPlugins.map { it.key.id });
} }
_bypassRateLimit = false; _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 feed = StateSubscriptions.instance.getFeed(group?.id);
val currentExs = feed?.exceptions ?: listOf(); val currentExs = feed?.exceptions ?: listOf();

View file

@ -101,7 +101,7 @@ class VideoDetailFragment() : MainFragment() {
} }
private fun isSmallWindow(): Boolean { 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 { private fun isAutoRotateEnabled(): Boolean {
@ -467,11 +467,15 @@ class VideoDetailFragment() : MainFragment() {
activity?.enterPictureInPictureMode(params); activity?.enterPictureInPictureMode(params);
} }
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, isStop: Boolean, newConfig: Configuration) { fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, isStop: Boolean, newConfig: Configuration) {
try {
if (isInPictureInPictureMode) { if (isInPictureInPictureMode) {
_viewDetail?.startPictureInPicture(); _viewDetail?.startPictureInPicture();
} else if (isInPictureInPicture) { } else if (isInPictureInPicture) {
leavePictureInPictureMode(isStop); leavePictureInPictureMode(isStop);
} }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to handle onPictureInPictureModeChanged", e)
}
} }
fun leavePictureInPictureMode(isStop: Boolean) { fun leavePictureInPictureMode(isStop: Boolean) {
isInPictureInPicture = false; isInPictureInPicture = false;
@ -623,11 +627,6 @@ class VideoDetailFragment() : MainFragment() {
showSystemUI() 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(); updateOrientation();
_view?.allowMotion = !fullscreen; _view?.allowMotion = !fullscreen;
} }

View file

@ -2,6 +2,8 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.app.RemoteAction import android.app.RemoteAction
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration 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.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException 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.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
@ -172,6 +175,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToLong import kotlin.math.roundToLong
@ -408,6 +412,14 @@ class VideoDetailView : ConstraintLayout {
showChaptersUI(); 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 { _buttonSubscribe.onSubscribed.subscribe {
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); _slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
@ -597,6 +609,10 @@ class VideoDetailView : ConstraintLayout {
} }
} }
_player.onReloadRequired.subscribe {
fetchVideo();
}
_player.onPlayChanged.subscribe { _player.onPlayChanged.subscribe {
if (StateCasting.instance.activeDevice == null) { if (StateCasting.instance.activeDevice == null) {
handlePlayChanged(it); handlePlayChanged(it);
@ -1115,7 +1131,7 @@ class VideoDetailView : ConstraintLayout {
when (Settings.instance.playback.backgroundPlay) { when (Settings.instance.playback.backgroundPlay) {
0 -> handlePause(); 0 -> handlePause();
1 -> { 1 -> {
if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio) if(!(video?.isLive ?: false))
_player.switchToAudioMode(); _player.switchToAudioMode();
StatePlayer.instance.startOrUpdateMediaSession(context, video); StatePlayer.instance.startOrUpdateMediaSession(context, video);
} }
@ -1399,8 +1415,8 @@ class VideoDetailView : ConstraintLayout {
onVideoChanged.emit(0, 0) onVideoChanged.emit(0, 0)
} }
if (video is JSVideoDetails) {
val me = this; val me = this;
if (video is JSVideoDetails) {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
//TODO: Implement video.getContentChapters() //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 ref = Models.referenceFromBuffer(video.url.toByteArray())
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null } val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
@ -1897,8 +1939,8 @@ class VideoDetailView : ConstraintLayout {
} }
updateQualityFormatsOverlay( updateQualityFormatsOverlay(
videoTrackFormats.distinctBy { it.height }.sortedBy { it.height }, videoTrackFormats.distinctBy { it.height }.sortedByDescending { it.height },
audioTrackFormats.distinctBy { it.bitrate }.sortedBy { it.bitrate }); audioTrackFormats.distinctBy { it.bitrate }.sortedByDescending { it.bitrate });
} }
} }
@ -2149,23 +2191,40 @@ class VideoDetailView : ConstraintLayout {
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate() 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( _overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
R.string.quality), null, true, 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 { 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 -> 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) { if (_isCasting) {
val ad = StateCasting.instance.activeDevice ?: return@subscribe val ad = StateCasting.instance.activeDevice ?: return@subscribe
if (!ad.canSetSpeed) { if (!ad.canSetSpeed) {
return@subscribe return@subscribe
} }
ad.changeSpeed(v.toDouble()) qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
setSelected(v); ad.changeSpeed(newPlaybackSpeed)
setSelected(playbackSpeedString);
} else { } else {
_player.setPlaybackRate(v.toFloat()); qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
setSelected(v); _player.setPlaybackRate(playbackSpeedString.toFloat());
setSelected(playbackSpeedString);
} }
}; };
} else null, } else null,
@ -2438,7 +2497,9 @@ class VideoDetailView : ConstraintLayout {
val url = _url; val url = _url;
if (!url.isNullOrBlank()) { if (!url.isNullOrBlank()) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
setLoading(true); setLoading(true);
}
_taskLoadVideo.run(url); _taskLoadVideo.run(url);
} }
} }
@ -2522,8 +2583,10 @@ class VideoDetailView : ConstraintLayout {
} }
fun saveBrightness() { fun saveBrightness() {
if (Settings.instance.gestureControls.useSystemBrightness) {
_player.gestureControl.saveBrightness() _player.gestureControl.saveBrightness()
} }
}
fun restoreBrightness() { fun restoreBrightness() {
_player.gestureControl.restoreBrightness() _player.gestureControl.restoreBrightness()
} }
@ -2702,6 +2765,8 @@ class VideoDetailView : ConstraintLayout {
if(it is IPlatformVideo) { if(it is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]"); UIDialogs.toast("Added to watch later\n[${it.name}]");
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
} }
onAddToQueueClicked.subscribe(this) { onAddToQueueClicked.subscribe(this) {
@ -2969,6 +3034,11 @@ class VideoDetailView : ConstraintLayout {
return@TaskHandler result; return@TaskHandler result;
}) })
.success { setVideoDetails(it, true) } .success { setVideoDetails(it, true) }
.exception<ScriptReloadRequiredException> {
StatePlatform.instance.handleReloadRequired(it, {
fetchVideo();
});
}
.exception<NoPlatformClientException> { .exception<NoPlatformClientException> {
Logger.w(TAG, "exception<NoPlatformClientException>", it) Logger.w(TAG, "exception<NoPlatformClientException>", it)

View file

@ -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.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -35,11 +36,15 @@ class Playlist {
this.videos = ArrayList(list); this.videos = ArrayList(list);
} }
fun makeCopy(newName: String? = null): Playlist {
return Playlist(newName ?: name, videos)
}
companion object { companion object {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? { fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? {
if(obj == null) if(obj == null)
return null; return null;
obj.ensureIsBusy();
val contextName = "Playlist"; val contextName = "Playlist";

View file

@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod
import android.text.style.URLSpan import android.text.style.URLSpan
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.TextView import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() { class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
@ -60,12 +63,15 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
val dx = event.x - downX val dx = event.x - downX
val dy = event.y - downY val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) { if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
runBlocking {
for (link in pressedLinks!!) { for (link in pressedLinks!!) {
Logger.i(TAG) { "Link clicked '${link.url}'." } Logger.i(TAG) { "Link clicked '${link.url}'." }
if (_context is MainActivity) { val c = _context
if (_context.handleUrl(link.url)) continue if (c is MainActivity) {
c.lifecycleScope.launch(Dispatchers.IO) {
if (c.handleUrl(link.url)) {
return@launch
}
if (timestampRegex.matches(link.url)) { if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':') val tokens = link.url.split(':')
var time_s = -1L var time_s = -1L
@ -77,12 +83,17 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
} }
if (time_s != -1L) { if (time_s != -1L) {
withContext(Dispatchers.Main) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
continue }
return@launch
} }
} }
withContext(Dispatchers.Main) {
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
}
} }
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
} }
} }
pressedLinks = null pressedLinks = null

View file

@ -62,7 +62,7 @@ class DownloadService : Service() {
Logger.i(TAG, "onStartCommand"); Logger.i(TAG, "onStartCommand");
synchronized(this) { synchronized(this) {
if(_started) if(_started)
return START_STICKY; return START_NOT_STICKY;
if(!FragmentedStorage.isInitialized) { if(!FragmentedStorage.isInitialized) {
Logger.i(TAG, "Attempted to start DownloadService without initialized files"); Logger.i(TAG, "Attempted to start DownloadService without initialized files");

View file

@ -156,6 +156,8 @@ class StateApp {
return thisContext; return thisContext;
} }
private var _mainId: String? = null;
//Files //Files
private var _tempDirectory: File? = null; private var _tempDirectory: File? = null;
private var _cacheDirectory: File? = null; private var _cacheDirectory: File? = null;
@ -295,9 +297,12 @@ class StateApp {
} }
//Lifecycle //Lifecycle
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) { fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null, mainId: String? = null) {
_mainId = mainId;
_context = context; _context = context;
_scope = coroutineScope _scope = coroutineScope
Logger.w(TAG, "Scope initialized ${(coroutineScope != null)}\n ${Log.getStackTraceString(Throwable())}")
} }
fun initializeFiles(force: Boolean = false) { fun initializeFiles(force: Boolean = false) {
@ -719,7 +724,9 @@ class StateApp {
migrateStores(context, managedStores, index + 1); 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"); Logger.i(TAG, "App ended");
_receiverBecomingNoisy?.let { _receiverBecomingNoisy?.let {
_receiverBecomingNoisy = null; _receiverBecomingNoisy = null;
@ -743,7 +750,8 @@ class StateApp {
fun dispose(){ fun dispose(){
_context = null; _context = null;
_scope = null; // _scope = null;
Logger.w(TAG, "StateApp disposed: ${Log.getStackTraceString(Throwable())}")
} }
private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() { private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() {

View file

@ -131,8 +131,13 @@ class StateHistory {
fun getHistoryPager(): IPager<HistoryVideo> { fun getHistoryPager(): IPager<HistoryVideo> {
return _historyDBStore.getObjectPager(); return _historyDBStore.getObjectPager();
} }
fun getHistorySearchPager(query: String): IPager<HistoryVideo> { fun getHistorySearchPager(query: String, withAuthor: Boolean = false): IPager<HistoryVideo> {
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10); 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? { fun getHistoryIndexByUrl(url: String): DBHistory.Index? {
return historyIndex[url]; return historyIndex[url];

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import androidx.collection.LruCache import androidx.collection.LruCache
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs 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.BatchedTaskHandler
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.fromPool import com.futo.platformplayer.fromPool
import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.getNowDiffSeconds
@ -316,7 +318,18 @@ class StatePlatform {
_platformOrderPersistent.save(); _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) { return withContext(Dispatchers.IO) {
val client = getClient(id); val client = getClient(id);
if (client !is JSClient) if (client !is JSClient)
@ -347,10 +360,27 @@ class StatePlatform {
_availableClients.removeIf { it.id == id }; _availableClients.removeIf { it.id == id };
_availableClients.add(newClient); _availableClients.add(newClient);
} }
afterReload?.invoke();
return@withContext newClient; 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<String>) { suspend fun enableClient(ids: List<String>) {
val currentClients = getEnabledClients().map { it.id }; val currentClients = getEnabledClients().map { it.id };
@ -361,6 +391,9 @@ class StatePlatform {
* If a client is disabled, NO requests are made to said client * If a client is disabled, NO requests are made to said client
*/ */
suspend fun selectClients(vararg ids: String) { suspend fun selectClients(vararg ids: String) {
selectClients(null, *ids);
}
suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
synchronized(_clientsLock) { synchronized(_clientsLock) {
val removed = _enabledClients.toMutableList(); val removed = _enabledClients.toMutableList();
@ -385,6 +418,7 @@ class StatePlatform {
onSourceDisabled.emit(oldClient); onSourceDisabled.emit(oldClient);
} }
} }
afterLoad?.invoke();
}; };
} }
@ -976,7 +1010,7 @@ class StatePlatform {
return EmptyPager(); return EmptyPager();
if(!StateApp.instance.privateMode) if(!StateApp.instance.privateMode)
return client.fromPool(_mainClientPool).getComments(url); return client.fromPool(_pagerClientPool).getComments(url);
else else
return client.fromPool(_privateClientPool).getComments(url); return client.fromPool(_privateClientPool).getComments(url);
} }

View file

@ -3,7 +3,6 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException 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.models.Playlist
import com.futo.platformplayer.sToOffsetDateTimeUTC import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringDateMapStorage 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.stores.v2.ReconstructStore
import com.futo.platformplayer.sync.internal.GJSyncOpcodes import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage 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 com.futo.platformplayer.sync.models.SyncWatchLaterPackage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@ -178,31 +173,30 @@ class StatePlaylists {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER); StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
} }
} }
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean { fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false): Boolean {
var wasNew = false;
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
if(!_watchlistStore.hasItem { it.url == video.url }) if (_watchlistStore.hasItem { it.url == video.url }) {
wasNew = true; return false
_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());
} }
_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(); onWatchLaterChanged.emit();
if(isUserInteraction) { if (isUserInteraction) {
val now = OffsetDateTime.now(); val now = OffsetDateTime.now();
_watchLaterAdds.setAndSave(video.url, now); _watchLaterAdds.setAndSave(video.url, now);
broadcastWatchLaterAddition(video, now); broadcastWatchLaterAddition(video, now);
} }
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
return wasNew; return true;
} }
fun getLastPlayedPlaylist() : Playlist? { fun getLastPlayedPlaylist() : Playlist? {
@ -423,17 +417,25 @@ class StatePlaylists {
class PlaylistBackup: ReconstructStore<Playlist>() { class PlaylistBackup: ReconstructStore<Playlist>() {
override fun toReconstruction(obj: Playlist): String { override fun toReconstruction(obj: Playlist): String {
val items = ArrayList<String>(); val items = ArrayList<String>();
items.add(obj.name); items.add(obj.name + ":::" + obj.id);
items.addAll(obj.videos.map { it.url }); items.addAll(obj.videos.map { it.url });
return items.map { it.replace("\n","") }.joinToString("\n"); return items.map { it.replace("\n","") }.joinToString("\n");
} }
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist { override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
var idToUse = id;
val items = backup.split("\n"); val items = backup.split("\n");
if(items.size <= 0) { if(items.size <= 0) {
throw IllegalStateException("Cannot reconstructor playlist ${id}"); 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 { val videos = items.drop(1).filter { it.isNotEmpty() }.map {
try { try {
val videoUrl = it; val videoUrl = it;
@ -465,7 +467,7 @@ class StatePlaylists {
throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex); throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex);
} }
}.filter { it != null }.map { it!! } }.filter { it != null }.map { it!! }
return Playlist(id, name, videos); return Playlist(idToUse, name, videos);
} }
} }
} }

View file

@ -329,8 +329,19 @@ class StateSubscriptions {
} }
} }
if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) {
getSubscriptionOtherOrCreate(sub.channel.url, sub.channel.name, sub.channel.thumbnail); 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; return sub;
} }

View file

@ -78,12 +78,19 @@ class StateSync {
onAuthorized = { sess, isNewlyAuthorized, isNewSession -> onAuthorized = { sess, isNewlyAuthorized, isNewSession ->
if (isNewSession) { if (isNewSession) {
deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess) 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 -> onUnauthorized = { sess ->
StateApp.instance.scope.launch(Dispatchers.Main) { StateApp.instance.scope.launch(Dispatchers.Main) {
try {
UIDialogs.showConfirmationDialog( UIDialogs.showConfirmationDialog(
context, context,
"Device Unauthorized: ${sess.displayName}", "Device Unauthorized: ${sess.displayName}",
@ -94,6 +101,9 @@ class StateSync {
}, },
cancelAction = {} cancelAction = {}
) )
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show unauthorized dialog.", e)
}
} }
} }
@ -118,12 +128,17 @@ class StateSync {
if (scope != null && activity != null) { if (scope != null && activity != null) {
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
UIDialogs.showConfirmationDialog(activity, "Allow connection from $remotePublicKey?", try {
UIDialogs.showConfirmationDialog(
activity, "Allow connection from $remotePublicKey?",
action = { action = {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
callback(true) callback(true)
Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation") Logger.i(
TAG,
"Connection authorized for $remotePublicKey by confirmation"
)
activity.finish() activity.finish()
} catch (e: Throwable) { } catch (e: Throwable) {
@ -142,6 +157,9 @@ class StateSync {
} }
} }
) )
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show authorized dialog.", e)
}
} }
} else { } else {
callback(false) 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) { private fun handleData(session: SyncSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
val remotePublicKey = session.remotePublicKey val remotePublicKey = session.remotePublicKey
when (subOpcode) { when (subOpcode) {
@ -307,7 +330,9 @@ class StateSync {
data.get(dataBody); data.get(dataBody);
val json = String(dataBody, Charsets.UTF_8); val json = String(dataBody, Charsets.UTF_8);
val subPackage = Serializer.json.decodeFromString<SyncSubscriptionsPackage>(json); val subPackage = Serializer.json.decodeFromString<SyncSubscriptionsPackage>(json);
synchronized(_lockSubscriptions) {
handleSyncSubscriptionPackage(session, subPackage); handleSyncSubscriptionPackage(session, subPackage);
}
if(subPackage.subscriptions.size > 0) { if(subPackage.subscriptions.size > 0) {
val newestSub = subPackage.subscriptions.maxOf { it.creationTime }; val newestSub = subPackage.subscriptions.maxOf { it.creationTime };
@ -327,6 +352,7 @@ class StateSync {
val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json); val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json);
var lastSubgroupChange = OffsetDateTime.MIN; var lastSubgroupChange = OffsetDateTime.MIN;
synchronized(_lockSubscriptionGroups) {
for(group in pack.groups){ for(group in pack.groups){
if(group.lastChange > lastSubgroupChange) if(group.lastChange > lastSubgroupChange)
lastSubgroupChange = group.lastChange; lastSubgroupChange = group.lastChange;
@ -344,6 +370,7 @@ class StateSync {
StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true); StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
} }
} }
}
for(removal in pack.groupRemovals) { for(removal in pack.groupRemovals) {
val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key); val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key);
val removalTime = removal.value.sToOffsetDateTimeUTC(); val removalTime = removal.value.sToOffsetDateTimeUTC();
@ -358,6 +385,7 @@ class StateSync {
val json = String(dataBody, Charsets.UTF_8); val json = String(dataBody, Charsets.UTF_8);
val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json); val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json);
synchronized(_lockPlaylists) {
for(playlist in pack.playlists) { for(playlist in pack.playlists) {
val existing = StatePlaylists.instance.getPlaylist(playlist.id); val existing = StatePlaylists.instance.getPlaylist(playlist.id);
@ -372,6 +400,7 @@ class StateSync {
StatePlaylists.instance.createOrUpdatePlaylist(existing, false); StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
} }
} }
}
for(removal in pack.playlistRemovals) { for(removal in pack.playlistRemovals) {
val creation = StatePlaylists.instance.getPlaylist(removal.key); val creation = StatePlaylists.instance.getPlaylist(removal.key);
val removalTime = removal.value.sToOffsetDateTimeUTC(); val removalTime = removal.value.sToOffsetDateTimeUTC();
@ -390,6 +419,7 @@ class StateSync {
Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})"); Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})");
val allExisting = StatePlaylists.instance.getWatchLater(); val allExisting = StatePlaylists.instance.getWatchLater();
synchronized(_lockWatchlater) {
for(video in pack.videos) { for(video in pack.videos) {
val existing = allExisting.firstOrNull { it.url == video.url }; 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 time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN;
@ -400,6 +430,7 @@ class StateSync {
StatePlaylists.instance.setWatchLaterAddTime(video.url, time); StatePlaylists.instance.setWatchLaterAddTime(video.url, time);
} }
} }
}
for(removal in pack.videoRemovals) { for(removal in pack.videoRemovals) {
val watchLater = allExisting.firstOrNull { it.url == removal.key } ?: continue; val watchLater = allExisting.firstOrNull { it.url == removal.key } ?: continue;
val creation = StatePlaylists.instance.getWatchLaterRemovalTime(watchLater.url) ?: OffsetDateTime.MIN; val creation = StatePlaylists.instance.getWatchLaterRemovalTime(watchLater.url) ?: OffsetDateTime.MIN;

View file

@ -274,10 +274,17 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?"; val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?";
val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize)); val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize));
return deserializeIndexes(dbDaoBase.getMultiple(query)); return deserializeIndexes(dbDaoBase.getMultiple(query));
}fun queryLike2Page(field: String, field2: String, obj: String, page: Int, pageSize: Int): List<I> {
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<T> { fun queryLikeObjectPage(field: String, obj: String, page: Int, pageSize: Int): List<T> {
return convertObjects(queryLikePage(field, obj, page, pageSize)); return convertObjects(queryLikePage(field, obj, page, pageSize));
} }
fun queryLike2ObjectPage(field: String, field2: String, obj: String, page: Int, pageSize: Int): List<T> {
return convertObjects(queryLike2Page(field, field2, obj, page, pageSize));
}
//Query Page Objects //Query Page Objects
@ -336,6 +343,13 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
queryLikePage(field, obj, it - 1, pageSize); queryLikePage(field, obj, it - 1, pageSize);
}); });
} }
fun queryLike2Pager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager<I> = queryLike2Pager(validateFieldName(field), validateFieldName(field2), obj, pageSize);
fun queryLike2Pager(field: String, field2: String, obj: String, pageSize: Int): IPager<I> {
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<T> = queryLikeObjectPager(validateFieldName(field), obj, pageSize); fun queryLikeObjectPager(field: KProperty<*>, obj: String, pageSize: Int): IPager<T> = queryLikeObjectPager(validateFieldName(field), obj, pageSize);
fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager<T> { fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager<T> {
return AdhocPager({ return AdhocPager({
@ -344,6 +358,14 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
}); });
} }
fun queryLike2ObjectPager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager<T> = queryLike2ObjectPager(validateFieldName(field), validateFieldName(field2), obj, pageSize);
fun queryLike2ObjectPager(field: String, field2: String, obj: String, pageSize: Int): IPager<T> {
return AdhocPager({
Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}");
queryLike2ObjectPage(field, field2, obj, it - 1, pageSize);
});
}
//Query Pager with convert //Query Pager with convert
fun <X> queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> = queryPager(validateFieldName(field), obj, pageSize, convert); fun <X> queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> = queryPager(validateFieldName(field), obj, pageSize, convert);
fun <X> queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> { fun <X> queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager<X> {

View file

@ -174,7 +174,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
if (resolve != null) { if (resolve != null) {
resolveCount = resolves.size; resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
for(result in resolve){ for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl }; val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) { if(task != null) {

View file

@ -75,7 +75,7 @@ class ChannelRelayed(
private var handshakeState: HandshakeState? = if (initiator) { private var handshakeState: HandshakeState? = if (initiator) {
HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR).apply { HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR).apply {
localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair) localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair)
remotePublicKey.setPublicKey(Base64.getDecoder().decode(publicKey), 0) remotePublicKey.setPublicKey(publicKey.base64ToByteArray(), 0)
} }
} else { } else {
HandshakeState(SyncService.protocolName, HandshakeState.RESPONDER).apply { HandshakeState(SyncService.protocolName, HandshakeState.RESPONDER).apply {
@ -177,7 +177,7 @@ class ChannelRelayed(
this.remoteVersion = remoteVersion this.remoteVersion = remoteVersion
val remoteKeyBytes = ByteArray(handshakeState!!.remotePublicKey.publicKeyLength) val remoteKeyBytes = ByteArray(handshakeState!!.remotePublicKey.publicKeyLength)
handshakeState!!.remotePublicKey.getPublicKey(remoteKeyBytes, 0) handshakeState!!.remotePublicKey.getPublicKey(remoteKeyBytes, 0)
this.remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes) this.remotePublicKey = remoteKeyBytes.toBase64()
handshakeState?.destroy() handshakeState?.destroy()
handshakeState = null handshakeState = null
this.transport = transport this.transport = transport
@ -316,7 +316,7 @@ class ChannelRelayed(
val channelMessage = ByteArray(1024) val channelMessage = ByteArray(1024)
val channelBytesWritten = handshakeState!!.writeMessage(channelMessage, 0, null, 0, 0) val channelBytesWritten = handshakeState!!.writeMessage(channelMessage, 0, null, 0, 0)
val publicKeyBytes = Base64.getDecoder().decode(publicKey) val publicKeyBytes = publicKey.base64ToByteArray()
if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes") if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes")
val (pairingMessageLength, pairingMessage) = if (pairingCode != null) { val (pairingMessageLength, pairingMessage) = if (pairingCode != null) {

View file

@ -13,6 +13,7 @@ import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.states.StateSync import com.futo.platformplayer.states.StateSync
import com.futo.polycentric.core.base64ToByteArray import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.base64UrlToByteArray
import com.futo.polycentric.core.toBase64 import com.futo.polycentric.core.toBase64
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -98,7 +99,7 @@ class SyncService(
override fun onServiceLost(service: NsdServiceInfo) { override fun onServiceLost(service: NsdServiceInfo) {
Log.e(TAG, "service lost: $service") Log.e(TAG, "service lost: $service")
val urlSafePkey = service.attributes["pk"]?.decodeToString() ?: return val urlSafePkey = service.attributes["pk"]?.decodeToString() ?: return
val pkey = Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')).toBase64() val pkey = urlSafePkey.base64UrlToByteArray().toBase64()
synchronized(_mdnsCache) { synchronized(_mdnsCache) {
_mdnsCache.remove(pkey) _mdnsCache.remove(pkey)
} }
@ -128,7 +129,7 @@ class SyncService(
} }
val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return
val pkey = Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')).toBase64() val pkey = urlSafePkey.base64UrlToByteArray().toBase64()
val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null) val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null)
synchronized(_mdnsCache) { synchronized(_mdnsCache) {
@ -157,7 +158,7 @@ class SyncService(
override fun onServiceLost() { override fun onServiceLost() {
Log.v(TAG, "onServiceLost: $service") Log.v(TAG, "onServiceLost: $service")
val urlSafePkey = service.attributes["pk"]?.decodeToString() ?: return val urlSafePkey = service.attributes["pk"]?.decodeToString() ?: return
val pkey = Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')).toBase64() val pkey = urlSafePkey.base64UrlToByteArray().toBase64()
synchronized(_mdnsCache) { synchronized(_mdnsCache) {
_mdnsCache.remove(pkey) _mdnsCache.remove(pkey)
} }
@ -327,7 +328,7 @@ class SyncService(
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
synchronized(_mdnsCache) { synchronized(_mdnsCache) {
for ((pkey, info) in _mdnsCache) { for ((pkey, info) in _mdnsCache) {
if (!database.isAuthorized(pkey) || isConnected(pkey)) continue if (!database.isAuthorized(pkey) || getLinkType(pkey) == LinkType.Direct) continue
val last = synchronized(_lastConnectTimesMdns) { val last = synchronized(_lastConnectTimesMdns) {
_lastConnectTimesMdns[pkey] ?: 0L _lastConnectTimesMdns[pkey] ?: 0L
@ -359,8 +360,8 @@ class SyncService(
while (_started) { while (_started) {
val authorizedDevices = database.getAllAuthorizedDevices() ?: arrayOf() val authorizedDevices = database.getAllAuthorizedDevices() ?: arrayOf()
val addressesToConnect = authorizedDevices.mapNotNull { val addressesToConnect = authorizedDevices.mapNotNull {
val connected = isConnected(it) val connectedDirectly = getLinkType(it) == LinkType.Direct
if (connected) { if (connectedDirectly) {
return@mapNotNull null return@mapNotNull null
} }
@ -467,8 +468,13 @@ class SyncService(
while (_started && !socketClosed) { while (_started && !socketClosed) {
val unconnectedAuthorizedDevices = val unconnectedAuthorizedDevices =
database.getAllAuthorizedDevices() database.getAllAuthorizedDevices()
?.filter { !isConnected(it) }?.toTypedArray() ?.filter {
?: arrayOf() if (Settings.instance.synchronization.connectLocalDirectThroughRelay) {
getLinkType(it) != LinkType.Direct
} else {
!isConnected(it)
}
}?.toTypedArray() ?: arrayOf()
relaySession.publishConnectionInformation( relaySession.publishConnectionInformation(
unconnectedAuthorizedDevices, unconnectedAuthorizedDevices,
settings.listenerPort, settings.listenerPort,
@ -496,7 +502,7 @@ class SyncService(
val potentialLocalAddresses = val potentialLocalAddresses =
connectionInfo.ipv4Addresses connectionInfo.ipv4Addresses
.filter { it != connectionInfo.remoteIp } .filter { it != connectionInfo.remoteIp }
if (connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) { if (getLinkType(targetKey) != LinkType.Direct && connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) {
Thread { Thread {
try { try {
Log.v( Log.v(
@ -528,7 +534,7 @@ class SyncService(
// TODO: Implement hole punching if needed // TODO: Implement hole punching if needed
} }
if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { if (!isConnected(targetKey) && connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) {
try { try {
Logger.v( Logger.v(
TAG, TAG,
@ -740,6 +746,7 @@ class SyncService(
) )
} }
fun getLinkType(publicKey: String): LinkType = synchronized(_sessions) { _sessions[publicKey]?.linkType ?: LinkType.None }
fun isConnected(publicKey: String): Boolean = synchronized(_sessions) { _sessions[publicKey]?.connected ?: false } fun isConnected(publicKey: String): Boolean = synchronized(_sessions) { _sessions[publicKey]?.connected ?: false }
fun isAuthorized(publicKey: String): Boolean = database.isAuthorized(publicKey) fun isAuthorized(publicKey: String): Boolean = database.isAuthorized(publicKey)
fun getSession(publicKey: String): SyncSession? = synchronized(_sessions) { _sessions[publicKey] } fun getSession(publicKey: String): SyncSession? = synchronized(_sessions) { _sessions[publicKey] }
@ -796,8 +803,12 @@ class SyncService(
_relaySession = null _relaySession = null
_serverSocket?.close() _serverSocket?.close()
_serverSocket = null _serverSocket = null
synchronized(_sessions) {
_sessions.values.toList()
}.forEach { it.close() }
synchronized(_sessions) { synchronized(_sessions) {
_sessions.values.forEach { it.close() }
_sessions.clear() _sessions.clear()
} }
} }

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.sync.internal
import android.os.Build import android.os.Build
import com.futo.platformplayer.ensureNotMainThread import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.findCandidateAddresses
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.noise.protocol.CipherStatePair import com.futo.platformplayer.noise.protocol.CipherStatePair
import com.futo.platformplayer.noise.protocol.DHState import com.futo.platformplayer.noise.protocol.DHState
@ -123,7 +124,7 @@ class SyncSocketSession {
val localPublicKey = ByteArray(localKeyPair.publicKeyLength) val localPublicKey = ByteArray(localKeyPair.publicKeyLength)
localKeyPair.getPublicKey(localPublicKey, 0) localKeyPair.getPublicKey(localPublicKey, 0)
_localPublicKey = Base64.getEncoder().encodeToString(localPublicKey) _localPublicKey = localPublicKey.toBase64()
} }
fun startAsInitiator(remotePublicKey: String, appId: UInt = 0u, pairingCode: String? = null) { fun startAsInitiator(remotePublicKey: String, appId: UInt = 0u, pairingCode: String? = null) {
@ -253,14 +254,14 @@ class SyncSocketSession {
val initiator = HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR) val initiator = HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR)
initiator.localKeyPair.copyFrom(_localKeyPair) initiator.localKeyPair.copyFrom(_localKeyPair)
initiator.remotePublicKey.setPublicKey(Base64.getDecoder().decode(remotePublicKey), 0) initiator.remotePublicKey.setPublicKey(remotePublicKey.base64ToByteArray(), 0)
initiator.start() initiator.start()
val pairingMessage: ByteArray val pairingMessage: ByteArray
val pairingMessageLength: Int val pairingMessageLength: Int
if (pairingCode != null) { if (pairingCode != null) {
val pairingHandshake = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR) val pairingHandshake = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR)
pairingHandshake.remotePublicKey.setPublicKey(Base64.getDecoder().decode(remotePublicKey), 0) pairingHandshake.remotePublicKey.setPublicKey(remotePublicKey.base64ToByteArray(), 0)
pairingHandshake.start() pairingHandshake.start()
val pairingCodeBytes = pairingCode.toByteArray(Charsets.UTF_8) val pairingCodeBytes = pairingCode.toByteArray(Charsets.UTF_8)
val pairingBuffer = ByteArray(512) val pairingBuffer = ByteArray(512)
@ -299,7 +300,7 @@ class SyncSocketSession {
_cipherStatePair = initiator.split() _cipherStatePair = initiator.split()
val remoteKeyBytes = ByteArray(initiator.remotePublicKey.publicKeyLength) val remoteKeyBytes = ByteArray(initiator.remotePublicKey.publicKeyLength)
initiator.remotePublicKey.getPublicKey(remoteKeyBytes, 0) initiator.remotePublicKey.getPublicKey(remoteKeyBytes, 0)
_remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes).base64ToByteArray().toBase64() _remotePublicKey = remoteKeyBytes.toBase64()
} }
private fun handshakeAsResponder(): Boolean { private fun handshakeAsResponder(): Boolean {
@ -516,7 +517,7 @@ class SyncSocketSession {
return return
} }
val channelHandshakeMessage = ByteArray(channelMessageLength).also { data.get(it) } val channelHandshakeMessage = ByteArray(channelMessageLength).also { data.get(it) }
val publicKey = Base64.getEncoder().encodeToString(publicKeyBytes) val publicKey = publicKeyBytes.toBase64()
val pairingCode = if (pairingMessageLength > 0) { val pairingCode = if (pairingMessageLength > 0) {
val pairingProtocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.RESPONDER).apply { val pairingProtocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.RESPONDER).apply {
localKeyPair.copyFrom(_localKeyPair) localKeyPair.copyFrom(_localKeyPair)
@ -671,7 +672,7 @@ class SyncSocketSession {
val records = mutableMapOf<String, Pair<ByteArray, Long>>() val records = mutableMapOf<String, Pair<ByteArray, Long>>()
repeat(recordCount) { repeat(recordCount) {
val publisherBytes = ByteArray(32).also { data.get(it) } val publisherBytes = ByteArray(32).also { data.get(it) }
val publisher = Base64.getEncoder().encodeToString(publisherBytes) val publisher = publisherBytes.toBase64()
val blobLength = data.int val blobLength = data.int
val encryptedBlob = ByteArray(blobLength).also { data.get(it) } val encryptedBlob = ByteArray(blobLength).also { data.get(it) }
val timestamp = data.long val timestamp = data.long
@ -712,7 +713,7 @@ class SyncSocketSession {
val numResponses = data.get().toInt() val numResponses = data.get().toInt()
val result = mutableMapOf<String, ConnectionInfo>() val result = mutableMapOf<String, ConnectionInfo>()
repeat(numResponses) { repeat(numResponses) {
val publicKey = Base64.getEncoder().encodeToString(ByteArray(32).also { data.get(it) }) val publicKey = ByteArray(32).also { data.get(it) }.toBase64()
val status = data.get().toInt() val status = data.get().toInt()
if (status == 0) { if (status == 0) {
val infoSize = data.int val infoSize = data.int
@ -813,7 +814,7 @@ class SyncSocketSession {
return return
} }
val decryptedPayload = channel.decrypt(data) val decryptedPayload = channel.decrypt(data)
val errorCode = SyncErrorCode.entries.find { it.value == decryptedPayload.int } ?: SyncErrorCode.ConnectionClosed val errorCode = decryptedPayload.int
Logger.e(TAG, "Received relayed error (errorCode = $errorCode) on connectionId $connectionId, closing") Logger.e(TAG, "Received relayed error (errorCode = $errorCode) on connectionId $connectionId, closing")
channel.close() channel.close()
_channels.remove(connectionId) _channels.remove(connectionId)
@ -824,7 +825,7 @@ class SyncSocketSession {
return return
} }
val connectionId = data.long val connectionId = data.long
val errorCode = SyncErrorCode.entries.find { it.value == data.int } ?: SyncErrorCode.ConnectionClosed val errorCode = data.int
val channel = _channels[connectionId] ?: run { val channel = _channels[connectionId] ?: run {
Logger.e(TAG, "Received error code $errorCode for non-existent channel with connectionId $connectionId") Logger.e(TAG, "Received error code $errorCode for non-existent channel with connectionId $connectionId")
return return
@ -994,7 +995,7 @@ class SyncSocketSession {
val deferred = CompletableDeferred<ConnectionInfo?>() val deferred = CompletableDeferred<ConnectionInfo?>()
_pendingConnectionInfoRequests[requestId] = deferred _pendingConnectionInfoRequests[requestId] = deferred
try { try {
val publicKeyBytes = Base64.getDecoder().decode(publicKey) val publicKeyBytes = publicKey.base64ToByteArray()
if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes") if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes")
val packet = ByteBuffer.allocate(4 + 32).order(ByteOrder.LITTLE_ENDIAN) val packet = ByteBuffer.allocate(4 + 32).order(ByteOrder.LITTLE_ENDIAN)
packet.putInt(requestId) packet.putInt(requestId)
@ -1017,7 +1018,7 @@ class SyncSocketSession {
packet.putInt(requestId) packet.putInt(requestId)
packet.put(publicKeys.size.toByte()) packet.put(publicKeys.size.toByte())
for (pk in publicKeys) { for (pk in publicKeys) {
val pkBytes = Base64.getDecoder().decode(pk) val pkBytes = pk.base64ToByteArray()
if (pkBytes.size != 32) throw IllegalArgumentException("Invalid public key length for $pk") if (pkBytes.size != 32) throw IllegalArgumentException("Invalid public key length for $pk")
packet.put(pkBytes) packet.put(pkBytes)
} }
@ -1078,20 +1079,9 @@ class SyncSocketSession {
) { ) {
if (authorizedKeys.size > 255) throw IllegalArgumentException("Number of authorized keys exceeds 255") if (authorizedKeys.size > 255) throw IllegalArgumentException("Number of authorized keys exceeds 255")
val ipv4Addresses = mutableListOf<String>() val candidateAddresses = findCandidateAddresses()
val ipv6Addresses = mutableListOf<String>() val ipv4Addresses = candidateAddresses.filterIsInstance<Inet4Address>()
for (nic in NetworkInterface.getNetworkInterfaces()) { val ipv6Addresses = candidateAddresses.filterIsInstance<Inet6Address>()
if (nic.isUp) {
for (addr in nic.inetAddresses) {
if (!addr.isLoopbackAddress) {
when (addr) {
is Inet4Address -> ipv4Addresses.add(addr.hostAddress)
is Inet6Address -> ipv6Addresses.add(addr.hostAddress)
}
}
}
}
}
val deviceName = getDeviceName() val deviceName = getDeviceName()
val nameBytes = getLimitedUtf8Bytes(deviceName, 255) val nameBytes = getLimitedUtf8Bytes(deviceName, 255)
@ -1103,12 +1093,12 @@ class SyncSocketSession {
data.put(nameBytes) data.put(nameBytes)
data.put(ipv4Addresses.size.toByte()) data.put(ipv4Addresses.size.toByte())
for (addr in ipv4Addresses) { for (addr in ipv4Addresses) {
val addrBytes = InetAddress.getByName(addr).address val addrBytes = addr.address
data.put(addrBytes) data.put(addrBytes)
} }
data.put(ipv6Addresses.size.toByte()) data.put(ipv6Addresses.size.toByte())
for (addr in ipv6Addresses) { for (addr in ipv6Addresses) {
val addrBytes = InetAddress.getByName(addr).address val addrBytes = addr.address
data.put(addrBytes) data.put(addrBytes)
} }
data.put(if (allowLocalDirect) 1 else 0) data.put(if (allowLocalDirect) 1 else 0)
@ -1125,7 +1115,7 @@ class SyncSocketSession {
publishBytes.put(authorizedKeys.size.toByte()) publishBytes.put(authorizedKeys.size.toByte())
for (key in authorizedKeys) { for (key in authorizedKeys) {
val publicKeyBytes = Base64.getDecoder().decode(key) val publicKeyBytes = key.base64ToByteArray()
if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes") if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes")
val protocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR) val protocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR)
@ -1183,7 +1173,7 @@ class SyncSocketSession {
packet.put(consumerPublicKeys.size.toByte()) packet.put(consumerPublicKeys.size.toByte())
for (consumer in consumerPublicKeys) { for (consumer in consumerPublicKeys) {
val consumerBytes = Base64.getDecoder().decode(consumer) val consumerBytes = consumer.base64ToByteArray()
if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes") if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes")
packet.put(consumerBytes) packet.put(consumerBytes)
val protocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR).apply { val protocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR).apply {
@ -1222,7 +1212,7 @@ class SyncSocketSession {
val deferred = CompletableDeferred<Pair<ByteArray, Long>?>() val deferred = CompletableDeferred<Pair<ByteArray, Long>?>()
_pendingGetRecordRequests[requestId] = deferred _pendingGetRecordRequests[requestId] = deferred
try { try {
val publisherBytes = Base64.getDecoder().decode(publisherPublicKey) val publisherBytes = publisherPublicKey.base64ToByteArray()
if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes")
val keyBytes = key.toByteArray(Charsets.UTF_8) val keyBytes = key.toByteArray(Charsets.UTF_8)
val packet = ByteBuffer.allocate(4 + 32 + 1 + keyBytes.size).order(ByteOrder.LITTLE_ENDIAN) val packet = ByteBuffer.allocate(4 + 32 + 1 + keyBytes.size).order(ByteOrder.LITTLE_ENDIAN)
@ -1253,7 +1243,7 @@ class SyncSocketSession {
packet.put(keyBytes) packet.put(keyBytes)
packet.put(publisherPublicKeys.size.toByte()) packet.put(publisherPublicKeys.size.toByte())
for (publisher in publisherPublicKeys) { for (publisher in publisherPublicKeys) {
val bytes = Base64.getDecoder().decode(publisher) val bytes = publisher.base64ToByteArray()
if (bytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") if (bytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes")
packet.put(bytes) packet.put(bytes)
} }
@ -1272,9 +1262,9 @@ class SyncSocketSession {
val deferred = CompletableDeferred<Boolean>() val deferred = CompletableDeferred<Boolean>()
_pendingDeleteRequests[requestId] = deferred _pendingDeleteRequests[requestId] = deferred
try { try {
val publisherBytes = Base64.getDecoder().decode(publisherPublicKey) val publisherBytes = publisherPublicKey.base64ToByteArray()
if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes")
val consumerBytes = Base64.getDecoder().decode(consumerPublicKey) val consumerBytes = consumerPublicKey.base64ToByteArray()
if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes") if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes")
val packetSize = 4 + 32 + 32 + 1 + keys.sumOf { 1 + it.toByteArray(Charsets.UTF_8).size } val packetSize = 4 + 32 + 32 + 1 + keys.sumOf { 1 + it.toByteArray(Charsets.UTF_8).size }
val packet = ByteBuffer.allocate(packetSize).order(ByteOrder.LITTLE_ENDIAN) val packet = ByteBuffer.allocate(packetSize).order(ByteOrder.LITTLE_ENDIAN)
@ -1301,9 +1291,9 @@ class SyncSocketSession {
val deferred = CompletableDeferred<List<Pair<String, Long>>>() val deferred = CompletableDeferred<List<Pair<String, Long>>>()
_pendingListKeysRequests[requestId] = deferred _pendingListKeysRequests[requestId] = deferred
try { try {
val publisherBytes = Base64.getDecoder().decode(publisherPublicKey) val publisherBytes = publisherPublicKey.base64ToByteArray()
if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes")
val consumerBytes = Base64.getDecoder().decode(consumerPublicKey) val consumerBytes = consumerPublicKey.base64ToByteArray()
if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes") if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes")
val packet = ByteBuffer.allocate(4 + 32 + 32).order(ByteOrder.LITTLE_ENDIAN) val packet = ByteBuffer.allocate(4 + 32 + 32).order(ByteOrder.LITTLE_ENDIAN)
packet.putInt(requestId) packet.putInt(requestId)

View file

@ -43,7 +43,6 @@ class HistoryListViewHolder : ViewHolder {
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) { constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) {
_root = itemView.findViewById(R.id.root); _root = itemView.findViewById(R.id.root);
_imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail); _imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail);
_imageThumbnail.clipToOutline = true;
_textName = itemView.findViewById(R.id.text_video_name); _textName = itemView.findViewById(R.id.text_video_name);
_textAuthor = itemView.findViewById(R.id.text_author); _textAuthor = itemView.findViewById(R.id.text_author);
_textMetadata = itemView.findViewById(R.id.text_video_metadata); _textMetadata = itemView.findViewById(R.id.text_video_metadata);

Some files were not shown because too many files have changed in this diff Show more