Compare commits

..

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

505 changed files with 4145 additions and 31086 deletions

View file

@ -1,19 +1,19 @@
name: Bug Report name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior. description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug"] labels: ["bug", "new"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for taking the time to fill out this bug report. # 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 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) For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report ## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally. 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. * 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 * if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
@ -41,21 +41,18 @@ body:
label: What plugins are you seeing the problem on? label: What plugins are you seeing the problem on?
multiple: true multiple: true
options: options:
- "All" - All
- "Youtube" - Youtube
- "Odysee" - BiliBili (CN)
- "Rumble" - Twitch
- "Kick" - Odysee
- "Twitch" - Rumble
- "PeerTube" - Kick
- "Patreon" - PeerTube
- "Nebula" - Patreon
- "BiliBili (CN)" - Nebula
- "Bitchute" - SoundCloud
- "SoundCloud" - Other
- "Dailymotion"
- "Apple Podcasts"
- "Other"
validations: validations:
required: true required: true
@ -75,17 +72,6 @@ body:
- label: While logged out - label: While logged out
- label: N/A - label: N/A
- type: dropdown
id: vpn
attributes:
label: Are you using a VPN?
multiple: false
options:
- "No"
- "Yes"
validations:
required: true
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:

View file

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

View file

@ -1,6 +1,6 @@
name: Feature Request name: Feature Request
description: Suggest a new feature or other enhancement. description: Suggest a new feature or other enhancement.
labels: ["Enhancement"] labels: ["enhancement", "new"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -9,6 +9,8 @@ body:
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues 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) For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
- type: textarea - type: textarea
@ -53,4 +55,4 @@ body:
attributes: attributes:
value: | 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. **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.

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

@ -70,27 +70,3 @@
[submodule "app/src/unstable/assets/sources/spotify"] [submodule "app/src/unstable/assets/sources/spotify"]
path = app/src/unstable/assets/sources/spotify path = app/src/unstable/assets/sources/spotify
url = ../plugins/spotify.git url = ../plugins/spotify.git
[submodule "app/src/stable/assets/sources/bitchute"]
path = app/src/stable/assets/sources/bitchute
url = ../plugins/bitchute.git
[submodule "app/src/unstable/assets/sources/bitchute"]
path = app/src/unstable/assets/sources/bitchute
url = ../plugins/bitchute.git
[submodule "app/src/unstable/assets/sources/dailymotion"]
path = app/src/unstable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
[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

View file

@ -49,23 +49,9 @@ We encourage developers to write their own plugins. Please refer to the "Getting
## Contributing to Core ## Contributing to Core
**We are currently not accepting contributions to the core.**
### License The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity.
The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/).
### How to Contribute
1. Fork the core repository.
2. Clone your fork.
3. Make your changes.
4. Commit and push your changes.
5. Open a pull request.
### Guidelines
- Ensure your code adheres to the existing style.
- Include documentation and unit tests (where applicable).
--- ---

View file

@ -1,4 +1,4 @@
# Source First License 1.1 # Grayjay Core License 1.0
## Acceptance ## Acceptance
By using the software, you agree to all of the terms and conditions below. By using the software, you agree to all of the terms and conditions below.
@ -16,7 +16,7 @@ Notwithstanding the above, you may not remove or obscure any functionality in th
You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensors trademarks is subject to applicable law. You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensors trademarks is subject to applicable law.
## Patents ## Patents
If you make any written claim that the software infringes or contributes to infringement of any patent, your license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company. If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
## Notices ## Notices
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section. You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.

View file

@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Video</td> <td>Video</td>
@ -24,10 +24,12 @@ The FUTO media app is a player that exposes multiple video websites as sources i
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Sources</td> <td>Sources (all enabled)</td>
<td>Sources (one disabled)</td>
</tr> </tr>
</table> </table>
@ -36,7 +38,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Install a new source</td> <td>Install a new source</td>
@ -52,8 +54,8 @@ When a user enters a search term into the search bar, the query is posted to th
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Search (list)</td> <td>Search (list)</td>
@ -69,7 +71,7 @@ Creators are able to configure their profile using NeoPass.
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Channel</td> <td>Channel</td>
@ -110,7 +112,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Settings</td> <td>Settings</td>
@ -123,8 +125,8 @@ Playlists allow you to make a collection of videos that you can create and custo
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Playlists</td> <td>Playlists</td>
@ -140,7 +142,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Downloads</td> <td>Downloads</td>
@ -155,7 +157,7 @@ For more information about casting please click [here](./docs/casting.md).
<table border="0"> <table border="0">
<tr> <tr>
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td> <td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
</tr> </tr>
<tr> <tr>
<td>Casting</td> <td>Casting</td>
@ -180,12 +182,6 @@ In the future we hope to offer users the choice of their desired recommendation
1. Download a copy of the repository. 1. Download a copy of the repository.
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository. 2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
3. Open the terminal in Android Studio by clicking on the terminal icon on bottom left and run the following command:
```sh
git submodule update --init --recursive
```
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator. 3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes. 4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
@ -203,6 +199,7 @@ Create a tag on the master branch, incrementing the last version number by 1 (fo
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update. Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
## Documentation ## Documentation
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview). The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).

View file

@ -2,7 +2,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21' id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
id 'org.ajoberstar.grgit' version '5.2.2' id 'org.ajoberstar.grgit' version '1.7.2'
id 'com.google.protobuf' id 'com.google.protobuf'
id 'kotlin-parcelize' id 'kotlin-parcelize'
id 'com.google.devtools.ksp' id 'com.google.devtools.ksp'
@ -144,19 +144,9 @@ android {
buildFeatures { buildFeatures {
buildConfig true buildConfig true
} }
sourceSets {
main {
assets {
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
}
}
}
} }
dependencies { dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core //Core
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
@ -194,10 +184,11 @@ dependencies {
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
//Other //Other
implementation 'org.jmdns:jmdns:3.5.1'
implementation 'org.jsoup:jsoup:1.15.3' implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS' implementation 'com.arthenica:ffmpeg-kit-full:5.1'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:core:3.4.1'

View file

@ -1,266 +0,0 @@
package com.futo.platformplayer
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.net.Socket
import java.nio.ByteBuffer
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
class SyncServerTests {
//private val relayHost = "relay.grayjay.app"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.175"
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: ((SyncSocketSession, String, String?) -> Boolean)? = 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()
socketSession.startAsInitiator(relayKey)
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()
}
}
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
}

View file

@ -11,7 +11,6 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/> <uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/> <uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
@ -36,12 +35,6 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider> </provider>
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service android:name=".services.MediaPlaybackService" <service android:name=".services.MediaPlaybackService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaPlayback" /> android:foregroundServiceType="mediaPlayback" />
@ -57,8 +50,9 @@
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true" android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleInstance" android:launchMode="singleTask"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true">
@ -151,30 +145,34 @@
<data android:scheme="polycentric" /> <data android:scheme="polycentric" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".activities.TestActivity" android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SettingsActivity" android:name=".activities.SettingsActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.DeveloperActivity" android:name=".activities.DeveloperActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.ExceptionActivity" android:name=".activities.ExceptionActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.CaptchaActivity" android:name=".activities.CaptchaActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.LoginActivity" android:name=".activities.LoginActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.AddSourceActivity" android:name=".activities.AddSourceActivity"
android:screenOrientation="portrait"
android:exported="true" android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"> android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter> <intent-filter>
@ -188,55 +186,44 @@
</activity> </activity>
<activity <activity
android:name=".activities.AddSourceOptionsActivity" android:name=".activities.AddSourceOptionsActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricHomeActivity" android:name=".activities.PolycentricHomeActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricBackupActivity" android:name=".activities.PolycentricBackupActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricCreateProfileActivity" android:name=".activities.PolycentricCreateProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricProfileActivity" android:name=".activities.PolycentricProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricWhyActivity" android:name=".activities.PolycentricWhyActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricImportProfileActivity" android:name=".activities.PolycentricImportProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.ManageTabsActivity" android:name=".activities.ManageTabsActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.QRCaptureActivity" android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.FCastGuideActivity" android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncHomeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncPairActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application> </application>
</manifest> </manifest>

File diff suppressed because one or more lines are too long

View file

@ -11,8 +11,7 @@ let Type = {
Streams: "STREAMS", Streams: "STREAMS",
Mixed: "MIXED", Mixed: "MIXED",
Live: "LIVE", Live: "LIVE",
Subscriptions: "SUBSCRIPTIONS", Subscriptions: "SUBSCRIPTIONS"
Shorts: "SHORTS"
}, },
Order: { Order: {
Chronological: "CHRONOLOGICAL" Chronological: "CHRONOLOGICAL"
@ -202,7 +201,7 @@ class PlatformContent {
obj = obj ?? {}; obj = obj ?? {};
this.id = obj.id ?? PlatformID(); //PlatformID this.id = obj.id ?? PlatformID(); //PlatformID
this.name = obj.name ?? ""; //string this.name = obj.name ?? ""; //string
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[] this.thumbnails = obj.thumbnails; //Thumbnail[]
this.author = obj.author; //PlatformAuthorLink this.author = obj.author; //PlatformAuthorLink
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long) this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
this.url = obj.url ?? ""; //String this.url = obj.url ?? ""; //String
@ -245,7 +244,6 @@ class PlatformVideo extends PlatformContent {
this.viewCount = obj.viewCount ?? -1; //Long this.viewCount = obj.viewCount ?? -1; //Long
this.isLive = obj.isLive ?? false; //Boolean this.isLive = obj.isLive ?? false; //Boolean
this.isShort = !!obj.isShort ?? false;
} }
} }
class PlatformVideoDetails extends PlatformVideo { class PlatformVideoDetails extends PlatformVideo {
@ -262,11 +260,6 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? []; this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
} }
} }
@ -285,49 +278,12 @@ class PlatformPostDetails extends PlatformPost {
super(obj); super(obj);
obj = obj ?? {}; obj = obj ?? {};
this.plugin_type = "PlatformPostDetails"; this.plugin_type = "PlatformPostDetails";
this.rating = obj.rating ?? new RatingLikes(-1); this.rating = obj.rating ?? RatingLikes(-1);
this.textType = obj.textType ?? 0; this.textType = obj.textType ?? 0;
this.content = obj.content ?? ""; this.content = obj.content ?? "";
} }
} }
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 {
constructor(type) {
this.type = type;
}
}
class ArticleTextSegment extends ArticleSegment {
constructor(content, textType) {
super(1);
this.textType = textType;
this.content = content;
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
super(2);
this.images = images;
}
}
class ArticleNestedSegment extends ArticleSegment {
constructor(nested) {
super(9);
this.nested = nested;
}
}
//Sources //Sources
class VideoSourceDescriptor { class VideoSourceDescriptor {
constructor(obj) { constructor(obj) {
@ -374,16 +330,6 @@ class VideoUrlSource {
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
} }
} }
class VideoUrlWidevineSource extends VideoUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "VideoUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class VideoUrlRangeSource extends VideoUrlSource { class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj) { constructor(obj) {
super(obj); super(obj);
@ -416,26 +362,8 @@ class AudioUrlWidevineSource extends AudioUrlSource {
super(obj); super(obj);
this.plugin_type = "AudioUrlWidevineSource"; this.plugin_type = "AudioUrlWidevineSource";
this.bearerToken = obj.bearerToken;
this.licenseUri = obj.licenseUri; this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
// deprecated api conversion
if(obj.bearerToken) {
this.getLicenseRequestExecutor = () => {
return {
executeRequest: (url, _headers, _method, license_request_data) => {
return http.POST(
url,
license_request_data,
{ Authorization: `Bearer ${obj.bearerToken}` },
false,
true
).body
}
}
}
}
} }
} }
class AudioUrlRangeSource extends AudioUrlSource { class AudioUrlRangeSource extends AudioUrlSource {
@ -478,49 +406,6 @@ class DashSource {
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
} }
} }
class DashWidevineSource extends DashSource {
constructor(obj) {
super(obj);
this.plugin_type = "DashWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class DashManifestRawSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashManifestRawAudioSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawAudioSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
this.manifest = obj.manifest ?? null;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class RequestModifier { class RequestModifier {
constructor(obj) { constructor(obj) {
@ -877,99 +762,3 @@ class URLSearchParams {
return searchString; return searchString;
} }
} }
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function btoa(input) {
input = String(input);
if (/[^\0-\xFF]/.test(input)) {
// Note: no need to special-case astral symbols here, as surrogates are
// matched, and the input is supposed to only contain ASCII anyway.
error(
'The string to be encoded contains characters outside of the ' +
'Latin1 range.'
);
}
var padding = input.length % 3;
var output = '';
var position = -1;
var a;
var b;
var c;
var buffer;
// Make sure any padding is handled outside of the loop.
var length = input.length - padding;
while (++position < length) {
// Read three bytes, i.e. 24 bits.
a = input.charCodeAt(position) << 16;
b = input.charCodeAt(++position) << 8;
c = input.charCodeAt(++position);
buffer = a + b + c;
// Turn the 24 bits into four chunks of 6 bits each, and append the
// matching character for each of them to the output.
output += (
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
__btoa_TABLE.charAt(buffer & 0x3F)
);
}
if (padding == 2) {
a = input.charCodeAt(position) << 8;
b = input.charCodeAt(++position);
buffer = a + b;
output += (
__btoa_TABLE.charAt(buffer >> 10) +
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
'='
);
} else if (padding == 1) {
buffer = input.charCodeAt(position);
output += (
__btoa_TABLE.charAt(buffer >> 2) +
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
'=='
);
}
return output;
};
function atob(input) {
input = String(input)
.replace(__REGEX_SPACE_CHARACTERS, '');
var length = input.length;
if (length % 4 == 0) {
input = input.replace(/==?$/, '');
length = input.length;
}
if (
length % 4 == 1 ||
// http://whatwg.org/C#alphanumeric-ascii-characters
/[^+a-zA-Z0-9/]/.test(input)
) {
error(
'Invalid character: the string to be decoded is not correctly encoded.'
);
}
var bitCounter = 0;
var bitStorage;
var buffer;
var output = '';
var position = -1;
while (++position < length) {
buffer = __btoa_TABLE.indexOf(input.charAt(position));
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
// Unless this is the first of a group of 4 characters…
if (bitCounter++ % 4) {
// …convert the first 8 bits to a single ASCII character.
output += String.fromCharCode(
0xFF & bitStorage >> (-2 * bitCounter & 6)
);
}
}
return output;
};

View file

@ -18,10 +18,7 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi @UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory { fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier(); val requestModifier = getRequestModifier();
val requestExecutor = getRequestExecutor(); return if (requestModifier != null) {
return if (requestExecutor != null) {
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
} else if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier); JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else { } else {
DefaultHttpDataSource.Factory(); DefaultHttpDataSource.Factory();

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,6 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.Inet4Address import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
@ -216,14 +215,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this); return InetAddress.getByAddress(this);
} }
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000 val timeout = 2000
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")})");
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
return null; return null;
} }

