Compare commits

..

No commits in common. "master" and "274" have entirely different histories.

464 changed files with 6681 additions and 19684 deletions

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
aar/* filter=lfs diff=lfs merge=lfs -text
app/aar/* filter=lfs diff=lfs merge=lfs -text

View file

@ -1,166 +0,0 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug", "Android"]
title: "Bug: "
type: bug
projects: ["futo-org/19"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: reproduction-steps
attributes:
label: Reproduction steps
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
placeholder: |
0. Play a Youtube video
1. Press on Download button
2. Select quality 1440p
3. Grayjay crashes when attempting to download
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: Actual result
description: What happend?
placeholder: Tell us what you saw!
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: Expected result
description: What was suppose to happen?
placeholder: Tell us what you expected to happen!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "311"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- "All"
- "Apple Podcasts"
- "BiliBili (CN)"
- "Bitchute"
- "Crunchyroll"
- "CuriosityStream"
- "Dailymotion"
- "Kick"
- "Nebula"
- "Odysee"
- "Patreon"
- "PeerTube"
- "Rumble"
- "SoundCloud"
- "Spotify"
- "TedTalks"
- "Twitch"
- "Youtube"
- "Other"
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: input
id: android-version
attributes:
label: Which android version are you using?
placeholder: "Android 15"
validations:
required: true
- type: input
id: phone-model
attributes:
label: Which device are you using?
placeholder: "Google Pixel 9"
validations:
required: true
- type: input
id: os-version
attributes:
label: Which operating system are you using?
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
validations:
required: true
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: dropdown
id: vpn
attributes:
label: Are you using a VPN?
multiple: false
options:
- "No"
- "Yes"
validations:
required: true
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.

80
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,80 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["bug", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "242"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- All
- Youtube
- BiliBili (CN)
- Twitch
- Odysee
- Rumble
- Kick
- PeerTube
- Patreon
- Nebula
- SoundCloud
- Other
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View file

@ -1,16 +1,13 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["Documentation"]
title: "Documentation: "
type: task
projects: ["futo-org/19"]
labels: ["documentation", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a documentation change request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

View file

@ -1,16 +1,15 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
labels: ["Enhancement", "Android"]
title: "Feature request: "
type: feature
projects: ["futo-org/19"]
labels: ["enhancement", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a feature request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

34
.github/workflows/labeler.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Issue labeler
on:
issues:
types: [ opened ]
permissions:
contents: read
jobs:
label-component:
runs-on: ubuntu-latest
permissions:
# required for all workflows
issues: write
steps:
- uses: actions/checkout@v3
- name: Parse issue form
uses: stefanbuck/github-issue-parser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- name: Set labels based on plugin field
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: plugin
block-list: |
None
Other
token: ${{ secrets.GITHUB_TOKEN }}

24
.gitmodules vendored
View file

@ -82,27 +82,3 @@
[submodule "app/src/stable/assets/sources/dailymotion"]
path = app/src/stable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
[submodule "app/src/stable/assets/sources/apple-podcast"]
path = app/src/stable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
path = app/src/unstable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git
[submodule "app/src/stable/assets/sources/tedtalks"]
path = app/src/stable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/unstable/assets/sources/tedtalks"]
path = app/src/unstable/assets/sources/tedtalks
url = ../plugins/tedtalks.git
[submodule "app/src/stable/assets/sources/curiositystream"]
path = app/src/stable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/curiositystream"]
path = app/src/unstable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/crunchyroll"]
path = app/src/unstable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/crunchyroll"]
path = app/src/stable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git

BIN
app/aar/ffmpeg-kit-full-6.0-2.LTS.aar (Stored with Git LFS)

Binary file not shown.

View file

@ -156,7 +156,6 @@ android {
dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.12.0'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
@ -181,7 +180,6 @@ 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'
@ -199,8 +197,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation fileTree(dir: 'aar', include: ['*.aar'])
implementation 'com.arthenica:smart-exception-java:0.2.1'
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1'

View file

@ -1,38 +0,0 @@
package com.futo.platformplayer
import android.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test
import toAndroidColor
class CSSColorTests {
@Test
fun test1() {
val androidHex = "#80336699"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#33669980"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor(),
)
}
@Test
fun test2() {
val androidHex = "#123ABC"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#123ABCFF"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor()
)
}
}

View file

@ -1,338 +0,0 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
import org.junit.Assert.*
import org.junit.Test
import java.net.Socket
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"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.138"
private val relayPort = 9000
/** Creates a client connected to the live relay server. */
private suspend fun createClient(
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
onException: ((Throwable) -> Unit)? = null
): SyncSocketSession = withContext(Dispatchers.IO) {
val p = Noise.createDH("25519")
p.generateKeyPair()
val socket = Socket(relayHost, relayPort)
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
val tcs = CompletableDeferred<Boolean>()
val socketSession = SyncSocketSession(
relayHost,
p,
inputStream,
outputStream,
onClose = { socket.close() },
onHandshakeComplete = { s ->
onHandshakeComplete?.invoke(s)
tcs.complete(true)
},
onData = onData ?: { _, _, _, _ -> },
onNewChannel = onNewChannel ?: { _, _ -> },
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
)
socketSession.authorizable = AlwaysAuthorized()
try {
socketSession.startAsInitiator(relayKey)
} catch (e: Throwable) {
onException?.invoke(e)
}
withTimeout(5000.milliseconds) { tcs.await() }
return@withContext socketSession
}
@Test
fun multipleClientsHandshake_Success() = runBlocking {
val client1 = createClient()
val client2 = createClient()
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
client1.stop()
client2.stop()
}
@Test
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
delay(100.milliseconds)
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
assertNotNull("Client B should receive connection info", infoB)
assertEquals(12345.toUShort(), infoB!!.port)
assertNull("Client C should not receive connection info (unauthorized)", infoC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun relayedTransport_Bidirectional_Success() = runBlocking {
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
val tcsDataA = CompletableDeferred<ByteArray>()
channelA.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
}
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
assertArrayEquals(maxSizeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
val data = byteArrayOf(1, 2, 3)
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
assertTrue(success)
assertNotNull(recordB)
assertArrayEquals(data, recordB!!.first)
assertNull("Unauthorized client should not access record", recordC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun getNonExistentRecord_ReturnsNull() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
assertNull("Getting non-existent record should return null", record)
clientA.stop()
clientB.stop()
}
@Test
fun updateRecord_TimestampUpdated() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val key = "updateKey"
val data1 = byteArrayOf(1)
val data2 = byteArrayOf(2)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
val record1 = clientB.getRecord(clientA.localPublicKey, key)
delay(1000.milliseconds)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
val record2 = clientB.getRecord(clientA.localPublicKey, key)
assertNotNull(record1)
assertNotNull(record2)
assertTrue(record2!!.second > record1!!.second)
assertArrayEquals(data2, record2.first)
clientA.stop()
clientB.stop()
}
@Test
fun deleteRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val data = byteArrayOf(1, 2, 3)
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
assertTrue(success)
assertNull(record)
clientA.stop()
clientB.stop()
}
@Test
fun listRecordKeys_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val keys = arrayOf("key1", "key2", "key3")
keys.forEach { key ->
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
}
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
clientA.stop()
clientB.stop()
}
@Test
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
assertArrayEquals(largeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetLargeRecord_Success() = runBlocking {
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
val clientA = createClient()
val clientB = createClient()
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
assertTrue(success)
assertNotNull(record)
assertArrayEquals(largeData, record!!.first)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithValidAppId_Success() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
)
val clientA = createClient()
// Act: Start relayed channel with valid appId
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
val channelB = withTimeout(5.seconds) { tcsB.await() }
withTimeout(5.seconds) { channelTask.await() }
// Assert: Channel is established
assertNotNull("Channel should be created on target with valid appId", channelB)
// Clean up
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val invalidAppId = 5678u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
onException = { }
)
val clientA = createClient()
// Act & Assert: Attempt with invalid appId should fail
try {
withTimeout(5.seconds) {
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
}
fail("Starting relayed channel with invalid appId should fail")
} catch (e: Throwable) {
// Expected: The channel creation should time out or fail
}
// Ensure no channel was created on client B
val completedTask = select {
tcsB.onAwait { "channel" }
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
}
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
// Clean up
clientA.stop()
clientB.stop()
}
}
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
}*/

View file

