mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ec7e821b | |||
| ca3454afbe | |||
| 1edc8aabf8 | |||
| 91060faac9 | |||
| 17027ba364 | |||
| 8569eaa5db | |||
| d32d817e0a | |||
| a0f4cc760c | |||
| 5247997ea5 | |||
| 453030d561 | |||
| e080702a52 | |||
| 3909343adc | |||
| dc76934d0e | |||
| 6cf47d592a | |||
| 1507c70729 | |||
| d6a23ac0de | |||
| 17df396672 | |||
| 0c5ba0cd39 | |||
| 183aeb18a0 | |||
| 8d08e19cd2 | |||
| a882d04d26 | |||
| c4d06c1ba2 | |||
| 4dfcd47901 | |||
| 4c0c1abb4b | |||
| 6f44071186 | |||
| 29910a2698 | |||
| b5da0d4462 | |||
| 99fb9b3462 | |||
| 5f0a89d13b | |||
| f311561e6f | |||
| 2fc944ddd9 | |||
| a2970b86ee | |||
| ac9a51f105 | |||
| 90dca2537a | |||
| 4df227147c | |||
| 1fb55dca0a | |||
| 3d7b347e49 | |||
| 769ec9f59a | |||
| dee310de3d | |||
| 0af4bad906 | |||
| 4731673ba3 | |||
| 8745221cbd | |||
| 742d95440e | |||
| 180b320cd7 | |||
| cc8dffc485 | |||
| a64fd2cf35 | |||
| 4aceb364d9 | |||
| 76d9bac0ec | |||
| 2b8dc41d0d | |||
| 33430c538c | |||
| 03e9cb398b | |||
| 2aef2ebec1 | |||
| 5e5fffbf97 | |||
| 51ac604e31 | |||
| 4e49b5bc63 | |||
| 658cbc5e00 | |||
| 2ceb4c5644 | |||
| 2738954af7 | |||
| db5aaf0b84 | |||
| e1abb7f8ae | |||
| 3310ac6008 | |||
| 09879c83e9 | |||
| 7aa8b6bc14 | |||
| cac8a8fde4 | |||
| 01cb544dfd | |||
| b9239b6177 | |||
| 96ca3f62a2 | |||
| 73ad783881 | |||
| 3bfcf65535 | |||
| 8b3b27a2a8 | |||
| a4d4835a89 | |||
| 56c0f7bfaf | |||
| 736424ae35 | |||
| 37dc778009 | |||
| cd3cea58a4 | |||
| 8b53e9e5e3 | |||
| 08e98b089c | |||
| 5528d71da8 | |||
| 83f520ca44 | |||
| cc247ce634 | |||
| c6caa59a90 | |||
| 00e28b9ce0 | |||
| 334f58979a | |||
| 940bf163da | |||
| 2bbe0e6133 | |||
| 861f34a287 | |||
| 82f214f155 | |||
| 4ee127fe13 | |||
| 86a4cf8d84 | |||
| 2c463dd5a1 | |||
| ed3820bec0 | |||
| 542a7f212d | |||
| 8fb0826d69 | |||
| deeaa55f56 | |||
| 5b954727a1 | |||
| fae77c1a63 | |||
| b69402dfe9 | |||
| 1f3e306a59 | |||
| a9605118fb | |||
| d22e918273 | |||
| bdcb94055a | |||
| d0644d39da | |||
| 8f3f776e22 | |||
| 548752e240 | |||
| 7f20250951 | |||
| 4d720b1d81 | |||
| 1e4aefb7d5 | |||
| 2a825a9f83 | |||
| a8921a1aba | |||
| edb9eda0a9 | |||
| 3a81676447 | |||
| 6695774037 | |||
| 03132ff77b | |||
| 49ddecdea4 | |||
| a10bc8c7de | |||
| c1e6e401cc | |||
| 44ff951ec6 | |||
| 11319e0ec5 | |||
| 100e98a960 | |||
| c6100ede70 | |||
| a2986a72bd | |||
| e0e90c5f74 | |||
| 11992af81b | |||
| 15d771f7fc | |||
| 5ede474253 | |||
| 7922aa6f80 | |||
| 0c1333fa15 | |||
| 53b9ba0368 | |||
| c3a8877796 | |||
| a464ae9df5 | |||
| 98b6213886 | |||
| b6671c653c | |||
| 55d042bee3 | |||
| 0d16dd0006 | |||
| 48a96140a7 | |||
| 603ef8f295 | |||
| ab07288ba0 | |||
| c0bbe5d491 | |||
| b953ff21e7 | |||
| c14378b534 | |||
| 80034ad131 | |||
| 33d3d9a29c | |||
| 7e83793586 | |||
| 6ba9ec8bc2 | |||
| 0b02ab0e2d | |||
| ff531b5e77 | |||
| b3f9de3b83 | |||
| 86bd71b89c | |||
| 2fca7e9a01 | |||
| 2cc873ef60 | |||
| 7a66ce6bcd | |||
| 2730569b6b | |||
| ede5c4409c | |||
| 0dbe398435 | |||
| bcab3bccbc | |||
| 58c9aeb1a2 | |||
| 4702787784 | |||
| 30c41044da | |||
| e369676808 | |||
| 2fa9e65bee | |||
| cf96bd1ec0 | |||
| 1f5a069877 | |||
| 13100dc38d | |||
| 5227041398 | |||
| adc5013ea4 | |||
| 515c5e00e9 | |||
| 9944842a2f | |||
| 99dc50894c | |||
| 6598dff6df | |||
| ba9f843368 | |||
| 623c47fa2e | |||
| 19861fe812 | |||
| c333300906 | |||
| 0653f88c49 | |||
| 4ce9f64808 | |||
| 4fa0229ccb | |||
| 42dd8d6152 | |||
| 0a839b4814 | |||
| 586db317dd | |||
| ae36a24ad1 | |||
| 9a435f8859 | |||
| 81162c5df2 | |||
| c7c3ddfc96 | |||
| 830d3a9022 | |||
| a1c2d19daf | |||
| bd87a47551 | |||
| 76103a2a8c | |||
| f63f9dd6db |
@@ -26,7 +26,7 @@ body:
|
||||
label: Reproduction steps
|
||||
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
||||
placeholder: |
|
||||
0. Play a Youtube video
|
||||
0. Play a YouTube video
|
||||
1. Press on Download button
|
||||
2. Select quality 1440p
|
||||
3. Grayjay crashes when attempting to download
|
||||
@@ -83,7 +83,7 @@ body:
|
||||
- "Spotify"
|
||||
- "TedTalks"
|
||||
- "Twitch"
|
||||
- "Youtube"
|
||||
- "YouTube"
|
||||
- "Other"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -106,3 +106,9 @@
|
||||
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
||||
path = app/src/stable/assets/sources/crunchyroll
|
||||
url = ../plugins/crunchyroll.git
|
||||
[submodule "app/src/stable/assets/sources/mixcloud"]
|
||||
path = app/src/stable/assets/sources/mixcloud
|
||||
url = ../plugins/mixcloud.git
|
||||
[submodule "app/src/unstable/assets/sources/mixcloud"]
|
||||
path = app/src/unstable/assets/sources/mixcloud
|
||||
url = ../plugins/mixcloud.git
|
||||
|
||||
+4
-2
@@ -154,9 +154,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
//implementation 'com.google.dagger:dagger:2.48'
|
||||
implementation 'androidx.test:monitor:1.7.2'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
@@ -180,6 +181,7 @@ dependencies {
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
|
||||
|
||||
//Exoplayer
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import java.nio.ByteBuffer
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/*
|
||||
class SyncServerTests {
|
||||
|
||||
//private val relayHost = "relay.grayjay.app"
|
||||
@@ -335,4 +335,4 @@ class SyncServerTests {
|
||||
|
||||
class AlwaysAuthorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean get() = true
|
||||
}
|
||||
}*/
|
||||
@@ -13,7 +13,7 @@ import kotlin.random.Random
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/*
|
||||
data class PipeStreams(
|
||||
val initiatorInput: LittleEndianDataInputStream,
|
||||
val initiatorOutput: LittleEndianDataOutputStream,
|
||||
@@ -509,4 +509,4 @@ class Authorized : IAuthorizable {
|
||||
|
||||
class Unauthorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean = false
|
||||
}
|
||||
}*/
|
||||
@@ -103,6 +103,12 @@ class UnavailableException extends ScriptException {
|
||||
super("UnavailableException", msg);
|
||||
}
|
||||
}
|
||||
class ReloadRequiredException extends ScriptException {
|
||||
constructor(msg, reloadData) {
|
||||
super("ReloadRequiredException", msg);
|
||||
this.reloadData = reloadData;
|
||||
}
|
||||
}
|
||||
class AgeException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("AgeException", msg);
|
||||
@@ -245,6 +251,9 @@ class PlatformVideo extends PlatformContent {
|
||||
this.duration = obj.duration ?? -1; //Long
|
||||
this.viewCount = obj.viewCount ?? -1; //Long
|
||||
|
||||
this.playbackTime = obj.playbackTime ?? -1;
|
||||
this.playbackDate = obj.playbackDate ?? undefined;
|
||||
|
||||
this.isLive = obj.isLive ?? false; //Boolean
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
}
|
||||
@@ -458,14 +467,20 @@ class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
this.getLicenseRequestExecutor = () => {
|
||||
return {
|
||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||
return http.POST(
|
||||
const response = http.POST(
|
||||
url,
|
||||
license_request_data,
|
||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||
false,
|
||||
true
|
||||
).body
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.body) {
|
||||
throw new ScriptException("Unable to acquire license key");
|
||||
}
|
||||
|
||||
return response.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,11 +716,12 @@ class LiveEventViewCount extends LiveEvent {
|
||||
}
|
||||
}
|
||||
class LiveEventRaid extends LiveEvent {
|
||||
constructor(targetUrl, targetName, targetThumbnail) {
|
||||
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
|
||||
super(100);
|
||||
this.targetUrl = targetUrl;
|
||||
this.targetName = targetName;
|
||||
this.targetThumbnail = targetThumbnail;
|
||||
this.isOutgoing = isOutgoing ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,6 +794,7 @@ let plugin = {
|
||||
//To override by plugin
|
||||
const source = {
|
||||
getHome() { return new ContentPager([], false, {}); },
|
||||
getShorts() { return new VideoPager([], false, {}); },
|
||||
|
||||
enable(config){ },
|
||||
disable() {},
|
||||
|
||||
@@ -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 0–1 --
|
||||
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 0–255 --
|
||||
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()
|
||||
@@ -2,10 +2,30 @@ package com.futo.platformplayer
|
||||
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.*
|
||||
import com.caoccao.javet.values.reference.IV8ValuePromise
|
||||
import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueError
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.selects.SelectClause0
|
||||
import kotlinx.coroutines.selects.SelectClause1
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
|
||||
|
||||
|
||||
//V8
|
||||
@@ -24,6 +44,10 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
|
||||
return handler(this);
|
||||
}
|
||||
|
||||
inline fun V8Value.getSourcePlugin(): V8Plugin? {
|
||||
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
|
||||
}
|
||||
|
||||
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
|
||||
if(this !is T)
|
||||
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
|
||||
@@ -89,7 +113,29 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
|
||||
.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 {
|
||||
if(false)
|
||||
ensureIsBusy();
|
||||
return when(T::class) {
|
||||
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
|
||||
Int::class -> {
|
||||
@@ -146,4 +192,137 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
|
||||
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
|
||||
map.put(prop, obj.getString(prop));
|
||||
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);
|
||||
|
||||
if(def.estDuration > 0)
|
||||
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
|
||||
|
||||
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));
|
||||
}
|
||||
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
|
||||
var result = this.invoke<V8Value>(method, *obj);
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
|
||||
var result = this.invoke<V8Value>(method, *obj);
|
||||
if(result is V8ValuePromise) {
|
||||
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
|
||||
return result;
|
||||
}
|
||||
return V8Deferred(CompletableDeferred(result));
|
||||
}
|
||||
@@ -584,6 +584,30 @@ class Settings : FragmentedStorageFileJson() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
||||
var shortsPregenerate: Boolean = false;
|
||||
}
|
||||
|
||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||
@@ -999,10 +1023,13 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||
var playlistAllowDups: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
|
||||
var watchLaterAddStart: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
|
||||
var polycentricEnabled: Boolean = true;
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -424,7 +424,7 @@ class UIDialogs {
|
||||
}
|
||||
|
||||
|
||||
fun showCastingDialog(context: Context) {
|
||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
@@ -432,6 +432,7 @@ class UIDialogs {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
} else {
|
||||
@@ -444,21 +445,24 @@ class UIDialogs {
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
fun showCastingTutorialDialog(context: Context) {
|
||||
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
val dialog = CastingHelpDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showCastingAddDialog(context: Context) {
|
||||
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
val dialog = CastingAddDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@@ -129,115 +129,163 @@ class UISlideOverlays {
|
||||
val originalVideo = subscription.doFetchVideos;
|
||||
val originalPosts = subscription.doFetchPosts;
|
||||
|
||||
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||
val menu = SlideUpMenuOverlay(
|
||||
container.context,
|
||||
container,
|
||||
"Subscription Settings",
|
||||
null,
|
||||
true,
|
||||
listOf()
|
||||
);
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||
val capabilities = plugin.getChannelCapabilities();
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||
val capabilities = plugin.getChannelCapabilities();
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
items.addAll(listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_notifications,
|
||||
"Notifications",
|
||||
"",
|
||||
tag = "notifications",
|
||||
call = {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||
"You can select which groups this subscription is part of.",
|
||||
-1, listOf()) else null,
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuRecycler(container.context, "as") {
|
||||
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||
.sortedBy { !it.selected });
|
||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
||||
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||
it.onClick.subscribe {
|
||||
if(it is SubscriptionGroup.Selectable) {
|
||||
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
||||
?: return@subscribe;
|
||||
groups.clear();
|
||||
if(it.selected)
|
||||
actualGroup.urls.remove(subscription.channel.url);
|
||||
else
|
||||
actualGroup.urls.add(subscription.channel.url);
|
||||
withContext(Dispatchers.Main) {
|
||||
items.addAll(
|
||||
listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_notifications,
|
||||
"Notifications",
|
||||
"",
|
||||
tag = "notifications",
|
||||
call = {
|
||||
subscription.doNotifications =
|
||||
menu?.selectOption(null, "notifications", true, true)
|
||||
?: subscription.doNotifications;
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.isNotEmpty()
|
||||
)
|
||||
SlideUpMenuGroup(
|
||||
container.context, "Subscription Groups",
|
||||
"You can select which groups this subscription is part of.",
|
||||
-1, listOf()
|
||||
) else null,
|
||||
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.isNotEmpty()
|
||||
)
|
||||
SlideUpMenuRecycler(container.context, "as") {
|
||||
val groups =
|
||||
ArrayList<SubscriptionGroup>(
|
||||
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map {
|
||||
SubscriptionGroup.Selectable(
|
||||
it,
|
||||
it.urls.contains(subscription.channel.url)
|
||||
)
|
||||
}
|
||||
.sortedBy { !it.selected });
|
||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
|
||||
null;
|
||||
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||
it.onClick.subscribe {
|
||||
if (it is SubscriptionGroup.Selectable) {
|
||||
val actualGroup =
|
||||
StateSubscriptionGroups.instance.getSubscriptionGroup(
|
||||
it.id
|
||||
)
|
||||
?: return@subscribe;
|
||||
groups.clear();
|
||||
if (it.selected)
|
||||
actualGroup.urls.remove(subscription.channel.url);
|
||||
else
|
||||
actualGroup.urls.add(subscription.channel.url);
|
||||
|
||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
||||
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||
.sortedBy { !it.selected });
|
||||
adapter?.notifyContentChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
return@SlideUpMenuRecycler adapter;
|
||||
} else null,
|
||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_live_tv,
|
||||
"Livestreams",
|
||||
"Check for livestreams",
|
||||
tag = "fetchLive",
|
||||
call = {
|
||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Streams",
|
||||
"Check for streams",
|
||||
tag = "fetchStreams",
|
||||
call = {
|
||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Videos",
|
||||
"Check for videos",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Content",
|
||||
"Check for content",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_chat,
|
||||
"Posts",
|
||||
"Check for posts",
|
||||
tag = "fetchPosts",
|
||||
call = {
|
||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null/*,,
|
||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(
|
||||
actualGroup
|
||||
);
|
||||
groups.addAll(
|
||||
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map {
|
||||
SubscriptionGroup.Selectable(
|
||||
it,
|
||||
it.urls.contains(subscription.channel.url)
|
||||
)
|
||||
}
|
||||
.sortedBy { !it.selected });
|
||||
adapter?.notifyContentChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
return@SlideUpMenuRecycler adapter;
|
||||
} else null,
|
||||
SlideUpMenuGroup(
|
||||
container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()
|
||||
),
|
||||
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_live_tv,
|
||||
"Livestreams",
|
||||
"Check for livestreams",
|
||||
tag = "fetchLive",
|
||||
call = {
|
||||
subscription.doFetchLive =
|
||||
menu?.selectOption(null, "fetchLive", true, true)
|
||||
?: subscription.doFetchLive;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Streams",
|
||||
"Check for streams",
|
||||
tag = "fetchStreams",
|
||||
call = {
|
||||
subscription.doFetchStreams =
|
||||
menu?.selectOption(null, "fetchStreams", true, true)
|
||||
?: subscription.doFetchStreams;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Videos",
|
||||
"Check for videos",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos =
|
||||
menu?.selectOption(null, "fetchVideos", true, true)
|
||||
?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_play,
|
||||
"Content",
|
||||
"Check for content",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos =
|
||||
menu?.selectOption(null, "fetchVideos", true, true)
|
||||
?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_chat,
|
||||
"Posts",
|
||||
"Check for posts",
|
||||
tag = "fetchPosts",
|
||||
call = {
|
||||
subscription.doFetchPosts =
|
||||
menu?.selectOption(null, "fetchPosts", true, true)
|
||||
?: subscription.doFetchPosts;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null/*,,
|
||||
|
||||
SlideUpMenuGroup(container.context, "Actions",
|
||||
"Various things you can do with this subscription",
|
||||
@@ -245,61 +293,82 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
||||
showCreateSubscriptionGroup(container, subscription.channel);
|
||||
}, false)*/
|
||||
).filterNotNull());
|
||||
).filterNotNull()
|
||||
);
|
||||
|
||||
menu.setItems(items);
|
||||
menu.setItems(items);
|
||||
|
||||
if(subscription.doNotifications)
|
||||
menu.selectOption(null, "notifications", true, true);
|
||||
if(subscription.doFetchLive)
|
||||
menu.selectOption(null, "fetchLive", true, true);
|
||||
if(subscription.doFetchStreams)
|
||||
menu.selectOption(null, "fetchStreams", true, true);
|
||||
if(subscription.doFetchVideos)
|
||||
menu.selectOption(null, "fetchVideos", true, true);
|
||||
if(subscription.doFetchPosts)
|
||||
menu.selectOption(null, "fetchPosts", true, true);
|
||||
if (subscription.doNotifications)
|
||||
menu.selectOption(null, "notifications", true, true);
|
||||
if (subscription.doFetchLive)
|
||||
menu.selectOption(null, "fetchLive", true, true);
|
||||
if (subscription.doFetchStreams)
|
||||
menu.selectOption(null, "fetchStreams", true, true);
|
||||
if (subscription.doFetchVideos)
|
||||
menu.selectOption(null, "fetchVideos", true, true);
|
||||
if (subscription.doFetchPosts)
|
||||
menu.selectOption(null, "fetchPosts", true, true);
|
||||
|
||||
menu.onOK.subscribe {
|
||||
subscription.save();
|
||||
menu.hide(true);
|
||||
menu.onOK.subscribe {
|
||||
subscription.save();
|
||||
menu.hide(true);
|
||||
|
||||
if(subscription.doNotifications && !originalNotif) {
|
||||
val mainContext = StateApp.instance.contextOrNull;
|
||||
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
||||
if (subscription.doNotifications && !originalNotif) {
|
||||
val mainContext = StateApp.instance.contextOrNull;
|
||||
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||
UIDialogs.toast(
|
||||
container.context,
|
||||
"Enable 'Background Update' in settings for notifications to work"
|
||||
);
|
||||
|
||||
if(mainContext is MainActivity) {
|
||||
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
|
||||
"You need to set a Background Updating interval for notifications", null, 0,
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Configure", {
|
||||
val intent = Intent(mainContext, SettingsActivity::class.java);
|
||||
intent.putExtra("query", mainContext.getString(R.string.background_update));
|
||||
mainContext.startActivity(intent);
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
return@subscribe;
|
||||
}
|
||||
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||
UIDialogs.toast(container.context, "Android notifications are disabled");
|
||||
if(mainContext is MainActivity) {
|
||||
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
||||
if (mainContext is MainActivity) {
|
||||
UIDialogs.showDialog(
|
||||
mainContext,
|
||||
R.drawable.ic_settings,
|
||||
"Background Updating Required",
|
||||
"You need to set a Background Updating interval for notifications",
|
||||
null,
|
||||
0,
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Configure", {
|
||||
val intent = Intent(
|
||||
mainContext,
|
||||
SettingsActivity::class.java
|
||||
);
|
||||
intent.putExtra(
|
||||
"query",
|
||||
mainContext.getString(R.string.background_update)
|
||||
);
|
||||
mainContext.startActivity(intent);
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
return@subscribe;
|
||||
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||
UIDialogs.toast(
|
||||
container.context,
|
||||
"Android notifications are disabled"
|
||||
);
|
||||
if (mainContext is MainActivity) {
|
||||
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
menu.onCancel.subscribe {
|
||||
subscription.doNotifications = originalNotif;
|
||||
subscription.doFetchLive = originalLive;
|
||||
subscription.doFetchStreams = originalStream;
|
||||
subscription.doFetchVideos = originalVideo;
|
||||
subscription.doFetchPosts = originalPosts;
|
||||
};
|
||||
};
|
||||
menu.onCancel.subscribe {
|
||||
subscription.doNotifications = originalNotif;
|
||||
subscription.doFetchLive = originalLive;
|
||||
subscription.doFetchStreams = originalStream;
|
||||
subscription.doFetchVideos = originalVideo;
|
||||
subscription.doFetchPosts = originalPosts;
|
||||
};
|
||||
|
||||
menu.setOk("Save");
|
||||
menu.setOk("Save");
|
||||
|
||||
menu.show();
|
||||
menu.show();
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show subscription overlay.", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,6 +1220,8 @@ class UISlideOverlays {
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
else
|
||||
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.others.LoginWebViewClient
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -74,6 +75,11 @@ class LoginActivity : AppCompatActivity() {
|
||||
finish();
|
||||
};
|
||||
var isFirstLoad = true;
|
||||
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
|
||||
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
|
||||
var currentScale = 100;
|
||||
var currentDesktop = false;
|
||||
_webView.setInitialScale(50);
|
||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||
_textUrl.setText(url ?: "");
|
||||
|
||||
@@ -86,6 +92,48 @@ class LoginActivity : AppCompatActivity() {
|
||||
//TODO: Find most reliable way to wait for page js to finish
|
||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||
}
|
||||
|
||||
if(loginWarnings.size > 0 && url != null) {
|
||||
synchronized(loginWarnings) {
|
||||
val warning = loginWarnings.find { url.matches(it.getRegex()) };
|
||||
if(warning != null) {
|
||||
if(warning.once == true)
|
||||
loginWarnings.remove(warning);
|
||||
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
|
||||
UIDialogs.Action("Understood", {
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
var specifiedScale = false;
|
||||
var specifiedDesktop = false;
|
||||
if(uiMods.size > 0 && url != null) {
|
||||
synchronized(uiMods) {
|
||||
val uimod = uiMods.find { url.matches(it.getRegex()) };
|
||||
if(uimod != null) {
|
||||
if(uimod.scale != null) {
|
||||
currentScale =(uimod.scale * 100).toInt();
|
||||
_webView.setInitialScale(currentScale);
|
||||
specifiedScale = true;
|
||||
}
|
||||
if(uimod.desktop != null && uimod.desktop) {
|
||||
_webView.settings.useWideViewPort = true;
|
||||
specifiedDesktop = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!specifiedScale && currentScale != 100) {
|
||||
currentScale = (100).toInt();
|
||||
_webView.setInitialScale(currentScale);
|
||||
}
|
||||
if(!specifiedDesktop && currentDesktop) {
|
||||
_webView.settings.useWideViewPort = false;
|
||||
currentDesktop = false;
|
||||
}
|
||||
*/
|
||||
}
|
||||
_webView.settings.domStorageEnabled = true;
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.whenStateAtLeast
|
||||
import androidx.lifecycle.withStateAtLeast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
@@ -63,6 +62,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||
@@ -114,7 +114,6 @@ import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
@@ -171,6 +170,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||
lateinit var _fragHistory: HistoryFragment;
|
||||
lateinit var _fragShorts: ShortsFragment;
|
||||
lateinit var _fragSourceDetail: SourceDetailFragment;
|
||||
lateinit var _fragDownloads: DownloadsFragment;
|
||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||
@@ -340,6 +340,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragWebDetail = WebDetailFragment.newInstance();
|
||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||
_fragHistory = HistoryFragment.newInstance();
|
||||
_fragShorts = ShortsFragment.newInstance();
|
||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||
_fragDownloads = DownloadsFragment();
|
||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||
@@ -610,6 +611,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
)
|
||||
}
|
||||
|
||||
//startActivity(Intent(this, TestActivity::class.java))
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1253,6 +1256,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
WebDetailFragment::class -> _fragWebDetail as T;
|
||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||
HistoryFragment::class -> _fragHistory as T;
|
||||
ShortsFragment::class -> _fragShorts as T;
|
||||
SourceDetailFragment::class -> _fragSourceDetail as T;
|
||||
DownloadsFragment::class -> _fragDownloads as T;
|
||||
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
|
||||
|
||||
@@ -14,10 +14,12 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.polycentric.core.ContentType
|
||||
@@ -29,6 +31,9 @@ import com.futo.polycentric.core.toBase64Url
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import userpackage.Protocol.ExportBundle
|
||||
import userpackage.Protocol.URLInfo
|
||||
@@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
private lateinit var _imageQR: ImageView;
|
||||
private lateinit var _exportBundle: String;
|
||||
private lateinit var _textQR: TextView;
|
||||
private lateinit var _loader: View
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
@@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_polycentric_backup);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
_buttonShare = findViewById(R.id.button_share);
|
||||
_buttonCopy = findViewById(R.id.button_copy);
|
||||
_imageQR = findViewById(R.id.image_qr);
|
||||
_textQR = findViewById(R.id.text_qr);
|
||||
_buttonShare = findViewById(R.id.button_share)
|
||||
_buttonCopy = findViewById(R.id.button_copy)
|
||||
_imageQR = findViewById(R.id.image_qr)
|
||||
_textQR = findViewById(R.id.text_qr)
|
||||
_loader = findViewById(R.id.progress_loader)
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
};
|
||||
|
||||
_exportBundle = createExportBundle();
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
_loader.visibility = View.VISIBLE
|
||||
_buttonShare.visibility = View.INVISIBLE
|
||||
_buttonCopy.visibility = View.INVISIBLE
|
||||
|
||||
try {
|
||||
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
|
||||
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
||||
_imageQR.setImageBitmap(qrCodeBitmap);
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
|
||||
_imageQR.visibility = View.INVISIBLE;
|
||||
_textQR.visibility = View.INVISIBLE;
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val pair = withContext(Dispatchers.IO) {
|
||||
val bundle = createExportBundle()
|
||||
val dimension = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
||||
).toInt()
|
||||
val qr = generateQRCode(bundle, dimension, dimension)
|
||||
Pair(bundle, qr)
|
||||
}
|
||||
|
||||
_exportBundle = pair.first
|
||||
_imageQR.setImageBitmap(pair.second)
|
||||
_imageQR.visibility = View.VISIBLE
|
||||
_textQR.visibility = View.VISIBLE
|
||||
_buttonShare.visibility = View.VISIBLE
|
||||
_buttonCopy.visibility = View.VISIBLE
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
_buttonShare.visibility = View.INVISIBLE
|
||||
_buttonCopy.visibility = View.INVISIBLE
|
||||
} finally {
|
||||
_loader.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
_buttonShare.onClick.subscribe {
|
||||
|
||||
@@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TestActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_test);
|
||||
|
||||
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
|
||||
view.startLoader(10000)
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(5000)
|
||||
view.startLoader()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
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.structures.IPager
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
|
||||
@@ -36,6 +37,11 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getHome(): IPager<IPlatformContent>
|
||||
|
||||
/**
|
||||
* Gets the shorts feed
|
||||
*/
|
||||
fun getShorts(): IPager<IPlatformVideo>
|
||||
|
||||
//Search
|
||||
/**
|
||||
* Gets search suggestion for the provided query string
|
||||
@@ -176,6 +182,10 @@ interface IPlatformClient {
|
||||
* Retrieves the subscriptions of the currently logged in user
|
||||
*/
|
||||
fun getUserSubscriptions(): Array<String>;
|
||||
/**
|
||||
* Retrieves the history of the currently logged in user
|
||||
*/
|
||||
fun getUserHistory(): IPager<IPlatformContent>;
|
||||
|
||||
|
||||
fun isClaimTypeSupported(claimType: Int): Boolean;
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
||||
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -26,12 +27,17 @@ class LiveChatManager {
|
||||
private val _emojiCache: EmojiCache = EmojiCache();
|
||||
private val _pager: IPager<IPlatformLiveEvent>?;
|
||||
|
||||
private var _position: Long = 0;
|
||||
private var _eventsPosition: Long = 0;
|
||||
|
||||
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
|
||||
|
||||
private var _startCounter = 0;
|
||||
|
||||
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
|
||||
|
||||
val isVOD get() = _pager is JSVODEventPager;
|
||||
|
||||
var viewCount: Long = 0
|
||||
private set;
|
||||
|
||||
@@ -39,8 +45,24 @@ class LiveChatManager {
|
||||
_scope = scope;
|
||||
_pager = pager;
|
||||
viewCount = initialViewCount;
|
||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||
handleEvents(pager.getResults());
|
||||
if(pager is JSVODEventPager)
|
||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||
else
|
||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||
|
||||
if(pager is JSVODEventPager) {
|
||||
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
|
||||
//TODO: Remove this once dripfeed is done properly
|
||||
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
|
||||
if(replayResults.size > 0) {
|
||||
_eventsPosition = replayResults.maxOf { it.time };
|
||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||
}
|
||||
else
|
||||
_eventsPosition = _eventsPosition + 1500;
|
||||
}
|
||||
else
|
||||
handleEvents(pager.getResults());
|
||||
}
|
||||
|
||||
fun start() {
|
||||
@@ -52,6 +74,10 @@ class LiveChatManager {
|
||||
_startCounter++;
|
||||
}
|
||||
|
||||
fun setVideoPosition(ms: Long) {
|
||||
_position = ms;
|
||||
}
|
||||
|
||||
fun getHistory(): List<IPlatformLiveEvent> {
|
||||
synchronized(_history) {
|
||||
return _history.toList();
|
||||
@@ -85,13 +111,34 @@ class LiveChatManager {
|
||||
try {
|
||||
while(_startCounter == counter) {
|
||||
var nextInterval = 1000L;
|
||||
if(_pager is JSVODEventPager && _eventsPosition > _position) {
|
||||
delay(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if(_pager == null || !_pager.hasMorePages())
|
||||
return@launch;
|
||||
_pager.nextPage();
|
||||
val newEvents = _pager.getResults();
|
||||
val newEvents = if(_pager is JSVODEventPager) {
|
||||
val requestPosition = _position;
|
||||
_pager.nextPage(requestPosition.toInt());
|
||||
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
|
||||
if(replayResults.size > 0) {
|
||||
_eventsPosition = replayResults.maxOf { it.time };
|
||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||
}
|
||||
else
|
||||
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||
replayResults;
|
||||
}
|
||||
else {
|
||||
_pager.nextPage();
|
||||
_pager.getResults();
|
||||
}
|
||||
if(_pager is JSLiveEventPager)
|
||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||
else if(_pager is JSVODEventPager)
|
||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||
|
||||
if(newEvents.size > 0)
|
||||
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
||||
|
||||
@@ -20,7 +20,8 @@ data class PlatformClientCapabilities(
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false,
|
||||
val hasGetChannelPlaylists: Boolean = false,
|
||||
val hasGetContentRecommendations: Boolean = false
|
||||
val hasGetContentRecommendations: Boolean = false,
|
||||
val hasGetUserHistory: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -34,8 +34,10 @@ class PlatformClientPool {
|
||||
isDead = true;
|
||||
onDead.emit(parentClient, this);
|
||||
|
||||
for(clientPair in _pool) {
|
||||
clientPair.key.disable();
|
||||
synchronized(_pool) {
|
||||
for (clientPair in _pool) {
|
||||
clientPair.key.disable();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
@@ -44,6 +45,7 @@ class PlatformID {
|
||||
val NONE = PlatformID("Unknown", null);
|
||||
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
||||
value.ensureIsBusy();
|
||||
val contextName = "PlatformID";
|
||||
return PlatformID(
|
||||
value.getOrThrow(config, "platform", contextName),
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -33,6 +34,7 @@ open class PlatformAuthorLink {
|
||||
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
|
||||
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||
value.ensureIsBusy();
|
||||
if(value.has("membershipUrl"))
|
||||
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
||||
value.ensureIsBusy();
|
||||
val context = "AuthorMembershipLink"
|
||||
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||
value.getOrThrow(config ,"name", context),
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.expectV8Variant
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
@@ -46,6 +47,7 @@ class ResultCapabilities(
|
||||
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
|
||||
val contextName = "ResultCapabilities";
|
||||
value.ensureIsBusy();
|
||||
return ResultCapabilities(
|
||||
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
|
||||
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
|
||||
@@ -69,6 +71,7 @@ class FilterGroup(
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
|
||||
value.ensureIsBusy();
|
||||
return FilterGroup(
|
||||
value.getString("name"),
|
||||
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
|
||||
@@ -90,6 +93,7 @@ class FilterCapability(
|
||||
|
||||
companion object {
|
||||
fun fromV8(obj: V8ValueObject): FilterCapability {
|
||||
obj.ensureIsBusy();
|
||||
val value = obj.get("value") as V8Value;
|
||||
return FilterCapability(
|
||||
obj.getString("name"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -31,6 +32,7 @@ class Thumbnails {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||
value.ensureIsBusy();
|
||||
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
||||
.toArray()
|
||||
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
|
||||
|
||||
@@ -2,14 +2,17 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
interface IPlatformLiveEvent {
|
||||
val type : LiveEventType;
|
||||
var time: Long;
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
|
||||
obj.ensureIsBusy();
|
||||
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||
return when(t) {
|
||||
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -17,16 +18,21 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
val colorName: String?;
|
||||
val badges: List<String>;
|
||||
|
||||
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
|
||||
this.name = name;
|
||||
this.message = message;
|
||||
this.thumbnail = thumbnail;
|
||||
this.colorName = colorName;
|
||||
this.badges = badges ?: listOf();
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
|
||||
obj.ensureIsBusy();
|
||||
|
||||
val contextName = "LiveEventComment"
|
||||
|
||||
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
|
||||
@@ -36,7 +42,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
obj.getOrThrow(config, "name", contextName),
|
||||
obj.getOrThrow(config, "thumbnail", contextName, true),
|
||||
obj.getOrThrow(config, "message", contextName),
|
||||
colorName, badges);
|
||||
colorName, badges,
|
||||
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -20,6 +21,8 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
|
||||
var expire: Int = 6000;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
|
||||
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
|
||||
this.name = name;
|
||||
@@ -37,6 +40,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventDonation"
|
||||
return LiveEventDonation(
|
||||
obj.getOrThrow(config, "name", contextName),
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class LiveEventEmojis: IPlatformLiveEvent {
|
||||
@@ -9,15 +10,17 @@ class LiveEventEmojis: IPlatformLiveEvent {
|
||||
|
||||
val emojis: HashMap<String, String>;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(emojis: HashMap<String, String>) {
|
||||
this.emojis = emojis;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventEmojis"
|
||||
return LiveEventEmojis(
|
||||
obj.getOrThrow(config, "emojis", contextName));
|
||||
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class LiveEventRaid: IPlatformLiveEvent {
|
||||
@@ -10,20 +12,26 @@ class LiveEventRaid: IPlatformLiveEvent {
|
||||
val targetName: String;
|
||||
val targetThumbnail: String;
|
||||
val targetUrl: String;
|
||||
val isOutgoing: Boolean;
|
||||
|
||||
constructor(name: String, url: String, thumbnail: String) {
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
|
||||
this.targetName = name;
|
||||
this.targetUrl = url;
|
||||
this.targetThumbnail = thumbnail;
|
||||
this.isOutgoing = isOutgoing;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventRaid"
|
||||
return LiveEventRaid(
|
||||
obj.getOrThrow(config, "targetName", contextName),
|
||||
obj.getOrThrow(config, "targetUrl", contextName),
|
||||
obj.getOrThrow(config, "targetThumbnail", contextName));
|
||||
obj.getOrThrow(config, "targetThumbnail", contextName),
|
||||
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class LiveEventViewCount: IPlatformLiveEvent {
|
||||
@@ -9,12 +10,15 @@ class LiveEventViewCount: IPlatformLiveEvent {
|
||||
|
||||
val viewCount: Int;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(viewCount: Int) {
|
||||
this.viewCount = viewCount;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventViewCount"
|
||||
return LiveEventViewCount(
|
||||
obj.getOrThrow(config, "viewCount", contextName));
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.orDefault
|
||||
import com.futo.platformplayer.serializers.IRatingSerializer
|
||||
@@ -13,8 +14,12 @@ interface IRating {
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
|
||||
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
|
||||
obj?.ensureIsBusy();
|
||||
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
|
||||
};
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
|
||||
obj.ensureIsBusy();
|
||||
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||
return when(t) {
|
||||
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
|
||||
obj.ensureIsBusy();
|
||||
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
|
||||
obj.ensureIsBusy();
|
||||
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
|
||||
obj.ensureIsBusy()
|
||||
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.video
|
||||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
/**
|
||||
* A search result representing a video (overview data)
|
||||
@@ -12,6 +13,9 @@ interface IPlatformVideo : IPlatformContent {
|
||||
val duration: Long;
|
||||
val viewCount: Long;
|
||||
|
||||
val playbackTime: Long;
|
||||
val playbackDate: OffsetDateTime?;
|
||||
|
||||
val isLive : Boolean;
|
||||
|
||||
val isShort: Boolean;
|
||||
|
||||
+6
-3
@@ -3,11 +3,10 @@ package com.futo.platformplayer.api.media.models.video
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.polycentric.core.combineHashCodes
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
@@ -18,7 +17,7 @@ open class SerializedPlatformVideo(
|
||||
override val contentType: ContentType = ContentType.MEDIA,
|
||||
override val id: PlatformID,
|
||||
override val name: String,
|
||||
override val thumbnails: Thumbnails,
|
||||
override val thumbnails: Thumbnails = Thumbnails(),
|
||||
override val author: PlatformAuthorLink,
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
@JsonNames("datetime", "dateTime")
|
||||
@@ -33,6 +32,10 @@ open class SerializedPlatformVideo(
|
||||
|
||||
override val isLive: Boolean = false;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
override fun toJson() : String {
|
||||
return Json.encodeToString(this);
|
||||
}
|
||||
|
||||
+4
-1
@@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
@@ -43,6 +42,10 @@ open class SerializedPlatformVideoDetails(
|
||||
) : IPlatformVideo, IPlatformVideoDetails {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
override val isLive: Boolean get() = false;
|
||||
|
||||
override val dash: IDashManifestSource? get() = null;
|
||||
|
||||
@@ -56,6 +56,7 @@ class DevJSClient : JSClient {
|
||||
|
||||
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
|
||||
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
|
||||
client.setReloadData(getReloadData(true));
|
||||
if (noSaveState)
|
||||
client.initialize()
|
||||
return client
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
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.platforms.js.internal.JSCallDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||
@@ -43,6 +44,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -59,9 +61,13 @@ import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Random
|
||||
import kotlin.Exception
|
||||
import kotlin.reflect.full.findAnnotations
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
@@ -83,6 +89,8 @@ open class JSClient : IPlatformClient {
|
||||
private var _channelCapabilities: ResultCapabilities? = null;
|
||||
private var _peekChannelTypes: List<String>? = null;
|
||||
|
||||
private var _usedReloadData: String? = null;
|
||||
|
||||
protected val _script: String;
|
||||
|
||||
private var _initialized: Boolean = false;
|
||||
@@ -98,14 +106,14 @@ open class JSClient : IPlatformClient {
|
||||
override val icon: ImageVariable;
|
||||
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
|
||||
|
||||
private val _busyLock = Object();
|
||||
private var _busyCounter = 0;
|
||||
private var _busyAction = "";
|
||||
val isBusy: Boolean get() = _busyCounter > 0;
|
||||
val isBusy: Boolean get() = _plugin.isBusy;
|
||||
val isBusyAction: String get() {
|
||||
return _busyAction;
|
||||
}
|
||||
|
||||
val declareOnEnable = HashMap<String, String>();
|
||||
|
||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||
|
||||
val flags: Array<String>;
|
||||
@@ -118,6 +126,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
|
||||
|
||||
fun getSubscriptionRateLimit(): Int? {
|
||||
val pluginRateLimit = config.subscriptionRateLimit;
|
||||
@@ -197,6 +206,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
|
||||
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
|
||||
client.setReloadData(getReloadData(true));
|
||||
if (noSaveState)
|
||||
client.initialize()
|
||||
return client
|
||||
@@ -213,14 +223,31 @@ open class JSClient : IPlatformClient {
|
||||
return plugin.httpClientOthers[id];
|
||||
}
|
||||
|
||||
fun setReloadData(data: String?) {
|
||||
if(data == null) {
|
||||
if(declareOnEnable.containsKey("__reloadData"))
|
||||
declareOnEnable.remove("__reloadData");
|
||||
}
|
||||
else
|
||||
declareOnEnable.put("__reloadData", data ?: "");
|
||||
}
|
||||
fun getReloadData(orLast: Boolean): String? {
|
||||
if(declareOnEnable.containsKey("__reloadData"))
|
||||
return declareOnEnable["__reloadData"];
|
||||
else if(orLast)
|
||||
return _usedReloadData;
|
||||
return null;
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
if (_initialized) return
|
||||
|
||||
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
||||
plugin.start();
|
||||
|
||||
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
|
||||
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
|
||||
|
||||
|
||||
descriptor.appSettings.loadDefaults(descriptor.config);
|
||||
|
||||
_initialized = true;
|
||||
@@ -245,7 +272,8 @@ open class JSClient : IPlatformClient {
|
||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
|
||||
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -260,19 +288,28 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
|
||||
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
|
||||
fun enable() {
|
||||
fun enable() = isBusyWith("enable") {
|
||||
if(!_initialized)
|
||||
initialize();
|
||||
for(toDeclare in declareOnEnable) {
|
||||
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
|
||||
}
|
||||
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
|
||||
|
||||
if(declareOnEnable.containsKey("__reloadData")) {
|
||||
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
|
||||
_usedReloadData = declareOnEnable["__reloadData"];
|
||||
declareOnEnable.remove("__reloadData");
|
||||
}
|
||||
_enabled = true;
|
||||
}
|
||||
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
|
||||
fun saveState(): String? {
|
||||
fun saveState(): String? = isBusyWith("saveState") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSaveState)
|
||||
return null;
|
||||
return@isBusyWith null;
|
||||
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
|
||||
return resp;
|
||||
return@isBusyWith resp;
|
||||
}
|
||||
|
||||
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
|
||||
@@ -295,6 +332,13 @@ open class JSClient : IPlatformClient {
|
||||
plugin.executeTyped("source.getHome()"));
|
||||
}
|
||||
|
||||
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
|
||||
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
|
||||
ensureEnabled()
|
||||
return@isBusyWith JSVideoPager(config, this,
|
||||
plugin.executeTyped("source.getShorts()"))
|
||||
}
|
||||
|
||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||
@@ -313,8 +357,10 @@ open class JSClient : IPlatformClient {
|
||||
return _searchCapabilities!!;
|
||||
}
|
||||
|
||||
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
|
||||
return _searchCapabilities!!;
|
||||
return busy {
|
||||
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
|
||||
return@busy _searchCapabilities!!;
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("getSearchCapabilities", ex);
|
||||
@@ -342,8 +388,10 @@ open class JSClient : IPlatformClient {
|
||||
if (_searchChannelContentsCapabilities != null)
|
||||
return _searchChannelContentsCapabilities!!;
|
||||
|
||||
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
|
||||
return _searchChannelContentsCapabilities!!;
|
||||
return busy {
|
||||
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
|
||||
return@busy _searchChannelContentsCapabilities!!;
|
||||
}
|
||||
}
|
||||
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
|
||||
@JSDocsParameter("channelUrl", "Channel url to search")
|
||||
@@ -375,14 +423,14 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
||||
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
||||
override fun isChannelUrl(url: String): Boolean {
|
||||
override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") {
|
||||
try {
|
||||
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
|
||||
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("isChannelUrl", ex);
|
||||
return false;
|
||||
return@isBusyWith false;
|
||||
}
|
||||
}
|
||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||
@@ -400,9 +448,10 @@ open class JSClient : IPlatformClient {
|
||||
if (_channelCapabilities != null) {
|
||||
return _channelCapabilities!!;
|
||||
}
|
||||
|
||||
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
|
||||
return _channelCapabilities!!;
|
||||
return busy {
|
||||
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
|
||||
return@busy _channelCapabilities!!;
|
||||
};
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("getChannelCapabilities", ex);
|
||||
@@ -513,14 +562,14 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
|
||||
@JSDocsParameter("url", "A content url (May not be your platform)")
|
||||
override fun isContentDetailsUrl(url: String): Boolean {
|
||||
override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") {
|
||||
try {
|
||||
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
|
||||
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("isContentDetailsUrl", ex);
|
||||
return false;
|
||||
return@isBusyWith false;
|
||||
}
|
||||
}
|
||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||
@@ -552,7 +601,7 @@ open class JSClient : IPlatformClient {
|
||||
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
|
||||
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
|
||||
if(tracker is V8ValueObject)
|
||||
return@isBusyWith JSPlaybackTracker(config, tracker);
|
||||
return@isBusyWith JSPlaybackTracker(this, tracker);
|
||||
else
|
||||
return@isBusyWith null;
|
||||
}
|
||||
@@ -594,7 +643,6 @@ open class JSClient : IPlatformClient {
|
||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||
}
|
||||
|
||||
|
||||
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
||||
@JSDocsParameter("url", "Url of content")
|
||||
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
||||
@@ -622,17 +670,19 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional
|
||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun isPlaylistUrl(url: String): Boolean {
|
||||
override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
|
||||
if (!capabilities.hasGetPlaylist)
|
||||
return false;
|
||||
return@isBusyWith false;
|
||||
|
||||
try {
|
||||
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
return@isBusyWith busy {
|
||||
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||
return false;
|
||||
return@isBusyWith false;
|
||||
}
|
||||
}
|
||||
@JSOptional
|
||||
@@ -663,6 +713,13 @@ open class JSClient : IPlatformClient {
|
||||
.toTypedArray();
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
|
||||
override fun getUserHistory(): IPager<IPlatformContent> {
|
||||
ensureEnabled();
|
||||
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
||||
}
|
||||
|
||||
fun validate() {
|
||||
try {
|
||||
plugin.start();
|
||||
@@ -734,19 +791,29 @@ open class JSClient : IPlatformClient {
|
||||
return urls;
|
||||
}
|
||||
|
||||
|
||||
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||
try {
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter++;
|
||||
}
|
||||
_busyAction = actionName;
|
||||
return handle();
|
||||
fun <T> busy(handle: ()->T): T {
|
||||
return _plugin.busy {
|
||||
return@busy handle();
|
||||
}
|
||||
finally {
|
||||
_busyAction = "";
|
||||
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;
|
||||
return@busy handle();
|
||||
|
||||
}
|
||||
finally {
|
||||
_busyAction = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-3
@@ -1,6 +1,10 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Dictionary
|
||||
|
||||
@Serializable
|
||||
class SourcePluginAuthConfig(
|
||||
val loginUrl: String,
|
||||
val completionUrl: String? = null,
|
||||
@@ -11,5 +15,44 @@ class SourcePluginAuthConfig(
|
||||
val userAgent: String? = null,
|
||||
val loginButton: String? = null,
|
||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||
val loginWarning: String? = null
|
||||
) { }
|
||||
val loginWarning: String? = null,
|
||||
val loginWarnings: List<Warning>? = null,
|
||||
val uiMods: List<UIMod>? = null
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
class Warning(
|
||||
val url: String,
|
||||
val text: String?,
|
||||
val details: String? = null,
|
||||
val once: Boolean? = true
|
||||
) {
|
||||
@Contextual
|
||||
private var _regex: Regex? = null;
|
||||
|
||||
fun getRegex(): Regex {
|
||||
return _regex ?: url.let {
|
||||
val reg = Regex(it);
|
||||
_regex = reg;
|
||||
return reg;
|
||||
}
|
||||
}
|
||||
}
|
||||
@Serializable
|
||||
class UIMod(
|
||||
val url: String,
|
||||
val scale: Float?,
|
||||
val desktop: Boolean?
|
||||
) {
|
||||
@Contextual
|
||||
private var _regex: Regex? = null;
|
||||
|
||||
fun getRegex(): Regex {
|
||||
return _regex ?: url.let {
|
||||
val reg = Regex(it);
|
||||
_regex = reg;
|
||||
return reg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import com.futo.platformplayer.SignatureProvider
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import kotlinx.serialization.Contextual
|
||||
@@ -47,6 +48,7 @@ class SourcePluginConfig(
|
||||
var subscriptionRateLimit: Int? = null,
|
||||
var enableInSearch: Boolean = true,
|
||||
var enableInHome: Boolean = true,
|
||||
var enableInShorts: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf(),
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null,
|
||||
@@ -168,12 +170,17 @@ class SourcePluginConfig(
|
||||
}
|
||||
|
||||
fun validate(text: String): Boolean {
|
||||
if(scriptPublicKey.isNullOrEmpty())
|
||||
throw IllegalStateException("No public key present");
|
||||
if(scriptSignature.isNullOrEmpty())
|
||||
throw IllegalStateException("No signature present");
|
||||
try {
|
||||
if (scriptPublicKey.isNullOrEmpty())
|
||||
throw IllegalStateException("No public key present");
|
||||
if (scriptSignature.isNullOrEmpty())
|
||||
throw IllegalStateException("No signature present");
|
||||
|
||||
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
|
||||
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun isUrlAllowed(url: String): Boolean {
|
||||
@@ -204,6 +211,8 @@ class SourcePluginConfig(
|
||||
obj.sourceUrl = sourceUrl;
|
||||
return obj;
|
||||
}
|
||||
|
||||
private val TAG = "SourcePluginConfig"
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
||||
+20
-2
@@ -5,10 +5,16 @@ import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -103,12 +109,22 @@ class SourcePluginDescriptor {
|
||||
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
||||
var enableHome: Boolean? = null;
|
||||
|
||||
|
||||
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
||||
var enableSearch: Boolean? = null;
|
||||
|
||||
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
|
||||
var enableShorts: Boolean? = null;
|
||||
}
|
||||
|
||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
|
||||
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync")
|
||||
var sync = Sync();
|
||||
@Serializable
|
||||
class Sync {
|
||||
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
|
||||
var enableHistorySync: Boolean? = null;
|
||||
}
|
||||
|
||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
||||
var rateLimit = RateLimit();
|
||||
@Serializable
|
||||
class RateLimit {
|
||||
@@ -143,6 +159,8 @@ class SourcePluginDescriptor {
|
||||
tabEnabled.enableHome = config.enableInHome
|
||||
if(tabEnabled.enableSearch == null)
|
||||
tabEnabled.enableSearch = config.enableInSearch
|
||||
if(tabEnabled.enableShorts == null)
|
||||
tabEnabled.enableShorts = config.enableInShorts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient {
|
||||
|
||||
}
|
||||
|
||||
fun resetAuthCookies() {
|
||||
_currentCookieMap.clear();
|
||||
if(!_auth?.cookieMap.isNullOrEmpty()) {
|
||||
for(domainCookies in _auth!!.cookieMap!!)
|
||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||
}
|
||||
if(!_captcha?.cookieMap.isNullOrEmpty()) {
|
||||
for(domainCookies in _captcha!!.cookieMap!!) {
|
||||
if(_currentCookieMap.containsKey(domainCookies.key))
|
||||
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
|
||||
else
|
||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
fun clearOtherCookies() {
|
||||
_otherCookieMap.clear();
|
||||
}
|
||||
|
||||
override fun clone(): ManagedHttpClient {
|
||||
val newClient = JSHttpClient(_jsClient, _auth);
|
||||
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -13,6 +14,7 @@ interface IJSContent: IPlatformContent {
|
||||
|
||||
companion object {
|
||||
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
|
||||
obj.ensureIsBusy();
|
||||
val config = plugin.config;
|
||||
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
|
||||
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
|
||||
|
||||
+2
@@ -6,12 +6,14 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
interface IJSContentDetails: IPlatformContent {
|
||||
|
||||
companion object {
|
||||
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
|
||||
obj.ensureIsBusy();
|
||||
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
|
||||
return when(ContentType.fromInt(type)) {
|
||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||
|
||||
+3
-2
@@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||
@@ -85,12 +86,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
|
||||
}
|
||||
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
@@ -60,7 +61,7 @@ class JSComment : IPlatformComment {
|
||||
if(!_hasGetReplies)
|
||||
return null;
|
||||
|
||||
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
|
||||
return JSCommentPager(_config!!, plugin, obj);
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") {
|
||||
super.nextPage();
|
||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||
}
|
||||
|
||||
+27
-18
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
|
||||
abstract class JSPager<T> : IPager<T> {
|
||||
@@ -18,8 +19,8 @@ abstract class JSPager<T> : IPager<T> {
|
||||
protected var pager: V8ValueObject;
|
||||
|
||||
private var _lastResults: List<T>? = null;
|
||||
private var _resultChanged: Boolean = true;
|
||||
private var _hasMorePages: Boolean = false;
|
||||
protected var _resultChanged: Boolean = true;
|
||||
protected var _hasMorePages: Boolean = false;
|
||||
//private var _morePagesWasFalse: Boolean = false;
|
||||
|
||||
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
|
||||
@@ -29,7 +30,9 @@ abstract class JSPager<T> : IPager<T> {
|
||||
this.pager = pager;
|
||||
this.config = config;
|
||||
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
plugin.busy {
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
}
|
||||
getResults();
|
||||
}
|
||||
|
||||
@@ -38,17 +41,20 @@ abstract class JSPager<T> : IPager<T> {
|
||||
}
|
||||
|
||||
override fun hasMorePages(): Boolean {
|
||||
return _hasMorePages;
|
||||
return _hasMorePages && !pager.isClosed;
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
warnIfMainThread("JSPager.nextPage");
|
||||
|
||||
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
||||
pager.invoke("nextPage", arrayOf<Any>());
|
||||
};
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
_resultChanged = true;
|
||||
val pluginV8 = plugin.getUnderlyingPlugin();
|
||||
pluginV8.busy {
|
||||
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
||||
pager.invokeV8("nextPage", arrayOf<Any>());
|
||||
};
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
_resultChanged = true;
|
||||
}
|
||||
/*
|
||||
try {
|
||||
}
|
||||
@@ -70,15 +76,18 @@ abstract class JSPager<T> : IPager<T> {
|
||||
return previousResults;
|
||||
|
||||
warnIfMainThread("JSPager.getResults");
|
||||
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
||||
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
|
||||
throw IllegalStateException("Runtime closed");
|
||||
val newResults = items.toArray()
|
||||
.map { convertResult(it as V8ValueObject) }
|
||||
.toList();
|
||||
_lastResults = newResults;
|
||||
_resultChanged = false;
|
||||
return newResults;
|
||||
|
||||
return plugin.getUnderlyingPlugin().busy {
|
||||
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
||||
if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
|
||||
throw IllegalStateException("Runtime closed");
|
||||
val newResults = items.toArray()
|
||||
.map { convertResult(it as V8ValueObject) }
|
||||
.toList();
|
||||
_lastResults = newResults;
|
||||
_resultChanged = false;
|
||||
return@busy newResults;
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun convertResult(obj: V8ValueObject): T;
|
||||
|
||||
+44
-23
@@ -2,37 +2,51 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
|
||||
class JSPlaybackTracker: IPlaybackTracker {
|
||||
private val _config: IV8PluginConfig;
|
||||
private val _obj: V8ValueObject;
|
||||
private lateinit var _client: JSClient;
|
||||
private lateinit var _config: IV8PluginConfig;
|
||||
private lateinit var _obj: V8ValueObject;
|
||||
|
||||
private var _hasCalledInit: Boolean = false;
|
||||
private val _hasInit: Boolean;
|
||||
private var _hasInit: Boolean = false;
|
||||
|
||||
private var _lastRequest: Long = Long.MIN_VALUE;
|
||||
|
||||
private val _hasOnConcluded: Boolean;
|
||||
private var _hasOnConcluded: Boolean = false;
|
||||
|
||||
override var nextRequest: Int = 1000
|
||||
private set;
|
||||
|
||||
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
warnIfMainThread("JSPlaybackTracker.constructor");
|
||||
if(!obj.has("onProgress"))
|
||||
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
|
||||
if(!obj.has("nextRequest"))
|
||||
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
|
||||
_hasOnConcluded = obj.has("onConcluded");
|
||||
|
||||
this._config = config;
|
||||
this._obj = obj;
|
||||
this._hasInit = obj.has("onInit");
|
||||
client.busy {
|
||||
if (!obj.has("onProgress"))
|
||||
throw ScriptImplementationException(
|
||||
client.config,
|
||||
"Missing onProgress on PlaybackTracker"
|
||||
);
|
||||
if (!obj.has("nextRequest"))
|
||||
throw ScriptImplementationException(
|
||||
client.config,
|
||||
"Missing nextRequest on PlaybackTracker"
|
||||
);
|
||||
_hasOnConcluded = obj.has("onConcluded");
|
||||
|
||||
this._client = client;
|
||||
this._config = client.config;
|
||||
this._obj = obj;
|
||||
this._hasInit = obj.has("onInit");
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInit(seconds: Double) {
|
||||
@@ -40,12 +54,15 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||
synchronized(_obj) {
|
||||
if(_hasCalledInit)
|
||||
return;
|
||||
if (_hasInit) {
|
||||
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
|
||||
_obj.invokeVoid("onInit", seconds);
|
||||
|
||||
_client.busy {
|
||||
if (_hasInit) {
|
||||
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
|
||||
_obj.invokeV8Void("onInit", seconds);
|
||||
}
|
||||
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
|
||||
_hasCalledInit = true;
|
||||
}
|
||||
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
|
||||
_hasCalledInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +72,12 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||
if(!_hasCalledInit && _hasInit)
|
||||
onInit(seconds);
|
||||
else {
|
||||
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
|
||||
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
|
||||
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
|
||||
_lastRequest = System.currentTimeMillis();
|
||||
_client.busy {
|
||||
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
|
||||
_obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying);
|
||||
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
|
||||
_lastRequest = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +86,9 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||
if(_hasOnConcluded) {
|
||||
synchronized(_obj) {
|
||||
Logger.i("JSPlaybackTracker", "onConcluded");
|
||||
_obj.invokeVoid("onConcluded", -1);
|
||||
_client.busy {
|
||||
_obj.invokeV8Void("onConcluded", -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
@@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
return null;
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
|
||||
+60
-54
@@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -46,52 +48,55 @@ class JSRequestExecutor {
|
||||
if (_executor.isClosed)
|
||||
throw IllegalStateException("Executor object is closed");
|
||||
|
||||
val result = if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers, method, body);
|
||||
} as V8Value;
|
||||
}
|
||||
return _plugin.getUnderlyingPlugin().busy {
|
||||
|
||||
val result = if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeV8("executeRequest", url, headers, method, body);
|
||||
} as V8Value;
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers, method, body);
|
||||
_executor.invokeV8("executeRequest", url, headers, method, body);
|
||||
} as V8Value;
|
||||
|
||||
try {
|
||||
if(result is V8ValueString) {
|
||||
val base64Result = Base64.getDecoder().decode(result.value);
|
||||
return base64Result;
|
||||
}
|
||||
if(result is V8ValueTypedArray) {
|
||||
val buffer = result.buffer;
|
||||
val byteBuffer = buffer.byteBuffer;
|
||||
val bytesResult = ByteArray(result.byteLength);
|
||||
byteBuffer.get(bytesResult, 0, result.byteLength);
|
||||
buffer.close();
|
||||
return bytesResult;
|
||||
}
|
||||
if(result is V8ValueObject && result.has("type")) {
|
||||
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
|
||||
when(type) {
|
||||
//TODO: Buffer type?
|
||||
try {
|
||||
if(result is V8ValueString) {
|
||||
val base64Result = Base64.getDecoder().decode(result.value);
|
||||
return@busy base64Result;
|
||||
}
|
||||
if(result is V8ValueTypedArray) {
|
||||
val buffer = result.buffer;
|
||||
val byteBuffer = buffer.byteBuffer;
|
||||
val bytesResult = ByteArray(result.byteLength);
|
||||
byteBuffer.get(bytesResult, 0, result.byteLength);
|
||||
buffer.close();
|
||||
return@busy bytesResult;
|
||||
}
|
||||
if(result is V8ValueObject && result.has("type")) {
|
||||
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
|
||||
when(type) {
|
||||
//TODO: Buffer type?
|
||||
}
|
||||
}
|
||||
if(result is V8ValueUndefined) {
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
|
||||
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
|
||||
}
|
||||
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
|
||||
}
|
||||
if(result is V8ValueUndefined) {
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
|
||||
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
|
||||
finally {
|
||||
result.close();
|
||||
}
|
||||
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
|
||||
}
|
||||
finally {
|
||||
result.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,24 +104,25 @@ class JSRequestExecutor {
|
||||
open fun cleanup() {
|
||||
if (!hasCleanup || _executor.isClosed)
|
||||
return;
|
||||
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeVoid("cleanup", null);
|
||||
};
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeVoid("cleanup", null);
|
||||
};
|
||||
_plugin.busy {
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeV8("cleanup", null);
|
||||
};
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
_config,
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invokeV8("cleanup", null);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
|
||||
+17
-10
@@ -11,12 +11,14 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
|
||||
class JSRequestModifier: IRequestModifier {
|
||||
private val _plugin: JSClient;
|
||||
private val _config: IV8PluginConfig;
|
||||
private var _modifier: V8ValueObject;
|
||||
override var allowByteSkip: Boolean;
|
||||
override var allowByteSkip: Boolean = false;
|
||||
|
||||
constructor(plugin: JSClient, modifier: V8ValueObject) {
|
||||
this._plugin = plugin;
|
||||
@@ -24,10 +26,13 @@ class JSRequestModifier: IRequestModifier {
|
||||
this._config = plugin.config;
|
||||
val config = plugin.config;
|
||||
|
||||
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
|
||||
plugin.busy {
|
||||
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
|
||||
|
||||
if(!modifier.has("modifyRequest"))
|
||||
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
|
||||
}
|
||||
|
||||
if(!modifier.has("modifyRequest"))
|
||||
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
|
||||
}
|
||||
|
||||
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
|
||||
@@ -35,13 +40,15 @@ class JSRequestModifier: IRequestModifier {
|
||||
return Request(url, headers);
|
||||
}
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
|
||||
_modifier.invoke("modifyRequest", url, headers);
|
||||
} as V8ValueObject;
|
||||
return _plugin.busy {
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
|
||||
_modifier.invokeV8("modifyRequest", url, headers);
|
||||
} as V8ValueObject;
|
||||
|
||||
val req = JSRequest(_plugin, result, url, headers);
|
||||
result.close();
|
||||
return req;
|
||||
val req = JSRequest(_plugin, result, url, headers);
|
||||
result.close();
|
||||
return@busy req;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
-2
@@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getSourcePlugin
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -35,8 +37,11 @@ class JSSubtitleSource : ISubtitleSource {
|
||||
override fun getSubtitles(): String {
|
||||
if(!hasFetch)
|
||||
throw IllegalStateException("This subtitle doesn't support getSubtitles..");
|
||||
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
|
||||
return v8String.value;
|
||||
|
||||
return _obj.getSourcePlugin()?.busy {
|
||||
val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
|
||||
return@busy v8String.value;
|
||||
} ?: "";
|
||||
}
|
||||
|
||||
override suspend fun getSubtitlesURI(): Uri? {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class JSVODEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
||||
override var nextRequest: Int;
|
||||
|
||||
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
|
||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||
}
|
||||
|
||||
fun nextPage(ms: Int) = plugin.isBusyWith("JSLiveEventPager.nextPage") {
|
||||
warnIfMainThread("VODEventPager.nextPage");
|
||||
|
||||
val pluginV8 = plugin.getUnderlyingPlugin();
|
||||
pluginV8.busy {
|
||||
val newPager: V8Value = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage(...)") {
|
||||
pager.invokeV8<V8Value>("nextPage", ms);
|
||||
};
|
||||
if(newPager is V8ValueObject)
|
||||
pager = newPager;
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
_resultChanged = true;
|
||||
}
|
||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||
}
|
||||
|
||||
override fun nextPage() = nextPage(0);
|
||||
|
||||
override fun convertResult(obj: V8ValueObject): IPlatformLiveEvent {
|
||||
return IPlatformLiveEvent.fromV8(config, obj, "LiveEventPager");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
@@ -17,6 +21,10 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
final override val duration: Long;
|
||||
final override val viewCount: Long;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
final override val isLive: Boolean;
|
||||
final override val isShort: Boolean;
|
||||
|
||||
@@ -29,5 +37,11 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||
playbackTime = _content.getOrDefault<Long>(config, "playbackTime", contextName, -1)?.toLong() ?: -1;
|
||||
val playbackDateInt = _content.getOrDefault<Int>(config, "playbackDate", contextName, null)?.toLong();
|
||||
if(playbackDateInt == null || playbackDateInt == 0.toLong())
|
||||
playbackDate = null;
|
||||
else
|
||||
playbackDate = OffsetDateTime.of(LocalDateTime.ofEpochSecond(playbackDateInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
+39
-15
@@ -7,6 +7,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
@@ -24,12 +25,17 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
private val _plugin: JSClient;
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
private val _hasGetPlaybackTracker: Boolean;
|
||||
private val _hasGetVODEvents: Boolean;
|
||||
|
||||
//Details
|
||||
override val description : String;
|
||||
@@ -45,9 +51,9 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
|
||||
override val subtitles: List<ISubtitleSource>;
|
||||
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
|
||||
val contextName = "VideoDetails";
|
||||
_plugin = plugin;
|
||||
val config = plugin.config;
|
||||
description = _content.getOrThrow(config, "description", contextName);
|
||||
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
|
||||
@@ -69,6 +75,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
_hasGetVODEvents = _content.has("getVODEvents");
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||
@@ -82,14 +89,16 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
return getPlaybackTrackerJS();
|
||||
}
|
||||
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
|
||||
return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
|
||||
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>())
|
||||
?: return@catchScriptErrors null;
|
||||
if(tracker is V8ValueObject)
|
||||
return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker);
|
||||
else
|
||||
return@catchScriptErrors null;
|
||||
};
|
||||
return _plugin.busy {
|
||||
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
|
||||
val tracker = _content.invokeV8<V8Value>("getPlaybackTracker", arrayOf<Any>())
|
||||
?: return@catchScriptErrors null;
|
||||
if(tracker is V8ValueObject)
|
||||
return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
|
||||
else
|
||||
return@catchScriptErrors null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
@@ -106,8 +115,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
return null;
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
return _plugin.busy {
|
||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return@busy JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
@@ -123,10 +134,23 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
|
||||
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
|
||||
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
||||
return null;
|
||||
return _plugin.busy {
|
||||
val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
|
||||
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
||||
return@busy null;
|
||||
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
return@busy JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
}
|
||||
|
||||
fun hasVODEvents(): Boolean{
|
||||
return _hasGetVODEvents;
|
||||
}
|
||||
fun getVODEvents(url: String): IPager<IPlatformLiveEvent>? = _plugin.busy {
|
||||
if(!_hasGetVODEvents)
|
||||
return@busy null;
|
||||
|
||||
return@busy JSVODEventPager(_plugin.config, _plugin,
|
||||
_content.invokeV8<V8ValueObject>("getVODEvents", arrayOf<Any>()));
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -6,6 +6,8 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
|
||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
@@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
|
||||
+72
-8
@@ -1,6 +1,8 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.V8Deferred
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
@@ -13,8 +15,14 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Async
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
@@ -50,6 +58,56 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null;
|
||||
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||
_pregenerate = generateAsync(scope);
|
||||
return _pregenerate;
|
||||
}
|
||||
|
||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||
if(!hasGenerate)
|
||||
return V8Deferred(CompletableDeferred(manifest));
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val pregenerated = _pregenerate;
|
||||
if(pregenerated != null) {
|
||||
Logger.w("JSDashManifestRawAudioSource", "Returning pre-generated audio");
|
||||
return pregenerated;
|
||||
}
|
||||
|
||||
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? {
|
||||
if(!hasGenerate)
|
||||
return manifest;
|
||||
@@ -62,21 +120,27 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
if(_plugin is DevJSClient)
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
_plugin.isBusyWith("dashAudio.generate") {
|
||||
_obj.invokeV8<V8ValueString>("generate").value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
_plugin.isBusyWith("dashAudio.generate") {
|
||||
_obj.invokeV8<V8ValueString>("generate").value;
|
||||
}
|
||||
}
|
||||
|
||||
if(result != null){
|
||||
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);
|
||||
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 result;
|
||||
|
||||
+100
-9
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.V8Deferred
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
@@ -15,11 +16,19 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Async
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
interface IJSDashManifestRawSource {
|
||||
val hasGenerate: Boolean;
|
||||
var manifest: String?;
|
||||
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
|
||||
fun generate(): String?;
|
||||
}
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
@@ -32,7 +41,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
|
||||
var url: String?;
|
||||
val url: String?;
|
||||
override var manifest: String?;
|
||||
|
||||
override val hasGenerate: Boolean;
|
||||
@@ -57,6 +66,56 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null;
|
||||
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||
_pregenerate = generateAsync(scope);
|
||||
return _pregenerate;
|
||||
}
|
||||
|
||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||
if(!hasGenerate)
|
||||
return V8Deferred(CompletableDeferred(manifest));
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
val pregenerated = _pregenerate;
|
||||
if(pregenerated != null) {
|
||||
Logger.w("JSDashManifestRawSource", "Returning pre-generated video");
|
||||
return pregenerated;
|
||||
}
|
||||
|
||||
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? {
|
||||
if(!hasGenerate)
|
||||
return manifest;
|
||||
@@ -67,22 +126,28 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
if(_plugin is DevJSClient) {
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
_plugin.isBusyWith("dashVideo.generate") {
|
||||
_obj.invokeV8<V8ValueString>("generate").value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
_plugin.isBusyWith("dashVideo.generate") {
|
||||
_obj.invokeV8<V8ValueString>("generate").value;
|
||||
}
|
||||
});
|
||||
|
||||
if(result != null){
|
||||
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);
|
||||
_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 result;
|
||||
@@ -110,6 +175,32 @@ class JSDashManifestMergingRawSource(
|
||||
override val priority: Boolean
|
||||
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? {
|
||||
val videoDash = video.generate();
|
||||
val audioDash = audio.generate();
|
||||
|
||||
+3
-1
@@ -9,6 +9,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
|
||||
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
IDashManifestWidevineSource, JSSource {
|
||||
@@ -45,7 +47,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
|
||||
+9
-2
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.orNull
|
||||
@@ -38,7 +39,13 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
|
||||
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? {
|
||||
obj?.ensureIsBusy();
|
||||
return obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }
|
||||
};
|
||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource {
|
||||
obj.ensureIsBusy();
|
||||
return JSHLSManifestAudioSource(plugin, obj)
|
||||
};
|
||||
}
|
||||
}
|
||||
+39
-17
@@ -14,7 +14,9 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.orNull
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
@@ -53,36 +55,39 @@ abstract class JSSource {
|
||||
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
|
||||
}
|
||||
|
||||
fun getRequestModifier(): IRequestModifier? {
|
||||
fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
|
||||
if(_requestModifier != null)
|
||||
return AdhocRequestModifier { url, headers ->
|
||||
return@isBusyWith AdhocRequestModifier { url, headers ->
|
||||
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
||||
};
|
||||
|
||||
if (!hasRequestModifier || _obj.isClosed)
|
||||
return null;
|
||||
return@isBusyWith null;
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
|
||||
_obj.invoke("getRequestModifier", arrayOf<Any>());
|
||||
_obj.invokeV8("getRequestModifier", arrayOf<Any>());
|
||||
};
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null;
|
||||
return@isBusyWith null;
|
||||
|
||||
return JSRequestModifier(_plugin, result)
|
||||
return@isBusyWith JSRequestModifier(_plugin, result)
|
||||
}
|
||||
open fun getRequestExecutor(): JSRequestExecutor? {
|
||||
open fun getRequestExecutor(): JSRequestExecutor? = _plugin.isBusyWith("getRequestExecutor") {
|
||||
if (!hasRequestExecutor || _obj.isClosed)
|
||||
return null;
|
||||
return@isBusyWith null;
|
||||
|
||||
Logger.v("JSSource", "Request executor for [${type}] requesting");
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
|
||||
_obj.invoke("getRequestExecutor", arrayOf<Any>());
|
||||
_obj.invokeV8("getRequestExecutor", arrayOf<Any>());
|
||||
};
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null;
|
||||
Logger.v("JSSource", "Request executor for [${type}] received");
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
if (result !is V8ValueObject)
|
||||
return@isBusyWith null;
|
||||
|
||||
return@isBusyWith JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
fun getUnderlyingPlugin(): JSClient? {
|
||||
@@ -105,8 +110,12 @@ abstract class JSSource {
|
||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
|
||||
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? {
|
||||
obj?.ensureIsBusy();
|
||||
return obj.orNull { fromV8Video(plugin, it as V8ValueObject) }
|
||||
};
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
|
||||
obj.ensureIsBusy()
|
||||
val type = obj.getString("plugin_type");
|
||||
return when(type) {
|
||||
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||
@@ -123,13 +132,26 @@ abstract class JSSource {
|
||||
}
|
||||
}
|
||||
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
|
||||
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
|
||||
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
|
||||
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
|
||||
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource{
|
||||
obj.ensureIsBusy();
|
||||
return JSDashManifestSource(plugin, obj)
|
||||
};
|
||||
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource{
|
||||
obj.ensureIsBusy()
|
||||
return JSDashManifestRawSource(plugin, obj);
|
||||
}
|
||||
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource {
|
||||
obj?.ensureIsBusy();
|
||||
return JSDashManifestRawAudioSource(plugin, obj)
|
||||
};
|
||||
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
|
||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource {
|
||||
obj.ensureIsBusy();
|
||||
return JSHLSManifestSource(plugin, obj)
|
||||
};
|
||||
|
||||
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
|
||||
obj.ensureIsBusy();
|
||||
val type = obj.getString("plugin_type");
|
||||
return when(type) {
|
||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
|
||||
@@ -31,6 +32,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
|
||||
|
||||
|
||||
fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
|
||||
obj.ensureIsBusy();
|
||||
val type = obj.getString("plugin_type")
|
||||
return when(type) {
|
||||
TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
|
||||
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
@@ -25,7 +26,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
|
||||
+5
-2
@@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
@@ -19,7 +18,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
@@ -53,6 +52,10 @@ class LocalVideoDetails: IPlatformVideoDetails {
|
||||
override val isLive: Boolean = false;
|
||||
override val isShort: Boolean = false;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
constructor(file: File) {
|
||||
id = PlatformID("Local", file.path, "LOCAL")
|
||||
name = file.name;
|
||||
|
||||
+5
-7
@@ -7,12 +7,12 @@ import java.util.stream.IntStream
|
||||
* A Content MultiPager that returns results based on a specified distribution
|
||||
* TODO: Merge all basic distribution pagers
|
||||
*/
|
||||
class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
||||
class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
|
||||
|
||||
private val dist : HashMap<IPager<IPlatformContent>, Float>;
|
||||
private val distConsumed : HashMap<IPager<IPlatformContent>, Float>;
|
||||
private val dist : HashMap<IPager<T>, Float>;
|
||||
private val distConsumed : HashMap<IPager<T>, Float>;
|
||||
|
||||
constructor(pagers : Map<IPager<IPlatformContent>, Float>) : super(pagers.keys.toMutableList()) {
|
||||
constructor(pagers : Map<IPager<T>, Float>, pageSize: Int = 9) : super(pagers.keys.toMutableList(), false, pageSize) {
|
||||
val distTotal = pagers.values.sum();
|
||||
dist = HashMap();
|
||||
|
||||
@@ -25,7 +25,7 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||
override fun selectItemIndex(options: Array<SelectionOption<T>>): Int {
|
||||
if(options.size == 0)
|
||||
return -1;
|
||||
var bestIndex = 0;
|
||||
@@ -42,6 +42,4 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
||||
distConsumed[options[bestIndex].pager.getPager()] = bestConsumed;
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
override var usedRemoteAddress: InetAddress? = null;
|
||||
override var localAddress: InetAddress? = null;
|
||||
override val canSetVolume: Boolean get() = true;
|
||||
override val canSetSpeed: Boolean get() = false; //TODO: Implement
|
||||
override val canSetSpeed: Boolean get() = true;
|
||||
|
||||
var addresses: Array<InetAddress>? = null;
|
||||
var port: Int = 0;
|
||||
@@ -62,6 +62,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
private val MAX_LAUNCH_RETRIES = 3
|
||||
private var _lastLaunchTime_ms = 0L
|
||||
private var _retryJob: Job? = null
|
||||
private var _autoLaunchEnabled = true
|
||||
|
||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||
this.name = name;
|
||||
@@ -144,6 +145,23 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json);
|
||||
}
|
||||
|
||||
override fun changeSpeed(speed: Double) {
|
||||
if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return
|
||||
|
||||
val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0)
|
||||
setSpeed(speedClamped)
|
||||
val mediaSessionId = _mediaSessionId ?: return
|
||||
val transportId = _transportId ?: return
|
||||
val setSpeedObject = JSONObject().apply {
|
||||
put("type", "SET_PLAYBACK_RATE")
|
||||
put("mediaSessionId", mediaSessionId)
|
||||
put("playbackRate", speedClamped)
|
||||
put("requestId", _requestId++)
|
||||
}
|
||||
|
||||
sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString())
|
||||
}
|
||||
|
||||
override fun changeVolume(volume: Double) {
|
||||
if (invokeInIOScopeIfRequired({ changeVolume(volume) })) {
|
||||
return;
|
||||
@@ -288,6 +306,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
return;
|
||||
}
|
||||
|
||||
_autoLaunchEnabled = true
|
||||
_started = true;
|
||||
_sessionId = null;
|
||||
_launchRetries = 0
|
||||
@@ -529,6 +548,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
if (appId == "CC1AD845") {
|
||||
sessionIsRunning = true;
|
||||
_autoLaunchEnabled = false
|
||||
|
||||
if (_sessionId == null) {
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
@@ -541,7 +561,6 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
_transportId = transportId;
|
||||
|
||||
requestMediaStatus();
|
||||
playVideo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,21 +570,22 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) {
|
||||
_sessionId = null
|
||||
_mediaSessionId = null
|
||||
setTime(0.0)
|
||||
_transportId = null
|
||||
|
||||
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
||||
_launchRetries++
|
||||
launchPlayer()
|
||||
} else if (!_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||
// Maybe the first GET_STATUS came back empty; still try launching
|
||||
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
||||
_launching = true
|
||||
_launchRetries++
|
||||
launchPlayer()
|
||||
if (_autoLaunchEnabled) {
|
||||
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
||||
_launchRetries++
|
||||
launchPlayer()
|
||||
} else {
|
||||
// Maybe the first GET_STATUS came back empty; still try launching
|
||||
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
||||
_launching = true
|
||||
_launchRetries++
|
||||
launchPlayer()
|
||||
}
|
||||
} else {
|
||||
Logger.e(TAG, "Player not found after $_launchRetries attempts; giving up.")
|
||||
Logger.e(TAG, "Player not found ($_launchRetries, _autoLaunchEnabled = $_autoLaunchEnabled); giving up.")
|
||||
Logger.i(TAG, "Unable to start media receiver on device")
|
||||
stop()
|
||||
}
|
||||
@@ -582,6 +602,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
} else {
|
||||
_launching = false
|
||||
_launchRetries = 0
|
||||
_autoLaunchEnabled = false
|
||||
}
|
||||
|
||||
val volume = status.getJSONObject("volume");
|
||||
@@ -619,10 +640,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
stopVideo();
|
||||
}
|
||||
}
|
||||
|
||||
val needsLoad = statuses.length() == 0 || (statuses.getJSONObject(0).getString("playerState") == "IDLE")
|
||||
if (needsLoad && _contentId != null && _mediaSessionId == null) {
|
||||
Logger.i(TAG, "Receiver idle, sending initial LOAD")
|
||||
playVideo()
|
||||
}
|
||||
} else if (type == "CLOSE") {
|
||||
if (message.sourceId == "receiver-0") {
|
||||
Logger.i(TAG, "Close received.");
|
||||
stop();
|
||||
stopCasting();
|
||||
} else if (_transportId == message.sourceId) {
|
||||
throw Exception("Transport id closed.")
|
||||
}
|
||||
@@ -659,6 +686,10 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
localAddress = null;
|
||||
_started = false;
|
||||
|
||||
_contentId = null
|
||||
_contentType = null
|
||||
_streamType = null
|
||||
|
||||
_retryJob?.cancel()
|
||||
_retryJob = null
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
headerBytesRead += read
|
||||
}
|
||||
|
||||
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
||||
val size = ((buffer[3].toUByte().toLong() shl 24) or (buffer[2].toUByte().toLong() shl 16) or (buffer[1].toUByte().toLong() shl 8) or buffer[0].toUByte().toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||
break
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.builders.DashBuilder
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
@@ -64,6 +65,7 @@ import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class StateCasting {
|
||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||
@@ -89,6 +91,7 @@ class StateCasting {
|
||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||
private var _nsdManager: NsdManager? = null
|
||||
val isCasting: Boolean get() = activeDevice != null;
|
||||
private val _castId = AtomicInteger(0)
|
||||
|
||||
private val _discoveryListeners = mapOf(
|
||||
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||
@@ -432,129 +435,112 @@ class StateCasting {
|
||||
action();
|
||||
}
|
||||
|
||||
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean {
|
||||
val ad = activeDevice ?: return false;
|
||||
if (ad.connectionState != CastConnectionState.CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
fun cancel() {
|
||||
_castId.incrementAndGet()
|
||||
}
|
||||
|
||||
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
|
||||
suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val ad = activeDevice ?: return@withContext false;
|
||||
if (ad.connectionState != CastConnectionState.CONNECTED) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
var sourceCount = 0;
|
||||
if (videoSource != null) sourceCount++;
|
||||
if (audioSource != null) sourceCount++;
|
||||
if (subtitleSource != null) sourceCount++;
|
||||
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
|
||||
val castId = _castId.incrementAndGet()
|
||||
|
||||
if (sourceCount < 1) {
|
||||
throw Exception("At least one source should be specified.");
|
||||
}
|
||||
var sourceCount = 0;
|
||||
if (videoSource != null) sourceCount++;
|
||||
if (audioSource != null) sourceCount++;
|
||||
if (subtitleSource != null) sourceCount++;
|
||||
|
||||
if (sourceCount > 1) {
|
||||
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
|
||||
if (ad is AirPlayCastingDevice) {
|
||||
Logger.i(TAG, "Casting as local HLS");
|
||||
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||
if (sourceCount < 1) {
|
||||
throw Exception("At least one source should be specified.");
|
||||
}
|
||||
|
||||
if (sourceCount > 1) {
|
||||
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
|
||||
if (ad is AirPlayCastingDevice) {
|
||||
Logger.i(TAG, "Casting as local HLS");
|
||||
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as local DASH");
|
||||
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||
}
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as local DASH");
|
||||
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
|
||||
}
|
||||
} else {
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||
if (isRawDash) {
|
||||
Logger.i(TAG, "Casting as raw DASH");
|
||||
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||
if (isRawDash) {
|
||||
Logger.i(TAG, "Casting as raw DASH");
|
||||
|
||||
try {
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||
}
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||
} else {
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else if (ad is AirPlayCastingDevice) {
|
||||
Logger.i(TAG, "Casting as HLS indirect");
|
||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else {
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else if (ad is AirPlayCastingDevice) {
|
||||
Logger.i(TAG, "Casting as HLS indirect");
|
||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as DASH indirect");
|
||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
}
|
||||
Logger.i(TAG, "Casting as DASH indirect");
|
||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
if (videoSource is IVideoUrlSource) {
|
||||
val videoPath = "/video-${id}"
|
||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
||||
Logger.i(TAG, "Casting as singular video");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
} else if (audioSource is IAudioUrlSource) {
|
||||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
||||
Logger.i(TAG, "Casting as singular audio");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
} else if(videoSource is IHLSManifestSource) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied HLS");
|
||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as non-proxied HLS");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||
}
|
||||
} else if(audioSource is IHLSManifestAudioSource) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||
}
|
||||
} else if (videoSource is LocalVideoSource) {
|
||||
Logger.i(TAG, "Casting as local video");
|
||||
castLocalVideo(video, videoSource, resumePosition, speed);
|
||||
} else if (audioSource is LocalAudioSource) {
|
||||
Logger.i(TAG, "Casting as local audio");
|
||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||
} else if (videoSource is JSDashManifestRawSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
|
||||
}
|
||||
}
|
||||
} else if (audioSource is JSDashManifestRawAudioSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var str = listOf(
|
||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
|
||||
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
|
||||
).filterNotNull().joinToString(", ");
|
||||
throw UnsupportedCastException(str);
|
||||
}
|
||||
}
|
||||
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
return true;
|
||||
if (videoSource is IVideoUrlSource) {
|
||||
val videoPath = "/video-${id}"
|
||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
||||
Logger.i(TAG, "Casting as singular video");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
} else if (audioSource is IAudioUrlSource) {
|
||||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
||||
Logger.i(TAG, "Casting as singular audio");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
} else if(videoSource is IHLSManifestSource) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied HLS");
|
||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as non-proxied HLS");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||
}
|
||||
} else if(audioSource is IHLSManifestAudioSource) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||
}
|
||||
} else if (videoSource is LocalVideoSource) {
|
||||
Logger.i(TAG, "Casting as local video");
|
||||
castLocalVideo(video, videoSource, resumePosition, speed);
|
||||
} else if (audioSource is LocalAudioSource) {
|
||||
Logger.i(TAG, "Casting as local audio");
|
||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||
} else if (videoSource is JSDashManifestRawSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||
} else if (audioSource is JSDashManifestRawAudioSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
|
||||
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||
} else {
|
||||
var str = listOf(
|
||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
|
||||
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
|
||||
).filterNotNull().joinToString(", ");
|
||||
throw UnsupportedCastException(str);
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext true;
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeVideo(): Boolean {
|
||||
@@ -766,7 +752,7 @@ class StateCasting {
|
||||
|
||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
@@ -1129,9 +1115,14 @@ class StateCasting {
|
||||
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||
}
|
||||
|
||||
private fun shouldProxyStreams(castingDevice: CastingDevice, videoSource: IVideoSource?, audioSource: IAudioSource?): Boolean {
|
||||
val hasRequestModifier = (videoSource as? JSSource)?.hasRequestModifier == true || (audioSource as? JSSource)?.hasRequestModifier == true
|
||||
return Settings.instance.casting.alwaysProxyRequests || castingDevice !is FCastCastingDevice || hasRequestModifier
|
||||
}
|
||||
|
||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
@@ -1236,7 +1227,7 @@ class StateCasting {
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
cleanExecutors()
|
||||
@@ -1283,20 +1274,48 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
|
||||
var dashContent = withContext(Dispatchers.IO) {
|
||||
var dashContent: String = withContext(Dispatchers.IO) {
|
||||
stopVideo()
|
||||
|
||||
//TODO: Include subtitlesURl in the future
|
||||
return@withContext if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||
val deferred = if (audioSource != null && videoSource != null) {
|
||||
JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO)
|
||||
} else if (audioSource != null) {
|
||||
audioSource.generate()
|
||||
audioSource.generateAsync(_scopeIO)
|
||||
} else if (videoSource != null) {
|
||||
videoSource.generate()
|
||||
videoSource.generateAsync(_scopeIO)
|
||||
} else {
|
||||
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||
null
|
||||
}
|
||||
|
||||
if (deferred != null) {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (deferred.estDuration >= 0) {
|
||||
onLoadingEstimate?.invoke(deferred.estDuration)
|
||||
} else {
|
||||
onLoading?.invoke(true)
|
||||
}
|
||||
}
|
||||
deferred.await()
|
||||
} finally {
|
||||
if (castId == _castId.get()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
onLoading?.invoke(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return@withContext null
|
||||
}
|
||||
} ?: throw Exception("Dash is null")
|
||||
|
||||
if (castId != _castId.get()) {
|
||||
Log.i(TAG, "Get DASH cancelled.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
for (representation in representationRegex.findAll(dashContent)) {
|
||||
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||
|
||||
@@ -47,10 +47,10 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||
|
||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject> {
|
||||
val remotes = mutableListOf<V8RemoteObject>();
|
||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject?> {
|
||||
val remotes = mutableListOf<V8RemoteObject?>();
|
||||
for(obj in objs)
|
||||
remotes.add(createRemoteObject(obj)!!);
|
||||
remotes.add(createRemoteObject(obj));
|
||||
return remotes;
|
||||
}
|
||||
private inline fun <reified T> createRemoteObject(obj: T): V8RemoteObject? {
|
||||
|
||||
@@ -106,7 +106,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||
};
|
||||
|
||||
_buttonTutorial.setOnClickListener {
|
||||
UIDialogs.showCastingTutorialDialog(context)
|
||||
UIDialogs.showCastingTutorialDialog(context, ownerActivity)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
private fun performDismiss(shouldShowCastingDialog: Boolean = true) {
|
||||
if (shouldShowCastingDialog) {
|
||||
UIDialogs.showCastingDialog(context);
|
||||
UIDialogs.showCastingDialog(context, ownerActivity);
|
||||
}
|
||||
|
||||
dismiss();
|
||||
|
||||
@@ -53,7 +53,7 @@ class CastingHelpDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||
dismiss()
|
||||
UIDialogs.showCastingAddDialog(context)
|
||||
UIDialogs.showCastingAddDialog(context, ownerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
_buttonClose.setOnClickListener { dismiss(); };
|
||||
_buttonAdd.setOnClickListener {
|
||||
UIDialogs.showCastingAddDialog(context);
|
||||
UIDialogs.showCastingAddDialog(context, ownerActivity);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
@@ -139,9 +139,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
|
||||
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
|
||||
@@ -6,12 +6,16 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||
import com.futo.platformplayer.readBytes
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ImportOptionsDialog: AlertDialog {
|
||||
private val _context: MainActivity;
|
||||
@@ -41,8 +45,17 @@ class ImportOptionsDialog: AlertDialog {
|
||||
_button_import_zip.onClick.subscribe {
|
||||
dismiss();
|
||||
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
|
||||
val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
|
||||
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val zipBytes = it?.readBytes(context) ?: return@launch;
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
_button_import_ezip.setOnClickListener {
|
||||
@@ -51,17 +64,35 @@ class ImportOptionsDialog: AlertDialog {
|
||||
_button_import_txt.onClick.subscribe {
|
||||
dismiss();
|
||||
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
|
||||
val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
|
||||
val txt = String(txtBytes);
|
||||
StateBackup.importTxt(_context, txt);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val txtBytes = it?.readBytes(context) ?: return@launch;
|
||||
val txt = String(txtBytes);
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
StateBackup.importTxt(_context, txt);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
_button_import_newpipe_subs.onClick.subscribe {
|
||||
dismiss();
|
||||
StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
|
||||
val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
|
||||
val json = String(jsonBytes);
|
||||
StateBackup.importNewPipeSubs(_context, json);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val jsonBytes = it?.readBytes(context) ?: return@launch;
|
||||
val json = String(jsonBytes);
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
StateBackup.importNewPipeSubs(_context, json);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
_button_import_platform.onClick.subscribe {
|
||||
|
||||
@@ -303,9 +303,10 @@ class VideoDownload {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
val resolvedPlaylistUrl = playlistResponse.url
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, source.url))
|
||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, resolvedPlaylistUrl))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -351,9 +352,10 @@ class VideoDownload {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
val resolvedPlaylistUrl = playlistResponse.url
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, source.url))
|
||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, resolvedPlaylistUrl))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -717,7 +719,7 @@ class VideoDownload {
|
||||
|
||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||
|
||||
var written = 0;
|
||||
var written: Long = 0;
|
||||
var indexCounter = 0;
|
||||
onProgress(foundCues.count().toLong(), 0, 0);
|
||||
for(cue in foundCues) {
|
||||
@@ -742,7 +744,7 @@ class VideoDownload {
|
||||
|
||||
indexCounter++;
|
||||
}
|
||||
sourceLength = written.toLong();
|
||||
sourceLength = written;
|
||||
|
||||
Logger.i(TAG, "$name downloadSource Finished");
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||
|
||||
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
//TODO: Offline subtitles
|
||||
override val subtitles: List<ISubtitleSource> = listOf();
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import com.caoccao.javet.exceptions.JavetException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
import com.caoccao.javet.interop.V8Host
|
||||
import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.interop.options.V8Flags
|
||||
import com.caoccao.javet.interop.options.V8RuntimeOptions
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.IV8ValuePromise
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -26,6 +26,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.engine.internal.V8Converter
|
||||
@@ -38,8 +39,18 @@ import com.futo.platformplayer.engine.packages.V8Package
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateAssets
|
||||
import com.futo.platformplayer.toList
|
||||
import com.futo.platformplayer.toV8ValueBlocking
|
||||
import com.futo.platformplayer.toV8ValueAsync
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class V8Plugin {
|
||||
val config: IV8PluginConfig;
|
||||
@@ -47,10 +58,14 @@ class V8Plugin {
|
||||
private val _clientAuth: ManagedHttpClient;
|
||||
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||
|
||||
private val _promises = ConcurrentHashMap<V8ValuePromise, ((V8ValuePromise)->Unit)?>();
|
||||
|
||||
val httpClient: ManagedHttpClient get() = _client;
|
||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
||||
|
||||
var runtimeId: Int = 0;
|
||||
|
||||
fun registerHttpClient(client: JSHttpClient) {
|
||||
synchronized(_clientOthers) {
|
||||
_clientOthers.put(client.clientId, client);
|
||||
@@ -67,10 +82,8 @@ class V8Plugin {
|
||||
var isStopped = true;
|
||||
val onStopped = Event1<V8Plugin>();
|
||||
|
||||
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial
|
||||
private val _busyCounterLock = Object();
|
||||
private var _busyCounter = 0;
|
||||
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||
private val _busyLock = ReentrantLock()
|
||||
val isBusy get() = _busyLock.isLocked;
|
||||
|
||||
var allowDevSubmit: Boolean = false
|
||||
private set(value) {
|
||||
@@ -140,6 +153,7 @@ class V8Plugin {
|
||||
synchronized(_runtimeLock) {
|
||||
if (_runtime != null)
|
||||
return;
|
||||
runtimeId = runtimeId + 1;
|
||||
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||
val host = V8Host.getV8Instance();
|
||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||
@@ -148,6 +162,8 @@ class V8Plugin {
|
||||
if (!host.isIsolateCreated)
|
||||
throw IllegalStateException("Isolate not created");
|
||||
|
||||
_runtimeMap.put(_runtime!!, this);
|
||||
|
||||
//Setup bridge
|
||||
_runtime?.let {
|
||||
it.converter = V8Converter();
|
||||
@@ -184,10 +200,13 @@ class V8Plugin {
|
||||
}
|
||||
fun stop(){
|
||||
Logger.i(TAG, "Stopping plugin [${config.name}]");
|
||||
isStopped = true;
|
||||
whenNotBusy {
|
||||
busy {
|
||||
Logger.i(TAG, "Plugin stopping");
|
||||
synchronized(_runtimeLock) {
|
||||
if(isStopped)
|
||||
return@busy;
|
||||
isStopped = true;
|
||||
runtimeId = runtimeId + 1;
|
||||
|
||||
//Cleanup http
|
||||
for(pack in _depsPackages) {
|
||||
@@ -197,6 +216,7 @@ class V8Plugin {
|
||||
}
|
||||
|
||||
_runtime?.let {
|
||||
_runtimeMap.remove(it);
|
||||
_runtime = null;
|
||||
if(!it.isClosed && !it.isDead) {
|
||||
try {
|
||||
@@ -211,62 +231,147 @@ class V8Plugin {
|
||||
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||
};
|
||||
}
|
||||
Logger.i(TAG, "Plugin stopped");
|
||||
onStopped.emit(this);
|
||||
}
|
||||
cancelAllPromises();
|
||||
}
|
||||
|
||||
fun isThreadAlreadyBusy(): Boolean {
|
||||
return _busyLock.isHeldByCurrentThread;
|
||||
}
|
||||
fun <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 {
|
||||
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 {
|
||||
warnIfMainThread("V8Plugin.executeTyped");
|
||||
if(isStopped)
|
||||
throw PluginEngineStoppedException(config, "Instance is stopped", js);
|
||||
|
||||
synchronized(_busyCounterLock) {
|
||||
_busyCounter++;
|
||||
}
|
||||
|
||||
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
|
||||
try {
|
||||
return catchScriptErrors("Plugin[${config.name}]", js) {
|
||||
val result = busy {
|
||||
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
|
||||
return@busy catchScriptErrors<V8Value>("Plugin[${config.name}]", js) {
|
||||
runtime.getExecutor(js).execute()
|
||||
};
|
||||
};
|
||||
if(result is V8ValuePromise) {
|
||||
return result.toV8ValueBlocking(this@V8Plugin);
|
||||
}
|
||||
finally {
|
||||
synchronized(_busyCounterLock) {
|
||||
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
|
||||
try {
|
||||
afterBusy.emit(_busyCounter - 1);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
|
||||
}
|
||||
_busyCounter--;
|
||||
}
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<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 executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value } }
|
||||
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 } }
|
||||
|
||||
fun whenNotBusy(handler: (V8Plugin)->Unit) {
|
||||
synchronized(_busyCounterLock) {
|
||||
if(_busyCounter == 0)
|
||||
handler(this);
|
||||
else {
|
||||
val tag = Object();
|
||||
afterBusy.subscribe(tag) {
|
||||
if(it == 0) {
|
||||
Logger.w(TAG, "V8Plugin afterBusy handled");
|
||||
afterBusy.remove(tag);
|
||||
handler(this);
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: V8Value> handlePromise(result: V8ValuePromise): CompletableDeferred<T> {
|
||||
val def = CompletableDeferred<T>();
|
||||
result.register(object: IV8ValuePromise.IListener {
|
||||
override fun onFulfilled(p0: V8Value?) {
|
||||
resolvePromise(result);
|
||||
def.complete(p0 as T);
|
||||
}
|
||||
override fun onRejected(p0: V8Value?) {
|
||||
resolvePromise(result);
|
||||
def.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
|
||||
}
|
||||
override fun onCatch(p0: V8Value?) {
|
||||
resolvePromise(result);
|
||||
def.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
|
||||
}
|
||||
});
|
||||
registerPromise(result) {
|
||||
if(def.isActive)
|
||||
def.cancel("Cancelled by system");
|
||||
}
|
||||
return def;
|
||||
}
|
||||
fun registerPromise(promise: V8ValuePromise, onCancelled: ((V8ValuePromise)->Unit)? = null) {
|
||||
Logger.v(TAG, "Promise registered for plugin [${config.name}]: ${promise.hashCode()}");
|
||||
if (onCancelled != null) {
|
||||
_promises.put(promise, onCancelled)
|
||||
};
|
||||
}
|
||||
fun resolvePromise(promise: V8ValuePromise, cancelled: Boolean = false) {
|
||||
Logger.v(TAG, "Promise resolved for plugin [${config.name}]: ${promise.hashCode()}");
|
||||
val found = synchronized(_promises) {
|
||||
val found = _promises.getOrDefault(promise, null);
|
||||
_promises.remove(promise);
|
||||
return@synchronized found;
|
||||
};
|
||||
if(found != null && cancelled)
|
||||
found(promise);
|
||||
}
|
||||
fun cancelAllPromises(){
|
||||
val promises = _promises.keys().toList();
|
||||
for(key in promises) {
|
||||
try {
|
||||
resolvePromise(key, true);
|
||||
}
|
||||
catch(ex: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
|
||||
//TODO: Auto get all package types?
|
||||
return when(packageName) {
|
||||
@@ -292,8 +397,14 @@ class V8Plugin {
|
||||
private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*");
|
||||
private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*");
|
||||
|
||||
private val _runtimeMap = ConcurrentHashMap<V8Runtime, 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 {
|
||||
var codeStripped = code;
|
||||
if(codeStripped != null) { //TODO: Improve code stripped
|
||||
@@ -327,14 +438,23 @@ class V8Plugin {
|
||||
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
||||
}
|
||||
catch(executeEx: JavetExecutionException) {
|
||||
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) {
|
||||
val pluginType = executeEx.scriptingError.context["plugin_type"].toString();
|
||||
val obj = executeEx.scriptingError?.context
|
||||
if(obj != null && obj.containsKey("plugin_type") == true) {
|
||||
val pluginType = obj["plugin_type"].toString();
|
||||
|
||||
//Captcha
|
||||
if (pluginType == "CaptchaRequiredException") {
|
||||
throw ScriptCaptchaRequiredException(config,
|
||||
executeEx.scriptingError.context["url"]?.toString(),
|
||||
executeEx.scriptingError.context["body"]?.toString(),
|
||||
obj["url"]?.toString(),
|
||||
obj["body"]?.toString(),
|
||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
|
||||
//Reload Required
|
||||
if (pluginType == "ReloadRequiredException") {
|
||||
throw ScriptReloadRequiredException(config,
|
||||
obj["msg"]?.toString(),
|
||||
obj["reloadData"]?.toString(),
|
||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
|
||||
@@ -348,6 +468,41 @@ class V8Plugin {
|
||||
codeStripped
|
||||
);
|
||||
}
|
||||
/* //Required for newer V8 versions
|
||||
if(executeEx.scriptingError?.context is IJavetEntityError) {
|
||||
val obj = executeEx.scriptingError?.context as IJavetEntityError
|
||||
if(obj.context.containsKey("plugin_type") == true) {
|
||||
val pluginType = obj.context["plugin_type"].toString();
|
||||
|
||||
//Captcha
|
||||
if (pluginType == "CaptchaRequiredException") {
|
||||
throw ScriptCaptchaRequiredException(config,
|
||||
obj.context["url"]?.toString(),
|
||||
obj.context["body"]?.toString(),
|
||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
|
||||
//Reload Required
|
||||
if (pluginType == "ReloadRequiredException") {
|
||||
throw ScriptReloadRequiredException(config,
|
||||
obj.context["msg"]?.toString(),
|
||||
obj.context["reloadData"]?.toString(),
|
||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
|
||||
//Others
|
||||
throwExceptionFromV8(
|
||||
config,
|
||||
pluginType,
|
||||
(extractJSExceptionMessage(executeEx) ?: ""),
|
||||
executeEx,
|
||||
executeEx.scriptingError?.stack,
|
||||
codeStripped
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
@@ -398,9 +553,4 @@ class V8Plugin {
|
||||
return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Methods available for scripts (bridge object)
|
||||
*/
|
||||
}
|
||||
@@ -136,7 +136,7 @@ class V8RemoteObject {
|
||||
}
|
||||
|
||||
|
||||
fun List<V8RemoteObject>.serialize() : String {
|
||||
fun List<V8RemoteObject?>.serialize() : String {
|
||||
return _gson.toJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class NoInternetException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||
@@ -11,6 +12,7 @@ open class NoInternetException(config: IV8PluginConfig, error: String, ex: Excep
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException {
|
||||
obj.ensureIsBusy();
|
||||
return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||
@@ -11,6 +12,7 @@ open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Except
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -9,6 +10,7 @@ class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?,
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "ScriptCaptchaRequiredException";
|
||||
return ScriptCaptchaRequiredException(config,
|
||||
obj.getOrDefault<String>(config, "url", contextName, null),
|
||||
|
||||
+2
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||
@@ -11,6 +12,7 @@ open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: E
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) {
|
||||
@@ -11,6 +12,7 @@ open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
|
||||
@@ -11,6 +12,7 @@ open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex:
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException"));
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) {
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
obj.ensureIsBusy();
|
||||
return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable {
|
||||
|
||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||
synchronized(this) {
|
||||
if(_runtimeObj != null)
|
||||
return _runtimeObj;
|
||||
//if(_runtimeObj != null)
|
||||
// return _runtimeObj;
|
||||
|
||||
val v8Obj = runtime.createV8ValueObject();
|
||||
v8Obj.bind(this);
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.media.MediaCodec
|
||||
import android.media.MediaCodecList
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.caoccao.javet.annotations.V8Property
|
||||
import com.caoccao.javet.interop.callback.JavetCallbackContext
|
||||
import com.caoccao.javet.utils.JavetResourceUtils
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueFunction
|
||||
@@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PackageBridge : V8Package {
|
||||
@Transient
|
||||
@@ -78,6 +80,15 @@ class PackageBridge : V8Package {
|
||||
return "android";
|
||||
}
|
||||
|
||||
@V8Property
|
||||
fun supportedFeatures(): Array<String> {
|
||||
return arrayOf(
|
||||
"ReloadRequiredException",
|
||||
"HttpBatchClient",
|
||||
"Async"
|
||||
);
|
||||
}
|
||||
|
||||
@V8Property
|
||||
fun supportedContent(): Array<Int> {
|
||||
return arrayOf(
|
||||
@@ -101,45 +112,54 @@ class PackageBridge : V8Package {
|
||||
}
|
||||
|
||||
var timeoutCounter = 0;
|
||||
var timeoutMap = HashSet<Int>();
|
||||
var timeoutMap = ConcurrentHashMap<Int, Any?>();
|
||||
@V8Function
|
||||
fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
|
||||
val id = timeoutCounter++;
|
||||
|
||||
val funcClone = func.toClone<V8ValueFunction>()
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
delay(timeout);
|
||||
synchronized(timeoutMap) {
|
||||
if(!timeoutMap.contains(id)) {
|
||||
JavetResourceUtils.safeClose(funcClone);
|
||||
return@launch;
|
||||
if (_plugin.isStopped)
|
||||
return@launch;
|
||||
if (!timeoutMap.containsKey(id)) {
|
||||
_plugin.busy {
|
||||
if (!_plugin.isStopped)
|
||||
JavetResourceUtils.safeClose(funcClone);
|
||||
}
|
||||
timeoutMap.remove(id);
|
||||
return@launch;
|
||||
}
|
||||
timeoutMap.remove(id);
|
||||
try {
|
||||
_plugin.whenNotBusy {
|
||||
funcClone.callVoid(null, arrayOf<Any>());
|
||||
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>());
|
||||
Logger.w(TAG, "setTimeout after");
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed timeout callback", ex);
|
||||
}
|
||||
finally {
|
||||
JavetResourceUtils.safeClose(funcClone);
|
||||
} finally {
|
||||
_plugin.busy {
|
||||
if (!_plugin.isStopped)
|
||||
JavetResourceUtils.safeClose(funcClone);
|
||||
}
|
||||
//_plugin.whenNotBusy {
|
||||
//}
|
||||
}
|
||||
};
|
||||
synchronized(timeoutMap) {
|
||||
timeoutMap.add(id);
|
||||
}
|
||||
timeoutMap.put(id, true);
|
||||
return id;
|
||||
}
|
||||
@V8Function
|
||||
fun clearTimeout(id: Int) {
|
||||
synchronized(timeoutMap) {
|
||||
if(timeoutMap.contains(id))
|
||||
timeoutMap.remove(id);
|
||||
}
|
||||
if (timeoutMap.containsKey(id))
|
||||
timeoutMap.remove(id);
|
||||
}
|
||||
@V8Function
|
||||
fun sleep(length: Int) {
|
||||
Thread.sleep(length.toLong());
|
||||
}
|
||||
|
||||
@V8Function
|
||||
@@ -147,7 +167,7 @@ class PackageBridge : V8Package {
|
||||
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.toast(str);
|
||||
UIDialogs.appToast(str);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show toast.", e);
|
||||
}
|
||||
@@ -174,7 +194,11 @@ class PackageBridge : V8Package {
|
||||
|
||||
val stackTrace = Thread.currentThread().stackTrace;
|
||||
val callerMethod = stackTrace.findLast {
|
||||
it.className == JSClient::class.java.name
|
||||
it.className == JSClient::class.java.name &&
|
||||
it.methodName != "isBusy" &&
|
||||
it.methodName != "busy" &&
|
||||
it.methodName != "getCopy" &&
|
||||
it.methodName != "isBusyWith"
|
||||
}?.methodName ?: "";
|
||||
val session = StateApp.instance.sessionId;
|
||||
val pluginId = _plugin.config.id;
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
@@ -44,6 +45,17 @@ class PackageHttp: V8Package {
|
||||
private val aliveSockets = mutableListOf<SocketResult>();
|
||||
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) {
|
||||
_config = config;
|
||||
@@ -112,6 +124,8 @@ class PackageHttp: V8Package {
|
||||
_plugin.registerHttpClient(httpClient);
|
||||
val client = PackageHttpClient(this, httpClient);
|
||||
|
||||
_clients.put(client.clientId() ?: "", client);
|
||||
|
||||
return client;
|
||||
}
|
||||
@V8Function
|
||||
@@ -246,18 +260,18 @@ class PackageHttp: V8Package {
|
||||
|
||||
@V8Function
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
fun DUMMY(): BatchBuilder {
|
||||
@@ -268,21 +282,21 @@ class PackageHttp: V8Package {
|
||||
//Client-specific
|
||||
|
||||
@V8Function
|
||||
fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
|
||||
_reqs.add(Pair(client, RequestDescriptor(method, url, headers)));
|
||||
fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
|
||||
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)));
|
||||
return BatchBuilder(_package, _reqs);
|
||||
}
|
||||
@V8Function
|
||||
fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
|
||||
_reqs.add(Pair(client, RequestDescriptor(method, url, headers, body)));
|
||||
fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
|
||||
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body)));
|
||||
return BatchBuilder(_package, _reqs);
|
||||
}
|
||||
@V8Function
|
||||
fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
|
||||
= clientRequest(client, "GET", url, headers);
|
||||
fun clientGET(clientId: String?, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
|
||||
= clientRequest(clientId, "GET", url, headers);
|
||||
@V8Function
|
||||
fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
|
||||
= clientRequestWithBody(client, "POST", url, body, headers);
|
||||
fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
|
||||
= clientRequestWithBody(clientId, "POST", url, body, headers);
|
||||
|
||||
|
||||
//Finalizer
|
||||
@@ -321,6 +335,7 @@ class PackageHttp: V8Package {
|
||||
@Transient
|
||||
private val _clientId: String?;
|
||||
|
||||
|
||||
@V8Property
|
||||
fun clientId(): String? {
|
||||
return _clientId;
|
||||
@@ -333,6 +348,17 @@ class PackageHttp: V8Package {
|
||||
_clientId = if(_client is JSHttpClient) _client.clientId else null;
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun resetAuthCookies(){
|
||||
if(_client is JSHttpClient)
|
||||
_client.resetAuthCookies();
|
||||
}
|
||||
@V8Function
|
||||
fun clearOtherCookies(){
|
||||
if(_client is JSHttpClient)
|
||||
_client.clearOtherCookies();
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
|
||||
for(pair in defaultHeaders)
|
||||
@@ -429,8 +455,23 @@ class PackageHttp: V8Package {
|
||||
};
|
||||
}
|
||||
@V8Function
|
||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
|
||||
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse {
|
||||
if(body is V8ValueString)
|
||||
return POSTInternal(url, body.value, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is String)
|
||||
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is V8ValueTypedArray)
|
||||
return POSTInternal(url, body.toBytes(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is ByteArray)
|
||||
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
|
||||
return POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
|
||||
else
|
||||
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
||||
}
|
||||
|
||||
|
||||
// = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
|
||||
fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
@@ -452,9 +493,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 {
|
||||
applyDefaultHeaders(headers);
|
||||
return logExceptions {
|
||||
@@ -630,7 +668,9 @@ class PackageHttp: V8Package {
|
||||
_isOpen = true;
|
||||
if(hasOpen && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_listeners?.invokeVoid("open", arrayOf<Any>());
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeV8Void("open", arrayOf<Any>());
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
|
||||
@@ -640,7 +680,9 @@ class PackageHttp: V8Package {
|
||||
override fun message(msg: String) {
|
||||
if(hasMessage && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_listeners?.invokeVoid("message", msg);
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeV8Void("message", msg);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {}
|
||||
}
|
||||
@@ -649,7 +691,9 @@ class PackageHttp: V8Package {
|
||||
if(hasClosing && _listeners?.isClosed != true)
|
||||
{
|
||||
try {
|
||||
_listeners?.invokeVoid("closing", code, reason);
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeV8Void("closing", code, reason);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
|
||||
@@ -660,7 +704,9 @@ class PackageHttp: V8Package {
|
||||
_isOpen = false;
|
||||
if(hasClosed && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_listeners?.invokeVoid("closed", code, reason);
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeV8Void("closed", code, reason);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
@@ -676,7 +722,9 @@ class PackageHttp: V8Package {
|
||||
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
|
||||
if(hasFailure && _listeners?.isClosed != true) {
|
||||
try {
|
||||
_listeners?.invokeVoid("failure", exception.message);
|
||||
_package._plugin.busy {
|
||||
_listeners?.invokeV8Void("failure", exception.message);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
|
||||
+7
-3
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IReplacerPager
|
||||
import com.futo.platformplayer.api.media.structures.MultiPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
@@ -61,7 +62,7 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||
private var _query: String? = null
|
||||
private var _searchView: SearchView? = null
|
||||
|
||||
val onContentClicked = Event2<IPlatformContent, Long>();
|
||||
val onContentClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>();
|
||||
val onContentUrlClicked = Event2<String, ContentType>();
|
||||
val onUrlClicked = Event1<String>();
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
@@ -208,10 +209,13 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||
_searchView = searchView
|
||||
updateSearchViewVisibility()
|
||||
|
||||
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
||||
_adapterResults = PreviewContentListAdapter(lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
||||
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
||||
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
||||
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
||||
this.onContentClicked.subscribe { content, num ->
|
||||
val results = ArrayList(_results)
|
||||
this@ChannelContentsFragment.onContentClicked.emit(content, num, Pair(_pager!!, results))
|
||||
}
|
||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||
_recyclerResults = view.findViewById(R.id.recycler_videos)
|
||||
|
||||
_adapterResults = PreviewContentListAdapter(
|
||||
view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
|
||||
lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
|
||||
).apply {
|
||||
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
|
||||
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
|
||||
|
||||
+4
-1
@@ -15,6 +15,7 @@ import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
@@ -375,6 +376,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
|
||||
fun newInstance() = MenuBottomBarFragment().apply { }
|
||||
|
||||
@UnstableApi
|
||||
//Add configurable buttons here
|
||||
var buttonDefinitions = listOf(
|
||||
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
|
||||
@@ -390,13 +392,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
||||
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(11, R.drawable.ic_smart_display, R.drawable.ic_smart_display_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment && !(it.currentMain as ShortsFragment).isChannelShortsMode }, { it.navigate<ShortsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>(withHistory = false) }),
|
||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>(withHistory = false) }),
|
||||
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>(withHistory = false) }),
|
||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||
val c = it.context ?: return@ButtonDefinition;
|
||||
val c = it.context ?: return@ButtonDefinition;
|
||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||
val intent = Intent(c, SettingsActivity::class.java);
|
||||
|
||||
+2
@@ -778,6 +778,8 @@ class ArticleDetailFragment : MainFragment {
|
||||
view.onAddToWatchLaterClicked.subscribe { a ->
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
else
|
||||
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||
}
|
||||
}
|
||||
else if(content is IPlatformPost) {
|
||||
|
||||
+11
-1
@@ -172,7 +172,7 @@ class ChannelFragment : MainFragment() {
|
||||
_buttonSubscribe = findViewById(R.id.button_subscribe)
|
||||
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
|
||||
_overlayLoading = findViewById(R.id.channel_loading_overlay)
|
||||
_overlayLoadingSpinner = findViewById(R.id.channel_loader)
|
||||
_overlayLoadingSpinner = findViewById(R.id.channel_loader_frag)
|
||||
_overlayContainer = findViewById(R.id.overlay_container)
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
|
||||
@@ -211,6 +211,14 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.onShortClicked.subscribe { v, _, pagerPair ->
|
||||
when (v) {
|
||||
is IPlatformVideo -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.onAddToClicked.subscribe { content ->
|
||||
_overlayContainer.let {
|
||||
if (content is IPlatformVideo) _slideUpOverlay =
|
||||
@@ -226,6 +234,8 @@ class ChannelFragment : MainFragment() {
|
||||
if (content is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
else
|
||||
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user