View file

@ -1,13 +1,13 @@
package com.futo.platformplayer package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol import userpackage.Protocol
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -40,25 +40,33 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) } 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? { fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
} }
fun Protocol.Claim.resolveChannelUrls(): List<String> { fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) 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

@ -1,9 +1,6 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.net.Uri import android.net.Uri
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URLEncoder import java.net.URLEncoder
@ -28,18 +25,4 @@ fun String?.yesNoToBoolean(): Boolean {
fun Boolean?.toYesNo(): String { fun Boolean?.toYesNo(): String {
return if (this == true) "YES" else "NO" return if (this == true) "YES" else "NO"
}
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
"[${hostAddress}]"
}
is Inet4Address -> {
hostAddress
}
else -> {
throw Exception("Invalid address type")
}
}
} }

View file

@ -1,192 +0,0 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.io.ByteStreams
import com.google.common.primitives.Ints
import com.google.common.primitives.Longs
import java.io.DataInput
import java.io.DataInputStream
import java.io.EOFException
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
class LittleEndianDataInputStream
/**
* Creates a `LittleEndianDataInputStream` that wraps the given stream.
*
* @param in the stream to delegate to
*/
(`in`: InputStream?) : FilterInputStream(Preconditions.checkNotNull(`in`)), DataInput {
/** This method will throw an [UnsupportedOperationException]. */
override fun readLine(): String {
throw UnsupportedOperationException("readLine is not supported")
}
@Throws(IOException::class)
override fun readFully(b: ByteArray) {
ByteStreams.readFully(this, b)
}
@Throws(IOException::class)
override fun readFully(b: ByteArray, off: Int, len: Int) {
ByteStreams.readFully(this, b, off, len)
}
@Throws(IOException::class)
override fun skipBytes(n: Int): Int {
return `in`.skip(n.toLong()).toInt()
}
@Throws(IOException::class)
override fun readUnsignedByte(): Int {
val b1 = `in`.read()
if (0 > b1) {
throw EOFException()
}
return b1
}
/**
* Reads an unsigned `short` as specified by [DataInputStream.readUnsignedShort],
* except using little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as an unsigned 16-bit integer in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readUnsignedShort(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
return Ints.fromBytes(0.toByte(), 0.toByte(), b2, b1)
}
/**
* Reads an integer as specified by [DataInputStream.readInt], except using little-endian
* byte order.
*
* @return the next four bytes of the input stream, interpreted as an `int` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readInt(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
return Ints.fromBytes(b4, b3, b2, b1)
}
/**
* Reads a `long` as specified by [DataInputStream.readLong], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `long` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readLong(): Long {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
val b5 = readAndCheckByte()
val b6 = readAndCheckByte()
val b7 = readAndCheckByte()
val b8 = readAndCheckByte()
return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1)
}
/**
* Reads a `float` as specified by [DataInputStream.readFloat], except using
* little-endian byte order.
*
* @return the next four bytes of the input stream, interpreted as a `float` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readFloat(): Float {
return java.lang.Float.intBitsToFloat(readInt())
}
/**
* Reads a `double` as specified by [DataInputStream.readDouble], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `double` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readDouble(): Double {
return java.lang.Double.longBitsToDouble(readLong())
}
@Throws(IOException::class)
override fun readUTF(): String {
return DataInputStream(`in`).readUTF()
}
/**
* Reads a `short` as specified by [DataInputStream.readShort], except using
* little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as a `short` in little-endian
* byte order.
* @throws IOException if an I/O error occurs.
*/
@Throws(IOException::class)
override fun readShort(): Short {
return readUnsignedShort().toShort()
}
/**
* Reads a char as specified by [DataInputStream.readChar], except using little-endian
* byte order.
*
* @return the next two bytes of the input stream, interpreted as a `char` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readChar(): Char {
return readUnsignedShort().toChar()
}
@Throws(IOException::class)
override fun readByte(): Byte {
return readUnsignedByte().toByte()
}
@Throws(IOException::class)
override fun readBoolean(): Boolean {
return readUnsignedByte() != 0
}
/**
* Reads a byte from the input stream checking that the end of file (EOF) has not been
* encountered.
*
* @return byte read from input
* @throws IOException if an error is encountered while reading
* @throws EOFException if the end of file (EOF) is encountered.
*/
@Throws(IOException::class, EOFException::class)
private fun readAndCheckByte(): Byte {
val b1 = `in`.read()
if (-1 == b1) {
throw EOFException()
}
return b1.toByte()
}
}

View file