@ -1,512 +0,0 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
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,
val responderInput: LittleEndianDataInputStream,
val responderOutput: LittleEndianDataOutputStream
)
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
typealias OnClose = (SyncSocketSession) -> Unit
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
class SyncSocketTests {
private fun createPipeStreams(): PipeStreams {
val initiatorOutput = PipedOutputStream()
val responderOutput = PipedOutputStream()
val responderInput = PipedInputStream(initiatorOutput)
val initiatorInput = PipedInputStream(responderOutput)
return PipeStreams(
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
)
}
fun generateKeyPair(): DHState {
val p = Noise.createDH("25519")
p.generateKeyPair()
return p
}
private fun createSessions(
initiatorInput: LittleEndianDataInputStream,
initiatorOutput: LittleEndianDataOutputStream,
responderInput: LittleEndianDataInputStream,
responderOutput: LittleEndianDataOutputStream,
initiatorKeyPair: DHState,
responderKeyPair: DHState,
onInitiatorHandshakeComplete: OnHandshakeComplete,
onResponderHandshakeComplete: OnHandshakeComplete,
onInitiatorClose: OnClose? = null,
onResponderClose: OnClose? = null,
onClose: OnClose? = null,
isHandshakeAllowed: IsHandshakeAllowed? = null,
onDataA: OnData? = null,
onDataB: OnData? = null
): Pair<SyncSocketSession, SyncSocketSession> {
val initiatorSession = SyncSocketSession(
"", initiatorKeyPair, initiatorInput, initiatorOutput,
onClose = {
onClose?.invoke(it)
onInitiatorClose?.invoke(it)
},
onHandshakeComplete = onInitiatorHandshakeComplete,
onData = onDataA,
isHandshakeAllowed = isHandshakeAllowed
)
val responderSession = SyncSocketSession(
"", responderKeyPair, responderInput, responderOutput,
onClose = {
onClose?.invoke(it)
onResponderClose?.invoke(it)
},
onHandshakeComplete = onResponderHandshakeComplete,
onData = onDataB,
isHandshakeAllowed = isHandshakeAllowed
)
return Pair(initiatorSession, responderSession)
}
@Test
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val invalidPairingCode = "wrong"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
responderSession.startAsResponder()
withTimeout(100.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val pairingCode = "unnecessary"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val smallData = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(smallData, receivedData)
}
@Test
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(maxData, receivedData)
}
@Test
fun stream_LargeData_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(largeData, receivedData)
}
@Test
fun authorizedSession_CanSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize both sessions
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(data, receivedData)
}
@Test
fun unauthorizedSession_CannotSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, _, _, _ -> }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize initiator but not responder
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Unauthorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
delay(1.seconds)
assertFalse(tcsDataReceived.isCompleted)
}
@Test
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
assertNotNull(initiatorSession.remotePublicKey)
assertNotNull(responderSession.remotePublicKey)
}
@Test
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val invalidAppId = 5678u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
}
class Authorized : IAuthorizable {
override val isAuthorized: Boolean = true
}
class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false
}*/

View file

@ -55,7 +55,7 @@
<activity
android:name=".activities.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleInstance"
@ -156,6 +156,7 @@
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.DeveloperActivity"

File diff suppressed because one or more lines are too long

View file

@ -11,8 +11,7 @@ let Type = {
Streams: "STREAMS",
Mixed: "MIXED",
Live: "LIVE",
Subscriptions: "SUBSCRIPTIONS",
Shorts: "SHORTS"
Subscriptions: "SUBSCRIPTIONS"
},
Order: {
Chronological: "CHRONOLOGICAL"
@ -32,8 +31,7 @@ let Type = {
Text: {
RAW: 0,
HTML: 1,
MARKUP: 2,
CODE: 3
MARKUP: 2
},
Chapter: {
NORMAL: 0,
@ -103,12 +101,6 @@ 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);
@ -251,11 +243,7 @@ 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;
}
}
class PlatformVideoDetails extends PlatformVideo {
@ -272,11 +260,6 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
}
}
@ -301,39 +284,15 @@ class PlatformPostDetails extends PlatformPost {
}
}
class PlatformWeb extends PlatformContent {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWeb";
}
}
class PlatformWebDetails extends PlatformWeb {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWebDetails";
this.html = obj.html;
}
}
class PlatformArticle extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticle";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class PlatformArticleDetails extends PlatformArticle {
class PlatformArticleDetails extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class ArticleSegment {
@ -349,17 +308,9 @@ class ArticleTextSegment extends ArticleSegment {
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images, caption) {
constructor(images) {
super(2);
this.images = images;
this.caption = caption;
}
}
class ArticleHeaderSegment extends ArticleSegment {
constructor(content, level) {
super(3);
this.level = level;
this.content = content;
}
}
class ArticleNestedSegment extends ArticleSegment {
@ -637,8 +588,6 @@ class PlatformComment {
this.date = obj.date ?? 0;
this.replyCount = obj.replyCount ?? 0;
this.context = obj.context ?? {};
if(obj.getReplies)
this.getReplies = obj.getReplies;
}
}
@ -710,12 +659,11 @@ class LiveEventViewCount extends LiveEvent {
}
}
class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
constructor(targetUrl, targetName, targetThumbnail) {
super(100);
this.targetUrl = targetUrl;
this.targetName = targetName;
this.targetThumbnail = targetThumbnail;
this.isOutgoing = isOutgoing ?? true;
}
}
@ -788,7 +736,6 @@ let plugin = {
//To override by plugin
const source = {
getHome() { return new ContentPager([], false, {}); },
getShorts() { return new VideoPager([], false, {}); },
enable(config){ },
disable() {},

View file

@ -1,319 +0,0 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 0f
max == r -> ((g - b) / d % 6f) * 60f
max == g -> (((b - r) / d) + 2f) * 60f
else -> (((r - g) / d) + 4f) * 60f
}.let { if (it < 0f) it + 360f else it }
_hslDirty = false
}
/**
* Set all three HSL channels at once.
* Hue in degrees [0...360), s/l [0...1].
*/
fun setHsl(h: Float, s: Float, l: Float) {
val hh = ((h % 360f) + 360f) % 360f
val cc = (1f - abs(2f * l - 1f)) * s
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
val m = l - cc / 2f
val (rp, gp, bp) = when {
hh < 60f -> Triple(cc, x, 0f)
hh < 120f -> Triple(x, cc, 0f)
hh < 180f -> Triple(0f, cc, x)
hh < 240f -> Triple(0f, x, cc)
hh < 300f -> Triple(x, 0f, cc)
else -> Triple(cc, 0f, x)
}
r = rp + m; g = gp + m; b = bp + m
_h = hh; _s = s; _l = l; _hslDirty = false
}
/** Return 0xRRGGBBAA int */
fun toRgbaInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
}
/** Return 0xAARRGGBB int */
fun toArgbInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
}
// — Convenience modifiers (chainable) —
/** Lighten by fraction [0...1] */
fun lighten(fraction: Float): CSSColor = apply {
lightness = (lightness + fraction).coerceIn(0f, 1f)
}
/** Darken by fraction [0...1] */
fun darken(fraction: Float): CSSColor = apply {
lightness = (lightness - fraction).coerceIn(0f, 1f)
}
/** Increase saturation by fraction [0...1] */
fun saturate(fraction: Float): CSSColor = apply {
saturation = (saturation + fraction).coerceIn(0f, 1f)
}
/** Decrease saturation by fraction [0...1] */
fun desaturate(fraction: Float): CSSColor = apply {
saturation = (saturation - fraction).coerceIn(0f, 1f)
}
/** Rotate hue by degrees (can be negative) */
fun rotateHue(degrees: Float): CSSColor = apply {
hue = (hue + degrees) % 360f
}
companion object {
/** Create from Android 0xAARRGGBB */
@JvmStatic fun fromArgb(color: Int): CSSColor {
val a = ((color ushr 24) and 0xFF) / 255f
val r = ((color ushr 16) and 0xFF) / 255f
val g = ((color ushr 8) and 0xFF) / 255f
val b = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
/** Create from Android 0xRRGGBBAA */
@JvmStatic fun fromRgba(color: Int): CSSColor {
val r = ((color ushr 24) and 0xFF) / 255f
val g = ((color ushr 16) and 0xFF) / 255f
val b = ((color ushr 8) and 0xFF) / 255f
val a = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
return fromArgb(color)
}
private val NAMED_HEX = mapOf(
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
"yellowgreen" to "9ACD32"
)
private val NAMED: Map<String, Int> = NAMED_HEX
.mapValues { (_, hexRgb) ->
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
val rgb = hexRgb.toInt(16)
(rgb shl 8) or 0xFF
} + ("transparent" to 0x00000000)
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
@JvmStatic
fun parseColor(s: String): CSSColor {
val str = s.trim()
// named
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
// hex
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
return parseHexPart(part)
}
// rgb/rgba
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseRgbParts(it.split(',').map(String::trim))
}
// hsl/hsla
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseHslParts(it.split(',').map(String::trim))
}
error("Cannot parse color: \"$s\"")
}
private fun parseHexPart(p: String): CSSColor {
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
val hex = when (p.length) {
3 -> p.map { "$it$it" }.joinToString("") + "FF"
4 -> p.map { "$it$it" }.joinToString("")
6 -> p + "FF"
8 -> p
else -> error("Invalid hex color: #$p")
}
val parsed = hex.toLong(16).toInt()
val alpha = (parsed and 0xFF) shl 24
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
val argb = alpha or rgbOnly
return fromArgb(argb)
}
private fun parseRgbParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
// r/g/b: "128" → 128/255, "50%" → 0.5
fun channel(ch: String): Float =
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
else ch.toFloat().coerceIn(0f, 255f) / 255f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
else a.toFloat().coerceIn(0f, 1f)
val r = channel(parts[0])
val g = channel(parts[1])
val b = channel(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(r, g, b, a)
}
private fun parseHslParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
fun hueOf(h: String): Float = when {
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
else -> h.toFloat()
}
// for s and l you only ever see percentages
fun pct(p: String): Float =
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) pct(a)
else a.toFloat().coerceIn(0f, 1f)
val h = hueOf(parts[0])
val s = pct(parts[1])
val l = pct(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
}
}
}
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
fun CSSColor.toAndroidColor(): Int = toArgbInt()

View file

@ -14,6 +14,7 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@ -225,25 +226,6 @@ fun Long.toHumanTime(isMs: Boolean): String {
else
return "${prefix}${minsStr}:${secsStr}"
}
fun Long.toHumanDuration(isMs: Boolean): String {
var scaler = 1;
if(isMs)
scaler = 1000;
val v = Math.abs(this);
val hours = Math.max(v/(secondsInHour*scaler), 0);
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
val minsStr = mins.toString();
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
val secsStr = seconds.toString().padStart(2, '0');
val prefix = if (this < 0) { "-" } else { "" };
return listOf(
if(hours > 0) "${hours}h" else null,
if(mins > 0) "${mins}m" else null ,
if(seconds > 0) "${seconds}s" else null
).filterNotNull().joinToString(" ");
}
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
fun String.fixHtmlWhitespace(): Spanned {
@ -375,19 +357,14 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
fun String.matchesDomain(queryDomain: String): Boolean {
if(queryDomain.startsWith(".")) {
val parts = this.lowercase().split(".");
val queryParts = queryDomain.lowercase().trimStart("."[0]).split(".");
if(queryParts.size < 2)
val parts = queryDomain.lowercase().split(".");
if(parts.size < 3)
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
else {
val possibleDomain = "." + queryParts.joinToString(".");
if(slds.contains(possibleDomain))
if(parts.size >= 3){
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
if(isSLD && parts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
/*
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
if(isSLD && queryParts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
*/
}
//TODO: Should be safe, but double verify if can't be exploited
@ -399,11 +376,9 @@ fun String.matchesDomain(queryDomain: String): Boolean {
fun String.getSubdomainWildcardQuery(): String {
val domainParts = this.split(".");
var wildcardDomain = if(domainParts.size > 2)
"." + domainParts.drop(1).joinToString(".")
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
if(slds.contains(sldParts))
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
else
"." + domainParts.joinToString(".");
if(slds.contains(wildcardDomain.lowercase()))
"." + domainParts.joinToString(".");
return wildcardDomain;
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
}

View file

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

View file

@ -1,13 +1,13 @@
package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol
import kotlin.math.abs
import kotlin.math.min
@ -40,21 +40,6 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
}
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
val urlData = if (this.startsWith("polycentric://")) {
this.substring("polycentric://".length)
} else this;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
@ -62,3 +47,26 @@ fun Protocol.Claim.resolveChannelUrl(): String? {
fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
addServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
for (pair in exceptions) {
val server = pair.key
val exception = pair.value
StateAnnouncement.instance.registerAnnouncement(
"backfill-failed",
"Backfill failed",
"Failed to backfill server $server. $exception",
AnnouncementType.SESSION_RECURRING
);
Logger.e("Backfill", "Failed to backfill server $server.", exception)
}
}

View file

@ -7,9 +7,6 @@ import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring
inline fun <reified T> Any.assume(): T?{
@ -36,37 +33,13 @@ fun Boolean?.toYesNo(): String {
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
val index = hostAddr.indexOf('%')
if (index != -1) {
val addrPart = hostAddr.substring(0, index)
val scopeId = hostAddr.substring(index + 1)
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
} else {
"[$hostAddr]"
}
"[${toString()}]"
}
is Inet4Address -> {
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
toString()
}
else -> {
throw Exception("Invalid address type")
}
}
}
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
}

View file

@ -2,30 +2,10 @@ 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
@ -44,10 +24,6 @@ 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}");
@ -113,29 +89,7 @@ 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 -> {
@ -193,136 +147,3 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
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));
}

View file