@ -1,144 +0,0 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.primitives.Longs
import java.io.*
class LittleEndianDataOutputStream
/**
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
*
* @param out the stream to delegate to
*/
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
DataOutput {
@Throws(IOException::class)
override fun write(b: ByteArray, off: Int, len: Int) {
// Override slow FilterOutputStream impl
out.write(b, off, len)
}
@Throws(IOException::class)
override fun writeBoolean(v: Boolean) {
(out as DataOutputStream).writeBoolean(v)
}
@Throws(IOException::class)
override fun writeByte(v: Int) {
(out as DataOutputStream).writeByte(v)
}
@Deprecated(
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
)
@Throws(
IOException::class
)
override fun writeBytes(s: String) {
(out as DataOutputStream).writeBytes(s)
}
/**
* Writes a char as specified by [DataOutputStream.writeChar], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChar(v: Int) {
writeShort(v)
}
/**
* Writes a `String` as specified by [DataOutputStream.writeChars], except
* each character is written using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChars(s: String) {
for (i in 0 until s.length) {
writeChar(s[i].code)
}
}
/**
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
* using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeDouble(v: Double) {
writeLong(java.lang.Double.doubleToLongBits(v))
}
/**
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeFloat(v: Float) {
writeInt(java.lang.Float.floatToIntBits(v))
}
/**
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeInt(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte(),
(0xFF and (v shr 16)).toByte(),
(0xFF and (v shr 24)).toByte()
)
out.write(bytes)
}
/**
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeLong(v: Long) {
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
write(bytes, 0, bytes.size)
}
/**
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeShort(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte()
)
out.write(bytes)
}
@Throws(IOException::class)
override fun writeUTF(str: String) {
(out as DataOutputStream).writeUTF(str)
}
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
// It should flush itself if necessary.
@Throws(IOException::class)
override fun close() {
out.close()
}
}

View file

@ -11,7 +11,6 @@ import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.SyncHomeActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@ -33,6 +32,7 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,7 +44,6 @@ import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@Serializable @Serializable
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean); data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
@ -58,16 +57,7 @@ class Settings : FragmentedStorageFileJson() {
@Transient @Transient
val onTabsChanged = Event0(); val onTabsChanged = Event0();
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8) @FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
@FormFieldButton(R.drawable.ic_update)
fun syncGrayjay() {
SettingsActivity.getActivity()?.let {
it.startActivity(Intent(it, SyncHomeActivity::class.java))
}
}
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
@FormFieldButton(R.drawable.ic_person) @FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() { fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
@ -83,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6) @FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
@FormFieldButton(R.drawable.ic_quiz) @FormFieldButton(R.drawable.ic_quiz)
fun openFAQ() { fun openFAQ() {
try { try {
@ -93,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
//Ignored //Ignored
} }
} }
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5) @FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
@FormFieldButton(R.drawable.ic_data_alert) @FormFieldButton(R.drawable.ic_data_alert)
fun openIssues() { fun openIssues() {
try { try {
@ -125,7 +115,7 @@ class Settings : FragmentedStorageFileJson() {
} }
}*/ }*/
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4) @FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
@FormFieldButton(R.drawable.ic_tabs) @FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() { fun manageTabs() {
try { try {
@ -139,15 +129,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3) @FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
@FormFieldButton(R.drawable.ic_move_up) @FormFieldButton(R.drawable.ic_move_up)
fun import() { fun import() {
val act = SettingsActivity.getActivity() ?: return; val act = SettingsActivity.getActivity() ?: return;
val intent = MainActivity.getImportOptionsIntent(act); val intent = MainActivity.getImportOptionsIntent(act);
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
act.startActivity(intent); act.startActivity(intent);
} }
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2) @FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
@FormFieldButton(R.drawable.ic_link) @FormFieldButton(R.drawable.ic_link)
fun manageLinks() { fun manageLinks() {
try { try {
@ -157,24 +148,6 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
@FormFieldButton(R.drawable.battery_full_24px)
fun ignoreBatteryOptimization() {
SettingsActivity.getActivity()?.let {
val intent = Intent()
val packageName = it.packageName
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.setData(Uri.parse("package:$packageName"))
it.startActivity(intent)
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
} else {
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
}
}
}*/
@FormField(R.string.language, "group", -1, 0) @FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings(); var language = LanguageSettings();
@Serializable @Serializable
@ -205,7 +178,7 @@ class Settings : FragmentedStorageFileJson() {
var home = HomeSettings(); var home = HomeSettings();
@Serializable @Serializable
class HomeSettings { 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) @DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1; var homeFeedStyle: Int = 1;
@ -216,11 +189,6 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL; 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;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@ -259,9 +227,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
var hidefromSearch: Boolean = false;
fun getSearchFeedStyle(): FeedStyle { fun getSearchFeedStyle(): FeedStyle {
if(searchFeedStyle == 0) if(searchFeedStyle == 0)
@ -299,9 +264,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5) @FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true; var showSubscriptionGroups: Boolean = true;
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@ -364,7 +326,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings(); var playback = PlaybackSettings();
@Serializable @Serializable
class PlaybackSettings { class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2) @FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.audio_languages) @DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0; var primaryLanguage: Int = 0;
@ -388,12 +350,10 @@ class Settings : FragmentedStorageFileJson() {
else -> null 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]; //= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0) @FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
@DropdownFieldOptionsId(R.array.playback_speeds) @DropdownFieldOptionsId(R.array.playback_speeds)
var defaultPlaybackSpeed: Int = 3; var defaultPlaybackSpeed: Int = 3;
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) { fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
@ -409,29 +369,37 @@ class Settings : FragmentedStorageFileJson() {
else -> 1.0f; else -> 1.0f;
}; };
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1) @FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredQuality: Int = 0; var preferredQuality: Int = 0;
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2) @FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredMeteredQuality: Int = 0; var preferredMeteredQuality: Int = 0;
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality); fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality); fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount(); fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3) @FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) @FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
var simplifySources: Boolean = true; @DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
var autoRotate: Int = 2;
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5) fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6) @FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
var autoRotateDeadZone: Int = 0;
fun getAutoRotateDeadZoneDegrees(): Int {
return autoRotateDeadZone * 5;
}
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@DropdownFieldOptionsId(R.array.player_background_behavior) @DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2; var backgroundPlay: Int = 2;
@ -482,44 +450,18 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13) @FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false; var fullscreenPortrait: Boolean = false;
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
var allowVideoToGoUnderCutout: Boolean = true;
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)
var comments = CommentSettings(); var comments = CommentSettings();
@Serializable @Serializable
class CommentSettings { class CommentSettings {
var didAskPolycentricDefault: Boolean = false;
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0) @FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections) @DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 2; var defaultCommentSection: Int = 0;
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0) @FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true; var badReputationCommentsFading: Boolean = true;
} }
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7) @FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
@ -568,7 +510,7 @@ class Settings : FragmentedStorageFileJson() {
class Browsing { class Browsing {
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0) @FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze? var videoCache: Boolean = true;
} }
@FormField(R.string.casting, "group", R.string.configure_casting, 9) @FormField(R.string.casting, "group", R.string.configure_casting, 9)
@ -583,15 +525,10 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@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) @Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false; var alwaysProxyRequests: Boolean = false;
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = false;
/*TODO: Should we have a different casting quality? /*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)
@ -659,9 +596,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;
@ -845,10 +779,10 @@ class Settings : FragmentedStorageFileJson() {
fun export() { fun export() {
val activity = SettingsActivity.getActivity() ?: return; val activity = SettingsActivity.getActivity() ?: return;
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {}, UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = { SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
StateBackup.shareExternalBackup(); StateBackup.shareExternalBackup();
}), }),
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = { SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
StateBackup.saveExternalBackup(activity); StateBackup.saveExternalBackup(activity);
}) })
) )
@ -864,14 +798,10 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2) @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun clearPayment() { fun clearPayment() {
SettingsActivity.getActivity()?.let { context -> StatePayment.instance.clearLicenses();
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", { SettingsActivity.getActivity()?.let {
StatePayment.instance.clearLicenses(); UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
SettingsActivity.getActivity()?.let { it.reloadSettings();
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
it.reloadSettings();
}
})
} }
} }
} }
@ -880,16 +810,12 @@ class Settings : FragmentedStorageFileJson() {
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) @FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
var playlistDeleteConfirmation: Boolean = true; @FormFieldWarning(R.string.bypass_rotation_prevention_warning)
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) var bypassRotationPrevention: Boolean = false;
var playlistAllowDups: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4) @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
var polycentricLocalCache: Boolean = true;
} }
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19) @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@ -921,33 +847,7 @@ class Settings : FragmentedStorageFileJson() {
var pan: Boolean = true; var pan: Boolean = true;
} }
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20) @FormField(R.string.info, FieldForm.GROUP, -1, 20)
var synchronization = Synchronization();
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = true;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false;
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
var connectDiscovered: Boolean = true;
@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.info, FieldForm.GROUP, -1, 21)
var info = Info(); var info = Info();
@Serializable @Serializable
class Info { class Info {

View file

@ -1,7 +1,6 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.content.Context import android.content.Context
import android.content.Intent
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.work.Data import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
@ -9,7 +8,6 @@ import androidx.work.WorkManager
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity import com.futo.platformplayer.activities.DeveloperActivity
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -236,17 +234,13 @@ class SettingsDev : FragmentedStorageFileJson() {
R.string.test_background_worker_description, 4) R.string.test_background_worker_description, 4)
fun triggerBackgroundUpdate() { fun triggerBackgroundUpdate() {
val act = SettingsActivity.getActivity()!!; val act = SettingsActivity.getActivity()!!;
try { UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
val wm = WorkManager.getInstance(act); val wm = WorkManager.getInstance(act);
val req = OneTimeWorkRequestBuilder<BackgroundWorker>() val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build()) .setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
.build(); .build();
wm.enqueue(req); wm.enqueue(req);
} catch (e: Throwable) {
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
}
} }
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, @FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 4) R.string.test_background_worker_description, 4)
@ -497,13 +491,6 @@ class SettingsDev : FragmentedStorageFileJson() {
} }
} }
} }
@FormField(R.string.test_playback, FieldForm.BUTTON,
R.string.test_playback, 1)
fun testPlayback(context: Context) {
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
}
} }

View file

@ -5,10 +5,7 @@ import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@ -200,51 +197,44 @@ class UIDialogs {
dialog.show(); 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, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context); val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view); builder.setView(view);
builder.setCancelable(defaultCloseAction > -2);
val dialog = builder.create(); val dialog = builder.create();
registerDialogOpened(dialog); registerDialogOpened(dialog);
view.findViewById<ImageView>(R.id.dialog_icon).apply { view.findViewById<ImageView>(R.id.dialog_icon).apply {
this.setImageResource(icon); this.setImageResource(icon);
if(animated)
this.drawable.assume<Animatable, Unit> { it.start() };
} }
view.findViewById<TextView>(R.id.dialog_text).apply { view.findViewById<TextView>(R.id.dialog_text).apply {
this.text = text; this.text = text;
}; };
view.findViewById<TextView>(R.id.dialog_text_details).apply { view.findViewById<TextView>(R.id.dialog_text_details).apply {
if (textDetails == null) if(textDetails == null)
this.visibility = View.GONE; this.visibility = View.GONE;
else { else
this.text = textDetails; this.text = textDetails;
}
}; };
view.findViewById<TextView>(R.id.dialog_text_code).apply { view.findViewById<TextView>(R.id.dialog_text_code).apply {
if (code == null) this.visibility = View.GONE; if(code == null)
this.visibility = View.GONE;
else { else {
this.text = code; this.text = code;
this.movementMethod = ScrollingMovementMethod.getInstance();
this.visibility = View.VISIBLE; this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
} }
}; };
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply { view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
val center = actions.any { it?.center == true };
val buttons = actions.map<Action, TextView> { act -> val buttons = actions.map<Action, TextView> { act ->
val buttonView = TextView(context); val buttonView = TextView(context);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt(); val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt(); val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt(); val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2; if(actions.size > 1)
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2; this.marginEnd = if(actions.size > 2) dp14 else dp28;
}; };
buttonView.setTextColor(Color.WHITE); buttonView.setTextColor(Color.WHITE);
buttonView.textSize = 14f; buttonView.textSize = 14f;
@ -266,7 +256,7 @@ class UIDialogs {
return@map buttonView; return@map buttonView;
}; };
if(actions.size <= 1 || center) if(actions.size <= 1)
this.gravity = Gravity.CENTER; this.gravity = Gravity.CENTER;
else else
this.gravity = Gravity.END; this.gravity = Gravity.END;
@ -281,7 +271,6 @@ class UIDialogs {
registerDialogClosed(dialog); registerDialogClosed(dialog);
} }
dialog.show(); dialog.show();
return dialog;
} }
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) { fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
@ -356,13 +345,6 @@ class UIDialogs {
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction) showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
} }
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
}
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) { fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
val dialog = AutoUpdateDialog(context); val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
@ -375,8 +357,8 @@ class UIDialogs {
} }
} }
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) { fun showChangelogDialog(context: Context, lastVersion: Int) {
val dialog = ChangelogDialog(context, changelogs); val dialog = ChangelogDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
@ -525,13 +507,11 @@ class UIDialogs {
val text: String; val text: String;
val action: ()->Unit; val action: ()->Unit;
val style: ActionStyle; val style: ActionStyle;
var center: Boolean;
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) { constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) {
this.text = text; this.text = text;
this.action = action; this.action = action;
this.style = style; this.style = style;
this.center = center;
} }
} }
enum class ActionStyle { enum class ActionStyle {

View file

@ -15,19 +15,14 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
@ -39,12 +34,12 @@ import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
@ -79,36 +74,6 @@ class UISlideOverlays {
return menu; 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 { fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
@ -126,17 +91,9 @@ class UISlideOverlays {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
items.addAll(listOf( items.addAll(listOf(
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
container.context, subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
R.drawable.ic_notifications, }, false),
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
},
invokeParent = false
),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty()) if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups", SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.", "You can select which groups this subscription is part of.",
@ -171,62 +128,22 @@ class UISlideOverlays {
SlideUpMenuGroup(container.context, "Fetch Settings", SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.", "Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()), -1, listOf()),
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem( if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
container.context, subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
R.drawable.ic_live_tv, }, false) else null,
"Livestreams", if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
"Check for livestreams", subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
tag = "fetchLive", }, false) else null,
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)) if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
container.context, subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
R.drawable.ic_play, }, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
"Videos", SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
"Check for videos", subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
tag = "fetchVideos", }, false) else null,
call = { if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos; subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
}, }, false) else null/*,,
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", SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription", "Various things you can do with this subscription",
@ -325,23 +242,11 @@ class UISlideOverlays {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
masterPlaylist.getAudioSources().forEach { it -> masterPlaylist.getAudioSources().forEach { it ->
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
val estSize = VideoHelper.estimateSourceSize(it); selectedAudioVariant = it
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; slideUpMenuOverlay.selectOption(audioButtons, it)
audioButtons.add(SlideUpMenuItem( slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
container.context, }, false))
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 -> /*masterPlaylist.getSubtitleSources().forEach { it ->
@ -353,24 +258,11 @@ class UISlideOverlays {
}*/ }*/
masterPlaylist.getVideoSources().forEach { masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it); videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; selectedVideoVariant = it
videoButtons.add(SlideUpMenuItem( slideUpMenuOverlay.selectOption(videoButtons, it)
container.context, slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
R.drawable.ic_movie, }, false))
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
))
} }
val newItems = arrayListOf<View>() val newItems = arrayListOf<View>()
@ -402,7 +294,7 @@ class UISlideOverlays {
UIDialogs.toast(container.context, "Variant video HLS playlist download started") UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) { } else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), 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") UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else { } else {
@ -429,8 +321,8 @@ class UISlideOverlays {
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor; val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
var selectedVideo: IVideoSource? = null; var selectedVideo: IVideoUrlSource? = null;
var selectedAudio: IAudioSource? = null; var selectedAudio: IAudioUrlSource? = null;
var selectedSubtitle: ISubtitleSource? = null; var selectedSubtitle: ISubtitleSource? = null;
val videoSources = descriptor.videoSources; val videoSources = descriptor.videoSources;
@ -449,93 +341,45 @@ class UISlideOverlays {
} }
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources, 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), container.context.getString(R.string.audio_only), "none", {
container.context, selectedVideo = null;
R.drawable.ic_movie, menu?.selectOption(videoSources, "none");
container.context.getString(R.string.none), if(selectedAudio != null || !requiresAudio)
container.context.getString(R.string.audio_only), menu?.setOk(container.context.getString(R.string.download));
tag = "none", }, false)) +
call = {
selectedVideo = null;
menu?.selectOption(videoSources, "none");
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)) else listOf()) +
videoSources videoSources
.filter { it.isDownloadable() } .filter { it.isDownloadable() }
.map { .map {
when (it) { when (it) {
is IVideoUrlSource -> { is IVideoUrlSource -> {
val estSize = VideoHelper.estimateSourceSize(it); SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; selectedVideo = it
SlideUpMenuItem( menu?.selectOption(videoSources, it);
container.context, if(selectedAudio != null || !requiresAudio)
R.drawable.ic_movie, menu?.setOk(container.context.getString(R.string.download));
it.name, }, false)
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
}
is JSDashManifestRawSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
} }
is IHLSManifestSource -> { is IHLSManifestSource -> {
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
container.context, showHlsPicker(video, it, it.url, container)
R.drawable.ic_movie, }, false)
it.name,
"HLS",
tag = it,
call = {
showHlsPicker(video, it, it.url, container)
},
invokeParent = false
)
} }
else -> { else -> {
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items"); throw Exception("Unhandled source type")
null;//throw Exception("Unhandled source type")
} }
} }
}.filterNotNull()).flatten().toList() }).flatten().toList()
)); ));
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) { if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
//TODO: Add HLS support here //TODO: Add HLS support here
selectedVideo = VideoHelper.selectBestVideoSource( selectedVideo = VideoHelper.selectBestVideoSource(
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(), videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(), Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
) as IVideoSource?; ) as IVideoUrlSource?;
} }
if (audioSources != null) { if (audioSources != null) {
@ -544,90 +388,43 @@ class UISlideOverlays {
.map { .map {
when (it) { when (it) {
is IAudioUrlSource -> { is IAudioUrlSource -> {
val estSize = VideoHelper.estimateSourceSize(it); SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; selectedAudio = it
SlideUpMenuItem( menu?.selectOption(audioSources, it);
container.context, menu?.setOk(container.context.getString(R.string.download));
R.drawable.ic_music, }, false);
it.name,
"${it.bitrate}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
);
}
is JSDashManifestRawAudioSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
"${it.bitrate}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
);
} }
is IHLSManifestAudioSource -> { is IHLSManifestAudioSource -> {
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
container.context, showHlsPicker(video, it, it.url, container)
R.drawable.ic_movie, }, false)
it.name,
"HLS Audio",
tag = it,
call = {
showHlsPicker(video, it, it.url, container)
},
invokeParent = false
)
} }
else -> { else -> {
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items"); throw Exception("Unhandled source type")
null;//throw Exception("Unhandled source type")
} }
} }
}.filterNotNull())); }));
//TODO: Add HLS support here //TODO: Add HLS support here
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(), selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
Settings.instance.playback.getPrimaryLanguage(container.context), Settings.instance.playback.getPrimaryLanguage(container.context),
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?; if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
} }
if(contentResolver != null && subtitleSources.isNotEmpty()) { if(contentResolver != null && subtitleSources.isNotEmpty()) {
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map { items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
container.context, if (selectedSubtitle == it) {
R.drawable.ic_edit, selectedSubtitle = null;
it.name, menu?.selectOption(subtitleSources, null);
"", } else {
tag = it, selectedSubtitle = it;
call = { menu?.selectOption(subtitleSources, it);
if (selectedSubtitle == it) { }
selectedSubtitle = null; }, false);
menu?.selectOption(subtitleSources, null);
} else {
selectedSubtitle = it;
menu?.selectOption(subtitleSources, it);
}
},
invokeParent = false
);
}) })
); );
} }
@ -645,18 +442,6 @@ class UISlideOverlays {
} }
menu.onOK.subscribe { menu.onOK.subscribe {
val sv = selectedVideo
if (sv is IHLSManifestSource) {
showHlsPicker(video, sv, sv.url, container)
return@subscribe
}
val sa = selectedAudio
if (sa is IHLSManifestAudioSource) {
showHlsPicker(video, sa, sa.url, container)
return@subscribe
}
menu.hide(); menu.hide();
val subtitleToDownload = selectedSubtitle; val subtitleToDownload = selectedSubtitle;
if(selectedAudio != null || !requiresAudio) { if(selectedAudio != null || !requiresAudio) {
@ -713,9 +498,8 @@ class UISlideOverlays {
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message); UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
handleUnknownDownload(); handleUnknownDownload();
loader.hide(true); loader.hide(true);
} }
@ -752,47 +536,23 @@ class UISlideOverlays {
); );
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map { items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
container.context, targetPxSize = it.third;
R.drawable.ic_movie, menu?.selectOption("Video", it.third);
it.first, }, false)
it.second,
tag = it.third,
call = {
targetPxSize = it.third;
menu?.selectOption("Video", it.third);
},
invokeParent = false
)
})); }));
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf( items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
SlideUpMenuItem( SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
container.context, targetBitrate = 1;
R.drawable.ic_movie, menu?.selectOption("Bitrate", 1);
container.context.getString(R.string.low_bitrate), menu?.setOk(container.context.getString(R.string.download));
"", }, false),
tag = 1, SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
call = { targetBitrate = 9999999;
targetBitrate = 1; menu?.selectOption("Bitrate", 9999999);
menu?.selectOption("Bitrate", 1); menu?.setOk(container.context.getString(R.string.download));
menu?.setOk(container.context.getString(R.string.download)); }, false)
},
invokeParent = false
),
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.high_bitrate),
"",
tag = 9999999,
call = {
targetBitrate = 9999999;
menu?.selectOption("Bitrate", 9999999);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
))); )));
@ -912,23 +672,12 @@ class UISlideOverlays {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist(); val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
if (it is JSClient)
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
else false;
} ?: false;
if (lastUpdated != null) { if (lastUpdated != null) {
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist", SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
SlideUpMenuItem(container.context, SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
R.drawable.ic_playlist_add, {
lastUpdated.name, StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
"${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);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})) }))
); );
@ -939,93 +688,43 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions", items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf( (listOf(
if(!isLimited && !video.isLive) SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
SlideUpMenuItem( showDownloadVideoOverlay(video, container, true);
container.context, }, false),
R.drawable.ic_download, SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
container.context.getString(R.string.download), val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.getString(R.string.download_the_video), container.context.startActivity(Intent.createChooser(Intent().apply {
tag = "download", action = Intent.ACTION_SEND;
call = { putExtra(Intent.EXTRA_TEXT, url);
showDownloadVideoOverlay(video, container, true); type = "text/plain";
}, }, null));
invokeParent = false }, false),
) else null, SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
SlideUpMenuItem( StateMeta.instance.addHiddenCreator(video.author.url);
container.context, UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
R.drawable.ic_share, }))
container.context.getString(R.string.share), + actions)
"Share the video",
tag = "share",
call = {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
},
invokeParent = false
),
SlideUpMenuItem(
container.context,
R.drawable.ic_visibility_off,
container.context.getString(R.string.hide_creator_from_home),
"",
tag = "hide_creator",
call = {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
}))
+ actions).filterNotNull()
)); ));
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto", SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(container.context, SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
R.drawable.ic_queue_add, { StatePlayer.instance.addToQueue(video); }),
container.context.getString(R.string.add_to_queue), 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), "watch later",
"${queue.size} " + container.context.getString(R.string.videos), { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
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,
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>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem( playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
container.context, showCreatePlaylistOverlay(container) {
R.drawable.ic_playlist_add, val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
container.context.getString(R.string.new_playlist), StatePlaylists.instance.createOrUpdatePlaylist(playlist);
container.context.getString(R.string.add_to_new_playlist), };
tag = "add_to_new_playlist", }, false))
call = {
showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
};
},
invokeParent = false
))
for (playlist in allPlaylists) { for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
R.drawable.ic_playlist_add, {
"${container.context.getString(R.string.add_to)} " + playlist.name + "", StatePlaylists.instance.addToPlaylist(playlist.id, video);
"${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);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})); }));
} }
@ -1046,14 +745,9 @@ class UISlideOverlays {
if (lastUpdated != null) { if (lastUpdated != null) {
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist", SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
SlideUpMenuItem(container.context, SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
R.drawable.ic_playlist_add, {
lastUpdated.name, StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
"${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);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})) }))
); );
@ -1064,49 +758,26 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add( items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other", SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(container.context, SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
R.drawable.ic_queue_add, { StatePlayer.instance.addToQueue(video); }),
container.context.getString(R.string.queue), SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
"${queue.size} " + container.context.getString(R.string.videos), { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
tag = "queue", SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
call = { StatePlayer.instance.addToQueue(video); }), { showDownloadVideoOverlay(video, container, true); }, false))
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);
}),
)
); );
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem( playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
container.context, slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
R.drawable.ic_playlist_add, val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
container.context.getString(R.string.new_playlist), StatePlaylists.instance.createOrUpdatePlaylist(playlist);
container.context.getString(R.string.add_to_new_playlist), });
tag = "add_to_new_playlist", }, false))
call = {
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
});
},
invokeParent = false
))
for (playlist in allPlaylists) { for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
R.drawable.ic_playlist_add, {
playlist.name, StatePlaylists.instance.addToPlaylist(playlist.id, video);
"${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);
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
})); }));
} }
@ -1130,74 +801,40 @@ class UISlideOverlays {
val views = arrayOf( val views = arrayOf(
hidden hidden
.map { btn -> SlideUpMenuItem( .map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
container.context, btn.handler?.invoke(btn);
btn.iconResource, }, invokeParents) as View }.toTypedArray(),
btn.text.text.toString(), arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
"", showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
tag = "", val selected = it
call = { .map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
btn.handler?.invoke(btn); .filter { it != null }
}, .map { it!! }
invokeParent = invokeParents .toList();
) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(
container.context,
R.drawable.ic_pin,
container.context.getString(R.string.change_pins),
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!!) }, {
val selected = it
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
.map { it!! }
.toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) }); onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
}); }
}, }, false))
invokeParent = false
))
).flatten().toTypedArray(); ).flatten().toTypedArray();
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() }; 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(); val selection: MutableList<Any> = mutableListOf();
var overlay: SlideUpMenuOverlay? = null; var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true, overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
listOf( options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
).filterNotNull() +
(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(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second)) { if(!selection.contains(it.second))
selection.add(it.second); selection.add(it.second);
if(overlayItem != null) {
overlayItem.setSubText(selection.indexOf(it.second).toString());
}
}
} else {
selection.remove(it.second);
if(overlayItem != null) {
overlayItem.setSubText("");
}
} }
}, else
invokeParent = false selection.remove(it.second);
) }, false)
})); });
overlay.onOK.subscribe { overlay.onOK.subscribe {
onOrdered.invoke(selection); onOrdered.invoke(selection);
overlay.hide(); overlay.hide();

View file

@ -13,7 +13,6 @@ import android.os.OperationCanceledException
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.WindowInsetsController import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -26,18 +25,12 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream import java.io.File
import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.time.OffsetDateTime
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "; private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String { fun getRandomString(sizeOfRandomString: Int): String {
@ -236,92 +229,4 @@ fun String.decodeUnicode(): String {
i++ i++
} }
return sb.toString() return sb.toString()
}
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
val newArrResult = targetArr.toMutableList();
for(missing in missingToMerge) {
val newIndex = findNewIndex(toMerge, newArrResult, missing);
newArrResult.add(newIndex, missing);
}
return newArrResult;
}
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
var originalIndex = originalArr.indexOf(item);
var newIndex = -1;
for(i in originalIndex-1 downTo 0) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr + 1;
break;
}
}
if(newIndex < 0) {
for(i in originalIndex+1 until originalArr.size) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr - 1;
break;
}
}
}
if(newIndex < 0)
return originalArr.size;
else
return newIndex;
}
fun ByteBuffer.toUtf8String(): String {
val remainingBytes = ByteArray(remaining())
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()
} }