@ -29,7 +29,6 @@ import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.AdvancedField
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
@ -176,10 +175,6 @@ class Settings : FragmentedStorageFileJson() {
}
}*/
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
var advancedSettings: Boolean = false;
@FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings();
@Serializable
@ -210,7 +205,7 @@ class Settings : FragmentedStorageFileJson() {
var home = HomeSettings();
@Serializable
class HomeSettings {
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
@DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1;
@ -221,16 +216,10 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL;
}
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
var showHomeFilters: Boolean = true;
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@ -259,17 +248,12 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
var hidefromSearch: Boolean = false;
fun getSearchFeedStyle(): FeedStyle {
if(searchFeedStyle == 0)
@ -285,7 +269,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true;
}
@ -308,23 +291,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@ -355,16 +331,13 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false;
@ -380,7 +353,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings();
@Serializable
class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
@DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0;
@ -404,8 +377,6 @@ class Settings : FragmentedStorageFileJson() {
else -> null
}
}
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true;
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@ -441,13 +412,11 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true;
@AdvancedField
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@FormField(R.string.force_enable_auto_rotate_in_full_screen, FieldForm.TOGGLE, R.string.force_enable_auto_rotate_in_full_screen_description, 5)
var forceAllowFullScreenRotation: Boolean = false
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@DropdownFieldOptionsId(R.array.player_background_behavior)
@ -456,7 +425,6 @@ class Settings : FragmentedStorageFileJson() {
fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
@AdvancedField
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
@DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1;
@ -483,10 +451,14 @@ class Settings : FragmentedStorageFileJson() {
};
}
@AdvancedField
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true;
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
var backgroundSwitchToAudio: Boolean = true;
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1;
@ -512,97 +484,8 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
@DropdownFieldOptionsId(R.array.seek_offset_duration)
var seekOffset: Int = 2;
fun getSeekOffset(): Long {
return when(seekOffset) {
0 -> 3_000L;
1 -> 5_000L;
2 -> 10_000L;
3 -> 20_000L;
4 -> 30_000L;
5 -> 60_000L;
else -> 10_000L;
}
}
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
@DropdownFieldOptionsId(R.array.min_playback_speed)
var minimumPlaybackSpeed: Int = 0;
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.max_playback_speed)
var maximumPlaybackSpeed: Int = 2;
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.step_playback_speed)
var stepPlaybackSpeed: Int = 1;
fun getPlaybackSpeedStep(): Double {
return when(stepPlaybackSpeed) {
0 -> 0.05
1 -> 0.1
2 -> 0.25
else -> 0.1;
}
}
fun getPlaybackSpeeds(): List<Double> {
val playbackSpeeds = mutableListOf<Double>();
playbackSpeeds.add(1.0);
val minSpeed = when(minimumPlaybackSpeed) {
0 -> 0.25
1 -> 0.5
2 -> 1.0
else -> 0.25
}
val maxSpeed = when(maximumPlaybackSpeed) {
0 -> 2.0
1 -> 2.25
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 2.25;
}
var testSpeed = 1.0;
while(testSpeed > minSpeed) {
val nextSpeed = (testSpeed - 0.25) as Double;
testSpeed = Math.max(nextSpeed, minSpeed);
playbackSpeeds.add(testSpeed);
}
testSpeed = 1.0;
while(testSpeed < maxSpeed) {
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
testSpeed = Math.min(nextSpeed, maxSpeed);
playbackSpeeds.add(testSpeed);
}
playbackSpeeds.sort();
return playbackSpeeds;
}
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
var holdPlaybackSpeed: Int = 4;
fun getHoldPlaybackSpeed(): Double {
return when(holdPlaybackSpeed) {
0 -> 1.0
1 -> 1.25
2 -> 1.5
3 -> 1.75
4 -> 2.0
5 -> 2.25
6 -> 2.5
7 -> 2.75
8 -> 3.0
else -> 2.0
}
}
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@ -618,7 +501,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@ -655,12 +537,10 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true;
@AdvancedField
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
@DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3;
@ -690,21 +570,10 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true;
@AdvancedField
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: Boolean = false;
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@ -772,11 +641,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true;
@ -977,23 +841,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
fun viewLicenseStatus() {
SettingsActivity.getActivity()?.let {
try {
if (StatePayment.instance.hasPaid) {
val paymentKey = StatePayment.instance.getPaymentKey()
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
} else {
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show license status dialog", e)
}
}
}
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun clearPayment() {
SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
@ -1011,20 +859,13 @@ class Settings : FragmentedStorageFileJson() {
var other = Other();
@Serializable
class Other {
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true;
@FormField(R.string.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)
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
var polycentricLocalCache: Boolean = true;
}
@ -1062,7 +903,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = false;
var enabled: Boolean = true;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false;
@ -1072,21 +913,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true;
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
var discoverThroughRelay: Boolean = true;
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
var pairThroughRelay: Boolean = true;
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
var connectThroughRelay: Boolean = true;
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
var connectLocalDirectThroughRelay: Boolean = true;
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
var localConnections: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 21)

View file

@ -1,7 +1,6 @@
package com.futo.platformplayer
import android.content.Context
import android.content.Intent
import android.webkit.CookieManager
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder

View file

@ -5,7 +5,6 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
@ -200,21 +199,16 @@ class UIDialogs {
dialog.show();
}
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view);
builder.setCancelable(defaultCloseAction > -2);
val dialog = builder.create();
registerDialogOpened(dialog);
view.findViewById<ImageView>(R.id.dialog_icon).apply {
this.setImageResource(icon);
if(animated)
this.drawable.assume<Animatable, Unit> { it.start() };
}
view.findViewById<TextView>(R.id.dialog_text).apply {
this.text = text;
@ -281,7 +275,6 @@ class UIDialogs {
registerDialogClosed(dialog);
}
dialog.show();
return dialog;
}
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
@ -319,11 +312,7 @@ class UIDialogs {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
try {
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
retryAction?.invoke();
}, UIDialogs.ActionStyle.PRIMARY)
);
else
@ -337,11 +326,7 @@ class UIDialogs {
closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), {
try {
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
retryAction?.invoke();
}, UIDialogs.ActionStyle.PRIMARY)
);
}
@ -383,8 +368,8 @@ class UIDialogs {
}
}
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
val dialog = ChangelogDialog(context, changelogs);
fun showChangelogDialog(context: Context, lastVersion: Int) {
val dialog = ChangelogDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
@ -424,7 +409,7 @@ class UIDialogs {
}
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
fun showCastingDialog(context: Context) {
val d = StateCasting.instance.activeDevice;
if (d != null) {
val dialog = ConnectedCastingDialog(context);
@ -432,7 +417,6 @@ class UIDialogs {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
} else {
@ -445,24 +429,21 @@ class UIDialogs {
if (c is Activity) {
dialog.setOwnerActivity(c);
}
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
}
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
fun showCastingTutorialDialog(context: Context) {
val dialog = CastingHelpDialog(context);
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
fun showCastingAddDialog(context: Context) {
val dialog = CastingAddDialog(context);
registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}

View file

@ -4,14 +4,8 @@ import android.app.NotificationManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
@ -43,9 +37,6 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.parsers.HLS.MediaRendition
import com.futo.platformplayer.parsers.HLS.StreamInfo
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
@ -72,8 +63,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import androidx.core.net.toUri
class UISlideOverlays {
companion object {
@ -90,36 +79,6 @@ class UISlideOverlays {
return menu;
}
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
UISlideOverlays.showOverlay(container, "Queue options", null, {
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text.trim()
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
StatePlayer.instance.saveQueueAsPlaylist(text);
UIDialogs.appToast("Playlist [${text}] created");
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
}, false));
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>();
@ -129,163 +88,115 @@ 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) {
try {
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
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",
@ -293,82 +204,61 @@ 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();
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show subscription overlay.", e)
menu.show();
}
}
@ -379,7 +269,6 @@ class UISlideOverlays {
}
@OptIn(UnstableApi::class)
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@ -391,8 +280,6 @@ class UISlideOverlays {
val masterPlaylistContent = masterPlaylistResponse.body?.string()
?: throw Exception("Master playlist content is empty")
val resolvedPlaylistUrl = masterPlaylistResponse.url
val videoButtons = arrayListOf<SlideUpMenuItem>()
val audioButtons = arrayListOf<SlideUpMenuItem>()
//TODO: Implement subtitles
@ -405,103 +292,53 @@ class UISlideOverlays {
val masterPlaylist: HLS.MasterPlaylist
try {
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
.parse(sourceUrl.toUri(), inputStream)
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
if (playlist is HlsMediaPlaylist) {
if (source is IHLSManifestAudioSource) {
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
variant.name,
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedAudioVariant = variant
slideUpMenuOverlay.selectOption(audioButtons, variant)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
} else {
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
variant.name,
"${variant.width}x${variant.height}",
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedVideoVariant = variant
slideUpMenuOverlay.selectOption(videoButtons, variant)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
} else if (playlist is HlsMultivariantPlaylist) {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false))
}*/
},
invokeParent = false
))
}
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false))
}*/
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
val newItems = arrayListOf<View>()
@ -529,11 +366,11 @@ class UISlideOverlays {
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
withContext(Dispatchers.Main) {
if (source is IHLSManifestSource) {
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide()
} else {
@ -580,7 +417,7 @@ class UISlideOverlays {
}
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
listOf(listOf(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.none),
@ -593,7 +430,7 @@ class UISlideOverlays {
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)) else listOf()) +
)) +
videoSources
.filter { it.isDownloadable() }
.map {
@ -815,10 +652,6 @@ class UISlideOverlays {
}
}
}
if(!Settings.instance.downloads.shouldDownload()) {
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
"(You can change this in settings)", true);
}
}
};
return menu.apply { show() };
@ -1062,8 +895,7 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
);
@ -1074,7 +906,7 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
if(!isLimited && !video.isLive)
if(!isLimited)
SlideUpMenuItem(
container.context,
R.drawable.ic_download,
@ -1115,30 +947,26 @@ class UISlideOverlays {
+ actions).filterNotNull()
));
items.add(
SlideUpMenuGroup(
container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(
container.context,
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.add_to_queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(
container.context,
SlideUpMenuItem(container.context,
R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem(
container.context,
SlideUpMenuItem(container.context,
R.drawable.ic_history,
container.context.getString(R.string.add_to_history),
"Mark as watched",
tag = "history",
call = { StateHistory.instance.markAsWatched(video); }),
));
));
val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(
@ -1163,8 +991,7 @@ class UISlideOverlays {
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StatePlaylists.instance.addToPlaylist(playlist.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
}
@ -1191,8 +1018,7 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
);
@ -1202,28 +1028,20 @@ class UISlideOverlays {
val queue = StatePlayer.instance.getQueue();
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(
SlideUpMenuGroup(
container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(
container.context,
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(
container.context,
SlideUpMenuItem(container.context,
R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
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))
}),
)
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
)
);
val playlistItems = arrayListOf<SlideUpMenuItem>();
@ -1249,8 +1067,7 @@ class UISlideOverlays {
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
StatePlaylists.instance.addToPlaylist(playlist.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
}
@ -1261,8 +1078,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
}
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
overlay.show();
return overlay;
}
@ -1292,7 +1109,7 @@ class UISlideOverlays {
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
tag = "",
call = {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
val selected = it
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
@ -1300,7 +1117,7 @@ class UISlideOverlays {
.toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
});
}
},
invokeParent = false
))
@ -1308,40 +1125,29 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
}
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
val selection: MutableList<Any> = mutableListOf();
var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
listOf(
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
).filterNotNull() +
(options.map { SlideUpMenuItem(
options.map { SlideUpMenuItem(
container.context,
R.drawable.ic_move_up,
it.first,
"",
tag = it.second,
call = {
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
if(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second)) {
if(!selection.contains(it.second))
selection.add(it.second);
if(overlayItem != null) {
overlayItem.setSubText(selection.indexOf(it.second).toString());
}
}
} else {
} else
selection.remove(it.second);
if(overlayItem != null) {
overlayItem.setSubText("");
}
}
},
invokeParent = false
)
}));
});
overlay.onOK.subscribe {
onOrdered.invoke(selection);
overlay.hide();

View file

@ -27,23 +27,14 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.time.OffsetDateTime
import java.nio.ByteOrder
import java.util.*
import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String {
@ -75,14 +66,7 @@ fun warnIfMainThread(context: String) {
}
fun ensureNotMainThread() {
val isMainLooper = try {
Looper.myLooper() == Looper.getMainLooper()
} catch (e: Throwable) {
//Ignore, for unit tests where its not mocked
false
}
if (isMainLooper) {
if (Looper.myLooper() == Looper.getMainLooper()) {
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
throw IllegalStateException("Cannot run on main thread")
}
@ -285,7 +269,7 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
}
}
if(newIndex < 0)
return newArr.size;
return originalArr.size;
else
return newIndex;
}
@ -295,167 +279,3 @@ fun ByteBuffer.toUtf8String(): String {
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
fun generateReadablePassword(length: Int): String {
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
val secureRandom = SecureRandom()
val randomBytes = ByteArray(length)
secureRandom.nextBytes(randomBytes)
val sb = StringBuilder(length)
for (byte in randomBytes) {
val index = (byte.toInt() and 0xFF) % validChars.length
sb.append(validChars[index])
}
return sb.toString()
}
fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val gzipTimeStart = OffsetDateTime.now();
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(this)
}
val result = outputStream.toByteArray();
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
return result;
}
fun ByteArray.fromGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val inputStream = ByteArrayInputStream(this)
val outputStream = ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
}
fun findCandidateAddresses(): List<InetAddress> {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.sortedWith(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
).map { it.second.address }
}
fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.minWithOrNull(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
)?.second?.address
}
private fun isUsableInterface(nif: NetworkInterface): Boolean {
val name = nif.name.lowercase()
return try {
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
nif.isUp
&& !nif.isLoopback
&& !nif.isPointToPoint
&& !nif.isVirtual
&& !name.startsWith("docker")
&& !name.startsWith("veth")
&& !name.startsWith("br-")
&& !name.startsWith("virbr")
&& !name.startsWith("vmnet")
&& !name.startsWith("tun")
&& !name.startsWith("tap")
} catch (e: SocketException) {
false
}
}
private fun isUsableAddress(addr: InetAddress): Boolean {
return when {
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
addr.isLoopbackAddress -> false
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
addr.isMulticastAddress -> false
else -> true
}
}
private fun interfaceScore(nif: NetworkInterface): Int {
val name = nif.name.lowercase()
return when {
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
name.startsWith("eth") || name.contains("ethernet") -> 0
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
name.contains("wi-fi") || name.contains("wifi") -> 1
else -> 2
}
}
fun addressScore(addr: InetAddress): Int {
return when (addr) {
is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF }
when {
octets[0] == 10 -> 0 // 10/8
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.1631/12
else -> 1 // public IPv4
}
}
is Inet6Address -> {
// ULA (fc00::/7) vs global vs others
val b0 = addr.address[0].toInt() and 0xFF
when {
(b0 and 0xFE) == 0xFC -> 2 // ULA
(b0 and 0xE0) == 0x20 -> 3 // global
else -> 4
}
}
else -> Int.MAX_VALUE
}
}
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)

View file

@ -10,13 +10,11 @@ import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.zxing.integration.android.IntentIntegrator
class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton;
lateinit var _overlayContainer: FrameLayout;
lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton;
@ -56,7 +54,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
setContentView(R.layout.activity_add_source_options);
setNavigationBarColorAndIcons();
_overlayContainer = findViewById(R.id.overlay_container);
_buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr);
@ -84,25 +81,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
}
_buttonURL.onClick.subscribe {
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
val content = nameInput.text;
val url = if (content.startsWith("https://")) {
content
} else if (content.startsWith("grayjay://plugin/")) {
content.substring("grayjay://plugin/".length)
} else {
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
return@showOverlay;
}
val intent = Intent(this, AddSourceActivity::class.java).apply {
data = Uri.parse(url);
};
startActivity(intent);
}, nameInput)
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
}
}
}

View file

@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
companion object {
private val TAG = "LoginActivity";
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
private var _callback: ((SourceAuth?) -> Unit)? = null;

View file

@ -1,15 +1,14 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.UiModeManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
@ -22,7 +21,6 @@ import android.widget.ImageView
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat
@ -32,7 +30,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStateAtLeast
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
@ -41,9 +38,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
@ -62,7 +57,6 @@ 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
@ -71,9 +65,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
@ -82,6 +74,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.receivers.MediaButtonReceiver
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
@ -114,7 +107,7 @@ import java.io.PrintWriter
import java.io.StringWriter
import java.lang.reflect.InvocationTargetException
import java.util.LinkedList
import java.util.UUID
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
@ -153,8 +146,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Frags Main
lateinit var _fragMainHome: HomeFragment;
lateinit var _fragPostDetail: PostDetailFragment;
lateinit var _fragArticleDetail: ArticleDetailFragment;
lateinit var _fragWebDetail: WebDetailFragment;
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
@ -170,7 +161,6 @@ 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;
@ -185,7 +175,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment;
//State
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList();
private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null;
@ -195,9 +185,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _isVisible = true;
private var _wasStopped = false;
private var _privateModeEnabled = false
private var _pictureInPictureEnabled = false
private var _isFullscreen = false
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@ -209,7 +196,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
try {
lifecycleScope.launch {
runBlocking {
handleUrlAll(content)
}
} catch (e: Throwable) {
@ -219,8 +206,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
val mainId = UUID.randomUUID().toString().substring(0, 5)
constructor() : super() {
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(
@ -272,15 +257,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi
override fun onCreate(savedInstanceState: Bundle?) {
Logger.w(TAG, "MainActivity Starting [$mainId]");
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope);
StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiMode = getSystemService(UiModeManager::class.java)
uiMode.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
}
setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout)
@ -288,11 +269,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking {
try {
StatePlatform.instance.updateAvailableClients(this@MainActivity);
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
}
StatePlatform.instance.updateAvailableClients(this@MainActivity);
}
//Preload common files to memory
@ -336,11 +313,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance();
_fragArticleDetail = ArticleDetailFragment.newInstance();
_fragWebDetail = WebDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
_fragShorts = ShortsFragment.newInstance();
_fragSourceDetail = SourceDetailFragment.newInstance();
_fragDownloads = DownloadsFragment();
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
@ -380,18 +354,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSubscriptionsFeed.setPreviewsEnabled(true);
_fragContainerVideoDetail.visibility = View.INVISIBLE;
updateSegmentPaddings();
updatePrivateModeVisibility()
};
_buttonIncognito = findViewById(R.id.incognito_button);
updatePrivateModeVisibility()
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering?
_privateModeEnabled = it
updatePrivateModeVisibility()
if (it) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
_buttonIncognito.setOnClickListener {
if (!StateApp.instance.privateMode)
return@setOnClickListener;
@ -408,16 +386,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
};
_fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}");
_isFullscreen = it
updatePrivateModeVisibility()
}
_fragVideoDetail.onMinimize.subscribe {
updatePrivateModeVisibility()
}
_fragVideoDetail.onMaximized.subscribe {
updatePrivateModeVisibility()
if (it) {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
} else {
if (StateApp.instance.privateMode) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
}
StatePlayer.instance.also {
@ -465,8 +446,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation;
_fragArticleDetail.topBar = _fragTopBarNavigation;
_fragWebDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation;
_fragSourceDetail.topBar = _fragTopBarNavigation;
@ -611,8 +590,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}, UIDialogs.ActionStyle.PRIMARY)
)
}
//startActivity(Intent(this, TestActivity::class.java))
}
/*
@ -636,18 +613,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
}*/
private var _qrCodeLoadingDialog: AlertDialog? = null
fun showUrlQrCodeScanner() {
try {
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
"Launching QR scanner",
"Make sure your camera is enabled", null, -2,
UIDialogs.Action("Close", {
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
}));
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
@ -663,36 +630,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
@OptIn(UnstableApi::class)
private fun updatePrivateModeVisibility() {
if (_privateModeEnabled && (_fragVideoDetail.state == State.CLOSED || !_pictureInPictureEnabled && !_isFullscreen)) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
_buttonIncognito.translationY = if (_fragVideoDetail.state == State.MINIMIZED) -60.dp(resources).toFloat() else 0f
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
override fun onResume() {
super.onResume();
Logger.w(TAG, "onResume [$mainId]")
Logger.v(TAG, "onResume")
_isVisible = true;
}
override fun onPause() {
super.onPause();
Logger.w(TAG, "onPause [$mainId]")
Logger.v(TAG, "onPause")
_isVisible = false;
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
}
override fun onStop() {
super.onStop()
Logger.w(TAG, "onStop [$mainId]");
Logger.v(TAG, "_wasStopped = true");
_wasStopped = true;
}
@ -726,7 +678,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"VIDEO" -> {
val url = intent.getStringExtra("VIDEO");
navigateWhenReady(_fragVideoDetail, url);
navigate(_fragVideoDetail, url);
}
"IMPORT_OPTIONS" -> {
@ -744,11 +696,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"Sources" -> {
runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
navigateWhenReady(_fragMainSources);
navigate(_fragMainSources);
}
};
"BROWSE_PLUGINS" -> {
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let {
if (it is MainActivity) {
@ -766,12 +718,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
try {
if (targetData != null) {
lifecycleScope.launch(Dispatchers.Main) {
try {
handleUrlAll(targetData)
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
}
runBlocking {
handleUrlAll(targetData)
}
}
} catch (ex: Throwable) {
@ -799,10 +747,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
startActivity(intent);
} else if (url.startsWith("grayjay://video/")) {
val videoUrl = url.substring("grayjay://video/".length);
navigateWhenReady(_fragVideoDetail, videoUrl);
navigate(_fragVideoDetail, videoUrl);
} else if (url.startsWith("grayjay://channel/")) {
val channelUrl = url.substring("grayjay://channel/".length);
navigateWhenReady(_fragMainChannel, channelUrl);
navigate(_fragMainChannel, channelUrl);
}
}
@ -868,29 +816,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return withContext(Dispatchers.IO) {
Logger.i(TAG, "handleUrl(url=$url) on IO");
if (StatePlatform.instance.hasEnabledContentClient(url)) {
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client");
withContext(Dispatchers.Main) {
lifecycleScope.launch(Dispatchers.Main) {
if (position > 0)
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
else
navigateWhenReady(_fragVideoDetail, url);
navigate(_fragVideoDetail, url);
_fragVideoDetail.maximizeVideoDetail(true);
}
return@withContext true;
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found channel client");
withContext(Dispatchers.Main) {
navigateWhenReady(_fragMainChannel, url);
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainChannel, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
return@withContext true;
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
withContext(Dispatchers.Main) {
navigateWhenReady(_fragMainRemotePlaylist, url);
lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainRemotePlaylist, url);
delay(100);
_fragVideoDetail.minimizeVideoDetail();
};
@ -1102,33 +1050,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
_pictureInPictureEnabled = isInPictureInPictureMode
updatePrivateModeVisibility()
}
override fun onDestroy() {
super.onDestroy();
Logger.w(TAG, "onDestroy [$mainId]")
StateApp.instance.mainAppDestroyed(this, mainId);
Logger.v(TAG, "onDestroy")
StateApp.instance.mainAppDestroyed(this);
}
inline fun <reified T> isFragmentActive(): Boolean {
return fragCurrent is T;
}
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
navigate(segment, parameter, withHistory, isBack)
} else {
lifecycleScope.launch {
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
navigate(segment, parameter, withHistory, isBack)
}
}
}
}
/**
* Navigate takes a MainFragment, and makes them the current main visible view
* A parameter can be provided which becomes available in the onShow of said fragment
@ -1190,6 +1123,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent;
fragCurrent = segment;
_parameterCurrent = parameter;
}
@ -1252,11 +1186,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T;
ArticleDetailFragment::class -> _fragArticleDetail as T;
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;
@ -1350,7 +1281,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (toast.long)
delay(5000);
else
delay(2500);
delay(3000);
}
Logger.i(TAG, "Ending appToast loop");
lifecycleScope.launch(Dispatchers.Main) {

View file

@ -14,12 +14,10 @@ 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
@ -31,9 +29,6 @@ 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
@ -44,7 +39,6 @@ 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))
@ -55,47 +49,24 @@ 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)
_loader = findViewById(R.id.progress_loader)
_buttonShare = findViewById(R.id.button_share);
_buttonCopy = findViewById(R.id.button_copy);
_imageQR = findViewById(R.id.image_qr);
_textQR = findViewById(R.id.text_qr);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
_exportBundle = createExportBundle();
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
}
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;
}
_buttonShare.onClick.subscribe {

View file

@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(ApiMethods.SERVER);
processHandle.addServer(PolycentricCache.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {

View file

@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(ApiMethods.SERVER);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();

View file

@ -21,8 +21,10 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons
@ -30,10 +32,8 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(ApiMethods.SERVER)
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
withContext(Dispatchers.Main) {
updateUI();

View file

@ -9,8 +9,6 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
@ -31,16 +29,6 @@ class SyncHomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (StateApp.instance.contextOrNull == null) {
Logger.w(TAG, "No main activity, restarting main.")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
return
}
setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons()
@ -66,6 +54,7 @@ class SyncHomeActivity : AppCompatActivity() {
val view = _viewMap[publicKey]
if (!session.isAuthorized) {
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey)
}
return@launch
@ -100,20 +89,6 @@ class SyncHomeActivity : AppCompatActivity() {
updateEmptyVisibility()
}
}
StateSync.instance.confirmStarted(this, onStarted = {
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
}
if (StateSync.instance.syncService?.relayConnected == false) {
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
}
if (StateSync.instance.syncService?.serverSocketStarted == false) {
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
}
}, onNotStarted = {
finish()
})
}
override fun onDestroy() {
@ -125,12 +100,9 @@ class SyncHomeActivity : AppCompatActivity() {
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
val authorized = session?.isAuthorized ?: false
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized")
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
.setName(publicKey)
.setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView
}

View file

@ -83,7 +83,6 @@ class SyncPairActivity : AppCompatActivity() {
_layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE
finish()
}
_layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE
@ -110,17 +109,11 @@ class SyncPairActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
StateSync.instance.connect(deviceInfo) { session, complete, message ->
lifecycleScope.launch(Dispatchers.Main) {
if (complete != null) {
if (complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textError.text = message
_layoutPairingError.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
}
if (complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textPairingStatus.text = message
}
@ -129,11 +122,7 @@ class SyncPairActivity : AppCompatActivity() {
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
_layoutPairingError.visibility = View.VISIBLE
if(e.message == "Failed to connect") {
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
}
else
_textError.text = e.message
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
@ -144,6 +133,8 @@ class SyncPairActivity : AppCompatActivity() {
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
}
}

View file

@ -67,18 +67,11 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
}
val ips = getIPs()
val publicKey = StateSync.instance.syncService?.publicKey
val pairingCode = StateSync.instance.syncService?.pairingCode
if (publicKey == null || pairingCode == null) {
setCode("Public key or pairing code was not known, is sync enabled?")
} else {
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
fun setCode(code: String?) {

View file

@ -2,24 +2,12 @@ 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 {

View file

@ -5,8 +5,6 @@ import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.FragmentedStorage
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
@ -65,7 +63,7 @@ open class ManagedHttpClient {
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
@ -90,7 +88,6 @@ open class ManagedHttpClient {
}
fun tryHead(url: String): Map<String, String>? {
ensureNotMainThread()
try {
val result = head(url);
if(result.isOk)
@ -105,7 +102,7 @@ open class ManagedHttpClient {
}
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
ensureNotMainThread()
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url);
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
@ -301,7 +298,6 @@ open class ManagedHttpClient {
}
fun send(msg: String) {
ensureNotMainThread()
socket.send(msg);
}

View file

@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
current += bytesToSend.toLong()
if (current > end) {
if (current >= end) {
Logger.i(TAG, "Expected amount of bytes sent")
break
}

View file

@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -13,7 +12,6 @@ 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
@ -37,11 +35,6 @@ interface IPlatformClient {
*/
fun getHome(): IPager<IPlatformContent>
/**
* Gets the shorts feed
*/
fun getShorts(): IPager<IPlatformVideo>
//Search
/**
* Gets search suggestion for the provided query string
@ -73,11 +66,6 @@ interface IPlatformClient {
*/
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages
/**
@ -182,10 +170,6 @@ 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;

View file

@ -11,7 +11,6 @@ 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
@ -27,17 +26,12 @@ 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;
@ -45,24 +39,8 @@ class LiveChatManager {
_scope = scope;
_pager = pager;
viewCount = initialViewCount;
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());
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());
}
fun start() {
@ -74,10 +52,6 @@ class LiveChatManager {
_startCounter++;
}
fun setVideoPosition(ms: Long) {
_position = ms;
}
fun getHistory(): List<IPlatformLiveEvent> {
synchronized(_history) {
return _history.toList();
@ -111,34 +85,13 @@ 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;
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();
}
_pager.nextPage();
val newEvents = _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(", ")}]");

View file

@ -20,8 +20,7 @@ data class PlatformClientCapabilities(
val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false,
val hasGetUserHistory: Boolean = false
val hasGetContentRecommendations: Boolean = false
) {
}

View file

@ -14,16 +14,14 @@ class PlatformClientPool {
private var _poolCounter = 0;
private val _poolName: String?;
private val _privatePool: Boolean;
private val _isolatedInitialization: Boolean
var isDead: Boolean = false
private set;
val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) {
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
_poolName = name;
_privatePool = privatePool;
_isolatedInitialization = isolatedInitialization
if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started");
@ -34,10 +32,8 @@ class PlatformClientPool {
isDead = true;
onDead.emit(parentClient, this);
synchronized(_pool) {
for (clientPair in _pool) {
clientPair.key.disable();
}
for(clientPair in _pool) {
clientPair.key.disable();
}
};
}
@ -57,7 +53,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool, _isolatedInitialization);
reserved = _parent.getCopy(_privatePool);
reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);

View file

@ -2,7 +2,6 @@ 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
@ -45,7 +44,6 @@ 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),

View file

@ -7,15 +7,13 @@ class PlatformMultiClientPool {
private var _isFake = false;
private var _privatePool = false;
private val _isolatedInitialization: Boolean
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) {
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
_name = name;
_maxCap = if(maxCap > 0)
maxCap
else 99;
_privatePool = isPrivatePool;
_isolatedInitialization = isolatedInitialization
}
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@ -23,7 +21,7 @@ class PlatformMultiClientPool {
return parentClient;
val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply {
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
this.onDead.subscribe { _, pool ->
synchronized(_clientPools) {
if(_clientPools[parentClient] == pool)

View file

@ -2,11 +2,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.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
@ -34,7 +30,6 @@ 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);
@ -48,20 +43,3 @@ open class PlatformAuthorLink {
}
}
}
interface IPlatformChannelContent : IPlatformContent {
val thumbnail: String?
val subscribers: Long?
}
open class JSChannelContent : JSContent, IPlatformChannelContent {
override val contentType: ContentType get() = ContentType.CHANNEL
override val thumbnail: String?
override val subscribers: Long?
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Channel";
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
}
}

View file

@ -3,7 +3,6 @@ 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
@ -21,7 +20,6 @@ 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),

View file

@ -5,7 +5,6 @@ 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
@ -31,7 +30,6 @@ class ResultCapabilities(
const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED";
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
const val TYPE_SHORTS = "SHORTS";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
@ -47,7 +45,6 @@ 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"); },
@ -71,7 +68,6 @@ 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)
@ -93,7 +89,6 @@ class FilterCapability(
companion object {
fun fromV8(obj: V8ValueObject): FilterCapability {
obj.ensureIsBusy();
val value = obj.get("value") as V8Value;
return FilterCapability(
obj.getString("name"),

View file

@ -4,7 +4,6 @@ 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
@ -32,7 +31,6 @@ 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) }

View file

@ -1,9 +0,0 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
interface IPlatformArticle: IPlatformContent {
val summary: String?;
val thumbnails: Thumbnails?;
}

View file

@ -1,12 +0,0 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
val segments: List<IJSArticleSegment>;
val rating : IRating;
}

View file

@ -8,12 +8,10 @@ enum class ContentType(val value: Int) {
POST(2),
ARTICLE(3),
PLAYLIST(4),
WEB(7),
URL(9),
NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70),

View file

@ -2,8 +2,6 @@ package com.futo.platformplayer.api.media.models.contents
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime
interface IPlatformContent {

View file

@ -2,17 +2,14 @@ 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);

View file

@ -4,7 +4,6 @@ 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
@ -18,21 +17,16 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
val colorName: String?;
val badges: List<String>;
override var time: Long = -1;
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
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);
@ -42,8 +36,7 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
obj.getOrThrow(config, "name", contextName),
obj.getOrThrow(config, "thumbnail", contextName, true),
obj.getOrThrow(config, "message", contextName),
colorName, badges,
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
colorName, badges);
}
}
}

View file

@ -3,7 +3,6 @@ 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
@ -21,8 +20,6 @@ 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;
@ -40,7 +37,6 @@ 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),

View file

@ -2,7 +2,6 @@ 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 {
@ -10,17 +9,15 @@ 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));
}
}
}

View file

@ -2,8 +2,6 @@ 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 {
@ -12,26 +10,20 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String;
val targetThumbnail: String;
val targetUrl: String;
val isOutgoing: Boolean;
override var time: Long = -1;
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
constructor(name: String, url: String, thumbnail: String) {
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.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
obj.getOrThrow(config, "targetThumbnail", contextName));
}
}
}

View file

@ -2,7 +2,6 @@ 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 {
@ -10,15 +9,12 @@ 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));

View file

@ -5,8 +5,7 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
enum class TextType(val value: Int) {
RAW(0),
HTML(1),
MARKUP(2),
CODE(3);
MARKUP(2);
companion object {
fun fromInt(value: Int): TextType

View file

@ -3,7 +3,6 @@ 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
@ -14,12 +13,8 @@ interface IRating {
companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
obj?.ensureIsBusy();
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
};
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = 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);

View file

@ -2,7 +2,6 @@ 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
/**
@ -15,7 +14,6 @@ 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"));
}
}

View file

@ -2,7 +2,6 @@ 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,7 +13,6 @@ class RatingLikes(val likes: Long) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
}
}

View file

@ -2,7 +2,6 @@ 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,7 +13,6 @@ class RatingScaler(val value: Float) : IRating {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
}
}

View file

@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.downloads.VideoLocal
class DownloadedVideoMuxedSourceDescriptor(
class LocalVideoMuxedSourceDescriptor(
private val video: VideoLocal
) : VideoMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();

View file

@ -13,8 +13,7 @@ class AudioUrlSource(
override val codec: String = "",
override val language: String = Language.UNKNOWN,
override val duration: Long? = null,
override var priority: Boolean = false,
override var original: Boolean = false
override var priority: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null;
@ -37,9 +36,7 @@ class AudioUrlSource(
source.container,
source.codec,
source.language,
source.duration,
source.priority,
source.original
source.duration
);
ret.streamMetaData = streamData;

View file

@ -27,7 +27,6 @@ class HLSVariantAudioUrlSource(
override val language: String,
override val duration: Long?,
override val priority: Boolean,
override val original: Boolean,
val url: String
) : IAudioUrlSource {
override fun getAudioUrl(): String {

View file

@ -8,5 +8,4 @@ interface IAudioSource {
val language : String;
val duration : Long?;
val priority: Boolean;
val original: Boolean;
}

View file

@ -15,7 +15,6 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
override val duration: Long? = null;
override var priority: Boolean = false;
override val original: Boolean = false;
val filePath : String;
val fileSize: Long;
@ -34,13 +33,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
}
companion object {
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
return LocalAudioSource(
source.name,
path,
fileSize,
source.bitrate,
overrideContainer ?: source.container,
source.container,
source.codec,
source.language
);

View file

@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
}
companion object {
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
return LocalVideoSource(
source.name,
path,
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
source.width,
source.height,
source.duration,
overrideContainer ?: source.container,
source.container,
source.codec,
source.bitrate?:0
);

View file

@ -2,7 +2,6 @@ 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)
@ -13,10 +12,5 @@ interface IPlatformVideo : IPlatformContent {
val duration: Long;
val viewCount: Long;
val playbackTime: Long;
val playbackDate: OffsetDateTime?;
val isLive : Boolean;
val isShort: Boolean;
}

View file

@ -3,39 +3,33 @@ 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
import java.time.OffsetDateTime
@kotlinx.serialization.Serializable
open class SerializedPlatformVideo(
override val contentType: ContentType = ContentType.MEDIA,
override val id: PlatformID,
override val name: String,
override val thumbnails: Thumbnails = Thumbnails(),
override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
@JsonNames("datetime", "dateTime")
override val datetime: OffsetDateTime? = null,
override val url: String,
override val shareUrl: String = "",
override val duration: Long,
override val viewCount: Long,
override val isShort: Boolean = false
) : IPlatformVideo, SerializedPlatformContent {
override val contentType: ContentType = ContentType.MEDIA;
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);
}
@ -49,7 +43,6 @@ open class SerializedPlatformVideo(
companion object {
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
return SerializedPlatformVideo(
ContentType.MEDIA,
video.id,
video.name,
video.thumbnails,

View file

@ -13,6 +13,7 @@ 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
@ -37,15 +38,10 @@ open class SerializedPlatformVideoDetails(
override val video: ISerializedVideoSourceDescriptor,
override val preview: ISerializedVideoSourceDescriptor?,
override val subtitles: List<SubtitleRawSource> = listOf(),
override val isShort: Boolean = false
override val subtitles: List<SubtitleRawSource> = listOf()
) : 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;

View file

@ -54,12 +54,8 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
}
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
override fun getCopy(privateCopy: Boolean): JSClient {
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
}
override fun initialize() {

View file

@ -10,7 +10,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -23,7 +22,6 @@ 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
@ -33,7 +31,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
@ -44,7 +41,6 @@ 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
@ -61,13 +57,9 @@ 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
@ -89,8 +81,6 @@ 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;
@ -106,14 +96,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() = _plugin.isBusy;
val isBusy: Boolean get() = _busyCounter > 0;
val isBusyAction: String get() {
return _busyAction;
}
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>;
@ -126,7 +116,6 @@ 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;
@ -204,12 +193,8 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
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
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
}
fun getUnderlyingPlugin(): V8Plugin {
@ -223,31 +208,12 @@ 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;
@ -272,8 +238,7 @@ 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,
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
);
try {
@ -288,28 +253,19 @@ open class JSClient : IPlatformClient {
}
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
fun enable() = isBusyWith("enable") {
fun 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? = isBusyWith("saveState") {
fun saveState(): String? {
ensureEnabled();
if(!capabilities.hasSaveState)
return@isBusyWith null;
return null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return@isBusyWith resp;
return resp;
}
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@ -332,13 +288,6 @@ 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") {
@ -357,10 +306,8 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!;
}
return busy {
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return@busy _searchCapabilities!!;
}
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return _searchCapabilities!!;
}
catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex);
@ -388,10 +335,8 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!;
return busy {
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return@busy _searchChannelContentsCapabilities!!;
}
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return _searchChannelContentsCapabilities!!;
}
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search")
@ -416,21 +361,17 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
}
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
ensureEnabled();
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
}
@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 = isBusyWith("isChannelUrl") {
override fun isChannelUrl(url: String): Boolean {
try {
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex);
return@isBusyWith false;
return false;
}
}
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@ -448,10 +389,9 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) {
return _channelCapabilities!!;
}
return busy {
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return@busy _channelCapabilities!!;
};
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return _channelCapabilities!!;
}
catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex);
@ -562,14 +502,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 = isBusyWith("isContentDetailsUrl") {
override fun isContentDetailsUrl(url: String): Boolean {
try {
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex);
return@isBusyWith false;
return false;
}
}
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@ -601,7 +541,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(this, tracker);
return@isBusyWith JSPlaybackTracker(config, tracker);
else
return@isBusyWith null;
}
@ -643,6 +583,7 @@ 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") {
@ -670,19 +611,17 @@ 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 = isBusyWith("isPlaylistUrl") {
override fun isPlaylistUrl(url: String): Boolean {
if (!capabilities.hasGetPlaylist)
return@isBusyWith false;
return false;
try {
return@isBusyWith busy {
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return@isBusyWith false;
return false;
}
}
@JSOptional
@ -713,13 +652,6 @@ 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();
@ -791,29 +723,19 @@ open class JSClient : IPlatformClient {
return urls;
}
fun <T> busy(handle: ()->T): T {
return _plugin.busy {
return@busy handle();
}
}
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();
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try {
synchronized(_busyLock) {
_busyCounter++;
}
finally {
_busyAction = "";
_busyAction = actionName;
return handle();
}
finally {
_busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
}
}
}

View file

@ -4,7 +4,6 @@ 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
@ -34,7 +33,6 @@ class SourcePluginConfig(
override val allowEval: Boolean = false,
override val allowUrls: List<String> = listOf(),
override val packages: List<String> = listOf(),
override val packagesOptional: List<String> = listOf(),
val settings: List<Setting> = listOf(),
@ -48,14 +46,12 @@ 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,
var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
var changelog: HashMap<String, List<String>>? = null
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@ -105,10 +101,6 @@ class SourcePluginConfig(
if(!packages.contains(pack))
return false;
}
for(pack in newConfig.packagesOptional) {
if(!packagesOptional.contains(pack))
return false;
}
//Developer Submit Url should be same or empty
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
return false;
@ -137,7 +129,7 @@ class SourcePluginConfig(
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
if (currentlyInstalledPlugin != null) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
list.add(Pair(
"Different Author",
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
@ -170,17 +162,12 @@ class SourcePluginConfig(
}
fun validate(text: String): Boolean {
try {
if (scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present");
if (scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
if(scriptPublicKey.isNullOrEmpty())
throw IllegalStateException("No public key present");
if(scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
return false
}
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
}
fun isUrlAllowed(url: String): Boolean {
@ -191,19 +178,6 @@ class SourcePluginConfig(
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
}
fun getChangelogString(version: String): String?{
if(changelog == null || !changelog!!.containsKey(version))
return null;
val changelog = changelog!![version]!!;
if(changelog.size > 1) {
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
}
else if(changelog.size == 1) {
return "Changelog (${version})\n" + changelog[0].trim();
}
return null;
}
companion object {
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
@ -211,8 +185,6 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl;
return obj;
}
private val TAG = "SourcePluginConfig"
}
@kotlinx.serialization.Serializable

View file

@ -5,16 +5,10 @@ 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
@ -109,22 +103,12 @@ 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.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)
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
var rateLimit = RateLimit();
@Serializable
class RateLimit {
@ -159,8 +143,6 @@ class SourcePluginDescriptor {
tabEnabled.enableHome = config.enableInHome
if(tabEnabled.enableSearch == null)
tabEnabled.enableSearch = config.enableInSearch
if(tabEnabled.enableShorts == null)
tabEnabled.enableShorts = config.enableInShorts
}
}

View file

@ -67,25 +67,6 @@ 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)) })
@ -146,7 +127,7 @@ class JSHttpClient : ManagedHttpClient {
}
if(doApplyCookies) {
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) {
if (_currentCookieMap.isNotEmpty()) {
val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap
@ -154,12 +135,6 @@ class JSHttpClient : ManagedHttpClient {
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
}
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");

View file

@ -1,12 +1,10 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.JSChannelContent
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
@ -14,7 +12,6 @@ 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);
@ -29,9 +26,6 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj);
ContentType.ARTICLE -> JSArticle(config, obj);
ContentType.WEB -> JSWeb(config, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}

View file

@ -6,20 +6,17 @@ 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);
ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
ContentType.WEB -> JSWebDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}

View file

@ -1,39 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
final override val contentType: ContentType get() = ContentType.ARTICLE;
override val summary: String;
override val thumbnails: Thumbnails?;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformArticle";
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
}
}

View file

@ -4,8 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -21,23 +19,22 @@ 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 {
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
override val rating: IRating;
val rating: IRating;
override val summary: String;
override val thumbnails: Thumbnails?;
override val segments: List<IJSArticleSegment>;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformArticle";
val contextName = "PlatformPost";
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName);
@ -86,12 +83,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
@ -102,7 +99,6 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.HEADER -> JSHeaderSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null;
}
@ -114,7 +110,6 @@ enum class SegmentType(val value: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2),
HEADER(3),
NESTED(9);
@ -155,17 +150,6 @@ class JSImagesSegment: IJSArticleSegment {
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
}
}
class JSHeaderSegment: IJSArticleSegment {
override val type = SegmentType.HEADER;
val content: String;
val level: Int;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSHeaderSegment";
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
level = obj.getOrDefault(client.config, "level", contextName, 1) ?: 1;
}
}
class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED;
val nested: IPlatformContent;

View file

@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
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.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {

View file

@ -12,7 +12,6 @@ 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
@ -61,7 +60,7 @@ class JSComment : IPlatformComment {
if(!_hasGetReplies)
return null;
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj);
}

View file

@ -49,8 +49,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
else
author = PlatformAuthorLink.UNKNOWN;
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
if(datetimeInt == null || datetimeInt == 0.toLong())
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
if(datetimeInt == 0.toLong())
datetime = null;
else
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.JSChannelContent
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
@ -17,13 +16,3 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
return IJSContent.fromV8(plugin, obj);
}
}
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return JSChannelContent(config, obj);
}
}

View file

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

View file

@ -10,7 +10,6 @@ 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> {
@ -19,8 +18,8 @@ abstract class JSPager<T> : IPager<T> {
protected var pager: V8ValueObject;
private var _lastResults: List<T>? = null;
protected var _resultChanged: Boolean = true;
protected var _hasMorePages: Boolean = false;
private var _resultChanged: Boolean = true;
private var _hasMorePages: Boolean = false;
//private var _morePagesWasFalse: Boolean = false;
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
@ -30,9 +29,7 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager;
this.config = config;
plugin.busy {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
getResults();
}
@ -41,20 +38,17 @@ abstract class JSPager<T> : IPager<T> {
}
override fun hasMorePages(): Boolean {
return _hasMorePages && !pager.isClosed;
return _hasMorePages;
}
override fun nextPage() {
warnIfMainThread("JSPager.nextPage");
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;
}
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;
/*
try {
}
@ -76,18 +70,15 @@ abstract class JSPager<T> : IPager<T> {
return previousResults;
warnIfMainThread("JSPager.getResults");
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;
}
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;
}
abstract fun convertResult(obj: V8ValueObject): T;

View file

@ -2,51 +2,37 @@ 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 lateinit var _client: JSClient;
private lateinit var _config: IV8PluginConfig;
private lateinit var _obj: V8ValueObject;
private val _config: IV8PluginConfig;
private val _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false;
private var _hasInit: Boolean = false;
private val _hasInit: Boolean;
private var _lastRequest: Long = Long.MIN_VALUE;
private var _hasOnConcluded: Boolean = false;
private val _hasOnConcluded: Boolean;
override var nextRequest: Int = 1000
private set;
constructor(client: JSClient, obj: V8ValueObject) {
constructor(config: IV8PluginConfig, 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");
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");
}
this._config = config;
this._obj = obj;
this._hasInit = obj.has("onInit");
}
override fun onInit(seconds: Double) {
@ -54,15 +40,12 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) {
if(_hasCalledInit)
return;
_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;
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("onInit", seconds);
}
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
}
}
@ -72,12 +55,10 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit)
onInit(seconds);
else {
_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();
}
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();
}
}
}
@ -86,9 +67,7 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
_client.busy {
_obj.invokeV8Void("onConcluded", -1);
}
_obj.invokeVoid("onConcluded", -1);
}
}
}

View file

@ -15,7 +15,6 @@ 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 {
@ -69,12 +68,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}

View file

@ -14,8 +14,6 @@ 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
@ -48,55 +46,52 @@ class JSRequestExecutor {
if (_executor.isClosed)
throw IllegalStateException("Executor object is closed");
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;
}
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;
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeV8("executeRequest", url, headers, method, body);
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
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);
try {
if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value);
return base64Result;
}
finally {
result.close();
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?
}
}
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);
}
finally {
result.close();
}
}
@ -104,25 +99,24 @@ class JSRequestExecutor {
open fun cleanup() {
if (!hasCleanup || _executor.isClosed)
return;
_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);
};
}
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);
};
}
protected fun finalize() {

View file

@ -11,14 +11,12 @@ 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 = false;
override var allowByteSkip: Boolean;
constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin;
@ -26,13 +24,10 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config;
val config = plugin.config;
plugin.busy {
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
@ -40,15 +35,13 @@ class JSRequestModifier: IRequestModifier {
return Request(url, headers);
}
return _plugin.busy {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invokeV8("modifyRequest", url, headers);
} as V8ValueObject;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers);
result.close();
return@busy req;
}
val req = JSRequest(_plugin, result, url, headers);
result.close();
return req;
}

View file

@ -6,8 +6,6 @@ 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
@ -37,11 +35,8 @@ class JSSubtitleSource : ISubtitleSource {
override fun getSubtitles(): String {
if(!hasFetch)
throw IllegalStateException("This subtitle doesn't support getSubtitles..");
return _obj.getSourcePlugin()?.busy {
val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
return@busy v8String.value;
} ?: "";
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
return v8String.value;
}
override suspend fun getSubtitlesURI(): Uri? {

View file

@ -1,44 +0,0 @@
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");
}
}

View file

@ -6,12 +6,7 @@ import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
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;
@ -21,12 +16,7 @@ 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;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformVideo";
@ -36,12 +26,5 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
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);
}
}

View file

@ -7,7 +7,6 @@ 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
@ -25,17 +24,12 @@ 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;
@ -51,9 +45,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));
@ -75,7 +69,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
_hasGetComments = _content.has("getComments");
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
_hasGetVODEvents = _content.has("getVODEvents");
}
override fun getPlaybackTracker(): IPlaybackTracker? {
@ -89,16 +82,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS();
}
private fun getPlaybackTrackerJS(): IPlaybackTracker? {
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;
}
}
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;
};
}
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
@ -115,10 +106,8 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return _plugin.busy {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
@ -134,23 +123,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
}
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
return _plugin.busy {
val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return@busy null;
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return null;
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>()));
return JSCommentPager(_pluginConfig, client, commentPager);
}
}

View file

@ -1,31 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSWeb : JSContent, IPluginSourced {
final override val contentType: ContentType get() = ContentType.WEB;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformWeb";
}
}

View file

@ -1,41 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSWebDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.WEB;
val html: String?;
//TODO: Options?
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformWeb";
html = obj.getOrDefault(client.config, "html", contextName, null);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
}

View file

@ -21,8 +21,6 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
override var priority: Boolean = false;
override var original: Boolean = false;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
val contextName = "AudioUrlSource";
val config = plugin.config;
@ -37,7 +35,6 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
}
override fun getAudioUrl() : String {

View file

@ -6,8 +6,6 @@ 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
@ -27,7 +25,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)

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