View file

@ -10,13 +10,11 @@ import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
class AddSourceOptionsActivity : AppCompatActivity() { class AddSourceOptionsActivity : AppCompatActivity() {
lateinit var _buttonBack: ImageButton; lateinit var _buttonBack: ImageButton;
lateinit var _overlayContainer: FrameLayout;
lateinit var _buttonQR: BigButton; lateinit var _buttonQR: BigButton;
lateinit var _buttonBrowse: BigButton; lateinit var _buttonBrowse: BigButton;
lateinit var _buttonURL: BigButton; lateinit var _buttonURL: BigButton;
@ -56,7 +54,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
setContentView(R.layout.activity_add_source_options); setContentView(R.layout.activity_add_source_options);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
_overlayContainer = findViewById(R.id.overlay_container);
_buttonBack = findViewById(R.id.button_back); _buttonBack = findViewById(R.id.button_back);
_buttonQR = findViewById(R.id.option_qr); _buttonQR = findViewById(R.id.option_qr);
@ -84,25 +81,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
} }
_buttonURL.onClick.subscribe { _buttonURL.onClick.subscribe {
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json"); UIDialogs.toast(this, getString(R.string.not_implemented_yet));
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)
} }
} }
} }

View file

@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
companion object { companion object {
private val TAG = "LoginActivity"; 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; private var _callback: ((SourceAuth?) -> Unit)? = null;

View file

@ -3,7 +3,6 @@ package com.futo.platformplayer.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
@ -11,16 +10,15 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric 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.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -29,7 +27,6 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton; private lateinit var _buttonHelp: ImageButton;
private lateinit var _profileName: EditText; private lateinit var _profileName: EditText;
private lateinit var _buttonCreate: LinearLayout; private lateinit var _buttonCreate: LinearLayout;
private lateinit var _loader: LoaderView;
private val TAG = "PolycentricCreateProfileActivity"; private val TAG = "PolycentricCreateProfileActivity";
private var _creating = false; private var _creating = false;
@ -46,7 +43,6 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help); _buttonHelp = findViewById(R.id.button_help);
_profileName = findViewById(R.id.edit_profile_name); _profileName = findViewById(R.id.edit_profile_name);
_buttonCreate = findViewById(R.id.button_create_profile); _buttonCreate = findViewById(R.id.button_create_profile);
_loader = findViewById(R.id.loader);
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
@ -69,49 +65,35 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
return@setOnClickListener; return@setOnClickListener;
} }
_profileName.isEnabled = false;
_buttonCreate.visibility = View.GONE;
_loader.start();
_loader.visibility = View.VISIBLE;
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val processHandle: ProcessHandle; val processHandle: ProcessHandle;
try { try {
try { processHandle = ProcessHandle.create();
processHandle = ProcessHandle.create(); Store.instance.addProcessSecret(processHandle.processSecret);
Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(ApiMethods.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
try { try {
Logger.i(TAG, "Started backfill"); PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e); Logger.e(TAG, "Failed to save process secret to secret storage.", e)
} }
processHandle.addServer(PolycentricCache.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
} }
finally {
withContext(Dispatchers.Main) { try {
_profileName.isEnabled = true; Logger.i(TAG, "Started backfill");
_buttonCreate.visibility = View.VISIBLE; processHandle.fullyBackfillServersAnnounceExceptions();
_loader.stop(); Logger.i(TAG, "Finished backfill");
_loader.visibility = View.GONE; } catch (e: Throwable) {
} Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View file

@ -8,7 +8,6 @@ import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
@ -29,7 +28,6 @@ class PolycentricHomeActivity : AppCompatActivity() {
private lateinit var _buttonNewProfile: BigButton; private lateinit var _buttonNewProfile: BigButton;
private lateinit var _buttonImportProfile: BigButton; private lateinit var _buttonImportProfile: BigButton;
private lateinit var _layoutButtons: LinearLayout; private lateinit var _layoutButtons: LinearLayout;
private lateinit var _scroll: ScrollView;
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -44,7 +42,6 @@ class PolycentricHomeActivity : AppCompatActivity() {
_buttonNewProfile = findViewById(R.id.button_new_profile); _buttonNewProfile = findViewById(R.id.button_new_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile); _buttonImportProfile = findViewById(R.id.button_import_profile);
_layoutButtons = findViewById(R.id.layout_buttons); _layoutButtons = findViewById(R.id.layout_buttons);
_scroll = findViewById(R.id.scroll);
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
@ -81,7 +78,6 @@ class PolycentricHomeActivity : AppCompatActivity() {
_layoutButtons.addView(profileButton, 0); _layoutButtons.addView(profileButton, 0);
} }
_scroll.invalidate();
_buttonHelp.setOnClickListener { _buttonHelp.setOnClickListener {
startActivity(Intent(this, PolycentricWhyActivity::class.java)); startActivity(Intent(this, PolycentricWhyActivity::class.java));

View file

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

View file

@ -21,20 +21,18 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState 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.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
@ -49,7 +47,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton; private lateinit var _buttonHelp: ImageButton;
private lateinit var _editName: EditText; private lateinit var _editName: EditText;
private lateinit var _buttonExport: BigButton; private lateinit var _buttonExport: BigButton;
private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonLogout: BigButton; private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton; private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String; private lateinit var _username: String;
@ -71,14 +68,10 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric); _imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name); _editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export); _buttonExport = findViewById(R.id.button_export);
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonLogout = findViewById(R.id.button_logout); _buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete); _buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay); _loaderOverlay = findViewById(R.id.loader_overlay);
_textSystem = findViewById(R.id.text_system) _textSystem = findViewById(R.id.text_system)
findViewById<TextView>(R.id.text_cta2).setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://harbor.social")))
}
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired(); saveIfRequired();
finish(); finish();
@ -99,16 +92,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java)); startActivity(Intent(this, PolycentricBackupActivity::class.java));
}; };
_buttonOpenHarborProfile.onClick.subscribe {
val processHandle = StatePolycentric.instance.processHandle!!;
processHandle?.let {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
}
}
_buttonLogout.onClick.subscribe { _buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null); StatePolycentric.instance.setProcessHandle(null);
startActivity(Intent(this, PolycentricHomeActivity::class.java)); startActivity(Intent(this, PolycentricHomeActivity::class.java));
@ -125,7 +108,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
StatePolycentric.instance.setProcessHandle(null); StatePolycentric.instance.setProcessHandle(null);
Store.instance.removeProcessSecret(processHandle.system); Store.instance.removeProcessSecret(processHandle.system);
PolycentricStorage.instance.removeProcessSecret(processHandle.system);
startActivity(Intent(this, PolycentricHomeActivity::class.java)); startActivity(Intent(this, PolycentricHomeActivity::class.java));
finish(); finish();
}); });
@ -145,7 +127,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
processHandle.fullyBackfillClient(ApiMethods.SERVER) processHandle.fullyBackfillClient(PolycentricCache.SERVER)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateUI(); updateUI();

View file

@ -18,7 +18,6 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
@ -185,19 +184,12 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent); resultLauncher.launch(intent);
} }
override fun onDestroy() {
super.onDestroy()
settingsActivityClosed.emit()
}
companion object { companion object {
//TODO: Temporary for solving Settings issues //TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private var _lastActivity: SettingsActivity? = null; private var _lastActivity: SettingsActivity? = null;
val settingsActivityClosed = Event0()
fun getActivity(): SettingsActivity? { fun getActivity(): SettingsActivity? {
val act = _lastActivity; val act = _lastActivity;
if(act != null && !act._isFinished) if(act != null && !act._isFinished)

View file

@ -1,140 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.LinkType
import com.futo.platformplayer.sync.internal.SyncSession
import com.futo.platformplayer.views.sync.SyncDeviceView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SyncHomeActivity : AppCompatActivity() {
private lateinit var _layoutDevices: LinearLayout
private lateinit var _layoutEmpty: LinearLayout
private val _viewMap: MutableMap<String, SyncDeviceView> = mutableMapOf()
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons()
_layoutDevices = findViewById(R.id.layout_devices)
_layoutEmpty = findViewById(R.id.layout_empty)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncPairActivity::class.java))
}
findViewById<LinearLayout>(R.id.button_show_pairing_code).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncShowPairingCodeActivity::class.java))
}
initializeDevices()
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { publicKey, session ->
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[publicKey]
if (!session.isAuthorized) {
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey)
}
return@launch
}
if (view == null) {
val syncDeviceView = SyncDeviceView(this@SyncHomeActivity)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val v = updateDeviceView(syncDeviceView, publicKey, session)
_layoutDevices.addView(v, 0)
_viewMap[publicKey] = v
} else {
updateDeviceView(view, publicKey, session)
}
updateEmptyVisibility()
}
}
StateSync.instance.deviceRemoved.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[it]
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(it)
}
updateEmptyVisibility()
}
}
}
override fun onDestroy() {
super.onDestroy()
StateSync.instance.deviceUpdatedOrAdded.remove(this)
StateSync.instance.deviceRemoved.remove(this)
}
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView
}
private fun updateEmptyVisibility() {
if (_viewMap.isNotEmpty()) {
_layoutEmpty.visibility = View.GONE
} else {
_layoutEmpty.visibility = View.VISIBLE
}
}
private fun initializeDevices() {
_layoutDevices.removeAllViews()
for (publicKey in StateSync.instance.getAll()) {
val syncDeviceView = SyncDeviceView(this)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val view = updateDeviceView(syncDeviceView, publicKey, StateSync.instance.getSession(publicKey))
_layoutDevices.addView(view)
_viewMap[publicKey] = view
}
updateEmptyVisibility()
}
companion object {
private const val TAG = "SyncHomeActivity"
}
}

View file

@ -1,148 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.util.Base64
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
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.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
class SyncPairActivity : AppCompatActivity() {
private lateinit var _editCode: EditText
private lateinit var _layoutPairing: LinearLayout
private lateinit var _textPairingStatus: TextView
private lateinit var _layoutPairingSuccess: LinearLayout
private lateinit var _layoutPairingError: LinearLayout
private lateinit var _textError: TextView
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
scanResult?.let {
if (it.contents != null) {
_editCode.text.clear()
_editCode.text.append(it.contents)
pair(it.contents)
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_pair)
setNavigationBarColorAndIcons()
_editCode = findViewById(R.id.edit_code)
_layoutPairing = findViewById(R.id.layout_pairing)
_textPairingStatus = findViewById(R.id.text_pairing_status)
_layoutPairingSuccess = findViewById(R.id.layout_pairing_success)
_layoutPairingError = findViewById(R.id.layout_pairing_error)
_textError = findViewById(R.id.text_error)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_scan_qr).setOnClickListener {
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true);
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
integrator.setBarcodeImageEnabled(true)
integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent())
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
pair(_editCode.text.toString())
}
_layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE
}
_layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE
}
_layoutPairingSuccess.visibility = View.GONE
_layoutPairingError.visibility = View.GONE
}
fun pair(url: String) {
try {
_layoutPairing.visibility = View.VISIBLE
_textPairingStatus.text = "Parsing text..."
if (!url.startsWith("grayjay://sync/")) {
throw Exception("Not a valid URL: $url")
}
val deviceInfo: SyncDeviceInfo = Json.decodeFromString<SyncDeviceInfo>(Base64.decode(url.substring("grayjay://sync/".length), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).decodeToString())
if (StateSync.instance.isAuthorized(deviceInfo.publicKey)) {
throw Exception("This device is already paired")
}
_textPairingStatus.text = "Connecting..."
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.connect(deviceInfo) { complete, message ->
lifecycleScope.launch(Dispatchers.Main) {
if (complete != null && complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textPairingStatus.text = message
}
}
}
} 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
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
}
}
} catch(e: Throwable) {
_layoutPairingError.visibility = View.VISIBLE
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
}
}
companion object {
private const val TAG = "SyncPairActivity"
}
}

View file

@ -1,142 +0,0 @@
package com.futo.platformplayer.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
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
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.NetworkInterface
class SyncShowPairingCodeActivity : AppCompatActivity() {
private lateinit var _textCode: TextView
private lateinit var _imageQR: ImageView
private lateinit var _textQR: TextView
private var _code: String? = null
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onDestroy() {
super.onDestroy()
activity = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity = this
setContentView(R.layout.activity_sync_show_pairing_code)
setNavigationBarColorAndIcons()
_textCode = findViewById(R.id.text_code)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_scan_qr)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_copy).setOnClickListener {
val code = _code ?: return@setOnClickListener
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
val clip = ClipData.newPlainText(getString(R.string.copied_text), code);
clipboard.setPrimaryClip(clip);
UIDialogs.toast(this, "Copied to clipboard")
}
val ips = getIPs()
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.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)
}
fun setCode(code: String?) {
_code = code
_textCode.text = code
if (code == null) {
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
return
}
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
_imageQR.setImageBitmap(qrCodeBitmap)
_imageQR.visibility = View.VISIBLE
_textQR.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
}
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
return bitMatrixToBitmap(bitMatrix);
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
val width = matrix.width;
val height = matrix.height;
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
}
}
return bmp;
}
private fun getIPs(): List<String> {
val ips = arrayListOf<String>()
for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) {
if (addr.isLoopbackAddress) {
continue
}
if (addr.address.size != 4) {
continue
}
addr.hostAddress?.let { ips.add(it) }
}
}
return ips
}
companion object {
private const val TAG = "SyncShowPairingCodeActivity"
var activity: SyncShowPairingCodeActivity? = null
private set
}
}

View file

@ -5,8 +5,6 @@ import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.FragmentedStorage
import okhttp3.Call import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -19,14 +17,13 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Duration
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
open class ManagedHttpClient { open class ManagedHttpClient {
protected var _builderTemplate: OkHttpClient.Builder; protected val _builderTemplate: OkHttpClient.Builder;
private var client: OkHttpClient; private var client: OkHttpClient;
@ -35,15 +32,6 @@ open class ManagedHttpClient {
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0" var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
fun setTimeout(timeout: Long) {
rebuildClient {
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
.connectTimeout(Duration.ofMillis(timeout));
}
}
private val trustAllCerts = arrayOf<TrustManager>( private val trustAllCerts = arrayOf<TrustManager>(
object: X509TrustManager { object: X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { } override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
@ -65,7 +53,7 @@ open class ManagedHttpClient {
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) { constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = 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); trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain -> client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request()); val request = beforeRequest(chain.request());
@ -74,15 +62,6 @@ open class ManagedHttpClient {
}.build(); }.build();
} }
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
_builderTemplate = modify(_builderTemplate);
client = _builderTemplate.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
return@addNetworkInterceptor response;
}.build();
}
open fun clone(): ManagedHttpClient { open fun clone(): ManagedHttpClient {
val clonedClient = ManagedHttpClient(_builderTemplate); val clonedClient = ManagedHttpClient(_builderTemplate);
clonedClient.user_agent = user_agent; clonedClient.user_agent = user_agent;

View file

@ -210,20 +210,6 @@ class HttpContext : AutoCloseable {
} }
} }
} }
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
if(headers.get("content-length").isNullOrEmpty()) {
if (body != null) {
headers.put("content-length", body.size.toString());
} else {
headers.put("content-length", "0")
}
}
respond(status, headers) { responseStream ->
if(body != null) {
responseStream.write(body);
}
}
}
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) { fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set"); val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");

View file

@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpHandler import com.futo.platformplayer.api.http.server.handlers.HttpHandler
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for(getMethod in getMethods) for(getMethod in getMethods)
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1) if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply { addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
if(!getMethod.second.contentType.isEmpty()) if(!getMethod.second.contentType.isEmpty())
this.withContentType(getMethod.second.contentType); this.withContentType(getMethod.second.contentType);
}.withContentType(getMethod.second.contentType); }.withContentType(getMethod.second.contentType);
for(postMethod in postMethods) for(postMethod in postMethods)
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1) if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply { addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
if(!postMethod.second.contentType.isEmpty()) if(!postMethod.second.contentType.isEmpty())
this.withContentType(postMethod.second.contentType); this.withContentType(postMethod.second.contentType);
}.withContentType(postMethod.second.contentType); }.withContentType(postMethod.second.contentType);
for(getField in getFields) { for(getField in getFields) {
getField.first.isAccessible = true; getField.first.isAccessible = true;
addHandler(HttpFunctionHandler("GET", getField.second.path) { addHandler(HttpFuntionHandler("GET", getField.second.path) {
val value = getField.first.get(obj) as String?; val value = getField.first.get(obj) as String?;
if(value != null) { if(value != null) {
val headers = HttpHeaders( val headers = HttpHeaders(

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") Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
current += bytesToSend.toLong() current += bytesToSend.toLong()
if (current > end) { if (current >= end) {
Logger.i(TAG, "Expected amount of bytes sent") Logger.i(TAG, "Expected amount of bytes sent")
break break
} }

View file

@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext import com.futo.platformplayer.api.http.server.HttpContext
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) { class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
override fun handle(httpContext: HttpContext) { override fun handle(httpContext: HttpContext) {
httpContext.setResponseHeaders(this.headers); httpContext.setResponseHeaders(this.headers);
handler(httpContext); handler(httpContext);

View file

@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media 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.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -67,11 +66,6 @@ interface IPlatformClient {
*/ */
fun searchChannels(query: String): IPager<PlatformAuthorLink>; fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages //Video Pages
/** /**

View file

@ -13,15 +13,13 @@ class PlatformClientPool {
private val _pool: HashMap<JSClient, Int> = hashMapOf(); private val _pool: HashMap<JSClient, Int> = hashMapOf();
private var _poolCounter = 0; private var _poolCounter = 0;
private val _poolName: String?; private val _poolName: String?;
private val _privatePool: Boolean;
var isDead: Boolean = false var isDead: Boolean = false
private set; private set;
val onDead = Event2<JSClient, PlatformClientPool>(); val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) { constructor(parentClient: IPlatformClient, name: String? = null) {
_poolName = name; _poolName = name;
_privatePool = privatePool;
if(parentClient !is JSClient) if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now"); throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started"); Logger.i(TAG, "Pool for ${parentClient.name} was started");
@ -53,7 +51,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy }; reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) { if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})"); Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool); reserved = _parent.getCopy();
reserved?.onCaptchaException?.subscribe { client, ex -> reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex); StateApp.instance.handleCaptchaException(client, ex);

View file

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

View file

@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
class Serializer { class Serializer {
companion object { companion object {
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true }; val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
} }
} }

View file

@ -2,10 +2,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.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.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -30,8 +27,6 @@ open class PlatformAuthorLink {
} }
companion object { companion object {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
if(value.has("membershipUrl")) if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value); return PlatformAuthorMembershipLink.fromV8(config, value);
@ -45,21 +40,4 @@ 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

@ -30,7 +30,6 @@ class ResultCapabilities(
const val TYPE_POSTS = "POSTS"; const val TYPE_POSTS = "POSTS";
const val TYPE_MIXED = "MIXED"; const val TYPE_MIXED = "MIXED";
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS"; const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
const val TYPE_SHORTS = "SHORTS";
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL"; const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";

View file

@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
companion object { companion object {
fun fromInt(value: Int): ChapterType fun fromInt(value: Int): ChapterType
{ {
val result = ChapterType.entries.firstOrNull { it.value == value }; val result = ChapterType.values().firstOrNull { it.value == value };
if(result == null) if(result == null)
throw UnknownPlatformException(value.toString()); throw UnknownPlatformException(value.toString());
return result; return result;

View file

@ -1,63 +0,0 @@
package com.futo.platformplayer.api.media.models.comments
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.ratings.RatingType
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Deferred
import java.time.OffsetDateTime
class LazyComment: IPlatformComment {
private var _commentDeferred: Deferred<IPlatformComment>;
private var _commentLoaded: IPlatformComment? = null;
private var _commentException: Throwable? = null;
override val contextUrl: String
get() = _commentLoaded?.contextUrl ?: "";
override val author: PlatformAuthorLink
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
override val message: String
get() = _commentLoaded?.message ?: "";
override val rating: IRating
get() = _commentLoaded?.rating ?: RatingLikes(0);
override val date: OffsetDateTime?
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
override val replyCount: Int?
get() = _commentLoaded?.replyCount ?: 0;
val isAvailable: Boolean get() = _commentLoaded != null;
private var _uiHandler: ((LazyComment)->Unit)? = null;
constructor(commentDeferred: Deferred<IPlatformComment>) {
_commentDeferred = commentDeferred;
_commentDeferred.invokeOnCompletion {
if(it == null) {
_commentLoaded = commentDeferred.getCompleted();
Logger.i("LazyComment", "Resolved comment");
}
else {
_commentException = it;
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
}
_uiHandler?.invoke(this);
}
}
fun getUnderlyingComment(): IPlatformComment? {
return _commentLoaded;
}
fun setUIHandler(handler: (LazyComment)->Unit){
_uiHandler = handler;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
return _commentLoaded?.getReplies(client);
}
}

View file

@ -12,7 +12,6 @@ enum class ContentType(val value: Int) {
URL(9), URL(9),
NESTED_VIDEO(11), NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70), LOCKED(70),
@ -22,7 +21,7 @@ enum class ContentType(val value: Int) {
companion object { companion object {
fun fromInt(value: Int): ContentType fun fromInt(value: Int): ContentType
{ {
val result = ContentType.entries.firstOrNull { it.value == value }; val result = ContentType.values().firstOrNull { it.value == value };
if(result == null) if(result == null)
throw UnknownPlatformException(value.toString()); throw UnknownPlatformException(value.toString());
return result; return result;

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.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime import java.time.OffsetDateTime
interface IPlatformContent { interface IPlatformContent {

View file

@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
companion object{ companion object{
fun fromInt(value : Int) : LiveEventType{ fun fromInt(value : Int) : LiveEventType{
return LiveEventType.entries.first { it.value == value }; return LiveEventType.values().first { it.value == value };
} }
} }
} }

View file

@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
companion object { companion object {
fun fromInt(value: Int): TextType fun fromInt(value: Int): TextType
{ {
val result = TextType.entries.firstOrNull { it.value == value }; val result = TextType.values().firstOrNull { it.value == value };
if(result == null) if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value"); throw IllegalArgumentException("Unknown Texttype: $value");
return result; return result;

View file

@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
companion object{ companion object{
fun fromInt(value : Int) : RatingType{ fun fromInt(value : Int) : RatingType{
return RatingType.entries.first { it.value == value }; return RatingType.values().first { it.value == value };
} }
} }
} }

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,6 @@
package com.futo.platformplayer.api.media.models.streams.sources package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource interface IAudioUrlWidevineSource : IAudioUrlSource {
val bearerToken: String
val licenseUri: String
}

View file

@ -1,5 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IDashManifestWidevineSource : IWidevineSource {
val url: String
}

View file

@ -1,3 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource

View file

@ -1,9 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
interface IWidevineSource {
val licenseUri: String
val hasLicenseRequestExecutor: Boolean
fun getLicenseRequestExecutor(): JSRequestExecutor?
}

View file

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

View file

@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
} }
companion object { 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( return LocalVideoSource(
source.name, source.name,
path, path,
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
source.width, source.width,
source.height, source.height,
source.duration, source.duration,
overrideContainer ?: source.container, source.container,
source.codec, source.codec,
source.bitrate?:0 source.bitrate?:0
); );

View file

@ -13,6 +13,4 @@ interface IPlatformVideo : IPlatformContent {
val viewCount: Long; val viewCount: Long;
val isLive : Boolean; val isLive : Boolean;
val isShort: Boolean;
} }

View file

@ -10,26 +10,23 @@ import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime import java.time.OffsetDateTime
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
open class SerializedPlatformVideo( open class SerializedPlatformVideo(
override val contentType: ContentType = ContentType.MEDIA,
override val id: PlatformID, override val id: PlatformID,
override val name: String, override val name: String,
override val thumbnails: Thumbnails, override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink, override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
@JsonNames("datetime", "dateTime") override val datetime: OffsetDateTime?,
override val datetime: OffsetDateTime? = null,
override val url: String, override val url: String,
override val shareUrl: String = "", override val shareUrl: String,
override val duration: Long, override val duration: Long,
override val viewCount: Long, override val viewCount: Long,
override val isShort: Boolean = false
) : IPlatformVideo, SerializedPlatformContent { ) : IPlatformVideo, SerializedPlatformContent {
override val contentType: ContentType = ContentType.MEDIA;
override val isLive: Boolean = false; override val isLive: Boolean = false;
@ -46,7 +43,6 @@ open class SerializedPlatformVideo(
companion object { companion object {
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo { fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
return SerializedPlatformVideo( return SerializedPlatformVideo(
ContentType.MEDIA,
video.id, video.id,
video.name, video.name,
video.thumbnails, video.thumbnails,

View file

@ -38,8 +38,7 @@ open class SerializedPlatformVideoDetails(
override val video: ISerializedVideoSourceDescriptor, override val video: ISerializedVideoSourceDescriptor,
override val preview: ISerializedVideoSourceDescriptor?, override val preview: ISerializedVideoSourceDescriptor?,
override val subtitles: List<SubtitleRawSource> = listOf(), override val subtitles: List<SubtitleRawSource> = listOf()
override val isShort: Boolean = false
) : IPlatformVideo, IPlatformVideoDetails { ) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA; final override val contentType: ContentType get() = ContentType.MEDIA;

View file

@ -54,8 +54,8 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings); return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
} }
override fun getCopy(privateCopy: Boolean): JSClient { override fun getCopy(): JSClient {
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID); return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
} }
override fun initialize() { 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.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities 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.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -32,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.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails 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.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.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment import com.futo.platformplayer.api.media.platforms.js.models.JSComment
@ -166,16 +164,13 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit); _plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
this._context = context; this._context = context;
this.config = descriptor.config; this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null); icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor; this.descriptor = descriptor;
_injectedSaveState = saveState; _injectedSaveState = saveState;
if(!withoutCredentials) _auth = descriptor.getAuth();
_auth = descriptor.getAuth();
else
_auth = null;
_captcha = descriptor.getCaptchaData(); _captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
@ -195,8 +190,8 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit); _plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
open fun getCopy(withoutCredentials: Boolean = false): JSClient { open fun getCopy(): JSClient {
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials); return JSClient(_context, descriptor, saveState(), _script);
} }
fun getUnderlyingPlugin(): V8Plugin { fun getUnderlyingPlugin(): V8Plugin {
@ -239,8 +234,7 @@ open class JSClient : IPlatformClient {
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false, hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false, hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false, hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
); );
try { try {
@ -363,10 +357,6 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSChannelPager(config, this, return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); 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") @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)") @JSDocsParameter("url", "A channel url (May not be your platform)")

View file

@ -1,7 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js
class JSClientConstants {
companion object {
val PLUGIN_SPEC_VERSION = 2;
}
}

View file

@ -4,9 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual
import java.net.URL import java.net.URL
import java.util.UUID import java.util.UUID
@ -33,7 +31,6 @@ class SourcePluginConfig(
override val allowEval: Boolean = false, override val allowEval: Boolean = false,
override val allowUrls: List<String> = listOf(), override val allowUrls: List<String> = listOf(),
override val packages: List<String> = listOf(), override val packages: List<String> = listOf(),
override val packagesOptional: List<String> = listOf(),
val settings: List<Setting> = listOf(), val settings: List<Setting> = listOf(),
@ -51,9 +48,6 @@ class SourcePluginConfig(
var primaryClaimFieldType: Int? = null, var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null, var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false, var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
var changelog: HashMap<String, List<String>>? = null
) : IV8PluginConfig { ) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl); val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@ -83,8 +77,7 @@ class SourcePluginConfig(
private var _allowUrlsLowerVal: List<String>? = null; private var _allowUrlsLowerVal: List<String>? = null;
private val _allowUrlsLower: List<String> get() { private val _allowUrlsLower: List<String> get() {
if(_allowUrlsLowerVal == null) if(_allowUrlsLowerVal == null)
_allowUrlsLowerVal = allowUrls.map { it.lowercase() } _allowUrlsLowerVal = allowUrls.map { it.lowercase() };
.filter { it.length > 0 };
return _allowUrlsLowerVal!!; return _allowUrlsLowerVal!!;
}; };
@ -103,10 +96,6 @@ class SourcePluginConfig(
if(!packages.contains(pack)) if(!packages.contains(pack))
return false; return false;
} }
for(pack in newConfig.packagesOptional) {
if(!packagesOptional.contains(pack))
return false;
}
//Developer Submit Url should be same or empty //Developer Submit Url should be same or empty
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl) if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
return false; return false;
@ -135,7 +124,7 @@ class SourcePluginConfig(
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id); val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
if (currentlyInstalledPlugin != null) { if (currentlyInstalledPlugin != null) {
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) { if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
list.add(Pair( list.add(Pair(
"Different Author", "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.")); "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."));
@ -181,20 +170,7 @@ class SourcePluginConfig(
return true; return true;
val uri = Uri.parse(url); val uri = Uri.parse(url);
val host = uri.host?.lowercase() ?: ""; val host = uri.host?.lowercase() ?: "";
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) }; return _allowUrlsLower.any { it == host };
}
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 { companion object {

View file

@ -38,7 +38,7 @@ class JSHttpClient : ManagedHttpClient {
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super( constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
//Temporary ugly solution for DevPortal proxy support //Temporary ugly solution for DevPortal proxy support
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && StateDeveloper.instance.devProxy != null) (if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP, OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port) InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
)) ))

View file

@ -1,7 +1,6 @@
package com.futo.platformplayer.api.media.platforms.js.models package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
@ -27,7 +26,6 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj); ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj); ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj); ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj)
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }

View file

@ -16,7 +16,6 @@ interface IJSContentDetails: IPlatformContent {
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj); ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj); ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }

View file

@ -1,162 +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 JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
val rating: IRating;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
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);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
else
thumbnails = null;
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
?.map { fromV8Segment(client, it) }
?.filterNotNull() ?: listOf());
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
return@handleDevCall getCommentsJS(client);
}
else if(client is JSClient)
return getCommentsJS(client);
return null;
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client);
}
else if(client is JSClient)
return getContentRecommendationsJS(client);
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
companion object {
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field");
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null;
}
}
}
}
enum class SegmentType(val value: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2),
NESTED(9);
companion object {
fun fromInt(value: Int): SegmentType
{
val result = SegmentType.entries.firstOrNull { it.value == value };
if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value");
return result;
}
}
}
interface IJSArticleSegment {
val type: SegmentType;
}
class JSTextSegment: IJSArticleSegment {
override val type = SegmentType.TEXT;
val textType: TextType;
val content: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
}
}
class JSImagesSegment: IJSArticleSegment {
override val type = SegmentType.IMAGES;
val images: List<String>;
val caption: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
}
}
class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED;
val nested: IPlatformContent;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSNestedSegment";
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
nested = IJSContent.fromV8(client, nestedObj);
}
}

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.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> { class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {

View file

@ -42,15 +42,10 @@ open class JSContent : IPlatformContent, IPluginSourced {
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName)); id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString(); name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null); val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
if(authorObj != null) if(datetimeInt == 0.toLong())
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
else
author = PlatformAuthorLink.UNKNOWN;
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
if(datetimeInt == null || datetimeInt == 0.toLong())
datetime = null; datetime = null;
else else
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC); datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
@ -59,8 +54,4 @@ open class JSContent : IPlatformContent, IPluginSourced {
_hasGetDetails = _content.has("getDetails"); _hasGetDetails = _content.has("getDetails");
} }
fun getUnderlyingObject(): V8ValueObject? {
return _content;
}
} }

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced 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.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@ -16,14 +15,4 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override fun convertResult(obj: V8ValueObject): IPlatformContent { override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(plugin, obj); 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

@ -71,8 +71,6 @@ abstract class JSPager<T> : IPager<T> {
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager"); val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed");
val newResults = items.toArray() val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) } .map { convertResult(it as V8ValueObject) }
.toList(); .toList();

View file

@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.ReusablePager import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import java.util.UUID
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails { class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager<IPlatformVideo>; override val contents: IPager<IPlatformVideo>;
@ -38,6 +37,6 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
onProgress?.invoke(videos.size); onProgress?.invoke(videos.size);
} }
return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)}); return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
} }
} }

View file

@ -1,134 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.primitive.V8ValueUndefined
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginException
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.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable
import java.util.Base64
class JSRequestExecutor {
private val _plugin: JSClient;
private val _config: IV8PluginConfig;
private var _executor: V8ValueObject;
val urlPrefix: String?;
private val hasCleanup: Boolean;
constructor(plugin: JSClient, executor: V8ValueObject) {
this._plugin = plugin;
this._executor = executor;
this._config = plugin.config;
val config = plugin.config;
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
if(!executor.has("executeRequest"))
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
hasCleanup = executor.has("cleanup");
}
//TODO: Executor properties?
@Throws(ScriptException::class)
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
if (_executor.isClosed)
throw IllegalStateException("Executor object is closed");
val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
try {
if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value);
return base64Result;
}
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<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();
}
}
open fun cleanup() {
if (!hasCleanup || _executor.isClosed)
return;
if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
protected fun finalize() {
cleanup();
}
}
//TODO: are these available..?
@Serializable
class ExecutorParameters {
var rangeStart: Int = -1;
var rangeEnd: Int = -1;
var segment: Int = -1;
}

View file

@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced { open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
@ -18,7 +17,6 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
final override val viewCount: Long; final override val viewCount: Long;
final override val isLive: Boolean; final override val isLive: Boolean;
final override val isShort: Boolean;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) { constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformVideo"; val contextName = "PlatformVideo";
@ -28,6 +26,5 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong(); duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
viewCount = _content.getOrThrow(config, "viewCount", contextName); viewCount = _content.getOrThrow(config, "viewCount", contextName);
isLive = _content.getOrThrow(config, "isLive", contextName); isLive = _content.getOrThrow(config, "isLive", contextName);
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
} }
} }

View file

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

View file

@ -3,39 +3,22 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource { class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val bearerToken: String
override val licenseUri: String override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary") @Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlWidevineSource" val contextName = "JSAudioUrlWidevineSource"
val config = plugin.config val config = plugin.config
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
} }
override fun toString(): String { override fun toString(): String {
val url = getAudioUrl() val url = getAudioUrl()
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)" return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
} }
} }

View file

@ -35,9 +35,4 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null); indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
audioChannels = _obj.getOrDefault(config, "audioChannels", contextName, 2) ?: 2; audioChannels = _obj.getOrDefault(config, "audioChannels", contextName, 2) ?: 2;
} }
override fun toString(): String {
return "RangeSource(url=[${getAudioUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
return super.toString()
}
} }

View file

@ -1,84 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String;
override val name : String;
override val codec: String;
override val bitrate: Int;
override val duration: Long;
override val priority: Boolean;
override var original: Boolean = false;
override val language: String;
val url: String;
override var manifest: String?;
override val hasGenerate: Boolean;
override var streamMetaData: StreamMetaData? = null;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
manifest = _obj.getOrThrow(config, "manifest", contextName);
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
hasGenerate = _obj.has("generate");
}
override fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: String? = null;
if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate");
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate");
}
if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
}
return result;
}
}

View file

@ -1,136 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.states.StateDeveloper
interface IJSDashManifestRawSource {
val hasGenerate: Boolean;
var manifest: String?;
fun generate(): String?;
}
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String;
override val name : String;
override val width: Int;
override val height: Int;
override val codec: String;
override val bitrate: Int?;
override val duration: Long;
override val priority: Boolean;
var url: String?;
override var manifest: String?;
override val hasGenerate: Boolean;
val canMerge: Boolean;
override var streamMetaData: StreamMetaData? = null;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
hasGenerate = _obj.has("generate");
}
override open fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
var result: String? = null;
if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate");
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate");
});
if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
}
return result;
}
}
class JSDashManifestMergingRawSource(
val video: JSDashManifestRawSource,
val audio: JSDashManifestRawAudioSource): JSDashManifestRawSource(video.getUnderlyingPlugin()!!, video.getUnderlyingObject()!!), IVideoSource {
override val name: String
get() = video.name;
override val bitrate: Int
get() = (video.bitrate ?: 0) + audio.bitrate;
override val codec: String
get() = video.codec
override val container: String
get() = video.container
override val duration: Long
get() = video.duration;
override val height: Int
get() = video.height;
override val width: Int
get() = video.width;
override val priority: Boolean
get() = video.priority;
override fun generate(): String? {
val videoDash = video.generate();
val audioDash = audio.generate();
if(videoDash != null && audioDash == null) return videoDash;
if(audioDash != null && videoDash == null) return audioDash;
if(videoDash == null) return null;
//TODO: Temporary simple solution..make more reliable version
var result: String? = null;
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
if(audioAdaptationSet != null) {
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
}
else
result = videoDash;
return result;
}
companion object {
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
}
}

View file

@ -1,60 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
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.getOrNull
import com.futo.platformplayer.getOrThrow
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource {
override val width: Int = 0
override val height: Int = 0
override val container: String = "application/dash+xml"
override val codec: String = "Dash"
override val name: String
override val bitrate: Int? = null
override val url: String
override val duration: Long
override var priority: Boolean = false
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
val contextName = "DashWidevineSource"
val config = plugin.config
name = _obj.getOrThrow(config, "name", contextName)
url = _obj.getOrThrow(config, "url", contextName)
duration = _obj.getOrThrow(config, "duration", contextName)
priority = obj.getOrNull(config, "priority", contextName) ?: false
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun getVideoUrl(): String {
return url
}
}

View file

@ -21,7 +21,6 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
override val language: String; override val language: String;
override var priority: Boolean = false; override var priority: Boolean = false;
override var original: Boolean = false;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
val contextName = "HLSAudioSource"; val contextName = "HLSAudioSource";
@ -33,7 +32,6 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
language = _obj.getOrThrow(config, "language", contextName); language = _obj.getOrThrow(config, "language", contextName);
priority = obj.getOrNull(config, "priority", contextName) ?: false; priority = obj.getOrNull(config, "priority", contextName) ?: false;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
} }

View file

@ -10,12 +10,10 @@ import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@ -23,17 +21,9 @@ abstract class JSSource {
protected val _plugin: JSClient; protected val _plugin: JSClient;
protected val _config: IV8PluginConfig; protected val _config: IV8PluginConfig;
protected val _obj: V8ValueObject; protected val _obj: V8ValueObject;
val hasRequestModifier: Boolean; val hasRequestModifier: Boolean;
private val _requestModifier: JSRequest?; private val _requestModifier: JSRequest?;
val hasRequestExecutor: Boolean;
private val _requestExecutor: JSRequest?;
val requiresCustomDatasource: Boolean get() {
return hasRequestModifier || hasRequestExecutor;
}
val type : String; val type : String;
constructor(type: String, plugin: JSClient, obj: V8ValueObject) { constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
@ -46,11 +36,6 @@ abstract class JSSource {
JSRequest(plugin, it, null, null, true); JSRequest(plugin, it, null, null, true);
} }
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier"); hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
_requestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
JSRequest(plugin, it, null, null, true);
}
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
} }
fun getRequestModifier(): IRequestModifier? { fun getRequestModifier(): IRequestModifier? {
@ -59,38 +44,20 @@ abstract class JSSource {
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers); return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
}; };
if (!hasRequestModifier || _obj.isClosed) if (!hasRequestModifier || _obj.isClosed) {
return null; return null;
}
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invoke("getRequestModifier", arrayOf<Any>()); _obj.invoke("getRequestModifier", arrayOf<Any>());
}; };
if (result !is V8ValueObject) if (result !is V8ValueObject) {
return null; return null;
}
return JSRequestModifier(_plugin, result) return JSRequestModifier(_plugin, result)
} }
open fun getRequestExecutor(): JSRequestExecutor? {
if (!hasRequestExecutor || _obj.isClosed)
return null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>());
};
if (result !is V8ValueObject)
return null;
return JSRequestExecutor(_plugin, result)
}
fun getUnderlyingPlugin(): JSClient? {
return _plugin;
}
fun getUnderlyingObject(): V8ValueObject? {
return _obj;
}
companion object { companion object {
const val TYPE_AUDIOURL = "AudioUrlSource"; const val TYPE_AUDIOURL = "AudioUrlSource";
@ -98,49 +65,33 @@ abstract class JSSource {
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource"; const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource"; const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
const val TYPE_DASH = "DashSource"; const val TYPE_DASH = "DashSource";
const val TYPE_DASH_WIDEVINE = "DashWidevineSource";
const val TYPE_DASH_RAW = "DashRawSource";
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
const val TYPE_HLS = "HLSSource"; const val TYPE_HLS = "HLSSource";
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource" const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) }; fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? { fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj); TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(plugin, obj); TYPE_HLS -> fromV8HLS(plugin, obj);
TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj)
TYPE_DASH -> fromV8Dash(plugin, obj); TYPE_DASH -> fromV8Dash(plugin, obj);
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj); else -> throw NotImplementedError("Unknown type ${type}");
else -> {
Logger.w("JSSource", "Unknown video type ${type}");
null;
};
} }
} }
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) }; fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj); fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj); fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? { fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj); TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj); TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj); TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
else -> { else -> throw NotImplementedError("Unknown type ${type}");
Logger.w("JSSource", "Unknown audio type ${type}");
null;
};
} }
} }
} }

View file

@ -23,11 +23,9 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray(); .toTypedArray();
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray() this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) } .map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray(); .toTypedArray();
} }
} }

View file

@ -21,7 +21,6 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray(); .toTypedArray();
} }

View file

@ -1,41 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
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
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlWidevineSource"
val config = plugin.config
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun toString(): String {
val url = getVideoUrl()
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
}
}

View file

@ -33,9 +33,4 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
indexStart = _obj.getOrDefault(config, "indexStart", contextName, null); indexStart = _obj.getOrDefault(config, "indexStart", contextName, null);
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null); indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
} }
override fun toString(): String {
return "RangeSource(url=[${getVideoUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
return super.toString()
}
} }

View file

@ -1,5 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local
class LocalClient {
//TODO
}

View file

@ -1,85 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
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.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.downloads.VideoLocal
import java.io.File
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneId
class LocalVideoDetails: IPlatformVideoDetails {
override val contentType: ContentType get() = ContentType.UNKNOWN;
override val id: PlatformID;
override val name: String;
override val author: PlatformAuthorLink;
override val datetime: OffsetDateTime?;
override val url: String;
override val shareUrl: String;
override val rating: IRating = RatingLikes(0);
override val description: String = "";
override val video: IVideoSourceDescriptor;
override val preview: IVideoSourceDescriptor? = null;
override val live: IVideoSource? = null;
override val dash: IDashManifestSource? = null;
override val hls: IHLSManifestSource? = null;
override val subtitles: List<ISubtitleSource> = listOf()
override val thumbnails: Thumbnails;
override val duration: Long;
override val viewCount: Long = 0;
override val isLive: Boolean = false;
override val isShort: Boolean = false;
constructor(file: File) {
id = PlatformID("Local", file.path, "LOCAL")
name = file.name;
author = PlatformAuthorLink.UNKNOWN;
url = file.canonicalPath;
shareUrl = "";
duration = 0;
thumbnails = Thumbnails(arrayOf());
datetime = OffsetDateTime.ofInstant(
Instant.ofEpochMilli(file.lastModified()),
ZoneId.systemDefault()
);
video = LocalVideoMuxedSourceDescriptor(LocalVideoFileSource(file));
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
return null;
}
override fun getPlaybackTracker(): IPlaybackTracker? {
return null;
}
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
return null;
}
}

View file

@ -1,13 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoMuxedSourceDescriptor(
private val video: LocalVideoFileSource
) : VideoMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
}

View file

@ -1,25 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models
import android.content.Context
import android.database.Cursor
import android.provider.MediaStore
import android.provider.MediaStore.Video
class MediaStoreVideo {
companion object {
val URI = MediaStore.Files.getContentUri("external");
val PROJECTION = arrayOf(Video.Media._ID, Video.Media.TITLE, Video.Media.DURATION, Video.Media.HEIGHT, Video.Media.WIDTH, Video.Media.MIME_TYPE);
val ORDER = MediaStore.Video.Media.TITLE;
fun readMediaStoreVideo(cursor: Cursor) {
}
fun query(context: Context, selection: String, args: Array<String>, order: String? = null): Cursor? {
val cursor = context.contentResolver.query(URI, PROJECTION, selection, args, order ?: ORDER, null);
return cursor;
}
}
}

View file

@ -1,31 +0,0 @@
package com.futo.platformplayer.api.media.platforms.local.models.sources
import android.content.Context
import android.provider.MediaStore
import android.provider.MediaStore.Video
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.helpers.VideoHelper
import java.io.File
class LocalVideoFileSource: IVideoSource {
override val name: String;
override val width: Int;
override val height: Int;
override val container: String;
override val codec: String = ""
override val bitrate: Int = 0
override val duration: Long;
override val priority: Boolean = false;
constructor(file: File) {
name = file.name;
width = 0;
height = 0;
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
duration = 0;
}
}

View file

@ -6,7 +6,7 @@ import com.futo.platformplayer.constructs.Event1
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager) * A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager * When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
*/ */
interface IRefreshPager<T>: IPager<T> { interface IRefreshPager<T> {
val onPagerChanged: Event1<IPager<T>>; val onPagerChanged: Event1<IPager<T>>;
val onPagerError: Event1<Throwable>; val onPagerError: Event1<Throwable>;

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